PPO 的训练循环:每一个部件,都是同一对矛盾的产物

从 REINFORCE 的"数据用一次就作废"出发,一步步推出 PPO 训练循环的全部部件——重要性采样比率、clip、critic、GAE、多 epoch 复用——再落到 RLHF 的四模型循环。读完你能口述一次 PPO 迭代的每个阶段,以及每个阶段为什么非存在不可。

前置阅读:本文假设你已经知道强化学习是什么、大模型为什么需要它,以及”策略/奖励/RLHF”这几个词的含义。如果还没有,强烈建议先读《示范教不会的东西:为什么大模型需要强化学习》——那篇不用一个公式,把这些地基铺好了再来,这篇会顺畅得多。

读 GRPO、RLOO、REINFORCE++ 这些新算法的论文时,PPO 总是那个被一句话带过的背景板:“传统 PPO 需要一个昂贵的 critic……”。但如果你只知道这一句,你其实读不懂它们在改什么。PPO 是现代 RL 后训练的坐标原点——之后所有算法都是对它训练循环里某个部件的替换或删减。这篇不背公式,我们从一个根本矛盾出发,把这个循环的每个部件推导出来:你会看到 ratio、clip、critic、GAE、多 epoch 复用,没有一个是拍脑袋加的,全是同一对矛盾的产物。

一句话主线

先把结论放在这里:

PPO 的一切设计都在解决同一对矛盾:采样太贵,所以必须把一批数据反复用;反复用会让策略偏离数据的来源,所以必须限制每次更新的偏离幅度。

“Proximal”(近端)这个词,说的就是后半句。带着这句话往下读,每个部件出场时你都能预判它为什么出现。

起点:REINFORCE,以及它的两个致命伤

策略梯度方法的原始形态是 REINFORCE(1992)。思路极简:让策略 πθ\pi_\theta 跑出一条轨迹,拿到总回报 GG,然后按回报大小调整每一步动作的概率——

θJ(θ)=Eτπθ[tθlogπθ(atst)Gt]\nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}\left[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t)\, G_t\right]

直觉翻译:回报高的轨迹,里面每个动作的概率都调高;回报低的,都调低。 就这么朴素。

但这个公式里藏着两个致命伤,整篇文章后面的一切都是在治它们:

致命伤一:数据用一次就作废。 注意期望的下标是 τπθ\tau \sim \pi_\theta——梯度必须用当前策略自己采的数据来估计。你用这批数据做一次梯度更新,θ\theta 变了,这批数据就不再来自当前策略,理论上就失效了。这叫 on-policy(同策略) 约束。

在经典控制任务里这只是低效;在大模型场景里这是灾难——采一条”轨迹”意味着让模型完整生成一段回答,rollout 的成本占了 RL 训练的大头。采一批昂贵的数据,只做一次梯度更新就扔,等于用茅台涮锅。

致命伤二:方差大到没法用。 GtG_t 是整条轨迹的回报,噪声极大。同一个好动作,出现在一条运气差的轨迹里就被惩罚,出现在运气好的轨迹里就被过度奖励。信号淹没在噪声里,训练慢且不稳。

治第一个伤,得到 ratio 和 clip;治第二个伤,得到 critic 和 GAE。我们一个一个来。

治”数据一次性”:从 ratio 到 clip

第一步:重要性采样,让旧数据合法

想复用旧策略 πθold\pi_{\theta_\text{old}} 采的数据来更新新策略 πθ\pi_\theta,统计学早有标准工具:重要性采样(importance sampling)——用一个分布的样本估另一个分布的期望,只要给每个样本乘上两个分布的概率比作为权重。于是目标函数变成:

L(θ)=Et[πθ(atst)πθold(atst)At]=Et[rt(θ)At]L(\theta) = \mathbb{E}_t\left[\frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_\text{old}}(a_t \mid s_t)}\, A_t\right] = \mathbb{E}_t\left[r_t(\theta)\, A_t\right]

这个 rt(θ)r_t(\theta) 就是 PPO 公式里那个著名的比率(ratio)。它的含义很直白:新策略比旧策略更倾向这个动作多少倍。 AtA_t 是优势(advantage),先理解为”这个动作比平均水平好多少”,下一节细讲。

