← 返回首页
10_dpo_viz.html
手搓 DPO:从"会答"到"答得合人意"
手搓三关的收尾。第 8、9 关用 SFT 把 base 教成会答题;这一关换任务 ——
偏好对齐。
用 DPO:
不训奖励模型、不走 RL,拿一份冻结的参考模型当锚,直接把"chosen 比 rejected 好"这件事用一个 sigmoid loss 学进去。
配套代码 phase2-sft-lora/08_dpo.py。
这一关
起点 SFT 模型 06 的产物
ref 冻结 SFT 的副本·当锚
β 0.1 能偏多远
偏好对 10 · 不走 RL
① 同一个问题,两个都"答到了",但有好坏之分
SFT 之后,模型已经会答题了。可"会答"不等于"答得好" —— 同一条指令,它可能给出好几种都说得通的回答,
但有的礼貌/简洁/得体,有的生硬/啰嗦/冒犯。偏好对齐要做的,就是让它更偏向左边那种。
点切换看几组真实偏好对(08_dpo.py 的训练数据):
注意:这里两个回答都不是"错字病句",纯是偏好之分 —— 这正是 SFT 难覆盖的部分。
SFT 教"标准答案长什么样";偏好对齐教"在多个合理答案里挑人更喜欢的那个"。所以它接在 SFT 之后。
↳ 代码:08_dpo.py 的 PREF 列表(指令, chosen, rejected)
↳ 下一步:怎么在不训奖励模型的情况下,给"哪个更好"打分?DPO 的巧办法。
② 两个模型 + 一个"隐式奖励"
RLHF 的做法是先训一个奖励模型打分、再用 RL 去最大化分数,链路长。
DPO 把这步省掉:它只用两个模型,"分数"直接从这两个模型的概率差里读出来。
policy(要训练)
πθ
从 SFT 模型出发,
DPO 会更新它
❄ ref(冻结)
πref
SFT 模型的副本,
永不更新,当锚点
某条回答 y 的
隐式奖励 r(y) =
β · ( logπθ(y) − logπref(y) )
= "policy 比 ref 多给了这条回答多少对数概率" × β
不需要额外的奖励模型:
"policy 相对 ref 把某条回答抬高了多少"本身就是分数。
ref 当锚点的意义是 —— 只奖励"
相对变化",防止 policy 为了刷分而整体跑偏、把语言能力学崩。
一条回答的
logπ 就是它每个 token 的 log 概率之和(
只算 response 段,和
SFT 的 loss mask 同一套)。
↳ 代码:08_dpo.py 的 seq_logprob(只累加 response 段)+ main 里 policy / ref 两份模型
↳ 下一步:有了两条回答各自的分数,DPO 的 loss 只是想让它俩"拉开差距"。
③ DPO loss:把 chosen 的分数拉到 rejected 之上
记 Δ = logπθ − logπref(policy 相对 ref 的偏移)。DPO 想让 chosen 的 Δ 高于 rejected 的 Δ。
loss 就是个二分类的 logistic 形式 —— 拨动下面两个滑块(模拟训练把它俩推开),看 loss 怎么掉:
0.1
0.0
52%
σ(β·margin)=把 chosen 排前面的概率
0.66
loss = −log σ(β·margin)
读出三件事:① margin 越大 → loss 越小;② 起点 policy≡ref 时 Δc=Δr=0、margin=0、loss=ln2≈0.693(瞎猜);
③ 把 rejected 往左压和把 chosen 往右抬都能拉大 margin —— 实测里 DPO 主要靠压低 rejected(下一步会看到)。
β 固定 0.1:它是"放大镜",margin 同样大、β 越大 loss 反应越猛。
↳ 代码:08_dpo.py 的 dpo_pass:loss = -F.logsigmoid(beta*(d_chosen - d_rejected))
↳ 下一步:真跑起来,看这条 margin 怎么被一步步拉开。
④ 训练起来:loss 往下、margin 往上
把全部 10 条偏好对的梯度累起来更新一次 policy(ref 始终不动),这就是一个 epoch。
点"跑一个 epoch"看 08_dpo.py 的真实曲线 —— 留意两条奖励:谁动得多?
关键观察(实测):margin(Δc−Δr,logπ 单位)一路从 0 拉到 +19 —— 换算成奖励 margin 就是 β·19 ≈ +1.9(正好等于下方 chosen≈0 与 rejected≈−1.9 两条奖励之差)。
它几乎全靠把 rejected 的奖励往下压(降到约 −1.9),chosen 的奖励基本贴着 0 没怎么动。这是 DPO 常见现象 —— "对齐"很多时候是学会少说不好的,而非多说好的。
所以 lr 要极小(5e-7)、配 ref 当锚,否则 policy 会把 rejected 压垮、连带把语言能力带崩。
↳ 代码:08_dpo.py 的 main 训练循环 · 真实曲线 loss 0.693→0.171 / margin 0→+19 / 准确率 0%→100%
↳ 下一步:训完采样,看回答风格有没有往 chosen 那边靠。
⑤ 训练前 vs 训练后 & 达标
下面是 08_dpo.py 的真实采样(起点=SFT 模型,终点=DPO 后)。
偏好对齐的变化常常很微妙,但有的指令上能看到明显往 chosen 风格靠:
达标玩具级 · 看硬指标不看刷榜
- ✓
DPO loss 从 ln2≈0.693 稳步下降到 0.171(不再"瞎猜")。
- ✓
偏好准确率 0% → 100%:隐式奖励把每一对的 chosen 都排到了 rejected 前面。
- ✓
margin(Δc−Δr)0 → +19(换算成奖励 margin = β·19 ≈ +1.9),主要由"压低 rejected"驱动;部分指令采样可见往 chosen 风格靠(如礼貌改写)。
诚实交代:玩具级(124M、10 对、纯英文)。采样里 "What is 2 + 2?" 训练后甚至答成了 "16" ——
DPO 在这么小的规模会轻微扰动已有知识。所以这一关里"DPO 起作用"的可靠证据是
loss↓ / 准确率↑ / margin↑ 这些硬指标,而不是某一条采样文字。
↳ 跑法:python 08_dpo.py --ckpt ckpt_sft/sft.pt --epochs 20 · 加 --lora 改用 LoRA 手段做 DPO
Q&A常见疑问
DPO 和 RLHF 到底什么关系?DPO 是 PPO 的变体吗?
都是为了
偏好对齐这同一个目标,但是
两条不同的路,DPO
不是 PPO 的变体。
RLHF 先训奖励模型、再用 PPO 这类 RL 算法去最大化奖励(要采样、要在线交互);
DPO 用数学推导证明:在同样的目标下,可以
跳过奖励模型和 RL,把它等价成一个直接在偏好对上做的
监督式分类 loss。
更省、更稳,是目前开源对齐的主流做法之一。详见
白皮书 step 5。
为什么训练里 chosen 的奖励几乎不涨,全靠压 rejected?
因为 chosen 本来就是 SFT 模型已经倾向给出的回答(它的 logπ 本就不低),抬升空间小;
而 rejected 那种冒犯/啰嗦的回答,模型压低它的概率要"容易"得多。
DPO 的 loss 只关心 两者之差(margin),从哪边把差距拉开都行 —— 优化器自然挑了阻力最小的那条:压 rejected。
那 第 8 章(全量 SFT)、第 9 章(LoRA)、第 10 章(DPO)三者怎么拼?
任务 × 手段是两个正交的轴(
白皮书的 2×2 表):
任务有 SFT(会答)/ 偏好对齐(答得好);手段有 全量 / LoRA。
第 8 章=全量×SFT,第 9 章=LoRA×SFT,第 10 章=全量×DPO。本关加
--lora 就补上了 LoRA×DPO 那一格 —— 四格都能跑通,证明两轴可自由组合。
典型生产流水线就是:
预训练 → SFT → 偏好对齐,每步都可选全量或 LoRA。
🎉 手搓 DPO · 通关 · Phase 2 手搓三关全部拿下
你已经亲手用 SFT(全量 / LoRA)把 base 教成会答题,又用 DPO 把它对齐到"答得合人意"。预训练 → SFT → 对齐,一条龙都摸过了一遍。