🏠 总目录📚 本教程 11 · PPO ← →
📑 本页目录(点开跳转)

11 · PPO

⏱ 30 分钟 | ⭐ 现在实际在用的那一个


🎯 一句话

策略梯度有一个要命的毛病:步子迈大一点,策略就崩了,而且再也回不来。 PPO 用一个极其简单的裁剪操作解决了它 —— 简单到几行代码, 但它是 ChatGPT、机器人控制、游戏 AI 背后共同的那个算法。

1−ε11+ε优势 A > 0(这动作是好的)优势 A < 0(这动作是差的)封顶:再涨也没奖励封底:再跌也没额外惩罚新旧策略的概率比 r = π_new / π_old只要策略走得太远,梯度就被【剪平】——直接变成 0⭐ 这就是 PPO 全部的秘密:不让一次更新把策略推太远因为策略一变,数据分布就变了 —— 走太远,手上这批数据就不再适用
横轴是新旧策略的概率比。一旦超出 [1−ε, 1+ε],目标就被剪平 —— 梯度直接变 0。⭐ 这就是 PPO 的全部秘密:不让一次更新把策略推太远。因为策略一变数据分布就变,走太远手上这批数据就不再适用。

💥 一、先看清那个毛病

结果对照

监督学习:参数走错了→下一批数据还是原来那批→能纠正回来 ✅
强化学习:参数走错了→策略变差
【采到的数据也跟着变差】⭐
用更差的数据继续训练
更差…… 💀 【正反馈崩溃】

🔑 这就是 RL 和监督学习最本质的区别之一: 你的模型决定了你的训练数据。 一次糟糕的更新不只是"这一步没学好",而是污染了后面所有的数据。

对照

⭐ 所以核心诉求变成了:

「每次更新,别让新策略离旧策略太远」


📏 二、怎么衡量"离得远"

TRPO 的答案(PPO 的前身):用 KL 散度做硬约束

$$\max_\theta \mathbb{E}\Big[\frac{\pi_\theta(a|s)}{\pi_{old}(a|s)}A\Big] \quad \text{s.t.}\quad \text{KL}(\pi_{old}\,\|\,\pi_\theta) \le \delta$$

🔗 KL 散度到底是什么、为什么它能当「两个策略离多远」的尺子: 数学原理 01b · KL 散度。 ⭐ 去那里主要是为了一件在这里会反复咬你的事:KL 不对称,$\text{KL}(p\|q)\ne\text{KL}(q\|p)$, 而且两个方向的行为完全相反(一个逼你「铺开、别漏掉任何峰」,一个逼你「找一个峰钻进去」)。 TRPO 这里写的是 $\text{KL}(\pi_{old}\|\pi_\theta)$,第六节 RLHF 那个惩罚项写的是反过来的方向—— 顺序不是随便写的,谁在前谁在后决定了它到底在罚什么。

结果对照

✅ 有理论保证(单调改进)
❌ 但要解一个带约束的优化问题:需要二阶导、共轭梯度、线搜索
实现复杂,跑得慢 💀

PPO 的答案:别做约束优化了,直接把目标函数裁掉。


✂️ 三、裁剪目标(PPO-Clip)

先定义概率比:

$$r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{old}(a_t|s_t)}$$

💡 人话:新策略选这个动作的概率,是旧策略的几倍。 r = 1 表示没变,r = 1.5 表示新策略更倾向于选它了。

目标函数:

$$L = \mathbb{E}\Big[\min\big(r_tA_t,\ \text{clip}(r_t, 1-\epsilon, 1+\epsilon)A_t\big)\Big]$$

(通常 ε = 0.2)

💡 用两种情况把它讲清楚

流程图

情况 ①:A > 0(这个动作比预期好,想提高它的概率)
r 从 1 涨到 1.2→目标跟着涨 ✅ 鼓励
r 涨到 1.5→clip 到 1.2→【目标不再增加】⭐
再怎么推高这个动作的概率,也拿不到更多好处
【失去了继续迈大步的动力】
情况 ②:A < 0(这个动作比预期差,想降低它的概率)
r 从 1 降到 0.8→目标涨 ✅
r 降到 0.5→clip 到 0.8→目标不再涨
同样刹住了

