TT Lab
开始
学习 学习路径 课程

MiniMind — 亲手从头到尾训练一个小型语言模型

用 DPO 改变风格,并检查内容是否保持

在 TT Lab 中继续学习

目标

亲手实现 MiniMind train_dpo.py 的损失(用回答令牌对数概率之和计算的 −logσ(β·Δ)),把 SFT 基准模型加载成策略和参考两份,做 100 步 DPO。偏好配对事实相同、只有口吻不同(chosen …입니다.,rejected …이다.)。同时测量口吻是否改变,以及忽略口吻的正确率是否得以保持。

为什么重要

DPO 不需要奖励模型和强化学习,只用偏好配对就能推动模型。实现只有十行,但因为损失只看“差值”,推错了,损失虽然下降得很好,模型却坏掉了——比如把 chosen 和 rejected 一起拉低,只把差距拉开。在这门课程的模型上,把学习率提高十倍,真的就会变成这样。 所以偏好训练不靠一个损失来判断。这个实验的要点,是在前后同时测量偏好指标(留出的配对中 chosen 更可信的份额、以期望口吻回答的份额)和原有能力指标(事实正确率)。

步骤

  1. 在 /root/mm/dpo/dpolib.py 中实现把偏好配对转换成批次的函数和回答令牌对数概率之和(seq_logp),并用参考模型把第一个配对的两个对数概率写入 /root/mm/dpo/logp.json。
  2. 在 dpolib.py 中实现 dpo_loss(정책 chosen, 정책 rejected, 기준 chosen, 기준 rejected, beta)(占位符依次为策略的 chosen、策略的 rejected、参考的 chosen、参考的 rejected 与 beta)。把第 1 步的 logp.json 里策略=参考时的损失(loss_policy_equals_ref)也写进去。
  3. 把 DPO 之前的参考模型的指标(偏好准确率、敬体比例、两个平均对数概率)写入 /root/mm/dpo/baseline.json。
  4. 以 β 0.1、lr 1e-5、每次 8 个配对做 100 步 DPO,把策略保存为 /root/mm/dpo/dpo.pth。
  5. 用 DPO 之后的策略测量同样的指标,写入 /root/mm/dpo/after.json。
  6. 在 SFT 数据最前面的 40 个问题上,测量 DPO 前后忽略口吻的正确率,写入 /root/mm/dpo/drift.json。
  7. 在 /root/mm/dpo/report.md 中写 ## 손실、## 무엇이 바뀌었나、## 무엇을 못 하나 三节(三个标题为韩文,依次意为“损失”“什么发生了变化”“什么做不到”),并放入 DPO 之后的敬体比例和 chosen 的平均对数概率。

参考

一句话的对数概率是回答令牌之和

在 /root/mm/dpo/dpolib.py 中实现 batch(쌍들)(占位符为配对列表,前一半是 chosen、后一半是 rejected 的输入和标签)和 seq_logp(모델, 입력, 라벨)(占位符依次为模型、输入与标签,每句话回答令牌 log p 之和),并用参考模型(/opt/mm/ref/sft.pth)把 dpo.jsonl 第一个配对的两个值,以 chosen、rejected 写入 /root/mm/dpo/logp.json。

先做 log_softmax,再用 torch.gather 只取出正确令牌的值,标签为 -100 的位置用掩码乘以 0 去掉(在 gather 之前要把 -100 换成 0,才不会出现索引错误)。两个回答只有敬体句尾和平体句尾不同,所以那一部分的概率之差,就是两个值之差。

按公式实现 DPO 损失

在 dpolib.py 中实现 dpo_loss(policy_chosen, policy_rejected, ref_chosen, ref_rejected, beta)——四个参数是每句话的对数概率之和(一维张量),返回 −logσ(β·[(π_c − ref_c) − (π_r − ref_r)]) 的平均值。用参考模型的前 8 个配对,把策略=参考时的值写入 logp.json 的 loss_policy_equals_ref。

策略与参考相同时,括号里是 0,所以是 −log σ(0) = ln 2。评分器会加载你的 dpo_loss,在随机六个配对、两个 β 下看它是否与论文公式一致,以及策略更喜欢 chosen 时损失是否小于 ln 2(符号)。用 F.logsigmoid 数值更稳定。

DPO 之前的指标

编写脚本 /root/mm/dpo/metrics.py(参数:权重路径、结果路径),用参考模型测出 dpo_val.jsonl 100 个配对的偏好准确率(pref_acc)、前 40 个配对的问题的敬体比例(polite_rate)、chosen 和 rejected 的平均对数概率(mean_logp_chosen、mean_logp_rejected),写入 /root/mm/dpo/baseline.json。

SFT 数据里两种口吻各占一半,所以参考模型会把两种口吻差不多各半地混着用。如果把测指标的脚本写成接收权重路径和结果路径作为参数,第 5 步就能直接用。

100 步 DPO

编写脚本 /root/mm/dpo/train_dpo.py,把 SFT 模型加载成策略和参考两份并冻结参考,从 dpo.jsonl 中每次取 8 个配对,以 β 0.1、lr 1e-5(AdamW)、100 步(随机种子 42)做 DPO,然后把策略的 state_dict 保存为 /root/mm/dpo/dpo.pth。

参考模型的对数概率在 torch.no_grad() 里求。MiniMind 的默认学习率是 4e-8,但在这个小模型和短训练下,1e-5 既能搬动口吻,又能保住内容。损失从 ln 2 开始下降。

DPO 之后的指标

用 dpo.pth 测量与第 3 步相同的指标,写入 /root/mm/dpo/after.json。评分器会看偏好准确率是否在 0.8 以上,以及敬体比例是否比 DPO 之前上升了 20 个百分点以上。

也请把两个平均对数概率在前后做比较。chosen 和 rejected 都下降,在 DPO 中很常见——问题在于 chosen 下降得太多,让模型给出哪一边都不是的回答。

内容保住了吗

让 sft.jsonl 中单轮对话的前 40 个问题用贪心解码回答,去掉末尾的 입니다.、이다.,在参考模型(fact_acc_ref)和 DPO 策略(fact_acc_dpo)上,分别测量与数据中的回答相同的份额,写入 /root/mm/dpo/drift.json。

去掉口吻再比较,剩下的就只有“说了什么”。偏好配对事实相同、只有口吻不同,所以如果 DPO 做得好,这个值应该几乎不变。如果下降超过 10 个百分点,就是偏离参考太远了。

偏好训练的效果与局限

在 /root/mm/dpo/report.md 中写 ## 손실、## 무엇이 바뀌었나、## 무엇을 못 하나 三节(三个标题为韩文,依次意为“损失”“什么发生了变化”“什么做不到”),并以数字放入第 5 步的 polite_rate 和 mean_logp_chosen。

最后一节写偏好配对教不了的东西(模型不知道的事实),以及提高学习率时看到的现象即可。