DPO / ORPO / SimPO:偏好对齐
7.1 为什么 DPO 系替代了 PPO
传统三段式 RLHF:SFT → Reward Model → PPO。难处在最后这一步:
- PPO 同时跑 actor / reference / reward / critic 四个模型,显存爆炸;
- 超参敏感、reward hacking 难控;
- 训练不稳定。
2023 年 5 月 DPO 论文给出一个数学结论:有最优奖励函数 $r^\star$ 的 KL-约束 RL 问题,存在闭式解。代入后 reward 模型就可以"省掉",直接在偏好数据上做一次二元分类即可:
$$\mathcal{L}_\text{DPO} = -\mathbb{E}_{(x,y_w,y_l)\sim\mathcal{D}}\!\left[\log \sigma\!\Big(\beta\,\big[\log\tfrac{\pi_\theta(y_w|x)}{\pi_\text{ref}(y_w|x)} - \log\tfrac{\pi_\theta(y_l|x)}{\pi_\text{ref}(y_l|x)}\big]\Big)\right]$$
- $y_w$ = chosen response,$y_l$ = rejected response;
- $\pi_\theta$ 是策略(我们在训的模型),$\pi_\text{ref}$ 是 reference(一般 = SFT 后冻结的模型);
- $\beta$ 是 KL 正则系数,对应 YAML 的
pref_beta,默认 0.1。
所以 LLaMA-Factory 的偏好对齐 = "只跑 actor + reference 两个模型,loss 是个 sigmoid",工程上比 PPO 简单很多。
7.2 一份 DPO YAML 长什么样
仓库自带 examples/train_lora/qwen3_lora_dpo.yaml。和第 3 章 SFT YAML 几乎一样,只改了三件事:
### method
stage: dpo # 1) 阶段从 sft 改成 dpo
do_train: true
finetuning_type: lora
lora_rank: 8
lora_target: all
pref_beta: 0.1 # 2) 新增:β
pref_loss: sigmoid # 3) 新增:偏好损失类型
### dataset
dataset: dpo_en_demo # 数据集换成 preference 格式
...
### train
learning_rate: 5.0e-6 # DPO 一般比 SFT 学习率小一个量级
{instruction, input, output};DPO 数据是
{instruction, input, chosen, rejected},必须有"被偏好 / 被拒绝"成对回答。
仓库 data/dpo_en_demo.json 是参考。
7.3 pref_loss:DPO 系算法一览
LLaMA-Factory 把 DPO 系所有变体都收口到 pref_loss 这一个字段。这是它和 TRL "各算法各 Trainer" 风格的最大差别。
pref_loss |
论文 / 别名 | 关键公式(与 DPO 的差别) | 什么时候用 |
|---|---|---|---|
sigmoid(默认) |
DPO | 上面 7.1 节那个公式 | 稳定首选 |
hinge |
SLiC / hinge loss DPO | 把 $\log\sigma$ 换成 hinge:$\max(0, 1 - \beta \cdot \Delta)$ | label 噪声大时 |
ipo |
IPO (Identity Pref. Optim.) | $(\Delta - \tfrac{1}{2\beta})^2$,把分类换回归 | 偏好数据极度不平衡 / 过拟合严重 |
orpo |
ORPO | $-\log\sigma(\lambda\cdot\text{odds-ratio})$,无需 reference | 没有好 SFT 模型时,可以 SFT+DPO 一步走 |
simpo |
SimPO | 用长度归一化的 reward,无需 reference | 显存极紧 / 想避免 reference 漂移 |
kto_pair |
KTO 的成对版 | 非对称 risk-seeking 损失 | 单边 reward 信号更强时 |
ORPO 的细节
ORPO 比 DPO 更激进,把 SFT loss 和偏好 loss 直接合在一起:
$$\mathcal{L}_\text{ORPO} = \mathcal{L}_\text{SFT}(y_w) + \lambda \cdot \mathcal{L}_\text{OR}$$
$\mathcal{L}_\text{OR} = -\log\sigma\!\Big(\log\tfrac{\mathrm{odds}_\theta(y_w|x)}{\mathrm{odds}_\theta(y_l|x)}\Big)$,odds = $\tfrac{P}{1-P}$。
实践含义:ORPO 可以从基座一次性训到对齐,省去 SFT 阶段。LLaMA-Factory 里这样配:
stage: dpo
pref_loss: orpo
# 不再需要 reference,框架自动跳过 ref forward
SimPO 的细节
SimPO 关键 idea:用长度归一化的对数似然做 reward,不再依赖 reference。loss 形式:
$$\mathcal{L}_\text{SimPO} = -\log\sigma\!\Big(\beta\big[\tfrac{1}{|y_w|}\log\pi_\theta(y_w|x) - \tfrac{1}{|y_l|}\log\pi_\theta(y_l|x)\big] - \gamma\Big)$$
$\gamma$ 是 reward margin。LLaMA-Factory 中:
stage: dpo
pref_loss: simpo
pref_beta: 2.0 # SimPO 推荐更大
simpo_gamma: 1.0 # margin(仅 SimPO 用)
7.4 KTO:基于二元偏好(无成对数据)
DPO 系都要"chosen / rejected 成对"标注,但真实业务里更容易拿到单边数据:"这条回答好"、"这条回答差",未必能配对。
KTO 用 Kahneman-Tversky 前景理论:对赢 / 输应用非对称的效用函数:
$$ \mathcal{L}_\text{KTO} = w_+ \mathbb{E}_{y\sim\mathrm{desirable}}\!\left[1-\sigma(\beta r - z_0)\right] + w_- \mathbb{E}_{y\sim\mathrm{undesirable}}\!\left[1-\sigma(z_0 - \beta r)\right] $$
其中 $r = \log\tfrac{\pi_\theta(y|x)}{\pi_\text{ref}(y|x)}$,$z_0$ 是 batch 内 KL 估计。
LLaMA-Factory 把 KTO 单独放到 stage: kto,数据格式不一样(单边 + kto_tag),代码在 src/llamafactory/train/kto/。
stage: kto
dataset: kto_en_demo # 注意是 kto_en_demo 不是 dpo_en_demo
pref_beta: 0.1
kto_chosen_weight: 1.0
kto_rejected_weight: 1.0
7.5 reference 模型在哪
对于 DPO / IPO / KTO(需要 ref 的算法),π_ref 默认 = "你启动训练时加载的基座 + 已挂的 LoRA",框架会再 clone 一份并冻结。
| 场景 | YAML 字段 |
|---|---|
| 从 SFT 后基座做 DPO | 填 model_name_or_path 即可,框架自动复制 |
| 显存太紧,不想加载 ref | ref_model: null + pref_loss: orpo / simpo(无 ref 算法) |
| 用别的模型当 ref | ref_model: path/to/other |
| 已有 LoRA 适配器做 DPO | adapter_name_or_path: saves/.../sft,会和基座叠加再 clone 为 ref |
7.6 DPO 全流程速写
┌─ Stage A: SFT ────────────────────────────────┐
│ stage: sft, finetuning_type: lora │
│ dataset: alpaca_zh_demo │
│ → 输出: saves/qwen3-4b/lora/sft │
└───────────────────────────────────────────────┘
│
▼
┌─ Stage B: 收偏好数据 ─────────────────────────┐
│ 让模型对同一 prompt 生成多个 response │
│ 人工 / GPT-4 标 chosen / rejected │
│ 写成 dpo_train.json (alpaca 偏好格式) │
│ 注册到 data/dataset_info.json │
└───────────────────────────────────────────────┘
│
▼
┌─ Stage C: DPO ────────────────────────────────┐
│ stage: dpo, pref_loss: sigmoid │
│ adapter_name_or_path: saves/.../sft │
│ create_new_adapter: true # 不覆盖 SFT 适配器│
│ dataset: dpo_train │
│ → 输出: saves/qwen3-4b/lora/dpo │
└───────────────────────────────────────────────┘
│
▼
┌─ Stage D: 试用 ───────────────────────────────┐
│ llamafactory-cli chat \ │
│ model_name_or_path=... \ │
│ adapter_name_or_path=saves/.../dpo \ │
│ template=qwen3_nothink │
└───────────────────────────────────────────────┘
7.7 调优清单
| 症状 | 原因 | 调哪几个字段 |
|---|---|---|
| 训完模型胡言乱语 | pref_beta 太小,策略漂离 reference 太远 | pref_beta: 0.1 → 0.3;或加 pref_ftx: 0.5 叠 SFT 锚定 |
| chosen / rejected reward 越拉越远但效果差 | reward hacking | 降学习率、减少训练 epoch、加偏好数据多样性 |
| 显存不够跑 DPO | actor + ref 双份 | 换成 ORPO / SimPO;或 ref_model: null |
| 训到一半 NaN | log-prob 数值不稳 | 关闭 fp16 改 bf16;pref_beta 不要太大 |
| chosen 长 rejected 短,模型变啰嗦 | 长度偏置 | 换 SimPO(自带长度归一化) |
7.8 pref_loss 全枚举与 use_ref_model 派生
本章前面讲了 DPO / ORPO / SimPO 三种主流算法,仓库实际支持的 pref_loss 取值更多。来源:hparams/finetuning_args.py 的 RLHFArguments mixin。
| pref_loss | 对应算法 | 需要 ref_model | 关键论文 / 备注 |
|---|---|---|---|
sigmoid | DPO 原版 | ✅ | arXiv:2305.18290 |
hinge | Hinge-DPO | ✅ | 取 hinge max(0, ...) |
ipo | IPO(Identity PO) | ✅ | arXiv:2310.12036;避免 DPO 中 logit 饱和 |
kto_pair | KTO 的 pairwise 变体 | ✅ | 用 KTO loss 但接受 chosen/rejected 对 |
orpo | ORPO | ❌ | 不需要 ref;同一 forward 算 OR + SFT loss |
simpo | SimPO | ❌ | arXiv:2405.14734;自带长度归一化 |
use_ref_model 是派生字段(finetuning_args.py:574):
self.use_ref_model = (stage == "dpo" and pref_loss not in ["orpo", "simpo"])
含义:写 stage: dpo, pref_loss: simpo 时框架不会加载 ref 模型,省掉一份 actor 显存。这就是 SimPO / ORPO 比经典 DPO "省一半显存"的真实落地点。显存紧但又要走偏好路线,SimPO 是首选。
7.9 Trainer 继承链对照
不同 stage + pref_loss 组合在源码层走不同的 Trainer 子类:
| stage | Trainer 类 | 父类 | 位置 |
|---|---|---|---|
dpo(任意 pref_loss) | CustomDPOTrainer | trl.DPOTrainer | train/dpo/trainer.py |
kto | CustomKTOTrainer | trl.KTOTrainer | train/kto/trainer.py |
rm | PairwiseTrainer | transformers.Trainer | train/rm/trainer.py |
所有 LLaMA-Factory 子类都在 TRL / HF 基类外面包了:
- 覆盖
compute_loss:按pref_loss切到正确的损失函数(同一 trainer 实现 6 种 loss); - 覆盖
create_optimizer:支持 LoRA+ / GaLore / APOLLO / BAdam / Muon; - 覆盖
_save/_load:处理 adapter 和 value head 单独存读; - 插入回调:
LogCallback / ReporterCallback / PissaConvertCallback等。
7.10 偏好数据的真实 schema
本章前面只描述了"alpaca 偏好格式",真正能被 PairwiseDatasetProcessor(data/processor/pairwise.py)吃下的列名取决于 dataset_info.json 里的 columns 字段:
// data/dataset_info.json 片段
{
"my_dpo_data": {
"file_name": "my_dpo.json",
"formatting": "alpaca",
"ranking": true, // ★ 标记为 pairwise 数据
"columns": {
"prompt": "instruction",
"query": "input",
"chosen": "preferred_answer",
"rejected": "rejected_answer"
}
}
}
三件易错事:
ranking: true必须有,否则会被当 SFT 数据走 SupervisedDatasetProcessor 报错;- sharegpt 偏好数据走另一套字段:
messages(包含 chosen 的多轮对话)+chosen/rejected单条响应; - KTO 数据要
kto_tag列(每条样本一个布尔 tag,不需要成对)。