CS336 · 从零构建语言模型
Lecture 17 · 对齐 III:RL 实战
源文件:lecture-17.md
Lecture 17 · 对齐 III:RL 实战
CS336: Language Modeling from Scratch · Stanford · Spring 2025
📅 May 27 · 💻 可执行讲义 · 讲者 Percy Liang
承上启下
上一讲(Lecture 16 · 对齐 II:RLVR):
- 讲了 PPO、GRPO 和 DeepSeek-R1 / Kimi / Qwen3 等 reasoning RL recipe。
- 核心观点:如果 reward 可测,就可以优化;但 RL 系统比预训练复杂得多。
本讲(RL 实战):
- 不再看大模型案例,而是把 policy gradient 的机械细节逐行拆开——这也与 CS221 的 Lecture 9 · 函数逼近与策略梯度(未找到对应页面) 直接呼应。
- 用一个玩具排序任务展示:reward 设计、baseline、advantage、GRPO 更新、KL 正则、冻结 old policy,以及 raw/centered/normalized rewards 的实验对比。
摘要
语言模型 RL 可以写成清晰的 policy gradient:采样回答、计算 reward、用$\nabla\log\pi(a\mid s)\cdot\delta$更新。难点不是公式,而是$\delta$怎么选以降低方差、old policy 怎么冻结、KL 怎么约束、reward 稀疏时怎么避免学崩。
1. 语言模型的 RL 形式化
先把 MDP 语言(参见 CS221 Lecture 7 · 马尔可夫决策过程(未找到对应页面))映射到语言模型:
| RL 概念 | 语言模型对应 |
|---|---|
| State$s$ | prompt + 已生成的 response 前缀 |
| Action$a$ | 下一个 token |
| Policy$\pi(a\mid s)$ | 当前语言模型 |
| Transition | 追加 token,确定性转移 |
| Trajectory | 完整生成序列 |
| Reward$R$ | 回答质量、是否正确、verifier 分数 |
语言模型的 MDP 有几个「退化」的好性质,让通用 RL 理论大幅简化:
- 转移是确定性的:选了 token 就是追加 token,没有环境随机性;
- outcome reward:只在完整回答结束后给一次分,中间步骤没有奖励;
- 不需要折扣与 bootstrapping:回合有限且 reward 在末尾,$\gamma=1$即可,也不必用 TD 学习去估中间价值。
目标是最大化期望奖励:
$$\max_\theta\ \mathbb{E}_{s\sim p,\ a\sim\pi_\theta(\cdot\mid s)}\left[R(s,a)\right]$$
期望同时对 prompt 分布$p(s)$和模型自己的采样分布$\pi_\theta(a\mid s)$取——后者依赖$\theta$,这正是 RL 与监督学习的本质区别:训练数据的分布随参数变化。
提示
既然转移确定、reward 只在末尾,可以干脆把「一整条回答」看作一个巨大的复合 action。这样问题退化成单步决策(contextual bandit):看到 prompt$s$,一次性选择回答$a$,拿到分数$R(s,a)$。本讲的推导都在这个视角下进行——它去掉了多步 RL 的全部复杂性(credit assignment、价值 bootstrap),留下的核心困难只有一个:如何用采样估计梯度,且方差不要爆炸。
2. Policy Gradient
把完整回答记作$a$,目标函数写开:
$$J(\theta) = \mathbb{E}_{s\sim p,\ a\sim\pi_\theta(\cdot\mid s)}\left[R(s,a)\right] = \int p(s)\,\pi_\theta(a\mid s)\,R(s,a)\,\mathrm{d}a\,\mathrm{d}s$$
难点:$\theta$藏在采样分布里,不能把梯度直接搬进期望。解法是 log-derivative trick(也叫 REINFORCE / score function estimator),利用恒等式$\nabla_\theta \pi_\theta = \pi_\theta\,\nabla_\theta\log\pi_\theta$:
$$\begin{aligned} \nabla_\theta J &= \int p(s)\,\nabla_\theta\pi_\theta(a\mid s)\,R(s,a)\,\mathrm{d}a\,\mathrm{d}s \\ &= \int p(s)\,\pi_\theta(a\mid s)\,\nabla_\theta\log\pi_\theta(a\mid s)\,R(s,a)\,\mathrm{d}a\,\mathrm{d}s \\ &= \mathbb{E}_{s,\,a\sim\pi_\theta}\left[\nabla_\theta\log\pi_\theta(a\mid s)\,R(s,a)\right] \end{aligned}$$
第二步把$\nabla\pi$改写成$\pi\nabla\log\pi$,目的正是凑回一个可以采样的期望。于是梯度可以用蒙特卡洛估计:采一条回答、算 reward、给这条回答的 logprob 加权:
loss = -logprob(response) * reward
loss.backward()
提示
Policy gradient 就是「加权版的 SFT」:SFT 对示范数据统一做最大似然;policy gradient 对自己生成的回答做最大似然,但权重是 reward——好回答学重一点,坏回答学轻一点(或反向)。公式一行,真正的战场全在「权重怎么定」上,这就是后面 baseline / advantage 的主题。
如果 reward 是 0/1 的稀疏信号,朴素估计有严重问题,且环环相扣:
- 只有答对的回答产生非零梯度,答错的回答权重为 0,没有负向更新——模型不知道「什么不该做」;
- 一批样本里可能一条都没答对,整批梯度为零,学习停滞;
- 单条轨迹的 reward 波动直接成为梯度波动,方差极高,需要巨大的 batch 才能得到可用的梯度方向。
3. Baseline:降低方差
标准修正:从 reward 里减去一个只依赖 state 的基线(baseline)$b(s)$:
$$\nabla_\theta J = \mathbb{E}\left[\nabla_\theta\log\pi_\theta(a\mid s)\,\bigl(R(s,a) - b(s)\bigr)\right]$$
这不改变期望梯度(无偏),因为被减掉的那一项期望为零:
$$\mathbb{E}_{a\sim\pi_\theta}\left[\nabla_\theta\log\pi_\theta(a\mid s)\,b(s)\right] = b(s)\,\nabla_\theta\sum_a \pi_\theta(a\mid s) = b(s)\,\nabla_\theta 1 = 0$$
关键在最后一步:概率对所有 action 求和恒等于 1,常数的梯度为零。注意这个论证要求$b(s)$不依赖$a$——依赖 action 的 baseline 会破坏无偏性。
讲义还给出理论上使方差最小的最优 baseline:
$$b^*(s) = \frac{\mathbb{E}\left[\|\nabla_\theta\log\pi_\theta(a\mid s)\|^2\, R \,\middle|\, s\right]}{\mathbb{E}\left[\|\nabla_\theta\log\pi_\theta(a\mid s)\|^2 \,\middle|\, s\right]}$$
即按梯度范数加权的平均 reward——不可计算,实践中用朴素的$b(s) = \mathbb{E}[R \mid s]$(该 state 的平均 reward)作为启发式替代。
3.1 为什么 baseline 有用?
讲义的数值例子——两个 prompt 难度悬殊:
- $s_1$(简单题):两条回答 reward 分别为 11 和 9,其中 9 其实是相对差的回答;
- $s_2$(难题):两条回答 reward 分别为 0 和 2,其中 2 是相对好的回答。
只看 raw reward,$s_1$的差回答(9 分)会比$s_2$的好回答(2 分)得到更强的正向推力——模型被推向「多做简单题的平庸答案」。减去各自 state 的均值后:
$$s_1:\ b=10 \Rightarrow \delta = +1, -1 \qquad s_2:\ b=1 \Rightarrow \delta = -1, +1$$
更新信号变成「相对同题的其他回答好不好」,题目难度这个干扰变量被消掉了。
提示
Reward 的绝对值里混着两种信息:「这道题本身好不好拿分」(state 的性质)和「这条回答比平均好多少」(action 的功劳)。policy 只能控制后者,前者对更新方向是纯噪声。baseline 就是把 state 的那部分减掉,只留 action 的功劳——这与 Lecture 15 · 对齐 I:SFT 与 RLHF 中「Bradley-Terry reward 只有相对值有意义」是同一个思想。
4. Advantage
用价值函数的语言把上面的直觉标准化:
$$V(s) = \mathbb{E}\left[R \mid s\right], \qquad Q(s,a) = \mathbb{E}\left[R \mid s, a\right], \qquad A(s,a) = Q(s,a) - V(s)$$
$V(s)$是「这个 state 平均能拿多少分」,$Q(s,a)$是「在这个 state 做这个 action 平均拿多少分」,优势$A(s,a)$就是「这个 action 比平均好多少」。在 outcome reward 且把完整回答当 action 的设定下,转移确定、reward 无中间随机性,所以:
$$Q(s,a) \approx R(s,a), \qquad A(s,a) \approx R(s,a) - b(s)$$
实际更新统一写成:
$$\nabla_\theta\log\pi_\theta(a\mid s)\cdot\delta$$
其中权重$\delta$的不同选法构成一个谱系,讲义实现了其中四种做实验(第 8 节):
- raw reward:$\delta = R$,无偏但方差大、受题目难度污染;
- centered reward:$\delta = R - \operatorname{mean}(R)$,组内减均值,即经验版 baseline;
- normalized reward:$\delta = (R - \operatorname{mean}(R))/\operatorname{std}(R)$,再除以组内标准差,即 GRPO 的 advantage;
- max reward:只保留组内最高分回答的$\delta$,其余置零——只模仿最好的那条(接近拒绝采样/Best-of-N 蒸馏);
- 更一般地还可以用 learned advantage(PPO + value network 的路线)。
5. GRPO 的机械流程
把上一讲的 GRPO 落到代码。对每个 prompt 采样$G$条回答并打分:
responses = model.generate(prompt, num_samples=G)
rewards = [verifier(prompt, r) for r in responses]
计算组内 advantage(centering 必做,除以 std 可选):
advantages = rewards - rewards.mean()
advantages = advantages / (rewards.std() + 1e-4) # 可选;Dr. GRPO 建议去掉
计算当前模型对这些回答的 logprob:
logps = model.log_prob(prompt, responses)
最朴素的 policy gradient loss(注意 detach:advantage 是权重,不参与求导):
loss = -(logps * advantages.detach()).mean()
PPO/GRPO 完整版加入新旧策略概率比与 clip(对应上一讲的$\mathcal{L}^{\mathrm{CLIP}}$):
ratio = torch.exp(logp_new - logp_old)
loss1 = ratio * advantage
loss2 = torch.clamp(ratio, 1 - eps, 1 + eps) * advantage
loss = -torch.min(loss1, loss2).mean()
ratio 的意义:这批数据是$\pi_{\mathrm{old}}$采的,用重要性比$\pi_\theta/\pi_{\mathrm{old}}$校正后可以对同一批数据做多步更新(提高样本效率);clip 则保证概率比不离开$[1-\epsilon, 1+\epsilon]$的可信区间。
注意
logp_old 必须冻结,绝不能让梯度穿过去——这是 LM RL 实现中最经典的隐蔽 bug,下一节专门拆。
6. 冻结 old policy
错误写法——logp_old 由当前模型算出,仍在计算图里:
logp_old = model.log_prob(prompt, response) # 仍在计算图里
ratio = torch.exp(logp_new - logp_old)
正确写法一——用独立的旧模型副本,且不进计算图:
with torch.no_grad():
logp_old = old_model.log_prob(prompt, response)
ratio = torch.exp(logp_new - logp_old)
正确写法二——至少显式截断梯度:
logp_old = logp_old.detach()
为什么这是致命 bug,因果链条如下:
- PPO/GRPO 的目标是「相对固定的旧策略改进」,ratio 的分母是参照系;
- 若梯度穿过
logp_old,优化器会发现「提高 ratio 最省力的方式是压低分母」——参照系自己动了,目标函数塌陷; - 极端情况:
logp_new与logp_old用同一次前向计算,则 ratio 恒等于 1,梯度恒为零,模型永远不更新; - 这类 bug 不报错、loss 曲线看起来还在动,只有对照「理应上涨的 reward 不涨」才能发现。
7. KL 正则:别把模型训飞
RL 只优化 reward,可能让模型遗忘通用能力,或塌缩成奇怪的分布。标准做法是对参考模型$\pi_{\mathrm{ref}}$(通常是 SFT 模型)加 KL 惩罚:
$$\mathbb{D}_{\mathrm{KL}}\left[\pi \,\|\, \pi_{\mathrm{ref}}\right] = \mathbb{E}_{x\sim\pi}\left[\log\pi(x) - \log\pi_{\mathrm{ref}}(x)\right]$$
直接按定义采样估计(即对每个样本取$\log\pi - \log\pi_{\mathrm{ref}}$)虽然无偏,但单样本可正可负、方差大。讲义采用 Schulman 提出的 k3 估计器:对$x\sim\pi$,令$r = \pi_{\mathrm{ref}}(x)/\pi(x)$,
$$\widehat{\mathrm{KL}} = r - \log r - 1$$
它同样是无偏估计($\mathbb{E}_{x\sim\pi}[r]=1$,而$\mathbb{E}[-\log r]$正是 KL 本身),且由$r - \log r - 1 \ge 0$保证每个样本的估计值非负,与 KL 恒非负的性质一致,方差显著更小。
KL 项的作用与代价:
- 作用:防止 policy 偏离 base/SFT 模型太远、保留通用语言能力、限制 reward hacking 的搜索空间;
- KL 太强:policy 被锚死在参考模型附近,学不动;
- KL 太弱:模型跑飞,出现乱码、复读、分布塌缩。
$\beta$(KL 系数)因此是 RLHF/RLVR 中最重要的超参之一;有些 RLVR 设置(如 R1-Zero 类实验,见 Lecture 16 · 对齐 II:RLVR)也会干脆去掉 KL 以换取更大的探索空间——前提是 reward 足够可信。
8. 讲义排序任务
讲义用一个能在 CPU 上几秒跑完的玩具任务,把上面所有组件串起来做对照实验。
任务设置:
- prompt:一串固定长度的数字(如
[1, 0, 2]); - response:模型输出的数字序列,目标是排好序的结果;
- reward:排序正确程度。
两个 reward 版本(对应稀疏与 shaped 两种设计哲学):
- 位置匹配(distance-based):response 与标准排序结果逐位置比较,每个位置对了得一分——信号较稀疏,接近全错时几乎无梯度可用;
- 包含 + 相邻有序(partial credit):prompt 中每个 token 出现在 response 里得一分,response 中每对相邻元素顺序正确再得一分——更稠密的塑形奖励(shaped reward),部分正确也有信号。
模型:刻意选了最简单的参数化——每个位置有独立的编码/解码矩阵,各位置独立解码、非自回归。它不是好的序列模型,但让 policy gradient 的行为一目了然,且训练极快。
实验对比($\delta$的四种选法):
| 更新信号 | 现象 |
|---|---|
| Raw rewards | 学得慢,容易停在差解(讲义原话:还没真正学会排序) |
| Centered rewards | 明显更好:差回答获得负更新,信号方向正确 |
| Normalized rewards | 相对 centered 增益不明显;std 归一化另有偏差 |
| Max rewards | 只强化组内最优回答,行为接近拒绝采样式模仿 |
讲义的观察与结论:
- centered reward 起效的机制:同一 prompt 下较差回答被显式压低,模型同时知道「该做什么」和「不该做什么」;
- 若同 prompt 所有回答 reward 相同(全对或全错),centered 后$\delta$全为 0,不产生更新——对应 GRPO 的零方差组问题;
- normalized 的 std 除法并非免费改进:Dr. GRPO 指出它引入难度偏差,且配合长度归一化会引入长度偏置,所以后续工作选择去掉;
- 总的教训(讲义原句):Reinforcement learning is not trivial, and you can easily get stuck in suboptimal states. ——即使在这么小的玩具任务上,RL 也会稳定地陷入局部最优。
提示
这个玩具实验的价值在于「控制变量」:模型、任务、数据全部固定,唯一变化的是$\delta$的定义,于是 raw/centered/normalized 的差异可以被干净地归因到方差与偏差上。在真实 LLM RL 里这些效应同样存在,只是被巨大的噪声和工程细节掩盖——先在玩具上建立直觉,再去读 DeepSeekMath/Dr. GRPO 的消融,因果就清楚了。
9. RL 工程直觉
9.1 Reward 设计决定学习信号
稀疏 0/1 reward 的优点是可信:判定规则简单,几乎无法被钻空子;缺点是方差大、冷启动难——模型一次都做不对时没有任何学习信号。Dense / shaped reward(如排序任务的 partial credit)让学习快得多,因为部分正确也有梯度;但塑形函数本身是人为设计的代理目标,设计不当就会被模型钻空子(比如只堆相邻有序对而不管完整性)。实践是谱系两端的折中:正确性用稀疏信号保真,辅以格式分、长度惩罚等少量塑形项。
9.2 Sampling 是训练的一部分
监督学习里数据是固定的;RL 里数据由采样超参实时生产。温度、top-p、每题采样数$G$都直接改变训练动态:温度太低则组内回答趋同、advantage 全零、无梯度;太高则质量崩坏、reward 全零。$G$越大,组内均值对$V(s)$的估计越准、advantage 方差越小,但 rollout 成本线性上涨。调 RL 时「生成配置」与「优化器配置」同等重要。
9.3 RL 比预训练更系统化
预训练的循环是纯计算流水线:
$$\text{data batch} \to \text{forward} \to \text{loss} \to \text{backward}$$
RL 的循环是一个带外部依赖的闭环:
$$\text{generate} \to \text{verify/reward} \to \text{compute advantage} \to \text{optimize policy}$$
其中 generate 是推理 workload(memory-bound,见 Lecture 10 · 推理),verify 可能是外部判分系统,而训练数据的分布又随 policy 每步漂移。任何一环的吞吐都可能成为全局瓶颈——上一讲第 7 节的系统性讨论正是本节的放大版。
总结
mindmap
root((RL 实战))
Setup
state
action
reward
rollout
Policy Gradient
log trick
sparse reward
high variance
Baseline
centered reward
advantage
variance reduction
GRPO
group samples
old policy
clipped ratio
KL
Experiments
raw reward
centered reward
normalized reward
max reward
local optima
关键要点:
-
Policy gradient 公式很简单
- $\nabla_\theta J = \mathbb{E}[\nabla_\theta\log\pi_\theta(a\mid s)\,R]$是核心;它就是「用 reward 加权的、对自己生成数据做的最大似然」。 -
Baseline 是降低方差的关键
- 减去只依赖 state 的$b(s)$严格无偏(因为$\mathbb{E}[\nabla\log\pi]=0$),却把「题目难度」这一噪声源整个消掉。 -
Advantage 是「比预期好多少」
- $A(s,a)=Q(s,a)-V(s)$;RL 应奖励相对好的动作,而不是绝对高分但相对平庸的动作。 -
GRPO 利用同 prompt 多回答形成自然 baseline
- 省掉 critic;但 std 归一化与长度归一化各自引入偏差,centering 是其中最稳的部分。 -
old policy 必须冻结
- ratio 的分母是参照系;让梯度穿过它,目标函数就塌了,且这个 bug 静默不报错。 -
RL 系统比公式复杂得多
- reward 设计、采样超参、KL 强度、生成调度共同决定结果;即使玩具任务也会稳定陷入局部最优。
课程收束:
到这里,CS336 走完了从零构建语言模型的全栈路径:从 Lecture 1 · 概览与分词 的 tokenizer,到 Lecture 3 · 架构与超参数 的 Transformer,经过 Lecture 5 · GPU 与 Lecture 8 · 并行 II:分布式训练实战 的系统层,Lecture 9 · 缩放定律 I:基础 的外推方法论,Lecture 10 · 推理 与 Lecture 12 · 评估,Lecture 13 · 数据 I 的数据工程,最后以 Lecture 15 · 对齐 I:SFT 与 RLHF → Lecture 16 · 对齐 II:RLVR → 本讲的对齐三部曲收尾。后两讲是嘉宾讲座,官方课表没有公开可整理材料,因此不在本系列中硬写。
复习自测
题目
因为期望所依据的分布$\pi_\theta$本身依赖$\theta$,$\nabla_\theta\int\pi_\theta R = \int(\nabla_\theta\pi_\theta)R$,被积函数里是$\nabla\pi$而不是「$\pi$乘以某函数」,无法写成对$\pi_\theta$的期望、也就无法采样。利用$\nabla\pi_\theta = \pi_\theta\nabla\log\pi_\theta$改写后得$\int \pi_\theta(\nabla\log\pi_\theta)R = \mathbb{E}_{a\sim\pi_\theta}[\nabla\log\pi_\theta\, R]$,重新凑出了期望形式,蒙特卡洛采样即可估计。
题目
$\mathbb{E}_{a\sim\pi}[\nabla\log\pi(a\mid s)\,b(s)] = b(s)\sum_a \pi(a\mid s)\nabla\log\pi(a\mid s) = b(s)\nabla\sum_a\pi(a\mid s) = b(s)\nabla 1 = 0$。关键性质:$b(s)$不依赖 action$a$,才能提到求和号外,让求和塌缩成常数 1。若$b$依赖$a$,提不出去,无偏性即被破坏。
题目
Raw:四条回答的权重为 11、9、0、2,$s_1$的差回答(9)被推高的力度远超$s_2$的好回答(2),模型被引向简单题的平庸解,难题的正确方向几乎没被强化。Centered(各自减组内均值 10 和 1):$\delta$变为$+1,-1$与$-1,+1$,每道题内部好回答被推高、差回答被压低,难度这一 state 变量被消除。
题目
若 old 与 new 共用同一次前向,ratio$\equiv 1$、梯度恒为零,模型静默地完全不学。若 old 是同一模型的另一次可导前向,优化器会通过压低分母来虚增 ratio,参照系随优化漂移,clip 失去意义,目标函数塌陷、训练发散或学到退化解。两种模式都不抛错误,只能靠「reward 曲线不涨」等外部证据发现。
题目
朴素估计单样本可正可负(虽然期望非负),方差大,小 batch 下 KL 估计会剧烈抖动甚至为负,作为惩罚项会引入错误方向的梯度。k3 因$r-\log r-1\ge 0$(等号当且仅当$r=1$)保证逐样本非负,与 KL 的非负性一致,且在$\pi\approx\pi_{\mathrm{ref}}$的常态下取值接近 0、方差小,作为正则项数值行为稳定得多。