8.1 off-policy 复用与重要性采样
设 为采样一个批次的策略, 为经过若干次更新之后的策略。目标函数是 下的期望,而数据来自 。重要性采样把一个期望改写成另一个(附录 A.1): 轨迹权重 看起来需要环境的转移概率,其实不需要。两个轨迹概率都可以按式 (6.2) 分解为策略 token 部分和其余部分,而“其余部分”(提示、模板、工具输出)在两个策略下的概率相同,因为环境没有变。它被约掉了: 这是式 (8.1),即精确的轨迹比率, 是逐 token 比率(per-token ratio)。对 求导得到 ,在 处它又回到 REINFORCE。所以精确的修正是存在的,而且无偏。问题在于它的方差。 我们来测量它。基座智能体采样 320 个回合,作为“旧”批次。然后基座智能体的一个副本在新批次上走 步 REINFORCE,以训练会造成的方式移动策略。对每个 ,我们为每个旧回合计算 、有效样本量(effective sample size)(加权批次相当于多少个等权样本)、新策略通过率的 IS 估计,以及在新样本上测得的通过率。逐 token 近似
PPO 不使用 。它使用每个 token 自己的比率 ,这只涉及一个 token 的概率,在策略变化不大时保持在 1 附近。其依据是性能差分恒等式(performance-difference identity;Kakade and Langford, 2002),这里写成 token 级形式: 内层期望在每个前缀上对动作重新加权,单个比率 恰好能做到这一点。外层期望针对的是新策略访问的前缀分布 ,正是这部分需要之前所有比率的乘积。代理目标(surrogate)用 (批次中的前缀)代替 : 这是式 (8.2)。它是一个近似,本书在它出现的每一处都如此说明:逐 token 比率修正了每个动作概率的变化,却忽略了所访问前缀的变化。 在 的一阶近似下与 一致(相差一个常数),因此在 处梯度相同,其误差随两个策略之间的距离增长。Schulman et al. (2015) 用策略之间最大 KL 散度的若干倍来界定这个误差,8.3 节的信赖域(trust region)就来源于此。我们在同一个旧批次上检验这个近似:比较代理目标对改进量的估计 与在新样本上测得的改进量。8.2 裁剪的代理目标
用许多步梯度更新去最大化式 (8.2),会把比率推到尽可能远的地方,进入代理目标出错的区域。PPO 的裁剪目标消除了去那里的动力: 这是式 (8.3)。取最小值使它成为未裁剪代理目标的一个悲观下界,其行为取决于优势的符号:- (该 token 应变得更可能)。在 达到 之前,该项是 ,之后保持常数。一旦好 token 的概率增长到 倍,它的梯度就为零。在 1 以下,最小值保留未裁剪项,因此一个概率下降了的好 token 总会被推回上去。
- (该 token 应变得更不可能)。在 降到 之前,该项是 ,之后保持常数。在 1 以上,保留更负的未裁剪项:一个概率上升了的坏 token 总会被推回下来,梯度随 增大。
aggregate_loss 聚合,并返回 8.5 节用作诊断的统计量。被裁剪的 token 是裁剪分支严格更小的 token,这恰好是梯度为零的地方。
approx_kl 统计量是用 的样本估计 的 估计量:,它逐 token 非负(附录 A;第 10 章将它与其他估计量比较)。损失对每个 token 对数概率的梯度展示了按符号区分的行为。取比率从 0.5 到 2 的六个 token,分别在 和 下计算;在单行上使用 seq_mean_token_sum,使每个 token 的梯度等于它的原始权重。
双重裁剪
时无界的那一段是有意为之的,但它有一种失效模式。如果一个坏 token 的比率非常大,例如因为批次陈旧或采样器的概率有误(第 13 章), 这一项就可能主导整个批次的梯度。双重裁剪(dual clipping;Ye et al., 2020)用 为目标设置下界,常数 ,通常取 3:8.3 信赖域视角
8.1 节表明代理目标在 附近是准确的。信赖域策略优化(Trust Region Policy Optimization,TRPO;Schulman et al., 2015)把这一点变成了算法。它的分析给出如下形式的下界: 其中这里的 是代理目标给出的改进量, 取决于折扣和最大的优势。最大化右边可以保证改进。实践中,TRPO 在平均 KL 约束 下最大化 ,并使用自然梯度:它借助 Fisher 向量积用共轭梯度法求解 ( 为 Fisher 信息矩阵),把步长缩放到约束边界,再用线搜索回退,直到代理目标得到改进且 KL 位于区域之内。对语言模型来说,每个 Fisher 向量积都是对整个网络的一次二阶计算,线搜索还需要额外的前向计算。这就是 TRPO 本身很少用于 LLM,而它的思想却无处不在的原因。 PPO 用裁剪代替约束,除了一阶优化器之外没有额外开销。两者并不等价。裁剪消除了把 token 比率推过 的动力,但参数在 token 之间共享,所以一步有利于其他 token 的更新可能把一个已被裁剪的 token 推得更远,而没有任何东西把它拉回来。下面的代码块取旧批次的一个小批次,在裁剪损失上运行 40 步 Adam,并作为对照在未裁剪的代理目标(裁剪设为无穷大)上同样运行,然后报告 KL 以及比率落在 之外的比例。8.4 价值损失与 PPO 循环
原始论文中的 PPO 与策略一起训练一个 critic,并使用 GAE 优势(7.3 节)。critic 在gae_advantages 返回的价值目标 上做回归。由于 critic 也在同一个批次上更新多次,同样的信赖域问题也会出现,PPO2 风格的实现以同样的方式对其裁剪:先把新旧价值之差裁剪到 范围内,再取裁剪前后较大的回归损失;这限制了改善损失的激励,并非把价值预测硬性限制在区间内,
取最大值使它成为悲观的,就像策略的取最小值一样。价值裁剪是否有帮助存在争议。Andrychowicz et al. (2021) 在一项针对连续控制任务上 on-policy 设计选择的大规模研究中发现,“PPO-style value loss clipping hurts the performance regardless of the clipping threshold”(无论裁剪阈值如何,PPO 风格的价值损失裁剪都会损害性能);这一证据并非来自语言模型。我们实现它是因为它是许多 LLM PPO 框架的默认设置,用 clip=None 可以关闭它。
critic 需要先行一步
7.4 节测量了 TinyAgent critic 有多弱:在新回合上解释方差只有 0.02 到 0.08。一个新输出头从随机权重开始的 critic 更糟,并且在 时,它的误差会直接进入策略梯度(图 7.1)。有两种标准的缓解办法,我们都会使用:在第一次策略更新之前,在初始策略的 rollout 上预热critic;并从 开始,此时 critic 只是一个基线,不会使梯度有偏。warm_critic 就是第 7 章中在蒙特卡洛回报上的回归。
一次 PPO 迭代
一次迭代分两个阶段。第一阶段冻结定义 的一切:采样 token 的对数概率、critic 的价值,以及由它们计算出的 GAE 优势和目标。然后像大多数 PPO 实现那样,在批次的所有策略 token 上对优势做白化(whitening,均值 0,标准差 1)。第二阶段把批次打乱并拆成小批次,重复训练epochs 遍,每个小批次做一步策略更新和一步 critic 更新,并记录每一轮的统计量。
证据。 本书测得,每种设置一个随机种子,30 次迭代,每次 64 个回合。 与 之间的比较处于单次运行的噪声范围内,不应被解读为对某一方的偏好。这次运行确实支持的是机制层面的结论:critic 这么弱时,GAE 带来的增益很小,而 critic 要付出第二个网络和第二个优化器的代价。
8.5 诊断与提前停止
有三个数字可以告诉工程师复用是否安全,每个 PPO 系列的训练器都应按轮次记录它们。- 裁剪比例(clip fraction):裁剪分支起作用的策略 token 所占的比例。接近 0 时,裁剪处于闲置状态,更新就是普通的策略梯度。在各轮次间上升时,说明策略正在到达信赖域的边缘。
- 与 之间的近似 KL,即在批次上的 估计。它用信赖域界所使用的量来衡量策略移动了多远。
- 比率统计量:策略 token 上比率的最小值和最大值,能捕捉被平均值掩盖的少数 token。
ppo_epochs 已经接受 target_kl;我们以 0.02 为目标重新运行 的设置,并统计每次迭代用了多少轮。
8.6 实践中的设计问题
问题:轮次太多。 现象: 裁剪比例和 KL 在各轮次间攀升,策略的熵坍缩,或在许多轮次之后发生跳变,训练奖励变得不稳定。 原因: 每一轮都使策略离 更远(8.3 节),代理目标的误差随这一距离增长(8.1 节),而裁剪并不能阻止漂移。 可选方案: (a) 减少轮次,这对每个样本浪费的计算比看上去少,因为后面的轮次大部分被裁剪了。(b) 基于 KL 目标的提前停止,按批次自适应调整轮次数。(c) 更小的学习率,使每一轮的 KL 开销更小,但会减慢学习。 建议做法: 对 LLM 智能体用 1 到 2 个轮次,以提前停止作为保护,因为钱主要花在 rollout 上,但一个被破坏的策略会浪费掉所有 rollout。下面的代码块在一个批次上更新 16 个轮次,每四轮打印一次。
问题:小批次大小与陈旧度。 现象: 把一个批次划分成更多的小批次,起初会加快按样本计的学习,随后会使其不稳定。 原因: 一轮中的第 个小批次是在一个已经在同一批次上走了 步的策略下评估的,因此即使在第一轮,后面的小批次也更加 off-policy。 可选方案: (a) 更少、更大的小批次,更接近 on-policy,但每个样本对应的步数更少。(b) 更多的小批次配合更小的学习率。(c) 按小批次步数而非轮次计数的提前停止。 建议做法: 每个批次 2 到 4 个小批次,并按小批次记录裁剪比例和 KL。下面的代码块在旧批次上用 1 到 16 个小批次各运行一轮,并报告最后一个小批次所见到的 KL。
问题:old_logp来自训练器还是来自采样器。 现象: 一次运行从第一个小批次起裁剪比例就很高,或者一次运行无论走多少轮裁剪都从不激活。 原因: 必须是实际采样这些 token 的分布。用训练器的前向计算重新计算old_logp,只有在训练器与采样器计算出相同概率时才正确。在更新循环内部用当前参数重新计算它是一个 bug:此时每个比率都恰好为 1。 可选方案: (a) 在第一次更新之前用训练器重新计算一次old_logp,本章和大多数开源训练器都这样做。(b) 使用采样器的对数概率,这对行为策略而言是精确的,但会让比率吸收两个引擎之间的任何数值不一致。(c) 两者都用:训练器的用于 PPO 比率,采样器的用于显式的不一致修正(第 13 章)。 建议做法: 在像这里这样的单进程设置中用 (a),在生产中用 (c)。下面的代码块比较第 5 章存储的sample_logp与训练器的重新计算结果,然后复现这个 bug。
old_logp 取自当前的前向计算时,每个比率都为 1,裁剪从不起作用,无论运行多少步,损失都只是普通的 REINFORCE。监控面板会显示恰好为 0 的裁剪比例和恰好为 0 的 KL,这本身就是该 bug 的特征。
问题:非对称裁剪。 现象: 训练过程中熵稳步下降,最终被证明是好的罕见动作,其概率增长得非常缓慢。 原因: 上裁剪把一个好 token 概率在每个批次上的相对增长限制在 倍,因此在 时, 的 token 最多只能达到 0.012,而 0.5 的 token 可以达到 0.6。下裁剪限制了从坏 token 移走概率的速度,而被移走的概率质量主要流向本来就很可能的 token。两种效应都使分布更集中。 可选方案: (a) clip-higher:提高 (DAPO 使用 0.28,;第 9 章),让罕见的好 token 增长得更快,代价是信赖域更松。(b) 熵奖励项(第 10 章)。(c) 降低 以减缓概率质量的移走。 建议做法: 以对称的 0.2 作为基线,测得熵坍缩时再使用 clip-higher。下面的代码块按 token 在 下的概率分组,查看在 16 轮的运行中上裁剪实际拦住了哪些 token。
小结
- 在一次更新之后复用批次需要重新加权。精确的轨迹权重是策略 token 上逐 token 比率的乘积(环境的项相互抵消),其方差随策略 token 数指数增长:三次 REINFORCE 更新之后,320 个基座智能体回合只相当于其数量的三分之一,新通过率的 IS 估计远低于真实值。
- PPO 的代理目标使用逐 token 比率。它修正每个动作的概率,却忽略了所访问前缀的变化,因此在一阶上与真实改进一致,并随策略移动而失去准确性:它对一次和三次更新的改进预测误差在半个百分点以内,对十次更新则少估了六分之一。
- 裁剪目标消除了把 token 比率朝其优势所要求的方向推过 的动力,并保留撤销错误方向移动的梯度。它是一个软信赖域:在一个小批次上走 40 步后,41% 的比率落在区间之外,尽管 KL 比不裁剪时小 25 倍。双重裁剪限制了负优势 token 上极大比率的影响。
- 带预热 critic 的 PPO 在 30 个批次内学会了训练任务。critic 解释了回报方差的 12% 到 26%,因此主要起基线的作用;大量复用使第 6 章的记忆问题在留出任务上更严重。
- 按轮次记录裁剪比例、近似 KL 和比率极值。前两者随每一个轮次和每一个额外的小批次增长;以 0.02 为 KL 目标的提前停止用约 63% 的梯度步达到了相同的训练通过率。在更新之前重新计算一次
old_logp,绝不要在循环内部计算。
练习
- 证明只要两个策略面对同一个环境,即使环境是随机的,式 (8.1) 中环境的因子也会相互抵消。当工具输出依赖于实际耗时或共享缓存时,哪个假设不成立?比率会发生什么?
- 沿 的一条轨迹对 做裂项求和,从 的定义推导 8.1 节的性能差分恒等式。逐 token 代理目标究竟在哪一步做了近似?
- 改变一个假设:用自归一化重要性采样 重做 8.1 节的 IS 表格。它是否更接近新样本上的通过率?它是有偏的;偏向哪个方向?为什么这种偏差常常更受青睐?
- 实现 PPO 的 KL 惩罚变体(“PPO-penalty”):不裁剪,在损失中加入 项,并在每个批次后将 加倍或减半以跟踪 KL 目标。在 8.4 节的运行上,将它的逐轮次 KL 与裁剪版本比较。
- 分别用
value_loss(..., clip=None)和clip=0.05运行 8.4 节的 PPO。跟踪 critic 在新回合上的解释方差。价值裁剪对这个 critic 是有帮助还是有害? - 在 16 轮的运行中,分别测量正优势和负优势 token 的裁剪比例。哪一侧更常起作用?把 提高到 0.28 会如何改变 16 轮后的熵?
- 设计评审:一位队友提议每个批次做 8 个轮次、每轮 8 个小批次,“让 rollout 便宜八倍”。利用 8.6 节的测量,列出你预期在监控面板上看到什么、你会要求做什么实验,以及在同意之前你会要求加上什么保护措施。