你可能背得出 DPO 的损失函数,却写不出它的训练循环;读过五篇后训练综述,还是说不清 PPO 和 GRPO 在代码层面到底差了哪几行。这不怪你——散文有歧义,公式藏细节,只有伪代码没有地方可躲:数据从哪来、几个模型在显存里、损失怎么算,每一行都必须交代。这篇把主流后训练策略全部写成伪代码,一次排黑。
一句话主线
先把地图给你:
所有后训练方法都是同一个循环的变体:
采样 → 打分 → 更新。它们的全部区别,只在三个槽位怎么填——回答由谁生成?监督信号是什么形式?信号怎么变成梯度?
写成母模板就是:
while not converged:
x, y = get_data() # 槽位1:回答 y 从哪来?(人写的 / 模型自己生成的)
signal = get_signal(x, y) # 槽位2:信号是什么?(标准答案 / 打分 / 偏好对 / 规则判定)
loss = to_loss(y, signal) # 槽位3:信号怎么变成损失?
policy.update(loss) # 这一行永远不变:梯度下降
下面每种方法出场时,你只需要盯住这三个槽位。读完你会发现:后训练没有六种算法,只有一个循环和六种填法。
阅读约定
伪代码是教学简化版,省略了 batch、padding、分布式这些工程细节,但保留所有决定算法身份的结构。几个记号贯穿全文:
policy:正在训练的模型(也叫策略,)ref:冻结的参考模型(通常是 SFT 之后的快照,),只做前向,不更新rm:奖励模型(Reward Model),给回答打标量分logp(model, y | x):模型model生成回答y的对数概率(逐 token 求和或保留逐 token 形式)
前置知识只需要两句话:预训练教会模型接话,SFT 教会它照做,但示范教不会”做得更好”——这是后训练存在的理由。如果这两句话对你还不透,先读《示范教不会的东西:为什么大模型需要强化学习》,零公式。
第 0 式:SFT——一切的地板
监督微调(Supervised Fine-Tuning)是后训练的起点,也是最诚实的一段代码:
# 槽位1:y 是人类(或更强模型)写好的示范
# 槽位2:信号 = 示范本身,"这就是标准答案"
# 槽位3:交叉熵,最大化示范的似然
for x, y_human in D_demo: # 几万条 (指令, 优质回答)
loss = -logp(policy, y_human | x) # 就是预训练的 next-token loss
policy.update(loss)
逐行要点:
- 循环里只有一个模型。没有采样、没有打分,生成开销为零——这是所有方法里最便宜的。
- 损失和预训练完全相同,换的只是数据:从”互联网上随便什么文本”换成”人类精心写的示范”。
- InstructGPT 用了约 1.3 万条示范 prompt 做这一步。
天花板也写在代码里:y_human 是谁写的,模型就最多学到谁的水平;而且循环里没有任何一行代码能表达”这个回答不好”——SFT 只能拉高好回答的概率,不能压低坏回答的概率。这两个缺陷,后面每一式都在补。
部件:奖励模型——把人类判断力做成函数
在进入 RL 之前,先造一个部件。人类判断”哪个回答更好”又快又准,但判断力没法直接塞进训练循环——得先把它蒸馏成一个可以无限调用的函数:
# 数据:人类对同一个 x 的两个回答标出偏好
# D_pref = [(x, y_win, y_lose), ...]
rm = policy_sft.clone_with_scalar_head() # 语言模型头换成"输出一个分数"的头
for x, y_win, y_lose in D_pref:
s_w = rm(x, y_win) # 标量分
s_l = rm(x, y_lose)
loss = -log(sigmoid(s_w - s_l)) # Bradley-Terry:赢家分数应高于输家
rm.update(loss)
逐行要点:
- 损失只关心分差
s_w - s_l,不关心绝对值——人类标的是”A 比 B 好”,不是”A 值 87 分”。这个”只会比较、不会绝对打分”的出身,埋下了后面 GRPO 组内归一化的伏笔。 - 训练完成后
rm冻结,变成一个”人类判断力模拟器”,可以每秒调用几千次——这是 RLHF 能规模化的前提。
第 1 式:RLHF-PPO——完整但昂贵的正统
有了奖励模型,就可以让模型自己生成、被打分、按分数调整。这是 InstructGPT 的第三步,也是 ChatGPT 背后的经典配方:
# 槽位1:y 由 policy 自己在线生成(on-policy)
# 槽位2:信号 = rm 给整段回答的标量分 − 逐 token 的 KL 惩罚
# 槽位3:PPO clip 损失,优势加权,一批数据复用多个 epoch
policy = policy_sft.clone()
ref = policy_sft.frozen() # 防跑偏的锚
rm = trained_rm.frozen() # 裁判
critic = value_model() # 第 4 个模型:预测"从这往后还能拿多少分"
for iteration in range(N):
# ---- A. 采样(最贵的一步)----
xs = sample(D_prompts)
ys = [policy.generate(x) for x in xs] # rollout:完整生成回答
# ---- B. 打分:造出每个 token 的优势 ----
for x, y in zip(xs, ys):
score = rm(x, y) # 整段一个分,只落在最后一个 token
for t in tokens(y): # 逐 token 的 KL 惩罚,防跑偏
kl_t = logp(policy, y_t | x, y_<t) - logp(ref, y_t | x, y_<t)
reward_t = -beta * kl_t + (score if t == last else 0)
A_t = GAE(reward_t, critic) # 优势:这个 token 比平均好多少
# ---- C. 复用这批昂贵数据,更新 K 个 epoch ----
old = policy.frozen_snapshot()
for epoch in range(K):
ratio_t = exp(logp(policy, y_t) - logp(old, y_t)) # 新旧策略概率比
loss = -mean(min(ratio_t * A_t,
clip(ratio_t, 1 - eps, 1 + eps) * A_t)) # PPO 的灵魂:clip
policy.update(loss)
critic.update(mse_loss) # critic 同步学"预测回报"
逐行要点:
- 数一数显存:
policy、ref、rm、critic——四个模型同时在线,其中两个还要训练。这就是”PPO 贵”的字面意思。 A_t是逐 token 的信号:不只知道”整段回答值多少分”,还试图分辨”哪个词贡献了分数”。这份精细由critic提供,也是它存在的唯一理由。ratio和clip那两行在解决”采样太贵所以要复用数据,复用数据策略就会偏离数据来源”的矛盾。每个部件的完整推导在《PPO 的训练循环》,这里不重复。- 逐 token 的 KL 惩罚是缰绳:奖励模型是个近似品,往死里优化它必然被钻空子(为什么必然,见《目标函数即命运》)。
适用边界:效果的正统上限,工程的正统噩梦——四模型编排、超参敏感、训练不稳,是后面所有”平替”存在的理由。
第 2 式:拒绝采样微调——把 RL 退化成”筛数据 + 再 SFT”
第一个平替思路简单到令人发笑:既然 SFT 便宜稳定,那就让模型自己生成一堆回答,用奖励模型筛出最好的,再拿去 SFT。Llama 2 的大部分对齐迭代用的就是它(论文里叫 rejection sampling fine-tuning),RAFT 给了这个范式一个正式名字。
# 槽位1:y 由 policy 批量生成,但离线筛选后再用
# 槽位2:信号 = rm 挑出 Best-of-K(或规则:答案对才保留)
# 槽位3:普通交叉熵——训练部分和 SFT 一模一样
for round in range(R): # 可以迭代多轮
D_new = []
for x in D_prompts:
ys = [policy.generate(x, temperature=1.0) for _ in range(K)] # 采 K 个
y_best = argmax(ys, key=lambda y: rm(x, y)) # 只留最好的
D_new.append((x, y_best))
policy = SFT(policy, D_new) # 复用第 0 式,一行都不用改
逐行要点:
- 更新阶段就是 SFT,所以继承了 SFT 的全部稳定性:没有 ratio、没有 clip、没有 critic,不会训崩。
- 与 SFT 的唯一区别在数据来源:示范不再是人写的,而是模型自己写、裁判挑出来的。模型的上限不再是标注员水平,而是”自己 K 次尝试里的最好水平”——K 越大,筛出的样本越好。
- 也继承了 SFT 的缺陷:
y_best之外的 K−1 个样本直接扔掉,“什么是坏”的信息依然一比特都没用上。 - Llama 2 直到 RLHF-V4 都只用这一式迭代,最后一版才叠加 PPO 收尾——工程上”先用便宜的吃掉大部分收益,再用贵的榨最后一段”是常见组合。
第 3 式:DPO——把 RL 问题变成分类问题
第二个平替更激进:连奖励模型都不训了。DPO(Direct Preference Optimization)的洞察是:KL 约束下的 RLHF 最优解有闭式形式,把它反过来代入奖励模型的训练损失,奖励模型就被消掉了——偏好数据可以直接训练策略:
# 槽位1:y 是离线的偏好对,谁生成的都行(人 / 别的模型 / 旧版本自己)
# 槽位2:信号 = 成对偏好 (y_win, y_lose)
# 槽位3:一个 sigmoid 分类损失,没有采样、没有 RL
policy = policy_sft.clone()
ref = policy_sft.frozen()
for x, y_win, y_lose in D_pref:
# "隐式奖励":策略相对参考模型,把这个回答的概率抬高了多少(对数尺度)
r_w = beta * (logp(policy, y_win | x) - logp(ref, y_win | x))
r_l = beta * (logp(policy, y_lose | x) - logp(ref, y_lose | x))
loss = -log(sigmoid(r_w - r_l)) # 和奖励模型的损失长得一模一样!
policy.update(loss)
逐行要点:
- 对照第 1 式数显存:四个模型 → 两个(policy + 冻结的 ref),没有 rollout、没有 critic、没有 RM。训练形态和普通微调无异,这是它横扫开源社区的原因。
loss那行和奖励模型的 Bradley-Terry 损失完全同形——区别只是分数换成了beta * log(policy/ref)。这就是论文标题《你的语言模型偷偷是个奖励模型》的字面意思。- 注意循环里没有
policy.generate()。这是 DPO 最大的优点,也是最大的软肋:它永远在别人(或过去的自己)生成的回答上学习,是 off-policy 的。训练越久,policy 的真实行为离偏好数据的分布越远,信号越失真——想缓解就得重新采样、重标偏好、再训一轮,“免 RL”的优势随之打折。
适用边界:有现成偏好数据、算力有限、要快速对齐——DPO 是默认起手式。要在自己的生成分布上持续爬坡——迟早要回到在线方法(第 1、5 式)。
第 4 式:KTO——连偏好对都凑不齐时
DPO 要求成对数据:同一个 prompt 必须有一好一坏两个回答。现实里更常见的是不成对的散装信号——用户对单条回答点了个赞或踩。KTO 直接吃这种数据:
# 槽位1:y 离线,单条即可,不需要成对
# 槽位2:信号 = 二元标签(好 / 坏),比如点赞点踩
# 槽位3:前景理论式的价值函数,好坏两头不对称
for batch in D_binary: # [(x, y, label), ...]
z0 = mean_KL(policy, ref, batch) # 参考点:本批数据的平均偏移量
for x, y, label in batch:
r = beta * (logp(policy, y | x) - logp(ref, y | x)) # 和 DPO 同款隐式奖励
if label == GOOD:
v = lambda_D * sigmoid(r - z0) # 好样本:抬得比参考点高才算赚
else:
v = lambda_U * sigmoid(z0 - r) # 坏样本:压得比参考点低才算赚
loss = -v
policy.update(loss)
逐行要点:
- 隐式奖励
r和 DPO 完全同款,变的是外层包装:不再比较两个回答,而是每条回答独立地和参考点z0比。 lambda_D/lambda_U两个系数不对称——损失厌恶(人对”变坏”比对”变好”敏感)是从行为经济学前景理论里搬来的设计,也是 KTO(Kahneman-Tversky Optimization)名字的由来。- 实践价值一句话:偏好对贵,点赞点踩遍地都是。 KTO 论文报告在 1B–30B 规模上效果匹配甚至超过 DPO,且对数据不平衡相当鲁棒。
第 5 式:GRPO——去掉 critic 的在线 RL,推理时代的主力
回到在线 RL。PPO 四个模型里最别扭的是 critic:和策略一样大、要单独训练、还训不太准。DeepSeekMath 提出的 GRPO(Group Relative Policy Optimization)用一个统计学老技巧把它整个删掉:同一个 prompt 采一组回答,用组内相对分数当优势。
# 槽位1:y 由 policy 在线生成,且同一个 x 采一组 G 个
# 槽位2:信号 = 规则奖励(答案对/错、格式合规)或 RM 分,组内归一化
# 槽位3:PPO 同款 clip 损失,但优势不来自 critic,来自组内对比
policy = base_model.clone() # R1-Zero 甚至直接从 base 起训
ref = base_model.frozen()
for step in range(N):
x, answer = sample(D_verifiable) # 带标准答案的题(数学/代码)
ys = [policy.generate(x) for _ in range(G)] # 一组 G 个回答(如 16 个)
rs = [reward(y, answer) for y in ys] # 规则打分:答对 1 分,答错 0 分,格式加分
A = (rs - mean(rs)) / std(rs) # 组内归一化 = 优势。critic 的活,均值干了
old = policy.frozen_snapshot()
for y_i, A_i in zip(ys, A):
ratio_t = exp(logp(policy, y_t) - logp(old, y_t))
loss_i = -min(ratio_t * A_i, clip(ratio_t, 1 - eps, 1 + eps) * A_i) \
+ beta * KL(policy, ref) # KL 直接加在损失上,不再混进奖励
policy.update(mean(loss_i))
逐行要点:
- 灵魂是
A = (rs - mean) / std这一行:“这道题 16 个回答里你比平均好多少”。奖励模型(和人类偏好)本来就只会比较不会绝对打分,组内相对化恰好对齐了这个天性。critic 及其全部训练麻烦,一行均值解决。 reward()若是规则函数(答案核对、单元测试),整个循环里连 RM 都没有——这就是 RLVR(可验证奖励强化学习)。DeepSeek-R1 靠”规则奖励 + GRPO”训出了长链推理,证明裁判可以是一个if语句。- 代价也写在代码里:每步要生成
G个完整回答,采样开销比 PPO 更重;且当一组回答全对或全错时,rs方差为零,优势全为 0,这条 prompt 的采样全部白费——著名的工程痛点,后续改进(动态采样、课程筛题)大多冲它去。 - 对比第 1 式:clip 结构原封不动,KL 从奖励挪到损失,critic 换成组内均值。GRPO 不是新物种,是 PPO 的减法。
全景对比:一张表看清三个槽位
| 方法 | 回答谁生成 | 信号形式 | 显存里几个模型 | 生成开销 | 能否”压坏” |
|---|---|---|---|---|---|
| SFT | 人 / 更强模型 | 标准答案 | 1 | 无 | 否 |
| 拒绝采样 RFT | 自己(离线批量) | Best-of-K 筛选 | 2(分时可 1) | 高(K 倍采样) | 否 |
| DPO | 任意来源(离线) | 成对偏好 | 2 | 无 | 是 |
| KTO | 任意来源(离线) | 单条好/坏标签 | 2 | 无 | 是 |
| RLHF-PPO | 自己(在线) | RM 标量分 | 4 | 高 | 是 |
| GRPO | 自己(在线,成组) | 规则 / RM 组内相对 | 2–3 | 最高(G 倍采样) | 是 |
两条读表线索:
- 纵向看”生成开销”和”能否压坏”的对角线:便宜的方法(SFT、DPO)用不上模型自己的探索,贵的方法(PPO、GRPO)在自己的分布上闭环学习。天下没有免费的对齐。
- “能否压坏”一列就是 SFT 家族和偏好/RL 家族的分水岭:前者只能拉高好的,后者能同时压低坏的——负梯度是花钱买来的能力。
组合拳:真实流水线怎么拼
没有哪个前沿模型只用一式。三条有代表性的流水线,用一行伪代码写完:
# InstructGPT (2022):三步正统
model = PPO(RM(SFT(base)))
# Llama 2-Chat (2023):便宜的先上,贵的收尾
model = SFT(base)
for v in [1, 2, 3, 4]:
model = RFT(model, rm_v) # 每轮都用新偏好数据重训 RM
model = PPO(RFT(model, rm_5)) # 最后一版才叠 PPO
# DeepSeek-R1 (2025):SFT 和 RL 交替两轮
model = SFT(base, cold_start_cot) # 几千条冷启动长思维链
model = GRPO(model, rule_rewards) # 推理 RL:答案 + 格式 + 语言一致性
data = rejection_sample(model, keep=correct) + general_sft_data # 约 80 万条
model = SFT(base, data) # 注意:回到 base 重新 SFT
model = GRPO(model, rule_rewards + preference_rm) # 全场景 RL 收尾
R1 那条最值得看:拒绝采样(第 2 式)在这里不是对齐算法,而是数据工厂——用 RL 训到高水平的模型给自己批量生产 SFT 数据。六式不只是并列的选项,还能互相当零件。
收官:其实只有一个算法
把六段伪代码的梯度全部展开,会发现它们是同一个形状:
全都是加权的 next-token 梯度,区别只在权重 从哪来:
- SFT:,人类示范说了算
- 拒绝采样:,但只给自己的 Best-of-K
- PPO / GRPO:(优势),可正可负,裁判说了算
- DPO:赢家拿正权重、输家拿负权重,权重大小由”当前模型把偏好排得多错”决定(这是 DPO 论文自己给出的梯度解读)
这个视角把整个领域压缩成一句话:后训练就是在决定”给哪些 token 的概率上调或下调、调多大力度”——所有算法之争,都是权重来源之争。 谁提供 ,谁就是事实上的目标函数;而目标函数即命运。
想继续往下钻:PPO 每个部件的完整推导在《PPO 的训练循环》,权重(目标)设计如何塑造模型性格在《目标函数即命运》。
参考来源
一手论文(arXiv):
- Training language models to follow instructions with human feedback(InstructGPT, 2203.02155)
- Llama 2: Open Foundation and Fine-Tuned Chat Models(2307.09288)
- RAFT: Reward rAnked FineTuning for Generative Foundation Model Alignment(2304.06767)
- Direct Preference Optimization: Your Language Model is Secretly a Reward Model(2305.18290)
- KTO: Model Alignment as Prospect Theoretic Optimization(2402.01306)
- DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models(GRPO, 2402.03300)
- DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning(2501.12948)
- Proximal Policy Optimization Algorithms(1707.06347)
工程实践与综述: