从策略梯度到DQN、PPO:深度强化学习入门与实战避坑指南
简介一套面向强化学习从基础到进阶学习者的完整代码与案例资源覆盖MDP、表格型方法、策略梯度、PPO、DQN、DDPG、SAC、TD3等主流算法配有悬崖寻路、CartPole、Pendulum等实战项目适合想通过动手实践掌握RL核心原理的学生、研究者或工程师。压缩包共223个文件大小173.43MB包含Python源码56个py、说明文档14个md、Jupyter notebook6个ipynb、训练结果图表57个png以及模型权重文件pth/pt/td3/sac系列可对照代码理解每个算法的训练细节和数据曲线。已有678人学习下载资源目录按章节和项目划分清晰既提供从零实现的基础代码也包含Q-learning、DQN、PPO、DDPG等算法的完整工程化示例便于逐章复现和迁移到自己的任务中。适合作为系统学习强化学习的配套码源帮助读者快速跨越理论与实践的鸿沟。1. 先别急着上深度强化学习用一段几十行的策略梯度把核心概念拆开讲很多人接触强化学习第一件事就是点开 DQN 的源码想从深度神经网络开始啃结果训练好几个小时loss 曲线在 0.1 上下抖动智能体在 CartPole 上撑不过 20 步。这不是代码写错了而是基础概念没落到位环境、回报、策略这三件事没在手里跑过直接上深度强化学习算法出问题都不知道往哪个方向查。这篇笔记按“从基础到进阶”的顺序把强化学习的核心概念、最小可复现代码、必调参数和血泪踩坑串成一条路径先用一段几十行的策略梯度把 CartPole 跑通再理解状态价值函数的作用最后落到 DQN、PPO、SAC 的选型和评估上。适合正在找案例和实践参考、手里有强化学习码源但不知道从哪开始的人。2. 先跑通最小闭环gymnasium CartPole 的策略梯度实现与参数说明2.1 环境为什么选 CartPole状态空间、动作空间与 gymnasium 接口强化学习入门环境里CartPole 是反馈最快的一个状态只有 4 维小车位置、速度、杆角度、角速度动作只有 2 个向左推、向右推每走一步奖励 1一个 episode 最长 500 步。你甚至不需要 GPU纯 CPU 跑一轮实验也就几分钟。相比之下Atari 环境是 210x160x3 的图像输入得上 CNN机械臂仿真要装 MuJoCo训练一轮要按小时算。新手在 CartPole 上跑通比在复杂环境里折腾半天有意义得多。环境库我推荐直接用 gymnasium而不是老的 gym。gymnasium 是社区维护的 forkreset()返回(obs, info)两个值step()返回(obs, reward, terminated, truncated, info)五个值。这个接口差异是很多老代码跑不起来的第一原因后面避坑章节会专门展开。pip install gymnasium numpy torch安装这三样就够了。装完先别急着写训练代码用一段脚本确认环境能起来import gymnasium as gym env gym.make(CartPole-v1) obs, info env.reset() # 新版接口reset 返回 obs 和 info print(obs:, obs) # 4 维连续值范围各不相同 print(action_space:, env.action_space) # Discrete(2) print(observation_space:, env.observation_space) # Box(4,) env.close()这段代码的要点是确认接口版本。如果你env.reset()报错“too many values to unpack”说明你装的是老版本 gym 或者代码是按老接口写的直接把赋值改成obs, _ env.reset()即可。obs 的 4 个维度分别对应小车位置、速度、杆角度和角速度取值范围差异很大这会影响后面要不要做输入归一化。2.2 策略梯度的最小可用实现与 reward 归一化策略梯度Policy GradientPG是理解强化学习最好的起点。核心思想很直白神经网络输出每个动作的概率采样执行动作一个 episode 结束后把每一步的动作概率乘上对应的累计回报来做梯度更新——回报高的动作概率就增大回报低的动作概率就减小。import gymnasium as gym import torch import torch.nn as nn import torch.optim as optim class PolicyNet(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, 64), nn.ReLU(), nn.Linear(64, act_dim), ) def forward(self, obs): logits self.net(obs) return torch.softmax(logits, dim-1) def train(): env gym.make(CartPole-v1) net PolicyNet(4, 2) optimizer optim.Adam(net.parameters(), lr3e-3) gamma 0.99 for episode in range(300): obs, _ env.reset() log_probs, rewards [], [] while True: obs_t torch.FloatTensor(obs) probs net(obs_t) dist torch.distributions.Categorical(probs) action dist.sample() log_probs.append(dist.log_prob(action)) obs, reward, terminated, truncated, _ env.step(action.item()) rewards.append(reward) if terminated or truncated: break # 计算每个时间步的累计折扣回报 returns [] G 0.0 for r in reversed(rewards): G r gamma * G returns.insert(0, G) returns torch.FloatTensor(returns) # 归一化回报降低方差 returns (returns - returns.mean()) / (returns.std() 1e-8) # 损失让高回报动作的概率变大 loss -torch.stack(log_probs) * returns optimizer.zero_grad() loss.mean().backward() optimizer.step() if (episode 1) % 50 0: print(fepisode {episode 1}, sum reward {sum(rewards)}) if __name__ __main__: train()这段代码的逻辑分四步。第一步网络输出动作概率分布Categorical从分布里采样一个动作第二步把整个 episode 的log_probs和rewards记下来第三步从后往前推算每个时间步的累计折扣回报 Ggamma是折扣因子让越靠后的奖励对当前决策影响越小第四步用“负对数概率乘以回报”作为损失回报高的动作概率会上升这就是 REINFORCE 算法的全部内容。两个参数需要按经验设。lr3e-3对 Adam 来说够用调大容易震荡调小收敛会明显变慢gamma0.99是大部分任务的标准值意思是未来 100 步之后的奖励几乎不影响当前决策。回报归一化这行很多人会省略但实际训练里它非常关键不归一化的话CartPole 前几十个 episode 的回报可能在 20 到 500 之间剧烈波动梯度更新会被极端样本主导收敛反而更慢。2.3 训练结果怎么看移动平均比单次 episode 更能说明问题跑完 300 个 episode你会发现单次得分忽高忽低有时 300 多分下一个 episode 只有 60 分。这是强化学习的常态策略在探索阶段有随机性单次结果根本不能代表真实水平。看进度时不要盯着某一个 episode要看最近几十步的移动平均。一种简单的做法是在训练循环里维护一个定长队列from collections import deque recent_scores deque(maxlen50) # 在每轮结束后记录 recent_scores.append(sum(rewards)) if len(recent_scores) recent_scores.maxlen: avg sum(recent_scores) / len(recent_scores) print(fepisode {episode}, recent 50 avg: {avg:.1f})deque(maxlen50)会自动踢掉最老的数据省去自己管理数组的麻烦。看到最近 50 轮的平均分稳定在 450 以上说明策略基本学到位了。想留下来做对比的话把episode和sum(rewards)追加到 CSV 文件里后面画收敛曲线用得上。做算法对比实验时有个常见的坑只看最终分数不看整条收敛曲线。两个算法可能一个 300 轮才到 450 分另一个 80 轮就到了前者最终成绩也不差但效率差了一倍。记录移动平均、横轴用 episode 数对齐才能看出真正的差别。3. 从价值函数到深度强化学习算法DQN 的原理、选型表与三个必调参数3.1 状态价值函数的作用从表格型 Q-learning 到 DQN 的两步跨越策略梯度直接从策略入手价值类算法则换了一条路先估计“当前状态下哪个动作值得做”再根据估值选动作。这里第一个要搞明白的概念就是状态价值函数 V(s)它表示“从状态 s 开始按当前策略走下去期望能拿到多少累计折扣回报”写成公式就是 V(s) E[G_t | S_t s]其中 G_t 是随后所有奖励的折扣和。那 Q(s, a) 和 V(s) 是什么关系Q 多了一个动作维度表示“在状态 s 先执行动作 a之后按策略继续走”的期望回报。两者差一点就是优势函数 A(s, a) Q(s, a) - V(s)表示“这个动作比平均水平好多少”。策略梯度类算法里经常出现优势函数它帮我们判断某个动作到底值不值得增加概率。早期 Q-learning 用一张表格存每个 (s, a) 的 Q 值更新公式是Q(s, a) - Q(s, a) alpha * (r gamma * max(Q(s, a)) - Q(s, a))这个公式在离散、有限状态的任务里没问题但 CartPole 的状态是连续浮点数表格根本存不完。于是有了 DQN用神经网络 Q(s, a) 代替表格。这一步替换带来了两个新问题DQN 的两个关键设计就是为它们准备的。一个是经验回放把transition存进 buffer训练时随机采样一小批打破相邻样本的强相关性另一个是目标网络每隔固定步数才同步一次参数避免训练目标跟着网络一起变、来回震荡。# DQN 单步更新核心逻辑 import random import torch.nn.functional as F batch random.sample(buffer, batch_size) # buffer 里存 (s, a, r, s, done) obs, act, reward, next_obs, done zip(*batch) obs torch.FloatTensor(obs) act torch.LongTensor(act).unsqueeze(1) reward torch.FloatTensor(reward).unsqueeze(1) done torch.FloatTensor(done).unsqueeze(1) with torch.no_grad(): max_next target_net(torch.FloatTensor(next_obs)).max(1, keepdimTrue)[0] td_target reward gamma * max_next * (1 - done) q_now q_net(obs).gather(1, act) loss F.mse_loss(q_now, td_target) optimizer.zero_grad() loss.backward() optimizer.step()这段代码是 DQN 训练循环最关键的部分。td_target是监督信号当前奖励加上下一个状态的最大 Q 值乘以折扣因子。(1 - done)这一项容易漏——如果下一状态是终止状态就不要加后续收益否则价值估计会偏高。gather(1, act)是按动作索引取出当前 Q 值。超参数方面batch_size用 64buffer 容量 10000目标网络每 200 步同步一次epsilon 从 1.0 退火到 0.01这些是常见的默认件按任务难度可以微调。3.2 深度强化学习算法横向对比DQN、PPO、SAC 各自的适用边界算法选型是工程里最常被问的问题。没有哪个算法通吃所有场景选错算法的代价比调错参数更大。我按实际工程经验整理了一张对照表算法学习方式样本效率稳定性适合场景DQN价值低中等离散动作、状态可观测PG策略最低低入门学习、理解核心机制A2C策略价值低中等小规模并行训练PPO策略价值中高大部分实际项目首选TD3深度确定性策略中高中等连续动作、有确定性策略需求SAC最大熵高高连续控制、机械臂仿真常用只看单表容易让人纠结我给出一个通用的选择路径。如果你做的是离散动作任务优先考虑 DQN 或 PPO如果是连续动作控制比如机械臂强化学习实战SAC 和 TD3 是更稳的起点。PPO 在工程里的地位比较特殊它对超参数不那么敏感收敛稳定许多大语言模型强化学习后训练方案也把 PPO 变体作为底座。你想跑通一个方向做验证的话PPO 通常是最不折腾的选项。理论方向进阶的话强化学习算法分类里还有一条线值得关注离线强化学习。典型代表是 IQLImplicit Q-Learning它假设手里只有一批固定数据不再和环境交互适合数据收集成本高的真实场景。这类算法和在线学习在实现上的差别主要在td_target的计算方式上理解 DQN 的更新后再去看 IQL 会有个清晰的参照物。3.3 三个必调参数学习率、折扣因子、reward scale真让我给一套能从入门用到进阶的调参优先级我会推荐这四个学习率、折扣因子、reward scale、探索率。新手拿到码源第一件事不要改网络结构先把这几个参数过一遍。参数合理范围调大后的影响调小后的影响学习率 lr3e-4 ~ 3e-3收敛快容易震荡稳定但训练变慢折扣因子 gamma0.9 ~ 0.99只看近期收益更长远但方差变大reward scale / reward 常数保持奖励量级在 0.1 ~ 10梯度爆炸不收敛梯度太平学不动epsilonDQN1.0 退火到 0.01探索多样本利用率低利用多容易原地打转reward scale 是最容易被忽略的一个。CartPole 每步 1最大 500 分这个量级对神经网络很友好但如果你自己写环境时给每步奖励设成 100PG 的损失一下就爆了所有动作的概率都会被推向极端。有人一遇到不收敛就怀疑网络结构其实先把奖励值缩到和 CartPole 差不多的量级问题往往就没了。gamma 的选择要看任务到底在优化什么。CartPole 用 0.99 没问题因为要保持杆子长时间不倒但如果是让智能体在迷宫里尽快找到出口0.95 左右的短视策略反而收敛更快。学习率方面Adam 优化器下 3e-3 是激进值3e-4 是保守值我从 3e-3 起步不收敛就降一档比一上来就用极小学习率省时间。4. 强化学习训练避坑排查不收敛、结果玄学、API 报错的三类高频现场4.1 现象一训练十几个 episode分数卡在个位数不动现象策略梯度跑了几十轮累计回报一直在 10 到 30 之间波动CartPole 的杆子每次都很快倒下网络结构、动作映射都没发现明显错误。原因最常见的有两个。一是输入观测没有归一化。CartPole 四个维度的取值范围差异很大位置可以到 ±4.8角度只有 ±0.42神经网络对这类尺度敏感深层激活容易饱和。二是 reward 量级不对环境里每步奖励如果是 50 甚至 100梯度会剧烈震荡策略完全学不出来。解决先打印每个 obs 维度的 min、max 和 std确认量级然后用一个简单的归一化层或直接除以各维度的区间宽度让输入落在 [-1, 1] 附近。reward 方面检查环境每一步返回的值把它压到 10 以内再训练。这一步做完大多数“完全学不动”的问题都能解决。4.2 现象二同一份代码换台机器结果完全相反现象本地训练正常收敛到 450 分以上把代码同步到另一台机器或换了操作系统同样的超参数跑了 400 轮只有 200 分换回来又能收敛整个过程像玄学。原因随机数种子没有统一。PyTorch、NumPy 和环境各自维护一套随机状态不固定 seed 的话不同机器、不同线程调度下生成的初始权重和探索路径都不一样。强化学习对初始状态和探索轨迹非常敏感一个小差异可能让结果完全不同。解决把种子设置放在训练脚本最前面统一管起来。import numpy as np import torch def set_seed(seed0): np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) env gym.make(CartPole-v1) env.reset(seedseed) # 环境也要固定种子固定种子之后还要记住一点固定随机种子的单次实验仍然不能代表算法水平因为不同的 seed 会带来不同的策略轨迹。做结论对比时至少跑 3 到 5 个种子取平均值才算一个可信的基线。4.3 现象三gym 版本升级后 reset 和 step 双双报错现象跑网上下载的老码源报错too many values to unpack或者step()返回的变量个数对不上。原因旧版 gym 的reset()只返回 obsstep()返回 4 个值新版 gymnasium 里reset()返回(obs, info)step()返回 5 个值。很多码源是照着老接口写的换环境后直接当场报废。解决统一按新版接口改。obs, _ env.reset()obs, reward, terminated, truncated, _ env.step(action)。其中terminated和truncated是两个不同的概念terminated表示环境真实结束比如杆子倒了truncated表示达到步数上限被截断。计算回报和价值函数时前者应该让后续价值归零后者不应该否则训练信号会出错。这是很多老代码在回报计算上翻车的根源。4.4 现象四训练曲线很漂亮用同策略评估却稀烂现象训练时打印的每轮得分在稳步上升最后也能到 450 分上下但拿训练好的模型出来做评估跑 50 轮平均只有 200 分差距大得离谱。原因训练时打印的是带随机采样的轨迹评估时如果还在用Categorical.sample()采样探索噪声会拖累表现另一个常见原因是评估期间环境没固定种子起点和物理参数每次不同单次分数方差很大。解决训练和评估必须分离。训练阶段用采样得到动作来保证探索评估阶段用argmax拿概率最大的动作采用确定性策略。评估时固定一组种子、连续跑 20 轮以上取平均回报和标准差作为对比指标。这个结论也适用于做算法对比实验两边都按同一套确定性评估逻辑跑结果才有可比性。5. 用评估协议把效果“钉死”从 CartPole 走向连续控制、机械臂与多智能体5.1 先定基线确定性策略评估与多种子统计走到这一步你已经能把 CartPole 跑通了。下一步不是急着堆新算法而是给项目定一套评估协议不然后面换算法、调参数都分不清是变好还是变坏。我一般用的评估函数长这样def evaluate(env_name, policy, n_episodes20, seed0): eval_env gym.make(env_name) returns [] for i in range(n_episodes): obs, _ eval_env.reset(seedseed i) # 每次不同的起点 episode_return 0.0 while True: obs_t torch.FloatTensor(obs) with torch.no_grad(): probs policy(obs_t) action probs.argmax().item() # 评估用确定性动作 obs, reward, terminated, truncated, _ eval_env.step(action) episode_return reward if terminated or truncated: break returns.append(episode_return) return float(np.mean(returns)), float(np.std(returns))评估函数返回均值加标准差是我验证一个策略最快的方式。均值低于训练分数很正常因为训练时探出的高分数是一条随机轨迹评估时去掉随机性后的均值才是真实水平。标准差反映策略稳定性标准差在 30 以内说明策略状态稳定超过 100 就要回头检查训练过程有没有异常。5.2 再谈迁移三处改动让 CartPole 码源走向机械臂与多智能体CartPole 只是入口真正把强化学习用起来一般要往三个方向走连续控制、多智能体、离线学习。从 CartPole 迁移到连续控制任务比如 Pendulum 或 MuJoCo 机械臂仿真需要改三处环境接口照新旧 API 对齐网络输出层从“动作概率分布”改成“均值 方差”或确定性动作reward 函数要重新设计单调递增的奖励很容易让 PPO 停在半路。机械臂强化学习实战项目很大一部分时间花在奖励塑形上不是花在算法上。多智能体强化学习是另一个热点训练时每个智能体各自看局部观测还要处理智能体之间的交互主流方案有 MAPPO、QMIX 这类专门算法。真跑到那一步你会发现单智能体阶段打下的评估习惯仍然适用只是奖励设计从“单目标”变成了“博弈协作”。离线强化学习则是另外一个价值点IQL 这一类算法能利用固定数据集训练适合真实场景中不便反复试错的项目。无论选哪条路先跑通基础版本再按评估协议对比改进都比我直接调参更稳。我现在的习惯是任何一份新环境或新码源到手先花十分钟把评估跑起来再做任何算法结构改动。强化学习项目卡住时大概率不是新算法不够好而是基线没定住、改动没法比较。希望帮到你。本文还有配套的精品资源点击获取