Chapter 07

DPO / ORPO / SimPO:偏好对齐

📌 本章数学公式参考各方法的原始论文,工程实现位置基于 commit 01398eb18dd4 的 src/llamafactory/train/dpo/ 与 src/llamafactory/train/kto/ 。

7.1 为什么 DPO 系替代了 PPO

传统三段式 RLHF:SFT → Reward Model → PPO。难处在最后这一步:

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]$$

所以 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 学习率小一个量级
数据格式必须换 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.pyRLHFArguments 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 子类:

stageTrainer 类父类位置
dpo(任意 pref_loss) CustomDPOTrainer trl.DPOTrainertrain/dpo/trainer.py
kto CustomKTOTrainer trl.KTOTrainertrain/kto/trainer.py
rm PairwiseTrainer transformers.Trainertrain/rm/trainer.py

所有 LLaMA-Factory 子类都在 TRL / HF 基类外面包了:

7.10 偏好数据的真实 schema

本章前面只描述了"alpaca 偏好格式",真正能被 PairwiseDatasetProcessordata/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"
    }
  }
}

三件易错事: