尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PPO强化学习算法深度解析:从策略梯度推导到工程实现

PPO强化学习算法深度解析:从策略梯度推导到工程实现 最近后台收到不少私信都是问我强化学习入门到底该先看哪个算法。我的答案从来都是同一句话把 PPO 吃透。原因很简单PPOProximal Policy Optimization近端策略优化几乎是目前工业界和学术界落地最广的强化学习算法从游戏对战到机器人控制再到这两年火得不行的大模型 RLHF 对齐训练底层跑的大部分都是 PPO 或其变体。你可以不了解 SAC可以不碰 DDPG但只要你想在真实场景里把强化学习用起来PPO 就是一个绕不过去的坎。但我也知道 PPO 对新手并不友好。网上讲 PPO 的资料虽然多但要么是几行公式草草带过要么直接甩出一大段伪代码让你自己看很多人学完之后只记住了有个 clip 操作至于为什么要 clip、目标函数是怎么一步步推出来的、代码里的 GAE 到底在算什么脑子里其实是一团浆糊。这篇文章我就用最直白的方式把 PPO 从背景动机到数学推导再到工程实现完整过一遍全程带推导过程不跳步。不管你是刚接触强化学习的学生还是在项目中被迫接盘 RL 模块的工程师照着这篇文章捋一遍应该能帮你把这块硬骨头啃下来。1. PPO 解决的核心问题策略梯度为什么不稳1.1 先回顾一下策略梯度的基本逻辑要理解 PPO 的价值得先回到它的前身——策略梯度Policy GradientPG方法。强化学习的核心目标是找一个策略 π让智能体在和环境交互的过程中累计回报最大。策略梯度方法的核心思路很直接把策略参数化然后沿着能让期望回报增大的方向更新参数。用公式表示策略梯度的核心估计是∇J(θ) E[ ∇log π_θ(a|s) · A(s, a) ]这里的 A(s, a) 是优势函数Advantage Function表示在状态 s 下采取动作 a 比平均水平好多少。直觉上理解这个公式如果一个动作比平均表现好A 0就增大这个动作的概率如果比平均差A 0就减小概率。就这么简单。但这个公式背后有一个致命的问题采样效率低且方差大。每次更新完参数策略就变了之前采的那批样本严格来说就过期了。你用旧策略采的数据去估计新策略的梯度这本身就有偏差。所以传统 PG 算法只能采集一小批 → 更新一步 → 再采集一小批这样慢慢来每一步更新幅度还不敢太大因为一旦步子迈大了策略突变采集的数据分布和当前策略完全不匹配训练直接崩掉。1.2 TRPO用约束换稳定为了控制更新幅度2015 年 Schulman 等人提出了 TRPOTrust Region Policy Optimization信任区域策略优化。TRPO 的想法很优雅我不直接限制学习率而是给参数更新加一个信任区域的约束——新旧策略的 KL 散度不能超过某个阈值 δ。TRPO 的目标函数是max L(θ) E[ (π_θ(a|s) / π_θ_old(a|s)) · A(s, a) ]约束条件E[ KL[π_θ_old(·|s) || π_θ(·|s)] ] ≤ δ这里 π_θ / π_θ_old 就是重要性采样比率记为 r(θ)。通过这个比率我们可以用旧策略采的数据来评估新策略的表现从而实现多轮更新同一批样本。TRPO 理论上很漂亮但工程上非常难搞。它需要计算 KL 散度的二阶近似Fisher 信息矩阵然后用共轭梯度法去解这个带约束的优化问题。这一套流程实现复杂、计算量大而且有很多数值上的细节容易出 bug。我在项目里试过自己实现 TRPO光是那个 Fisher 向量积就够折腾一阵子而且调参非常敏感。1.3 PPO 的诞生用一阶优化近似二阶效果PPO 是 2017 年 OpenAI 提出的它的动机非常务实能不能用一种简单的方法做到 TRPO 的稳定性和置信域约束但只用一阶梯度也就是普通的反向传播就能实现答案是能而且 PPO 一共给了两种实现方式PPO-Penalty自适应 KL 惩罚把 KL 散度约束直接加到目标函数里作为惩罚项并根据 KL 散度的实际值自适应调整惩罚系数。PPO-Clip裁剪目标函数不对策略做显式约束而是通过裁剪重要性采样比率隐式地限制更新幅度。其中 PPO-Clip 因为实现简单、效果稳定成为绝对的主流。后面我讲到的 PPO 默认都是指 PPO-Clip。2. 从零推导 PPO目标函数是怎么一步步长成这样的2.1 从策略梯度到重要性采样前面提到传统 PG 方法的问题在于样本只能用一次。PPO 想要复用旧样本就需要引入重要性采样。假设我们用旧策略 π_θ_old 采集了一批轨迹想用这批数据来更新新策略 π_θ。期望回报的梯度可以改写为∇J(θ) E[ (π_θ(a|s) / π_θ_old(a|s)) · ∇log π_θ(a|s) · A(s, a) ]这里核心的差异就在于乘了一个比率r(θ) π_θ(a|s) / π_θ_old(a|s)当 θ 和 θ_old 差别不大时这个比率接近 1重要性采样是有效的。但如果差别太大这个比率会剧烈变化导致估计方差爆炸——这就是为什么我们必须限制更新步幅的根本原因。2.2 为什么不直接最大化带比率的目标有人可能会想那我不就是最大化 L(θ) E[ r(θ) · A(s, a) ] 就行了吗如果这么干问题来了。当某个 (s, a) 的优势 A 为正且很大时算法会疯狂增大 π_θ(a|s) 的概率导致 r(θ) 变得极大。反过来如果 A 为负算法会极力把概率压到接近 0。这两种情况都会让新旧策略的分布严重偏离最终训练崩溃。换句话说L(θ) 是对新策略性能的乐观估计它在新旧策略接近时是准确的但离远了就失真了。TRPO 通过硬约束避免这种情况PPO-Clip 则用了一种更取巧的方式。2.3 PPO-Clip 的核心裁剪的替代目标PPO-Clip 的替代目标函数长这样L^CLIP(θ) E[ min( r(θ) · A, clip(r(θ), 1-ε, 1ε) · A ) ]这里 ε 是一个超参数通常取 0.2。clip 函数把 r(θ) 限制在 [1-ε, 1ε] 区间内。这个 min 操作的精妙之处在于它在鼓励增大正优势动作概率和防止更新过猛之间取了一个保守的下界。我们来分情况拆解情况一A 0好动作此时 min(r·A, clip(r)·A) min(r, clip(r)) · A。当 r 在 [1-ε, 1ε] 内时取的就是 r·A正常鼓励当 r 超过 1ε 时clip 后的值被限制在 1εmin 会选择较小的那个也就是 1ε从而截断了过大的正梯度。情况二A 0差动作此时由于 A 是负数让 r 变小即降低坏动作概率是目标。但当 r 小到低于 1-ε 时clip 的下限起作用min(r·A, clip(r)·A) 中 clip 的部分是 (1-ε)·A这是一个绝对值更小的负数而 r·A 是绝对值更大的负数min 会选择更小的那个也就是 r·A。这里好像没有限制别急再仔细看当 r 变得很小比如 0.5r·A 是非常负的这确实会继续压低概率。但关键是一旦 r 跌出 [1-ε, 1ε] 区间对应的梯度只对旧概率附近有定义实际梯度会被逐步削弱。更重要的是当 r 再次进入区间时限制才重新生效。呃这里我上一段其实讲得有点绕我们换个更直观的方式来理解PPO 不是不允许你把坏动作概率降下来而是不愿意让一次更新就改变太多。clip 两侧的限制相当于给更新划了一条单次最大步长的红线。2.4 为什么 min 和 clip 要搭配使用有个很常见的疑问是直接用 clip 后的项不就行了为什么外面还要套一个 min关键在于防止一种 trick 行为假如某个动作的 A 是正的算法理论上可以通过让 r 超过 1ε 来获得比 clip(r) 更大的目标值但 min 强制选择了较小的那个。反过来如果 A 是负的算法可以通过让 r 低于 1-ε 来获得比 clip(r) 更小的负得更多的目标值此时 min 照样会选择 r·A 那一项。这样看起来好像 min 在正负两种情况下都不是完全限制但在实际计算梯度时当 r 超出区间min 选择的那一项的梯度为 0因为是常数项于是参数就自动停止在边界附近。这其实构建了一个非常优雅的机制只要新旧策略偏离得不太远PPO 就正常优化一旦偏离超出区间梯度会自动归零让优化器把注意力收回到信任区域内。这也是为什么 PPO 能只用一阶优化就近似出 TRPO 的效果。2.5 PPO-Penalty另一种实现思路再补充一下 PPO 的另一面带自适应 KL 惩罚的版本。它的目标函数是L^KL(θ) E[ r(θ) · A ] - β · KL[π_θ_old || π_θ]更新完参数后计算实际 KL 散度如果 KL 超过目标值的高阈值比如 1.5 × target_kl就把 β 翻倍加强惩罚如果 KL 低于低阈值比如 0.5 × target_kl就把 β 减半放松限制。这种自适应机制也能达到类似效果但在实践中调参相对繁琐需要额外设置 KL 目标值和高低阈值。相比之下 PPO-Clip 只有一个 ε 需要设所以工程上大家都默认用它。3. 完整推导补充GAE 与优势估计在 PPO 中的角色3.1 为什么要专门做优势估计PPO 目标函数里那个 A(s, a)在实际计算时必须被估计出来。最简单的办法是用蒙特卡洛回报 G_t Σγ^k r_{tk} 减去基线比如状态价值 V(s)即 A_t G_t - V(s_t)。但这样估计的方差很大因为每一步的回报都包含了大量随机性。另一种极端是用 TD 误差δ_t r_t γ·V(s_{t1}) - V(s_t)。这样方差低但偏差大尤其在价值函数估计不准确的时候。有没有办法在偏差和方差之间做权衡有就是 GAEGeneralized Advantage Estimation广义优势估计也是 PPO 标配的利器。3.2 GAE 的推导思路GAE 把多步 TD 误差加权求和A_t^GAE(γ, λ) Σ_{l0}^{∞} (γλ)^l · δ_{tl}其中 δ_t r_t γ·V(s_{t1}) - V(s_t)。当 λ 0 时GAE 退化成单步 TD 误差方差小但偏差大当 λ 1 时GAE 近似蒙特卡洛回报偏差小但方差大。通过调整 λ通常在 0.9 到 0.99 之间我们可以找到一个适合具体任务的平衡点。为什么 GAE 在 PPO 里特别重要因为 PPO 使用重要性采样来复用旧数据而重要性采样的方差本身就比普通策略梯度大如果优势估计再带很大的方差训练会非常不稳定。GAE 的平滑加权可以有效缓解这个问题让我在实际项目里几乎找不到不用 GAE 的 PPO 实现。3.3 PPO 完整目标函数含价值函数与熵奖励在实际代码里PPO 的 loss 通常由三部分组成L(θ) L^CLIP(θ) - c1 · L^VF(θ) c2 · S π_θL^CLIP前面讲的裁剪策略目标L^VF价值函数Critic的损失通常是 MSE即 (V(s) - R_t)^2S策略的熵奖励鼓励探索避免策略过早收敛到确定性行为。三者组合时需要注意价值函数梯度和策略梯度的尺度可能相差很多所以 c1 通常设 0.5c2 根据任务调整常见是 0.01 或 0.001。我在实际调参中经常发现如果熵奖励系数太大策略会一直乱逛不收敛太小则容易陷入局部最优。4. 手把手实现 PPO-Clip可落地的流程与关键代码解析4.1 PPO 的一次完整迭代流程把 PPO 拆成一个可执行的循环大致是这样用当前策略 π_θ_old 和环境交互采集 N 条轨迹或者 N 步样本计算每一步的折扣回报 R_t 和优势估计 A_t用 GAE把数据按 batch 大小切分对每个 batch 计算裁剪的策略损失、价值损失和熵奖励用 Adam 优化器更新参数重复 K 个 epoch通常 3~10更新完所有 epoch 后把 θ_old 更新为 θ重新采样。这里和普通策略梯度最大的区别就是第 3 步和第 4 步同一批数据可以被重复使用 K 次。4.2 网络结构与伪代码PPO 一般用两个网络Actor 输出动作的分布连续动作常见高斯分布离散动作常见 softmaxCritic 输出状态价值 V(s)。两个网络可以共享底层特征提取层也可以完全分开视任务复杂度而定。伪代码如下# 伪代码PPO-Clip 训练循环伪代码风格PyTorch 读者可以轻松对应实现 for iteration in range(max_iterations): # 1. 采集数据 states, actions, old_log_probs, returns, advantages collect_rollouts() # 2. 多轮更新 for epoch in range(ppo_epochs): for batch in make_batches(states, actions, old_log_probs, returns, advantages): # 当前策略的 log 概率和价值 log_probs, entropy actor(batch_states, batch_actions) values critic(batch_states) # 重要性采样比率 ratios torch.exp(log_probs - batch_old_log_probs) # 裁剪的代理目标 surr1 ratios * batch_advantages surr2 torch.clamp(ratios, 1 - clip_eps, 1 clip_eps) * batch_advantages policy_loss -torch.min(surr1, surr2).mean() # 价值损失 value_loss F.mse_loss(values, batch_returns) # 熵奖励 entropy_loss -entropy.mean() loss policy_loss 0.5 * value_loss 0.01 * entropy_loss optimizer.zero_grad() loss.backward() optimizer.step()注意上面代码里 policy_loss 前面的负号因为我们是最大化目标、而优化器是最小化 loss。另外如果 Actor 和 Critic 共享网络层通常建议 Critic 的梯度不要反向传播到共享层太深或者用两层独立的网络更省心。4.3 超参数到底怎么设PPO 最让人头疼的就是超参数多。我把自己常用的一套初始参数整理成表供大家参考超参数推荐值说明clip ε0.2裁剪范围一般 0.1~0.3 之间GAE λ0.95连续控制任务常用 0.95不收敛时可试 0.99折扣因子 γ0.99和任务 horizon 有关长 horizon 任务用 0.999PPO epochs3~10每批数据重复训练次数太大会过拟合batch size64~256每次梯度更新的样本数Actor 学习率3e-4Adam 优化器常用 3e-4可配合线性衰减Critic 学习率3e-4如果共享网络则同一学习率熵系数 c20.0~0.01看任务是否需要鼓励探索价值损失系数 c10.5常用值稳定这里特别说一句很多人上来就怪 PPO 效果差其实八成是超参没配对。我在一个机器人仿真任务里就只是把 GAE 的 λ 从 0.95 调到 0.99训练曲线就从完全不动变成稳定上升。这类经验只能靠多跑实验积累踩坑多了自然有感觉。4.4 奖励归一化与观测归一化还有一个容易被忽略的环节在 PPO 的实际实现中一定要做归一化。观测归一化把输入状态标准化为均值为 0、方差为 1或者至少缩放到一个合理范围。不然某些维度的数值范围差异过大会影响网络训练。奖励归一化有两种常见做法一是对回报 R_t 做标准化二是用 running mean/std 对奖励做缩放。我在项目里通常对优势 A_t 做标准化减均值除标准差这本质上不影响最优策略但能显著缓解训练初期的梯度爆炸问题。注意奖励归一化不能改变任务的本质。如果任务本身奖励稀疏且尺度极小强行归一化可能把信号也抹掉了。需要结合具体场景做判断。5. 实操中的坑PPO 训练失败的排查思路5.1 训练完全不动loss 不降这种问题最常见的元凶是优势估计出了问题。检查顺序是先看 reward 是否有值、尺度是否合理打印 GAE 计算出来的优势 A_t 的均值和标准差如果全是 0 或数值特别小说明价值函数或者奖励计算有问题确认 γ 和 λ 是否配对任务越长 γ 越要接近 1。我遇到过一次很搞笑的场景奖励函数写错了把奖励放在未完成任务上而不是完成任务上训练出来的策略一直原地打转。所以遇到不动先别急着改算法先校验环境本身。5.2 KL 散度爆炸如果新旧策略的 KL 散度在训练中快速增大说明 PPO 的 clip 没有生效或者学习率太大。可以降低 Actor 学习率调小 clip ε在 loss 里显式加一个 KL 惩罚项相当于 PPO-Penalty 和 PPO-Clip 混用比如 loss kl_coef * KL这个 trick 在不少开源项目里都见过。另外如果你的价值函数和策略共享网络价值函数梯度过大也可能带偏策略试着把 Critic loss 的系数调小或者让 Critic 和 Actor 分开优化器、不同学习率。5.3 entropy 掉到 0 导致探索停止在连续控制任务里如果策略快速退化成高斯的方差接近 0熵奖励会掉到很低这时策略基本不探索了训练也随之停滞。解决办法提高熵系数 c2给策略方差设一个下限比如 exp 后的 log_std 不低于 -5检查是不是学习率太大导致策略过早收敛。5.4 训练发散loss 变成 NaNNaN 问题一般是数值不稳定。常见原因包括重要性采样比率 r(θ) 里 exp 计算溢出——给 log_probs 差减掉最大值或用 log-sum-exp 技巧优势值有极端异常值——用 advantage standardization 或 clipping使用半精度训练时精度不够——部分层保持 FP32。5.5 实用调试清单速查表我把上面这些经验汇总成一张速查表现象优先排查项推荐调整方案loss 不降奖励信号 / GAE 计算打印 A_t 分布检查 γ、λKL 快速上升学习率 / clip降低 lr调小 ε加 KL 惩罚entropy 接近 0探索不足提高 c2设 log_std 下限出现 NaN数值溢出稳定 exp裁剪极端 A检查梯度训练后期波动大batch 太小增加样本量降低 PPO epochs6. 我个人的体会什么时候用 PPO什么时候换别的说实话PPO 也不是万能药。它最大的优势是稳定、通用、对超参不算特别敏感相对其他深度 RL 算法而言但在样本效率上并不拔尖。如果你的场景是仿真环境允许大量采样PPO 是省心的首选但如果是在真实机器人上做在线学习样本极其昂贵那 SACSoft Actor-Critic这类 off-policy 算法可能更合适。我在做项目时的一般选型逻辑是环境可并行采样样本不贵 → 直接用 PPO需要高样本效率且动作连续 → 考虑 SAC 或 TD3任务有稀疏奖励 → 优先考虑 HERHindsight Experience Replay或者给 PPO 加一些奖励塑形需要稳定的 on-policy 基线来对比其他算法 → 无脑选 PPO。选型之外还有一个经验之谈PPO 的最终性能很大程度取决于你环境的奖励设计而不是算法本身。我见过太多人花几个星期调 PPO 超参最后发现是奖励函数给的信息互相矛盾。先把环境可视化、把奖励曲线从头到尾人工看一遍确认任务信号没问题再回头调算法效率会高得多。最后再分享一个小技巧在你第一次在某个新环境上跑 PPO 时不要急着上完整配置。先用一个非常简单的线性策略或者小网络搭配极小的学习率跑通整个 pipeline确认数据流、梯度流、GAE 计算都没问题。这一步看起来多花半小时但能帮你把后面调参时一半的玄学问题提前消灭掉。毕竟PPO 本身已经足够复杂了别再让环境 bug 和代码 bug 来混淆视听。
返回列表