SFT 之后的模型会对话了,但「会对话」和「答得好」是两回事。这一篇讲怎么让模型学会在多个都通顺的回答里挑更好的那个,产出 dpo.py。
前置:06 篇跑完的 SFT checkpoint。
零、开始之前:SFT 之后还缺什么
0.1 SFT 的天花板在哪
SFT 干的事是模仿:给一堆「问题 → 标准回答」,让模型学着照做。
这套办法有个天生的上限。模仿只能让模型逼近训练数据的水平,没法超过它。 而且模仿是无差别的,训练数据里写得好的和写得一般的,模型一视同仁地学。
更关键的是,SFT 只告诉模型「什么是对的」,从来没告诉它「什么是不好的」。模型不知道哪些回答该避免。
0.2 一个具体的例子
同一个问题,两个都通顺的回答:
问:帮我写一封请假邮件
回答 A:
好的,这是一封请假邮件:
尊敬的领导,我因身体不适需请假一天,望批准。此致敬礼。
回答 B:
好的,这是一封请假邮件:
主题:请假申请(3 月 5 日)
尊敬的张经理:
我因感冒发热需于 3 月 5 日请假一天, 期间工作已交接给李明,
紧急事项可电话联系我。给您带来不便,敬请谅解。
王强
2026 年 3 月 4 日
两个都没错,语法都通,都是「请假邮件」。但 B 明显更好。
SFT 没法表达这种差别——它的数据里只有一个标准答案,没有「这两个之中 B 更好」这种信息。
偏好对齐要做的就是把这种比较信息喂给模型。
关键不在于 A 写得差,而在于 A 完全合格。SFT 的损失函数没有任何一项能区分「合格」和「更好」,所以哪怕训练数据里全是 B 这样的回答,模型学到的也只是「像 B 一样写」,而 不是「B 比 A 好」这条可以外推的判断。
0.3 偏好数据长什么样
不再是「问题 + 答案」,而是「问题 + 更好的答案 + 更差的答案」:
{
"prompt": [{"role": "user", "content": "帮我写一封请假邮件"}],
"chosen": "主题:请假申请……(详细版)",
"rejected": "尊敬的领导,我因身体不适需请假一天……(简略版)"
}
行业黑话叫 chosen 和 rejected,或者 yw(win)和 yl(lose)。
标注这种数据比标注 SFT 数据容易得多:让人从头写一个好回答很难,但让人在两个回答里选一个更好的,很快。这是偏好学习能规模化的原因。
数据结构上只多了一个字段,工程上却打开了一个新维度:模型第一次拿到了「相对」的信号。后面 DPO 的整个推导都建立在这一点上 —— 它的损失函数里,chosen 和 rejected 永远是成对出现、相减的。
0.4 三条路线
从偏好数据到对齐的模型,主流有三条路:
| 路线 | 一句话 | 出现时间 |
|---|
| PPO | 先训个奖励模型打分,再用强化学习去最大化分数 | 2022,ChatGPT 用的 |
| DPO | 数学上把 RL 消掉,变成一个监督学习问题 | 2023 |
| GRPO | 回到 RL,但去掉最重的那个部件 | 2024,DeepSeek 推广 |
这一篇以 DPO 为主(简单、稳、单卡跑得动),但会先讲 PPO,因为不理解 PPO 就理解不了 DPO 在消掉什么。最后讲 GRPO 为什么又转回 RL。
三条路线在时间上是顺序的,在能力上不是。DPO 用一个巧妙的数学变换换来了极大的工程简化,代价是失去在线采样和探索;GRPO 承认这个代价在数学、代码这类任务上付不起,于是回到 RL,只把 PPO 里最贵的那件东西扔掉。
一、经典 RLHF:PPO 那条路
1.1 三个阶段
06 篇做完的是阶段一。
1.2 奖励模型
阶段二训一个奖励模型(reward model,RM)。它的输入是「问题 + 回答」,输出是一个标量分数。
训练方式是让它在偏好对上排序正确,loss 是:
LRM=−logσ(r(x,yw)−r(x,yl))
含义很直白:让 chosen 的分数高于 rejected,差距越大 loss 越小。σ 是 sigmoid。
记住这个式子的形状,DPO 的 loss 跟它长得极像,这不是巧合。
1.3 PPO 阶段要同时装四个模型
阶段三用强化学习。模型生成回答,奖励模型打分,用分数当奖励信号去更新模型。
麻烦在于显存里要同时驻留四个模型:
| 模型 | 作用 | 要训吗 |
|---|
| policy | 正在训练的模型 | 要 |
| ref | SFT 后的模型,冻结,用来算 KL 约束 | 不 |
| reward | 打分 | 不 |
| critic | 估计状态价值,给 PPO 算优势函数 | 要 |
三条路线各要驻留几个模型,是它们工程复杂度的核心差异:
PPO 的 critic 与 policy 同样大且同样要训,占了显存的一半 ,这是它工程复杂度高的主要来源。GRPO 用一组采样的平均分替代 critic 的价值估计,DPO 则通过 2.2 节的推导把奖励模型也消掉,只剩策略与参考两个模型。
00-pretrain-0.5b/align_math.py 算出来的账:
路线 模型数 字节/参数 显存
PPO(经典 RLHF) 4 36 18.08 GB
GRPO 3 20 10.04 GB
DPO 2 18 9.04 GB
PPO 要 18.08 GB,是 DPO 的两倍。而且 PPO 训练过程中要不断采样生成,超参多、调起来不稳,工程复杂度远高于另外两条路。
1.4 KL 惩罚在防什么
PPO 的目标函数里有一项 KL 惩罚,约束 policy 别离 ref 太远:
πmax E[r(x,y)]−βKL(π∥πref)
为什么需要它。因为奖励模型是个近似,它有漏洞。如果放任模型只管把分数刷高,它会找到奖励模型的破绽,生成一些分数极高但人看了莫名其妙的东西。这叫奖励攻陷(reward hacking)。
KL 惩罚就是拴住模型的绳子:可以变好,但别变得面目全非。
这一项在 DPO 里也在,只是形式变了,2.2 节会看到。
奖励攻陷不是罕见的失败,是无约束优化的默认结局:只要奖励是学出来的近似,优化器就一定会去找它和真实偏好之间的缝隙。所以这一项不是调优手段,是必需品 —— 记住这一点,2.3 节看到 DPO 里那个比值时才知道它是从哪来的。
二、DPO:把 RL 消掉
2.1 核心洞察
DPO 那篇论文(Rafailov et al., 2023)的关键发现是:1.4 节那个「带 KL 约束的奖励最大化」问题,有闭式解。
解出来长这样:
π∗(y∣x)=Z(x)1πref(y∣x)exp(β1r(x,y))
意思是最优策略等于参考模型乘一个跟奖励有关的指数项。Z(x) 是归一化因子。
这个式子本身没法直接用,因为 Z(x) 要对所有可能的回答求和,算不出来。
但可以把它反过来解出 r:
r(x,y)=βlogπref(y∣x)π∗(y∣x)+βlogZ(x)
这一步是整个 DPO 的转折点。它说明:奖励函数可以用策略模型自己表示出来,不需要单独训一个奖励模型。
2.2 Z(x) 怎么消掉的
上面那个式子还带着算不出来的 Z(x)。但注意 1.2 节奖励模型的 loss 里,r 只以差值的形式出现:r(x,yw)−r(x,yl)。
两个回答对应同一个问题 x,所以它们的 βlogZ(x) 完全相同,一减就没了:
r(x,yw)−r(x,yl)=βlogπref(yw∣x)π(yw∣x)−βlogπref(yl∣x)π(yl∣x)
代回 1.2 节那个 loss,就得到 DPO 的目标:
LDPO=−logσ(βlogπref(yw∣x)π(yw∣x)−βlogπref(yl∣x)π(yl∣x))
没有奖励模型,没有 critic,没有采样,没有 RL。 就是一个可以直接反向传播的监督损失。
这就是 DPO 这个名字的由来:Direct Preference Optimization,直接用偏好优化,中间不绕奖励模型。
这五格里最该慢下来看的是 ③ 到 ④:③ 得到的式子带着一个算不出来的 Z(x),看起来是死路;④ 发现它根本不需要被算出来,因为它在差值里自己抵消了。整个 DPO 的价值就压在这一步上,其余四步都是常规推导。
2.3 每一项在做什么
把式子拆开看: