DPO 与变体:IPO / KTO / SimPO / ORPO / CPO
DPO 是 2023 年最重要的对齐论文之一:把 PPO 的 RL 训练替换成离线偏好学习。本章拆解 DPO 数学,然后给出 TRL 里实现的 6 个变体的差异。
6.1DPO 的公式(关键一推)
PPO 的目标可以写成 $\max_\pi R(x,y) - \beta\mathrm{KL}(\pi\|\pi_\text{ref})$,闭式解是:
$$\pi^*(y|x) = \frac{1}{Z(x)}\,\pi_\text{ref}(y|x)\,\exp\!\big(R(x,y)/\beta\big)$$
反过来 $R(x,y) = \beta\log\frac{\pi^*(y|x)}{\pi_\text{ref}(y|x)} + \beta\log Z(x)$。 $Z(x)$ 在 Bradley-Terry pairwise 中被消掉,得到 DPO loss:
$$\mathcal{L}_\text{DPO} = -\mathbb{E}_{(x,y_w,y_l)}\Big[\log\sigma\Big(\beta\log\tfrac{\pi(y_w|x)}{\pi_\text{ref}(y_w|x)} - \beta\log\tfrac{\pi(y_l|x)}{\pi_\text{ref}(y_l|x)}\Big)\Big]$$
关键洞察:RM 被消掉了,直接用偏好对学。
6.2用 DPOTrainer
from trl import DPOConfig, DPOTrainer
from datasets import load_dataset
ds = load_dataset("trl-lib/ultrafeedback_binarized", split="train[:1000]")
# 每条 = {"chosen": [...], "rejected": [...], "prompt": "..."}
cfg = DPOConfig(
output_dir="./dpo_out",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
num_train_epochs=1,
learning_rate=5e-7, # ★ DPO 用极小 lr
beta=0.1,
max_length=2048,
bf16=True,
)
trainer = DPOTrainer(
model=sft_model,
ref_model=None, # 用 LoRA 时自动构造
args=cfg,
train_dataset=ds,
processing_class=tok,
)
trainer.train()
6.36 个变体一图表
| 算法 | loss 核心 | 需要 ref | 论文 |
|---|---|---|---|
| DPO | $-\log\sigma(\beta\Delta_\text{logratio})$ | ✅ | Rafailov 2023 |
| IPO | $(\Delta_\text{logratio} - 1/(2\beta))^2$(square loss 不饱和) | ✅ | Azar 2024 |
| KTO | 逐样本的 prospect theory loss,不需要 pair | ✅ | Ethayarajh 2024 |
| SimPO | $-\log\sigma(\beta(\bar{\log\pi}(y_w)-\bar{\log\pi}(y_l))-\gamma)$,平均 log-prob | ❌ | Meng 2024 |
| ORPO | SFT loss + $\lambda\cdot$odds ratio penalty,单阶段 | ❌ | Hong 2024 |
| CPO | SFT + $\lambda\cdot$对比 loss,单阶段 | ❌ | Xu 2024 |
关键差异点
- IPO:DPO 在偏好已经清晰时容易"过自信"导致崩;IPO 用 square loss 避免;
- KTO:不需要 (chosen, rejected) pair,单条样本带 "thumbs up/down" 标签也能用;
- SimPO / ORPO / CPO:不需要 reference 模型,显存 ÷ 2;
- ORPO / CPO:可以直接从 base 模型跑(跳过 SFT 阶段)。
6.4DPOConfig 关键字段
| 字段 | 含义 | 建议 |
|---|---|---|
beta | KL 强度 | 0.01-0.5;0.1 常用 |
loss_type | "sigmoid" / "hinge" / "ipo" / "kto_pair" / "simpo" / ... | 试一试 |
reference_free | 把 ref 视为 uniform | 极端用法 |
label_smoothing | conservative DPO (cDPO) | 偏好数据有噪用 0.1 |
rpo_alpha | RPO regularization(NLL 项权重) | 0.1-1.0 可试 |
sync_ref_model | 训练中同步 ref 到 policy | online DPO 才用 |
max_length | 截断 | 2048-4096 |
max_prompt_length | prompt 截断 | 1024 常用 |
6.5什么时候选什么
| 需求 | 选 |
|---|---|
| 第一次尝试,要稳 | DPO(beta=0.1) |
| 偏好数据噪声大 | cDPO(DPO + label_smoothing=0.1)或 IPO |
| 只有 thumbs up/down 单标签 | KTO |
| 显存极紧 | SimPO / ORPO(不要 ref) |
| 想跳过 SFT | ORPO 或 CPO |
| 需要在线采样(rollout) | GRPO(下一章)或 online DPO |
6.6实现位置
trl/trainer/
├── dpo_trainer.py + dpo_config.py (DPO / IPO / cDPO 都在这)
├── kto_trainer.py + kto_config.py
├── orpo_trainer.py + orpo_config.py
├── cpo_trainer.py + cpo_config.py
└── ...
trl/trainer/utils.py compute_loss 的实现细节
6.7常见踩坑
| 现象 | 原因 |
|---|---|
| DPO loss 一直在 ln(2) 附近不降 | ref / policy 是同一个,没拉开差距;检查 LoRA 配置 |
| 训完后乱码 / 重复 | beta 太小(< 0.01),policy 偏离过多 |
| 训完打榜变差 | 偏好数据质量差;或 lr 太大(DPO 应 ≤ 1e-6 全参 / 5e-6 LoRA) |
| OOM | 4 份模型 + log-prob 缓存;先开 LoRA + ref_model=None |
6.8DPOConfig.loss_type 全枚举(15 种)
TRL 的 DPOTrainer 通过 同一个 trainer + 不同 loss_type 实现整个 "DPO 家族"。这是它替代为每个论文写独立 trainer 的核心抽象。来源 trl/trainer/dpo_config.py:211–219。
| loss_type | 核心公式 | 需要 ref | 论文 |
|---|---|---|---|
"sigmoid"(默认) | $-\log\sigma(\beta\Delta)$ | ✅ | 2305.18290 经典 DPO |
"hinge" | $\max(0, 1 - \beta\Delta)$ | ✅ | SLiC |
"ipo" | $(\Delta - 1/(2\beta))^2$ | ✅ | 2310.12036 IPO(长度归一化) |
"exo_pair" | $KL(\sigma(\beta\Delta) \;\|\; [1-\epsilon, \epsilon])$ | ✅ | 2402.00856 EXO |
"nca_pair" | Noise Contrastive Alignment 形式 | ✅ | NCA 系 |
"robust" | label-flip 鲁棒 DPO | ✅ | 处理偏好噪声 |
"bco_pair" | Binary Classification Objective 的 pair 版 | ✅ | BCO |
"sppo_hard" | SPPO 硬概率 margin | ✅ | 2405.00675 |
"aot" | Advantage-based Optimal Transport(排序版) | ✅ | AOT |
"aot_unpaired" | AOT 不需要成对 | ✅ | AOT |
"apo_zero" | $\Delta_\text{chosen}$ 优于默认时的 APO 分支 | ✅ | 2408.06266 |
"apo_down" | $\Delta_\text{chosen}$ 不如默认时的 APO 分支 | ✅ | 2408.06266 |
"discopop" | 调制 loss,含 discopop_tau 字段 | ✅ | 2406.08414 DiscoPOP |
"sft" | 只对 chosen 做 SFT loss | — | multi-loss 锚定项 |
"sigmoid_norm" | sigmoid + 长度归一 | ✅ | 长度敏感场景 |
核心 dispatch 在 dpo_trainer.py:1160–1459 的 _compute_loss,按 loss_type 走 switch。
6.9loss_type 可以是 list:multi-loss DPO
v1 让 loss_type 接受 list,配合 loss_weights 同时算多种 loss 加权求和:
cfg = DPOConfig(
output_dir="./out",
beta=0.1,
loss_type=["sigmoid", "sft"], # ★ 两个 loss 同时优化
loss_weights=[1.0, 0.1], # sigmoid 主、sft 辅
...
)
典型用法:"sigmoid" + "sft" = MPO(Mixed Preference Optimization),既学偏好又锚定 chosen 的 SFT loss,训练更稳。
6.10f_divergence_type 4 种
DPO 的 KL 正则不一定非要是 reverse-KL。DPOConfig.f_divergence_type(dpo_config.py:237–242)有 4 种:
| f_divergence_type | 含义 |
|---|---|
"reverse_kl"(默认) | $KL(\pi_\theta \;\|\; \pi_\text{ref})$,经典 DPO |
"forward_kl" | $KL(\pi_\text{ref} \;\|\; \pi_\theta)$,更宽容 |
"js_divergence" | 对称化版本,两边都不偏 |
"alpha_divergence" | 带超参 $\alpha$ 的广义 f-divergence |
实战 reverse_kl 已经够用;选 forward_kl 会让 policy 更激进偏离 ref,配合小 beta 用。
6.11v1 新功能:LD-DPO 长度差异化
DPOConfig.ld_alpha(dpo_config.py:228–236)启用 LD-DPO(Length-Differentiated DPO)。原理:
- chosen / rejected 长度不同时 sigmoid loss 会有 length bias;
ld_alpha ∈ [0, 1]给短回复加权、长回复降权,对抗 length hacking;- 默认 0(关闭);开启典型
ld_alpha=0.5。
对长度偏置敏感的场景(用户评分通常偏向长回答),ld_alpha 比换 SimPO 更精细可调。
6.12这章你需要带走的
- DPO = 把 PPO 闭式解反代得到的离线 loss,消掉了 RM;
- 6 个变体的差异主要在"loss 形状"和"要不要 ref";
- 显存紧选 SimPO / ORPO;噪声大选 IPO / cDPO;只有单标签选 KTO;
- DPO 的 lr 要极小(全参 1e-6、LoRA 5e-6),否则训坏。