现在旧数据合法了,一批数据可以做多次更新。但新问题立刻出现——

第二步:偏得越远,估计越不可信

重要性采样有个众所周知的软肋:两个分布差得越远,权重方差越大,估计越不可信。放到这里:你用旧数据更新得越狠,πθ\pi_\thetaπθold\pi_{\theta_\text{old}} 越远,这个目标函数就越是自欺欺人——它会告诉你”目标值在涨”,而真实性能可能已经跳崖。策略梯度训练里著名的”性能突然崩掉再也起不来”(policy collapse),根源多半在此。

所以必须给”偏离幅度”上一道约束。2015 年的 TRPO(Trust Region Policy Optimization,信赖域策略优化)给出了理论严格的答案:每次更新时约束新旧策略的 KL 散度不超过一个阈值,在这个”信赖域”内优化。理论漂亮,工程昂贵——它需要二阶优化(计算 Fisher 矩阵与共轭梯度),实现复杂,而且和参数共享、dropout 等常规技巧犯冲。

第三步:clip——用一行代码近似信赖域

PPO(2017,Schulman 等)的贡献,本质是把 TRPO 的二阶约束换成一个一阶的、写出来只有一行的目标函数:

LCLIP(θ)=Et[min(rt(θ)At,  clip(rt(θ),1ϵ,1+ϵ)At)]L^{\text{CLIP}}(\theta) = \mathbb{E}_t\left[\min\Big(r_t(\theta)\, A_t,\ \ \text{clip}\big(r_t(\theta),\, 1-\epsilon,\, 1+\epsilon\big)\, A_t\Big)\right]

ϵ\epsilon 通常取 0.2。这个公式值得拆开看,因为它是全文最容易被误解的地方。

常见的误解是:“clip 把 ratio 限制在 [1ϵ,1+ϵ][1-\epsilon, 1+\epsilon] 之间。” 不对。clip 不是投影、不是硬约束,ratio 完全可以跑出这个区间。它做的事情更微妙:取消超出区间之后的激励

分两种情况看:

  • At>0A_t > 0(这个动作比平均好,想加大概率):当 rtr_t 涨过 1+ϵ1+\epsilon,clip 项变成常数 (1+ϵ)At(1+\epsilon)A_t,min 选中它——梯度变成零。优化器失去继续加大这个动作概率的动力,但也没有任何力量把它拉回来。
  • At<0A_t < 0(这个动作比平均差,想压低概率):当 rtr_t 跌破 1ϵ1-\epsilon,同理梯度归零,停止继续压。

再看那个 min:它让目标函数成为原始目标的悲观下界——只在”更新让目标看起来变好”的方向上截断激励,而在”变差”的方向上不截断(比如 At>0A_t>0rtr_t 因为其他样本的梯度被带到了 1ϵ1-\epsilon 以下,此时 min 选中未截断项,梯度仍然存在,会把它修正回来)。一句话:clip 对”贪功”设限,对”纠错”不设限。

这就是”Proximal”的全部含义:不硬性锁死策略,只是让每一批数据能提供的更新动力有个天花板,策略自然就走不远。代价是它只是信赖域的粗糙近似——所以成熟实现(如 OpenAI Spinning Up)还会额外监控新旧策略的近似 KL,超阈值就提前终止本批更新。工程上防线从来不止一道。

治”方差大”:critic 与 GAE

回头治第二个致命伤。REINFORCE 用整条轨迹的回报 GtG_t 当信号,噪声太大。降方差的思路是:别问”这个动作后来拿了多少分”,问”这个动作比我在这个状态下的平均预期好多少”。 后者就是优势:

At=Q(st,at)V(st)A_t = Q(s_t, a_t) - V(s_t)

减掉的 V(st)V(s_t) 叫基线(baseline)。数学上可以证明减基线不改变梯度的期望,只降方差——这是免费的午餐。但 V(st)V(s_t)(“从这个状态出发,按当前策略平均能拿多少回报”)没人告诉你,得再训练一个网络去估计它。这个网络就是 critic(价值网络)。它不参与决策,只当”记分参谋”:告诉 actor 每个状态值多少分,好让 actor 知道自己每一步是超常发挥还是失常。

