简介:这份资源是《Deep-Reinforcement-Learning-Hands-On》配套的深度强化学习实践资料,面向已具备机器学习基础、希望从理论走向代码落地的开发者与研究者,帮助解决高维状态空间下传统Q表难以存储更新、算法实现细节模糊等痛点。压缩包为zip格式,整体约32.83MB,文件总数暂未提供明细,内容围绕DQN、DDQN、A3C、DDPG、TD3、PPO、SAC等主流DRL算法的实现展开,并涉及OpenAI Gym环境模拟与超参数调优。已有378人学习下载,说明其在强化学习入门与进阶群体中具备一定参考价值。读者可借助代码理解经验回放、固定Q目标、双重Q学习、异步并行探索、确定性策略梯度及熵最大化探索等机制,掌握从价值函数近似到策略优化的完整脉络,为游戏控制、机器人控制、资源调度等复杂决策任务打下实践基础。
1. 从一份 Deep Reinforcement Learning Hands-On 的目录说起:为什么多数人卡在“能跑但不会改”
如果你手里已经有一份 Deep-Reinforcement-Learning-Hands-On 的代码目录,或者正准备照着它入门深度强化学习,大概率会遇到同一个尴尬:CartPole 能跑出曲线,换到自己的环境就不知道从哪下手。这不是你笨,而是这类 hands-on 项目天然把“算法实现”和“工程决策”揉在一起,前者能抄,后者抄不了。
这份标题对应的内容,本质是一套以 PyTorch 为主线、从交叉熵到 DQN、从策略梯度到分布式 A2C/PPO 的深度强化学习实操集合。它解决的不是“强化学习是什么”,而是“给定一个环境,我该选哪个算法、网络怎么搭、超参怎么设、训练不收敛时先看哪”。适合两类人:一类是刚学完 Q-learning 和贝尔曼方程、想把它落到代码上的新手;另一类是做过监督学习、想切到强化学习但被样本效率和奖励稀疏劝退的工程师。下面按“先立住理论、再动手复现、最后讲坑”的顺序拆开讲,中间会穿插 deepreinforcement 这类关键词对应的常见实现套路。
2. 先分清三类算法:值函数、策略梯度、以及它们的混合体
2.1 值函数方法:DQN 及其变体到底在拟合什么
值函数方法的核心是学一个 Q(s,a),然后用 argmax 选动作。DQN 用神经网络替代 Q 表,靠经验回放打散样本相关性,靠目标网络稳住自举目标。很多人第一次看 DQN 代码会觉得“不就是监督学习吗”,确实像,但区别在于标签是自己算出来的,而且会随训练漂移。
一个最小可跑的 DQN 结构通常包含三块:在线网络、目标网络、回放缓冲区。在线网络每步更新,目标网络每隔 N 步同步一次。回放缓冲区存 (s, a, r, s', done),采样时随机抽 batch。这里最容易翻车的是 done 的处理:如果 s' 是终止状态,目标值只取 r,不能加 gamma * max Q(s'),否则会把终止后的“虚无”当成零奖励继续传播。
import torch import torch.nn as nn import random from collections import deque class QNet(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, act_dim) ) def forward(self, x): return self.net(x) class ReplayBuffer: def __init__(self, capacity=100000): self.buf = deque(maxlen=capacity) def push(self, s, a, r, s_next, done): self.buf.append((s, a, r, s_next, done)) def sample(self, batch_size): batch = random.sample(self.buf, batch_size) s, a, r, s_next, done = zip(*batch) return (torch.tensor(s, dtype=torch.float32), torch.tensor(a, dtype=torch.long), torch.tensor(r, dtype=torch.float32), torch.tensor(s_next, dtype=torch.float32), torch.tensor(done, dtype=torch.float32)) def dqn_loss(q_net, target_net, batch, gamma=0.99): s, a, r, s_next, done = batch q_values = q_net(s).gather(1, a.unsqueeze(1)).squeeze(1) with torch.no_grad(): next_q = target_net(s_next).max(1)[0] # done=1 时只保留即时奖励,避免自举到终止后的状态 target = r + gamma * next_q * (1 - done) return nn.MSELoss()(q_values, target)上面这段代码里,capacity决定回放池大小,太小会导致样本重复、过拟合近期经验;gamma是折扣因子,0.99 适合回合较长的任务,0.9 适合短回合。target_net的同步频率一般取 100 到 1000 步,太频繁等于没有目标网络,太慢会导致目标滞后。注意done必须是 float,否则乘法会广播出错。
2.2 策略梯度方法:REINFORCE 到 PPO 的演进逻辑
策略梯度直接参数化策略 π(a|s),用回报的梯度更新。REINFORCE 是最朴素的版本,用整条轨迹的回报乘以 log 概率,但方差极大。后面加基线、加优势函数、加重要性采样,才有了 A2C、TRPO、PPO。PPO 之所以成为默认选择,是因为它用裁剪把策略更新限制在一个信任域内,既不像 TRPO 那样要算二阶,又比 A2C 稳。
PPO 的关键参数是 clip 系数,通常 0.1 到 0.3。太小更新慢,太大等于没裁剪。另一个关键是 GAE 的 lambda,控制偏差和方差的权衡,0.95 是常见起点。很多人 PPO 跑不起来,不是算法错,而是优势归一化和学习率没调好。优势归一化能显著稳住训练,学习率 3e-4 是安全起点,超过 1e-3 很容易崩。
def ppo_loss(logp_new, logp_old, advantages, clip=0.2): ratio = torch.exp(logp_new - logp_old) surr1 = ratio * advantages surr2 = torch.clamp(ratio, 1 - clip, 1 + clip) * advantages return -torch.min(surr1, surr2).mean()这段损失函数里,logp_old是采样时的旧策略对数概率,必须 detach,否则梯度会回传到旧策略。advantages建议做标准化,减均值除标准差。clip控制每次更新的幅度,0.2 是论文默认值,实际任务里 0.1 到 0.3 都有人用。
2.3 混合与离线:为什么 IQL 和基于模型的强化学习开始流行
纯在线方法样本效率低,于是有了离线强化学习和基于模型的强化学习。离线强化学习(比如 IQL)从固定数据集里学策略,不与环境交互,适合机器人、医疗这类采样昂贵的场景。基于模型的强化学习先学一个环境动力学模型,再在模型里规划或训练策略,样本效率高但模型误差会累积。这两类方法在 Deep-Reinforcement-Learning-Hands-On 这类项目里通常作为进阶章节出现,入门阶段先把在线方法跑通再碰。
选型上有个简单判断:环境便宜、能无限采样,用 PPO 或 DQN;环境贵、只能离线数据,看 IQL 或 CQL;环境可微分、状态连续,可以试基于模型的路径。不要一上来就追最新算法,先把一个基线跑稳,再谈改进。
3. 把算法落到代码:环境封装、训练循环与超参设置
3.1 环境封装:Gym 接口的五个必须实现点
无论用 Gym 还是自定义环境,接口必须一致:reset 返回初始观测,step 返回 (obs, reward, done, info),动作空间和观测空间要有明确的 shape 和 dtype。很多人自己写环境时忘了设 seed,导致结果不可复现;或者 done 和 truncated 不分,导致自举目标算错。Gym 新版本把 terminated 和 truncated 分开,terminated 表示真正终止,truncated 表示时间上限截断,后者仍然要自举。
import gym env = gym.make("CartPole-v1") obs, info = env.reset(seed=42) done = False while not done: action = env.action_space.sample() obs, reward, terminated, truncated, info = env.step(action) done = terminated or truncated # terminated 才需要屏蔽自举,truncated 仍可自举这段代码里,seed=42保证可复现,terminated和truncated分开处理是新手最容易忽略的点。如果你的算法在时间上限附近表现异常,先检查这里。
3.2 训练循环:采样、更新、日志三件事的节奏
一个稳定的训练循环应该把采样和更新分开,采样多少步更新多少次要有比例。比如 PPO 常见做法是采 2048 步,然后做 10 个 epoch 的小批量更新。DQN 则是每步都更新,但用回放池打散。日志至少记录回合回报、回合长度、损失值、熵。熵是策略探索程度的指标,熵骤降往往意味着策略过早收敛到次优。
for episode in range(num_episodes): obs, _ = env.reset() episode_reward = 0 while True: action = policy.act(obs) next_obs, reward, terminated, truncated, _ = env.step(action) buffer.push(obs, action, reward, next_obs, terminated) obs = next_obs episode_reward += reward if len(buffer) >= batch_size: batch = buffer.sample(batch_size) loss = dqn_loss(q_net, target_net, batch) optimizer.zero_grad() loss.backward() optimizer.step() if terminated or truncated: break # 每回合结束记录回报,观察趋势而不是单点 writer.add_scalar("reward", episode_reward, episode)这里batch_size一般 32 到 256,太小梯度噪声大,太大更新慢。target_net的同步可以放在 episode 循环里,也可以按步数。日志用 TensorBoard 或 wandb 都行,关键是看趋势,不要因为某一回合高就以为调对了。
3.3 超参设置:学习率、折扣因子、熵系数的取值边界
学习率是强化学习里最敏感的参数。DQN 常用 1e-4 到 1e-3,PPO 常用 3e-4,A2C 可以到 7e-4。折扣因子 gamma 在连续控制里常取 0.99,在回合很短的任务里可以取 0.9。熵系数在 PPO 里常取 0.01,用来维持探索,太大策略会一直随机,太小会过早收敛。
| 参数 | 常见范围 | 作用 | 调错的表现 |
|---|---|---|---|
| 学习率 | 1e-4 ~ 1e-3 | 控制更新步长 | 太大震荡,太小不收敛 |
| gamma | 0.9 ~ 0.99 | 折扣未来奖励 | 太小短视,太大方差高 |
| 熵系数 | 0.001 ~ 0.05 | 维持探索 | 太大不收敛,太小早熟 |
| clip | 0.1 ~ 0.3 | PPO 更新幅度 | 太大等于没裁,太小更新慢 |
| batch size | 32 ~ 256 | 梯度估计质量 | 太小噪声大,太大更新慢 |
这张表不是让你照抄,而是给你一个排查方向。训练不收敛时,先固定其他参数,只动学习率,通常能解决一半问题。
4. 避坑与排查:训练不收敛时先看这五件事
4.1 奖励曲线震荡不上升
现象:回报在某个值附近来回跳,没有上升趋势。原因通常是学习率太大,或者回放池太小导致样本相关性高。解决:把学习率降一个数量级,回放池至少存 10 万条,目标网络同步频率调低。如果还不行,检查奖励是否做了归一化,奖励尺度差异大会让梯度不稳定。
4.2 策略过早收敛到次优
现象:熵快速下降,回报停在较低水平。原因通常是熵系数太小,或者探索噪声衰减太快。解决:把熵系数调大,DQN 里用 epsilon-greedy 时让 epsilon 衰减更慢,PPO 里可以加动作噪声。注意不要为了探索把熵系数调到 0.1 以上,那样策略会一直随机。
4.3 损失变成 NaN
现象:训练几步后损失变成 NaN。原因通常是梯度爆炸,或者奖励里有 inf。解决:加梯度裁剪,torch.nn.utils.clip_grad_norm_(params, 0.5),检查环境奖励是否做了 clip。另外,如果观测没归一化,输入值过大会让网络输出爆炸,建议对观测做 running mean/std 归一化。
4.4 评估和训练表现差距大
现象:训练回报很高,评估回报很低。原因通常是训练时用了探索噪声,评估时没关;或者环境有随机性,评估次数太少。解决:评估时把策略设为确定性模式,多跑几个回合取平均。如果环境本身随机,评估回合数至少 10 次以上。
4.5 换环境后完全跑不动
现象:在 CartPole 上好好的,换到 LunarLander 或自定义环境就崩。原因通常是观测维度、动作空间、奖励尺度变了,但超参没变。解决:先跑随机策略看环境本身是否正常,再检查观测是否归一化,奖励是否缩放。动作空间从离散变连续时,DQN 不能直接用,要换 DDPG、TD3 或 PPO。
5. 进阶技巧:用向量化环境和课程学习把训练速度提上来
当你把单环境跑通后,下一步瓶颈通常是训练太慢。向量化环境是最直接的加速手段,用gym.vector.make或SubprocVecEnv同时跑多个环境实例,采样效率能提升几倍。注意向量化环境返回的观测是 batch 维度,网络输入要相应调整,done 的处理也要按子环境分别屏蔽自举。
from stable_baselines3.common.vec_env import SubprocVecEnv def make_env(rank): def _init(): env = gym.make("CartPole-v1") env.reset(seed=42 + rank) return env return _init vec_env = SubprocVecEnv([make_env(i) for i in range(8)]) obs = vec_env.reset() # shape: (8, obs_dim)这段代码里,SubprocVecEnv用子进程并行,适合 CPU 密集型环境;如果环境本身很轻,用DummyVecEnv反而更快。seed要按 rank 错开,否则多个环境会同步随机。观测的 batch 维度意味着你的策略网络要能处理批量输入,更新时也要按 batch 算优势。
另一个技巧是课程学习:先让环境简单,再逐步增加难度。比如机械臂抓取,先固定物体位置,再随机化;导航任务,先短距离,再长距离。课程学习能显著提升稀疏奖励任务的成功率,但课程推进太快会让策略遗忘,建议用成功率作为推进条件,而不是固定步数。
最后说一个我自己的习惯:每次改完超参,先跑三个 seed,看均值和方差,不要只看一条曲线。强化学习的随机性很大,单次结果说明不了问题。希望帮到你。
本文还有配套的精品资源,点击获取