关键信息

⭐ 那个 min 的作用:
它让目标函数成为真实目标的一个【悲观下界】
只在"改进有限"的方向上给奖励,
在"步子太大"的方向上不给任何额外好处

🔑 PPO 的精妙之处在于它的朴素: 不是"禁止"迈大步(那需要约束优化),而是"迈大步没有奖励" —— 用一个 min 和一个 clip 就把二阶优化问题变成了普通的梯度下降。


🔁 四、为什么能重复利用数据

因果链

⭐ 有了概率比 r,同一批数据可以【重复训练多轮】(通常 3~10 轮)
因为:即使策略已经更新过了,
r 会如实反映"现在离采样时那个策略有多远"
clip 会自动刹住
✅ 这大幅提升了 on-policy 算法的样本效率

💡 这是 PPO 实用性的关键: 纯 on-policy 算法(如 REINFORCE)每批数据只能用一次,样本效率极低。 PPO 靠重要性采样比 + 裁剪,让数据能重复用几轮,同时不会跑飞。


🔨 五、完整实现

# 🧩 骨架,不是可运行脚本:policy / opt / compute_gae / minibatches 都来自你自己的代码
# 采样阶段(用旧策略跑一批,记下 logp 和优势)
import torch
import torch.nn as nn
import torch.nn.functional as F
with torch.no_grad():
    old_logp = dist.log_prob(actions)
    advantages = compute_gae(rewards, values, dones, gamma=0.99, lam=0.95)
    advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)  # ⭐ 标准化

# 训练阶段(同一批数据跑 K 轮)
for _ in range(K_EPOCHS):
    for batch in minibatches(data):
        logp  = policy(batch.states).log_prob(batch.actions)
        ratio = torch.exp(logp - batch.old_logp)          # ⭐ 在 log 空间做除法

        s1 = ratio * batch.adv
        s2 = torch.clamp(ratio, 1 - eps, 1 + eps) * batch.adv
        policy_loss = -torch.min(s1, s2).mean()           # ⭐ 核心就这三行

        value_loss  = F.mse_loss(critic(batch.states), batch.returns)
        entropy     = dist.entropy().mean()

        loss = policy_loss + 0.5 * value_loss - 0.01 * entropy
        opt.zero_grad(); loss.backward()
        nn.utils.clip_grad_norm_(params, 0.5)             # ⭐ 梯度裁剪,必加
        opt.step()

⚠️ 六个真正影响成败的实现细节

细节 为什么
优势标准化 ⭐ 不做的话不同批次的优势量级差很多,学习率没法统一
梯度裁剪(0.5) RL 的梯度偶尔会爆炸
ratio 在 log 空间算 exp(logp − old_logp) 比直接除稳定得多
熵奖励系数 太小 → 过早收敛;太大 → 学不到东西
K_EPOCHS 不能太大 ⭐ 太多轮会让新旧策略偏离过远,clip 也救不回来
观测归一化 连续控制任务里几乎是必需的

💡 有一篇著名的论文专门研究这件事("实现细节对 PPO 有多重要"): 结论是 PPO 的效果很大程度上来自这些工程细节,而不只是那个 clip 公式。 这也是复现 RL 论文特别难的原因。


🎯 六、PPO 在 RLHF 里的样子

算一算

语言模型的 RLHF 里,PPO 的各个部件对应:

状态 s = 到目前为止的对话 + 已生成的 token

动作 a = 下一个 token ⭐

策略 π = 语言模型本身

奖励 r = 奖励模型给整个回复打的分(只在结尾给)⭐

Critic = 一个额外的价值头

⭐ 额外一项:KL 惩罚

r_total = 奖励模型分数 − β·KL(π ‖ π_SFT)

↑ 别离原始模型太远

🔑 为什么需要那个 KL 惩罚: 只优化奖励模型的分数,模型会去钻奖励模型的漏洞 (生成一些人类觉得莫名其妙、但奖励模型给高分的文本)——这叫奖励攻击。 KL 惩罚把它拴在原始模型附近。