这就是 PPO 需要两个网络的原因——不是设计者贪心,是降方差的代价。GRPO 那句”省掉 critic”省的就是它,代价我们最后再算。

critic 有了,优势具体怎么算?这里有一个经典的偏差-方差光谱:

  • 完整回报减基线:无偏,但方差大(又回到老问题);
  • 单步 TD 残差 δt=rt+γV(st+1)V(st)\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t):方差小,但严重依赖 critic 估得准不准——critic 不准就有偏。

GAE(Generalized Advantage Estimation,广义优势估计)用一个参数 λ\lambda 把两端连成滑竿:

AtGAE=l=0(γλ)lδt+lA_t^{\text{GAE}} = \sum_{l=0}^{\infty} (\gamma\lambda)^l\, \delta_{t+l}

λ=0\lambda = 0 退化为单步 TD(低方差高偏差),λ=1\lambda = 1 退化为完整回报(高方差无偏),实践常取 0.95——这不是玄学调参,是你对”critic 有多可信”的显式表态:越信 critic,λ\lambda 越小。

把零件装回去:训练循环本体

现在所有零件都有了,而且每个零件的存在理由都清楚了。装配起来,这就是 PPO 的训练循环:

初始化 actor π_θ 和 critic V_φ
循环直到收敛:
  # 阶段一:采集(rollout)—— 贵,所以后面要榨干
  用当前策略 π_θ_old 与环境交互,采集一批轨迹
  记录每一步的 (s_t, a_t, r_t) 和 log π_θ_old(a_t|s_t)

  # 阶段二:打分 —— 为复用做准备,全部只算一次
  用 critic 给每个状态估值 V_φ(s_t)
  用 GAE 算出每一步的优势 A_t,并归一化
  优势 + 估值 → 回报目标 R_t = A_t + V_φ(s_t)

  # 阶段三:榨干这批数据 —— ratio 和 clip 在这里兑现
  重复 K 个 epoch(典型 3~10):
    把这批数据打乱、切成 minibatch,对每个 minibatch:
      算 ratio r_t = π_θ(a_t|s_t) / π_θ_old(a_t|s_t)
      L = L_CLIP(θ)                 # 策略损失(clip 目标)
          - c1 · (V_φ(s_t) - R_t)²  # critic 回归损失
          + c2 · 熵奖励              # 防止策略过早变成确定性
      梯度下降更新 θ 和 φ

  # 阶段四:换代
  θ_old ← θ,旧数据作废,回到阶段一

对着主线复查一遍:阶段一贵,所以有阶段三的 K 个 epoch 复用;复用需要合法性,所以有 ratio;复用会偏离,所以有 clip;信号要降噪,所以阶段二有 critic 和 GAE。没有一行是多余的。

两个容易被忽略的细节,恰恰最能检验你是否真懂这个循环:

  • 优势在阶段二算一次就冻结,K 个 epoch 里不重算。重算需要重新 rollout,那就失去复用的意义了。所以 epoch 越往后,你在用”越陈旧的优势”指导”越新的策略”——这正是 K 不能取太大的原因,clip 只能兜住一部分。
  • logπθold\log \pi_{\theta_\text{old}} 在采样时就记下来,阶段三只需要对新策略做前向。第一个 epoch 的第一个 minibatch 上 rt1r_t \equiv 1、clip 完全不起作用——clip 是为第 2 到第 K 个 epoch 准备的。如果你只打算每批数据用一次,PPO 就退化成带基线的 actor-critic,clip 形同虚设。

落到大模型:RLHF 里的同一个循环

上面的循环是通用形态。落到 RLHF(基于人类反馈的强化学习)训练大模型时,骨架一模一样,只是每个格子填入了具体内容——外加多出两个模型。

先对齐 MDP 的对应关系:状态 = prompt 加上已生成的 token 前缀,动作 = 生成下一个 token,一条轨迹 = 一次完整回答。奖励很稀疏:只有回答结束时,奖励模型(Reward Model)才给整段回答打一个分。

于是循环里同时站着四个模型:

