时长 7–8 小时:§1 策略梯度(1.5h)· §2 价值函数与 GAE(1h)· §3 PPO(2h)· §4 工程:四个模型与一轮 PPO(1h)· §A–§C(1.5h)
对应正课 Lecture 5 的中间部分:RL 方法(PPO 及其变体)。学习计划点名要会的:PPO 在 LLM 上的具体形态、四个模型同时在显存里、KL 惩罚、clipping 的作用。
配套代码
w3day3_checks.py(纯 numpy):策略梯度的无偏性与基线的方差缩减、GAE 的偏差-方差权衡、裁剪目标的梯度、三种 KL 估计量、在”回答池”上完整训练 PPO(收敛到闭式最优,去掉 KL 就 reward hacking)、四种训练方式的显存账§B 参考实现(
code/目录):w3d3_ppo.py(从零实现 RLHF 的 PPO:mini-GPT 策略、Day 2 训练的奖励模型、参考模型、价值模型;对比有无 KL 惩罚)。今天的目标:能推出策略梯度,说清基线为什么不改变期望却能降方差;能写出 GAE 并解释 λ;能写出 PPO 的裁剪目标,说清它截断了什么;能画出一轮 RLHF-PPO 的完整流程,说出四个模型各自的作用和显存代价。
§0 今天的主线
Day 2 的最后留下了一个优化问题(Day 2 §3.3):
难点在于期望是对模型自己的采样取的: 是从 里采出来的离散 token 序列,采样这一步不可导,没法把奖励直接反传回参数。强化学习的策略梯度就是为这种情形准备的工具。
今天的路线:先推出最朴素的策略梯度(REINFORCE)并解决它方差太大的问题(§1);再引入价值函数,得到更精细的”优势”估计(§2);然后是 PPO:让一批昂贵的采样数据可以反复使用、又不至于走得太远(§3);最后看它在 LLM 上的完整形态和工程代价(§4)。
今天要用到的旧结论:softmax 与交叉熵的梯度 (Week 0 Day 1);Adam(Week 0 Day 2 §2.6);KL 散度与最小 KL 的解释(Week 0 Day 5 §1);采样与温度(Week 2 Day 3 §2);Bradley-Terry 奖励模型与归一化(Day 2 §2);过度优化与 KL 约束(Day 2 §3.2–3.3)。
§1 策略梯度
1.1 把生成看成强化学习
| 强化学习 | LLM 生成 |
|---|---|
| 状态 | 提示 + 已生成的前 个 token |
| 动作 | 下一个 token |
| 策略 | 语言模型的下一个 token 分布 |
| 一个回合(episode) | 生成一整个回答,到结束符为止 |
| 奖励 | 通常只在最后一个 token 给出(奖励模型的打分) |
| 状态转移 | 确定的:把 拼到后面 |
整个回答的概率是逐 token 概率之积,所以 。
1.2 得分函数估计:推导
动机:。奖励 不依赖 ,依赖 的是”采到哪个 的概率”。
推导:
中间一步用的是 (log-derivative trick)。于是梯度变成了对采样的期望,可以用样本估计:采 个回答,算 。这就是 REINFORCE(Williams 1992)。
直觉: 是”让 更可能”的方向,用奖励加权:奖励高的回答被推高,奖励低的被压低。它和 SFT 的梯度形式一模一样,只是每个样本多了一个权重 ,而且样本是模型自己生成的。
softmax 策略的具体形式:logits 为 时 (Week 0 Day 1 推过的交叉熵梯度),于是真实梯度是 。w3day3_checks.py 第 1 组用这个解析式检验估计量。
1.3 基线:不改变期望,却能降低方差
问题:REINFORCE 的方差很大。设想所有奖励都是正数:每个采到的回答都会被推高,只是推多推少的区别,梯度的方向几乎全靠”碰巧采到了谁”。
做法:减去一个不依赖动作的基线 :
为什么期望不变:,所以减去的那一项期望为 0。
第 1 组(10 个动作,每个估计用 16 个样本,重复 20000 次):
| 奖励 | 基线 | 偏差 | 方差 |
|---|---|---|---|
| 无 | 0.0012 | 0.0120 | |
| 0.0012 | 0.0109 | ||
| 同批其他样本的均值 | 0.0007 | 0.0119 | |
| 无 | 0.0206 | 5.3373 | |
| 0.0004 | 0.0110 | ||
| 同批其他样本的均值 | 0.0008 | 0.0120 |
偏差都只是抽样误差(估计量无偏);奖励整体平移 10,没有基线时方差放大了几百倍,有基线时完全不受影响。
意义:这是后面所有方法的共同骨架,区别只在基线从哪来:
- PPO:训练一个价值网络 当基线(§2)。
- RLOO(REINFORCE Leave-One-Out):同一个提示采 个回答,每个回答用”其余 个的平均奖励”当基线,仍然无偏(上表第三行)。
- GRPO:同一个提示采一组回答,用组内均值当基线、再除以组内标准差(Day 5)。
- ReMax:用贪心解码那个回答的奖励当基线。
学习计划里的”PPO 及其变体”,本质上就是在”基线怎么来""一批数据能用几次”这两件事上做不同选择。
1.4 为什么不把奖励模型直接反传
奖励模型本身是可导的,但采样那一步不可导: 是离散的 token,从 logits 到 token 的 argmax 或随机采样没有梯度。Gumbel-softmax 之类的松弛在长序列上又不稳定。得分函数估计完全绕开了这个问题:它只需要 可导,不需要奖励可导。这也意味着奖励可以是任何东西:一个人、一个规则、一个编译器、一个数学答案的比对(Day 5 的可验证奖励)。
§2 价值函数与优势
2.1 优势:比”平均”好多少
- 状态价值 :从状态 出发、按策略 继续生成,能拿到的期望总奖励。
- 动作价值 :在 先选 ,之后按 继续,期望总奖励。
- 优势 :选这个 token 比”平均水平”好多少。
用 当基线,每个 token 的权重就是它的优势。好处是逐 token 的信用分配:一个回答整体得分高,并不意味着每个 token 都好;优势试图估计每个 token 的贡献。
2.2 时序差分
和 都不知道,只能估计。训练一个价值网络 ,然后用时序差分残差
作为优势的一步估计。如果 是准确的,。LLM 的 RLHF 里通常取 (回答长度有限)。
2.3 GAE:在偏差与方差之间插值
问题: 只看一步,完全依赖 的准确性(偏差大);用整条轨迹的实际回报 不依赖 ,但混进了之后所有步骤的随机性(方差大)。
GAE(Schulman et al. 2016)取所有 步估计的指数加权平均,化简后是
就是 , 就是蒙特卡洛回报减 (参考代码自检的前两项)。
第 2 组:100 步的轨迹,奖励噪声的标准差 0.3,价值网络在每个状态上有标准差 3 的固定误差。误差分成两部分:来自价值网络误差的偏差,来自奖励噪声的方差。理论值是 :
| 偏差² | 方差 | 均方误差 | |
|---|---|---|---|
| 0 | 8.977 | 0.089 | 9.066 |
| 0.5 | 2.993 | 0.120 | 3.112 |
| 0.8 | 1.001 | 0.247 | 1.247 |
| 0.9 | 0.476 | 0.473 | 0.949 |
| 0.95 | 0.231 | 0.907 | 1.137 |
| 0.99 | 0.039 | 3.928 | 3.967 |
| 1 | 0.000 | 8.885 | 8.885 |
意义: 是一个旋钮,价值网络越不准,最优 越靠近 1。PPO 常用 。
LLM 上的特殊困难:奖励只在最后一个 token 出现,价值网络要从一个半截的回答预测最终得分,这很难学准。这是 Day 5 的 GRPO 干脆去掉价值网络的主要动机之一。
§3 PPO
3.1 动机:采样很贵,想多用几次
在 LLM 上,一次采样意味着生成几百个 token,比一次反向传播贵得多。策略梯度要求样本来自当前策略(on-policy),参数一更新,旧样本就”过期”了。能不能在同一批样本上多更新几步?
重要性采样:用旧策略 采的样本,也能估计新策略的目标:
但 离 太远时,比值 的方差爆炸,优势 也不再准确(它是在旧策略下估的)。需要一个”信任域”:每次只在旧策略附近更新。TRPO(Schulman et al. 2015)用 KL 约束和二阶优化做到这一点,实现复杂。PPO 用一个简单的裁剪做到近似的效果。
3.2 裁剪目标
第 3 组列出了它对 的导数:
| 0.5 | 0.8 | 1.0 | 1.2 | 1.5 | |
|---|---|---|---|---|---|
| :目标 / 梯度 | 0.50 / 1 | 0.80 / 1 | 1.00 / 1 | 1.20 / 0.5(边界) | 1.20 / 0 |
| :目标 / 梯度 | −0.80 / 0 | −0.80 / −0.5(边界) | −1.00 / −1 | −1.20 / −1 | −1.50 / −1 |
读这张表:
- 好动作():概率已经提高到 倍以上,就不再给梯度,防止在这一批数据上过度追逐。
- 坏动作():概率已经降到 倍以下,就不再继续压。
- “对自己不利”的一侧从不截断:好动作的概率被错误地降低了(),梯度仍在,会被拉回来;坏动作被错误地提高了(),同样。取 使 是未裁剪目标的一个悲观下界。
完整的损失还有两项:价值网络的回归损失(让 拟合 GAE 算出的回报,参考代码里也做了裁剪),以及可选的熵奖励(鼓励探索;LLM 上常常不用)。
3.3 LLM 上的 PPO:KL 进入奖励
InstructGPT 式的实现把 KL 惩罚摊到每个 token 的奖励里:
- 每个 token 都有一个小的负奖励,大小是它在策略和参考模型下的 log 概率之差(KL 的单样本估计,§3.4)。
- 奖励模型的分数只加在最后一个 token 上。
- 然后照常算 GAE、做裁剪更新。
对整条回答求和,,期望正是 Day 2 §3.3 的目标 。KL 项变成了密集的奖励信号,这也部分缓解了”奖励只在最后一步”的稀疏问题。
另一种做法(GRPO 用的,Day 5)是不放进奖励,而是作为损失里单独的一项:。
3.4 KL 怎么估计
每个 token 的 KL 只能用样本估计。设 (当前策略),( 是参考模型),常见的三种单样本估计(Schulman 的博客《Approximating KL Divergence》):
| 估计 | 公式 | 无偏 | 非负 |
|---|---|---|---|
| 是 | 否 | ||
| 否 | 是 | ||
| 是 | 是 |
第 4 组(、,100 万个样本,偏差 / 标准差都除以真值):
| 真实 KL | |||
|---|---|---|---|
| 0.005 | −0.001 / 19.98 | +0.001 / 1.41 | −0.002 / 1.41 |
| 0.5 | +0.001 / 2.00 | +0.250 / 1.74 | −0.000 / 1.70 |
意义:RLHF 里每个 token 的 KL 通常很小。这时 的相对标准差是 20,而且经常为负(单个 token 上”奖励”它偏离参考模型); 同样无偏,方差小得多,而且永远非负。 在 KL 小时偏差很小,但 KL 大时明显高估。GRPO 在损失里用的就是 (Day 5 会看到它作为损失时的一个微妙之处)。
3.5 在一个玩具上跑完整的 PPO
第 5 组把 Day 2 的”回答池”搬过来:一个提示、200 个候选回答, 是它们上的一个分布,代理奖励和真实质量与 Day 2 第 4、5 组相同。每轮从 采 64 个回答,奖励减去 ,优势用批内均值和标准差归一化,在这批数据上做 4 个 epoch 的裁剪更新,共 300 轮:
| E[代理] | E[真实] | KL(π‖π_ref) | KL(π‖π*_β) | 最大的单个概率 | |
|---|---|---|---|---|---|
| 0 | 3.89 | 0.66 | 5.45 | — | 1.000 |
| 0.3 | 3.64 | 1.09 | 4.19 | 0.011 | 0.663 |
| 0.6 | 2.77 | 1.05 | 2.20 | 0.011 | 0.289 |
| 1.0 | 1.86 | 0.61 | 1.00 | 0.014 | 0.141 |
(参考: 下 E[代理] = −0.12、E[真实] = −0.35;代理奖励最高的那个回答,真实质量只有 0.66。)
读这张表:
- 时,PPO 收敛到闭式最优 附近(倒数第二列的 KL 只有 0.01 左右)。PPO 只是在用采样去逼近一个本来就有解析形式的分布,Day 4 的 DPO 正是从这里出发的。
- 时,策略坍缩到代理奖励最高的那一个回答上(最大概率 1.000):代理奖励最高,真实质量却不如 时。这就是 reward hacking,外加多样性的彻底丧失。
- 越大越保守,学到的越少。
§4 工程:一轮 RLHF-PPO
4.1 一轮的流程
1. 采样(推理) 从提示集取一批 x;用 π_θ 生成回答 y(温度 1,不加 top-p,见 §A.3)
2. 打分(前向) r_φ(x, y):奖励模型的分数,归一化
log π_old(y_t):生成时的策略 log 概率
log π_ref(y_t):参考模型的 log 概率
V(s_t):价值模型对每个位置的估计
3. 算优势 r_t = −β·(log π_old − log π_ref),最后一个 token 加上 r_φ;GAE 得到 Â_t 和回报
4. 更新(训练) 在这批数据上做若干个 epoch:策略用裁剪目标,价值模型用回归损失
5. 回到 1
4.2 几个关键的实现细节
- 价值模型从奖励模型初始化:两者都要”读懂回答并给出一个标量”,奖励模型是一个比随机初始化好得多的起点。
- 奖励归一化、优势白化:奖励减均值除标准差(Day 2 §2.2),优势在批内再归一化一次(§1.3 的基线思想)。
- 关掉 dropout:训练模式下两次前向的 log 概率不同, 在没有更新时就不等于 1。
- 监控:代理奖励、KL、裁剪比例(长期高于 0.2–0.3 说明步子太大)、回答长度、熵、价值损失。KL 和长度一起快速上涨,几乎总是 reward hacking 的信号。
- 采样和训练用同一套 log 概率:生成时用了 top-p、重复惩罚之类的改动,实际的采样分布就不是 了,重要性比值会有偏(§A.3)。
4.3 四个模型与显存
学习计划的原话:“policy / reference / reward / value 四个模型同时在显存里(这是它工程上难搞的根本原因)。”
| 模型 | 作用 | 训练吗 | 7B 时每参数字节 |
|---|---|---|---|
| 策略 | 生成、被优化 | 是 | 16 |
| 价值模型 | 估计每个位置的价值,给 GAE 用 | 是 | 16 |
| 参考模型 | 算 KL | 否 | 2 |
| 奖励模型 | 打分 | 否 | 2 |
第 6 组(7B,不含激活和 KV Cache):
| 方法 | 字节/参数 | 显存 |
|---|---|---|
| SFT | 16 | 100.4 GiB |
| PPO | 36 | 226.0 GiB |
| GRPO | 20 | 125.5 GiB |
| DPO | 18 | 113.0 GiB |
还有一笔没算进去的开销:每一轮都要用当前策略生成回答,这是推理,常常占了训练时间的大头。大规模实现(OpenRLHF、veRL 等)会把生成交给 vLLM 这样的推理引擎,训练和生成之间来回同步权重。
常见的省法:策略和价值模型都用 LoRA;参考模型就是”关掉 LoRA 适配器的策略”,不用单独存一份(peft 的 disable_adapter());奖励模型和参考模型卸载到 CPU,用的时候再搬回来。
4.4 PPO 的优点与代价(为 Day 4 做铺垫)
- 优点:在线采样,策略在自己的分布上学习,能发现示范和偏好数据里没有的好回答;奖励可以是任何东西;业界最成熟的 RLHF 方法(InstructGPT、早期的 ChatGPT、Llama 2)。
- 代价:四个模型、在线生成、十几个相互影响的超参数(、、、学习率、epoch 数、批大小……),训练不稳定、难复现。
Day 4 的 DPO 问的是:既然最优解有闭式形式(§3.5),能不能不做 RL,直接用偏好数据拟合它?
§A PPO:API 用法与边界
A.1 现成的实现
- trl:提供
PPOTrainer,但它的接口在几个大版本里重写过不止一次(参数名、需要传入的模型、数据格式都变过),以你安装版本的文档和示例为准。学习阶段建议先读懂参考代码的手写版本。 - OpenRLHF、veRL:面向大规模训练,用 Ray 调度、vLLM 生成,支持 PPO、GRPO、RLOO 等多种算法。
A.2 LoRA 下的参考模型
with policy.disable_adapter(): # peft:临时关掉适配器,得到的就是原始模型 = π_ref
logp_ref = token_logprobs(policy, seq, prompt_len)
省掉一整份参考模型的显存。前提是参考模型确实就是 LoRA 的底座(通常是 SFT 模型)。
A.3 采样设置与重要性比值
- rollout 时只用温度采样,不要加 top-k、top-p、重复惩罚:它们会改变实际的采样分布,而计算 时用的是原始分布,两者不一致,比值 在更新前就不等于 1。
- 温度不等于 1 时,计算 log 概率也要除以同样的温度。
- 大规模系统里生成用 vLLM、训练用 PyTorch,两者的数值(内核、精度)略有差异,同一个 token 的 log 概率会差一点。这也是为什么有些实现用训练框架重新计算 ,而不是直接用推理引擎返回的值。
A.4 结束符与 padding
回答长度不一。结束符之后的位置要从损失和 KL 里掩掉;奖励模型的分数加在结束符那个位置(或最后一个真实 token)上,不是加在 padding 上。忘了处理结束符的实现,常见症状是模型学会了”永远不停”或”立刻停”。
§B 实操
- 跑通
w3day3_checks.py,6 组结论都看懂。第 5 组改变 、学习率、epoch 数,观察收敛与坍缩。 - 推导(1h):(a) 策略梯度与基线不改变期望;(b) GAE 在 、 时的两个特例;(c) 裁剪目标对 的导数(四种情况);(d) 把每个 token 的 KL 奖励加起来,得到 Day 2 §3.3 的目标。
- 从零实现 PPO(2h):
python code/w3d3_ppo.py --selftest,然后在 Day 2 的奖励模型上跑--train()和--train --beta 0,对比两次的 gold 曲线和样本。 - 读论文:PPO(Schulman et al. 2017)的第 3 节;InstructGPT 的附录 C(RLHF 的训练细节);The N+ Implementation Details of RLHF with PPO(Huang et al. 2024)挑读。
参考实现:code/w3d3_ppo.py。
§C 自测
动机与直觉层
- 为什么不能把奖励模型的梯度直接反传给策略?策略梯度是怎么绕开这个问题的?
- 基线为什么不改变梯度的期望?为什么能降低方差?奖励整体加一个常数,没有基线会怎样?
- PPO 为什么要重要性采样?为什么要裁剪?裁剪截断了哪些情况、保留了哪些?
- LLM 的 RLHF 里 KL 惩罚是怎么进入训练的?有哪两种放法?
- 为什么 在 KL 很小时几乎没用?
推导与计算层
- 推出 。
- 写出 GAE 的公式,说明 和 各是什么,以及为什么价值网络越不准,最优 越大。
- 写出 PPO 的裁剪目标,分四种情况写出它对 的导数。
- 7B 模型做 PPO,四个模型的参数状态一共多少显存?GRPO、DPO 各省了什么?
工程层
- 画出一轮 RLHF-PPO 的流程,标出每一步哪些模型参与、哪些在训练。
- 为什么 rollout 时不应该用 top-p?为什么要关 dropout?
- 训练日志里 KL 和回答长度同时快速上涨,你怎么判断、怎么处理?
出关标准
- 能推出策略梯度,说清基线的作用,并知道 RLOO、ReMax、GRPO 在基线上的不同选择
- 能写出 GAE,解释 的偏差-方差权衡
- 能写出 PPO 的裁剪目标,分四种情况说清梯度
- 能说清 KL 惩罚在 LLM 的 PPO 里的两种放法,以及 、 的区别
- 能说出四个模型各自的作用和显存代价
- 跑通
w3d3_ppo.py的两次训练,看到(或解释了为什么没看到) 时的 reward hacking