💡 注意这里有两层"别走太远": clip 管的是"这一步别更新太多",KL 惩罚管的是"整体别偏离 SFT 模型太远"。 两者解决不同的问题。

🔗 第 12 章会把整个流程串起来。


📊 七、超参速查

超参 常用值
clip ε 0.2(RLHF 里常用 0.2 或更小)
γ 0.99(RLHF 里常用 1.0)
GAE λ 0.95
K_EPOCHS 3–10(RLHF 里常用 1–4)⭐
学习率 3e-4(控制)/ 1e-6 ~ 1e-5(RLHF)⭐
熵系数 0.01(RLHF 里常设 0)
梯度裁剪 0.5

⚠️ 注意 RLHF 的学习率比常规 RL 小两三个数量级 —— 因为你是在微调一个已经很好的模型,不是从零学。


🔗 和站内其他章的关系

相关的地方 这里的位置
第 9 章 策略梯度 PPO 是它的稳定化版本
第 10 章 GAE PPO 的标配优势估计 ⭐
第 10 章 熵奖励 PPO 里同样保留
全景导论主线 4 · 它怎样从续写机变助手 RLHF 本章是它的算法内核 ⭐
第 13 章 DPO 试图完全绕开 PPO 的路线

✅ 检查点

  1. RL 里"一次糟糕的更新"为什么比监督学习严重得多?
  2. TRPO 的做法是什么?它的问题在哪?
  3. 概率比 r 的含义是什么?r=1.5 表示什么?
  4. 用 A>0 和 A<0 两种情况解释 clip 怎么起作用。
  5. 那个 min 的作用是什么?
  6. PPO 为什么能重复利用同一批数据?
  7. 六个实现细节里,哪两个最容易被忽略?
  8. RLHF 里 PPO 的状态、动作、奖励分别是什么?
  9. 为什么 RLHF 需要 KL 惩罚?它和 clip 解决的是同一个问题吗?
  10. 为什么 RLHF 的学习率比常规 RL 小那么多?
👀 答案
  1. 因为你的模型决定了你的训练数据。参数走错 → 策略变差 → 采到的数据也跟着变差 → 用更差的数据继续训练 → 正反馈崩溃。监督学习里数据是固定的,能纠正回来。
  2. 用 KL 散度做硬约束(最大化代理目标,约束 KL ≤ δ)。问题:要解带约束的优化问题,需要二阶导、共轭梯度、线搜索,实现复杂且慢。
  3. 新策略选这个动作的概率是旧策略的几倍。r=1.5 表示新策略更倾向于选它了(概率涨了 50%)。
  4. A>0(想提高概率):r 涨到 1.2 内目标跟着涨,超过就被 clip 到 1.2,目标不再增加 → 失去继续迈大步的动力。A<0(想降低概率):r 降到 0.8 内目标涨,再低就被 clip 住,同样刹车。
  5. 让目标函数成为真实目标的悲观下界——只在"改进有限"的方向给奖励,在"步子太大"的方向不给任何额外好处。
  6. 因为概率比 r 会如实反映"现在离采样时那个策略有多远",即使已经更新过几轮,clip 也会自动刹住。这大幅提升了 on-policy 的样本效率。
  7. 优势标准化(不做的话不同批次量级差很多,学习率无法统一)和 K_EPOCHS 不能太大(太多轮会让新旧策略偏离过远,clip 也救不回来)。
  8. 状态 = 到目前为止的对话+已生成 token;动作 = 下一个 token;奖励 = 奖励模型给整个回复的打分(只在结尾给)。
  9. 因为只优化奖励分数,模型会去钻奖励模型的漏洞(生成人类觉得莫名其妙但奖励模型给高分的文本)——奖励攻击。不是同一个问题:clip 管"这一步别更新太多",KL 惩罚管"整体别偏离 SFT 模型太远"。
  10. 因为是在微调一个已经很好的模型,不是从零学。常规 RL 用 3e-4,RLHF 用 1e-6~1e-5。

🛑 可以停在这里

⚡ 走神救援

先记住这几件事

下一节 👉 12-RLHF全流程.md ⭐

打卡记录保存在你的浏览器里,首页能看到总进度