角色训练吗在循环哪个阶段出场干什么
Actor(策略)阶段一采样,阶段三更新生成回答,是唯一最终交付的模型
Critic(价值)阶段二估值,阶段三更新给每个 token 位置估”从这往后平均能拿几分”
Reward Model❄️ 冻结阶段二给完整回答打分,人类偏好的代理
Reference(参考)❄️ 冻结阶段二初始 SFT 模型的副本,算 KL 用

Reference 模型是新面孔,它对应一道新增的防线:每个 token 的实际奖励里要扣一项与参考模型的 KL 惩罚,rt=rtRMβKL(πθπref)r_t = r_t^{\text{RM}} - \beta \cdot \text{KL}(\pi_\theta \| \pi_{\text{ref}})。注意它和 clip 管的不是同一件事:clip 管的是”这一批更新别离上一批太远”(步子别太大),KL 惩罚管的是”整场训练别离出发点太远”(别为了讨好奖励模型把语言能力练没了,即 reward hacking)。 一个是局部约束,一个是全局锚点,缺一不可。

四个模型(常常同样大)同时驻留显存、交替执行推理和训练——这就是 RLHF 工程上臭名昭著的复杂度来源,也是 verl 那篇RayPPOTrainer.fit() 要用一整套混合引擎调度的原因:那个 fit() 函数里的 generate → reward → advantage → update 各阶段,与上面伪代码的四个阶段一一对应。读懂了本文的循环,那个 1300 行的 fit() 就只是它的分布式翻译。

现在可以算 GRPO 那笔账了

回到开头那句”传统 PPO 需要昂贵的 critic”。读到这里你应该能精确说出这句话省略了什么。

DeepSeek-R1 那篇讲过,GRPO 对同一道题采一组回答,用组内平均分当基线替代 critic。现在你有了看清这笔交易的坐标系:GRPO 砍掉的是”治方差”支线上的 critic 和 GAE,换上了统计学里最朴素的基线——样本均值;而”治数据一次性”支线上的 ratio 和 clip 原封不动地保留。训练循环的四阶段骨架也没变,只是阶段二从”critic 估值 + GAE”换成了”组内减均值除标准差”。

这笔交易什么时候划算?看你需不需要 critic 提供的那个东西:逐 token 的信用分配(credit assignment)。

  • 结果可验证、奖励只在结尾(数学题对错、代码过不过测试):整条回答共享一个分数,critic 试图估计的”中间每一步值多少分”本来就没有可靠的监督信号,估不准的 critic 只贡献偏差。砍掉它,用组内相对分,便宜且没损失什么——这就是 R1 的场景,GRPO 赢。
  • 过程奖励密集、或轨迹极长、或每次采样极贵:你负担不起对每个 prompt 采一大组回答,或者你确实有逐步的奖励信号需要沿轨迹分摊——这时 critic 的估值是真金白银的方差削减,PPO 仍是更稳的选择。

换句话说,GRPO 不是 PPO 的升级版,是 PPO 在”结尾一锤定音式奖励”这个特定场景下的减配版——减得恰到好处,是因为那个场景里被减掉的部件本来就发挥不了作用。脱离场景说谁先进,是伪问题。

带走的模型

最后把这篇压缩成三条,方便你带去读任何后续算法:

  1. PPO = 策略梯度 + 数据复用(ratio)+ 偏离限制(clip)+ 方差控制(critic/GAE)。 四个部件对应两对矛盾,没有装饰品。
  2. 训练循环四阶段:采集 → 打分 → 榨干 → 换代。 从 CartPole 到千卡 RLHF 集群,骨架不变,变的只是每个阶段的实现规模。
  3. 读新算法只需要问一个问题:它替换了循环里的哪个部件,用什么替换的,在什么场景下这个替换不亏? GRPO 换掉 critic(用组均值),RLOO 换掉 GAE(用留一法基线),DPO 干脆把整个循环折叠成一个监督损失。坐标原点在手,变体就不再是一堆需要分别记忆的名词。

下一次在论文里撞见”我们对 PPO 做了如下简化”,你就不再是听结论的人,而是能自己验算这笔账的人。