第三章:使用 DPO 进行偏好对齐
配套代码:
dpotrainer.py
承上启下
在前两章中,我们学习了:
- 第一章:使用原生 Trainer 进行 SFT,理解了 Loss Masking 的核心概念
- 第二章:使用 SFTTrainer 简化流程,实现了自动化的 SFT 训练
通过 SFT,模型学会了:
- 按照指令格式回答问题
- 生成符合语法的响应
- 理解用户的意图
但是,SFT 有一个局限性:它只能让模型学习”如何回答”,却不能让模型学习”什么样的回答更好”。
本章将介绍 DPO(Direct Preference Optimization)——一种让模型学习人类偏好的训练方法!为了能够更好地理解 DPO 的数学理论,建议参考原文。下面的数学原理不会非常完整,仅做简单介绍。
本章学习目标
完成本章后,你将理解:
- 偏好对齐 的概念和重要性
- DPO 与传统 RLHF 的区别
- 偏好数据集 的格式和构建
- DPOTrainer 的使用方法
- 参考模型 的作用和配置
1. 为什么需要偏好对齐?
1.1 SFT 的局限性
SFT 无法区分回答质量
用户提问: “如何提高编程效率?”
SFT 模型可能的回答 A(更好):
提高编程效率可以通过以下方式:
- 学习快捷键
- 使用代码片段
- 定期休息
SFT 模型可能的回答 B(较差):
多写代码就行了。
问题: 两个回答语法都正确,但 A 明显更详细、更实用!SFT 无法区分回答的”质量”差异。
1.2 偏好对齐的目标
让模型学习:给定相同的问题,哪个回答更符合人类偏好?
偏好数据格式:
{
"prompt": "如何提高编程效率?",
"chosen": "提高编程效率可以通过以下方式:1. 学习快捷键...", ← 更好的回答
"rejected": "多写代码就行了。" ← 较差的回答
}
2. RLHF vs DPO
2.1 传统 RLHF (PPO) 流程
传统 RLHF (PPO) 流程
- 收集偏好数据 → 人类标注者标记 chosen vs rejected
- 训练奖励模型 → 用偏好数据训练rewarder模型,为了实现baseline保证梯度稳定,还需要构建一个value模型,从而导致大量的显存开销
- PPO 优化策略 → 从策略模型采样,用奖励模型评分,更新策略
需要 4 个模型: 策略、参考、奖励、价值模型
问题:
- 流程复杂,需要多个模型
- 训练不稳定,需要大量调参
- 计算成本高(需要采样生成)
2.2 DPO:简化的偏好对齐
DPO 流程
- 收集偏好数据 → 与 RLHF 相同
- 直接优化策略 ← 关键创新!
只需 2 个模型: 策略 + 参考模型
优势:
- 流程简单,易于实现
- 训练稳定,无需复杂调参
- 计算效率高
- 代码逻辑接近 SFT,仍旧是去优化损失
2.3 数学原理(从直觉到公式)
2.3.1 RLHF 的核心目标
在传统 RLHF 中,我们的目标很简单:让模型更倾向于生成人类偏好的回答。
用数学语言说,对于一组偏好数据 $(x, y_w, y_l)$(其中 $y_w$ 是更好的回答,$y_l$ 是较差的回答),我们希望:
\[P(y_w > y_l \mid x) > 0.5\]即:模型应该认为 chosen 回答比 rejected 回答更好。
2.3.2 传统 RLHF 的做法:训练奖励模型
为了实现这个目标,传统 RLHF 分两步:
第一步:训练奖励模型 $R(x, y)$
奖励模型的作用是给每个回答打分。我们用偏好数据训练它,使得:
\[R(x, y_w) > R(x, y_l)\]第二步:用强化学习优化策略
使用 PPO 等算法,调整策略模型 $\pi_\theta$ 的参数,让它生成的回答能获得更高奖励:
\[\text{目标:最大化 } E[R(x, y)]\]问题:这需要训练一个独立的奖励模型,还要用复杂的强化学习算法,流程繁琐且不稳定。
2.3.3 DPO 的洞察:奖励函数可以解析表达
DPO 的核心发现是:如果我们知道了最优策略的奖励函数形式,就可以绕过奖励模型,直接优化策略!
关键定理:在 RLHF 设置下,最优策略的奖励函数可以表示为:
\[R^*(x, y) = \beta \cdot \log\left(\frac{\pi^*(y \mid x)}{\pi_{\text{ref}}(y \mid x)}\right) + Z(x)\]其中:
- $\pi^*(y \mid x)$ = 最优策略(我们想学到的策略)
- $\pi_{\text{ref}}(y \mid x)$ = 参考策略(SFT 后的模型,作为起点)
- $\beta$ = 温度参数,控制偏离程度
- $Z(x)$ = 归一化常数(与 $y$ 无关,优化时可忽略)
直觉理解:
这个公式告诉我们:奖励的大小取决于策略模型相对于参考模型的”偏好变化”。
- 如果 $\pi^*$ 相比 $\pi_{\text{ref}}$ 更倾向于生成某个回答 → 该回答的奖励高
- 如果 $\pi^*$ 相比 $\pi_{\text{ref}}$ 更不倾向于生成某个回答 → 该回答的奖励低
2.3.4 DPO 的优化目标
有了奖励函数的表达式,我们可以直接写出优化目标。
RLHF 的目标(用奖励模型):
\[\mathcal{L}_{\text{RLHF}} = \log \sigma\left(\beta \left[R(x, y_w) - R(x, y_l)\right]\right)\]其中 $\sigma$ 是 sigmoid 函数。这个损失鼓励 $R(x, y_w) > R(x, y_l)$。
DPO 的目标(用策略模型):
将奖励函数的表达式代入,消去 $Z(x)$,得到:
\[\mathcal{L}_{\text{DPO}} = \log \sigma\left(\beta \cdot \log\frac{\pi_\theta(y_w \mid x)}{\pi_{\text{ref}}(y_w \mid x)} - \beta \cdot \log\frac{\pi_\theta(y_l \mid x)}{\pi_{\text{ref}}(y_l \mid x)}\right)\]简化后:
\[\mathcal{L}_{\text{DPO}} = \log \sigma\left(\beta \cdot \log\frac{\pi_\theta(y_w \mid x) \cdot \pi_{\text{ref}}(y_l \mid x)}{\pi_\theta(y_l \mid x) \cdot \pi_{\text{ref}}(y_w \mid x)}\right)\]这个损失的含义:
- 分子(我们想要的):$\pi_\theta(y_w \mid x)$ 要大,$\pi_{\text{ref}}(y_l \mid x)$ 是基准
- 分母(我们要避免的):$\pi_\theta(y_l \mid x)$ 要小,$\pi_{\text{ref}}(y_w \mid x)$ 是基准
训练时,我们通过梯度上升最大化这个损失,使得:
- $\pi_\theta(y_w \mid x)$ 相比参考模型上升 → 模型更倾向于生成 chosen 回答
- $\pi_\theta(y_l \mid x)$ 相比参考模型下降 → 模型更不倾向于生成 rejected 回答
2.3.5 为什么需要参考模型?
你可能会问:为什么不能直接最大化 $\pi_\theta(y_w \mid x)$,最小化 $\pi_\theta(y_l \mid x)$?
问题:这样会导致模型崩溃(Mode Collapse)
如果没有参考模型约束,模型可能学会:
- 对 chosen 回答输出极高概率(接近 1)
- 对 rejected 回答输出极低概率(接近 0)
但这会让模型:
- 忘记 SFT 阶段学到的知识
- 生成奇怪、不自然的回答
- 失去泛化能力
参考模型的作用:
$\pi_{\text{ref}}$ 就像一个”锚点”,确保:
- $\pi_\theta$ 不能随意偏离原始模型
- 只在偏好对齐的方向上调整
- 保持模型的语言能力和稳定性
数学上,这通过 KL 散度正则化实现:
\[\text{KL}(\pi_\theta \Vert \pi_{\text{ref}}) = \sum_y \pi_\theta(y \mid x) \log\frac{\pi_\theta(y \mid x)}{\pi_{\text{ref}}(y \mid x)}\]DPO 的损失函数中已经隐式包含了 KL 约束(通过 $\beta$ 参数控制)。
2.3.6 总结:DPO vs RLHF
| 方面 | 传统 RLHF (PPO) | DPO |
|---|---|---|
| 第一步 | 训练奖励模型 $R(x, y)$ | [否] 不需要 |
| 第二步 | 用强化学习优化策略 | [是] 直接优化策略 |
| 优化目标 | 最大化 $E[R(x, y)]$ | 最大化 $\mathcal{L}_{\text{DPO}}$ |
| 参考模型 | 用于 KL 正则化 | 直接嵌入损失函数 |
| 计算复杂度 | 需要采样生成,计算量大 | 类似 SFT,计算高效 |
核心优势:DPO 将强化学习问题转化为了监督学习问题,不需要训练奖励模型,不需要复杂的 RL 算法,代码实现接近 SFT!
3. 偏好数据集格式
3.1 标准格式
# 格式1: 简单格式
{
"prompt": "The sky is",
"chosen": " blue.",
"rejected": " green."
}
# 格式2: 对话格式
{
"prompt": [{"role": "user", "content": "What color is the sky?"}],
"chosen": [{"role": "assistant", "content": "It is blue."}],
"rejected": [{"role": "assistant", "content": "It is green."}]
}
3.2 常用偏好数据集
| 数据集 | 描述 | 大小 |
|---|---|---|
trl-lib/ultrafeedback_binarized |
TRL 官方提供 | ~60k |
Anthropic/hh-rlhf |
Anthropic 的有用+无害数据 | ~170k |
argilla/ultrafeedback-binarized-preferences-cleaned |
清洗后的 UltraFeedback | ~60k |
3.3 自建偏好数据集
# 偏好数据的构建方式:
# 1. 人类标注:让标注者选择更好的回答
# 2. AI 辅助:用强模型(如 GPT-4)判断偏好
# 3. 启发式规则:如选择更长/更详细的回答
preference_data = {
"prompt": "解释什么是机器学习?",
"chosen": "机器学习是人工智能的一个分支,它使计算机系统能够从数据中学习和改进,而无需进行明确的编程。主要类型包括:监督学习、无监督学习和强化学习...",
"rejected": "就是让机器学东西。"
}
4. DPOTrainer 使用指南
4.1 基本配置
from trl import DPOConfig, DPOTrainer
# DPO 配置
training_args = DPOConfig(
output_dir="./dpo-output",
# DPO 核心参数
beta=0.1, # [重要] KL 正则化强度
loss_type="sigmoid", # 损失函数类型
# 训练参数
num_train_epochs=1, # DPO 通常只需 1-3 个 epoch
per_device_train_batch_size=4,
learning_rate=5e-7, # DPO 通常用更小的学习率
# 其他
gradient_checkpointing=True,
bf16=True,
)
4.2 关键参数:beta
beta 是 DPO 最重要的超参数:
beta 的作用:控制模型偏离参考模型的程度
beta 大 (如 0.5):
└─→ 模型更保守,不敢偏离参考模型太远
└─→ 适用于:偏好数据噪声较大时
beta 小 (如 0.05):
└─→ 模型更激进,可能偏离参考模型较远
└─→ 适用于:偏好数据质量高时
推荐起始值: 0.1
调整范围: 0.05 - 0.5
4.3 损失函数类型
DPO的核心就在于如何将强化学习对齐转换为简单的损失函数优化,因此有很多衍生形式,就通过这里进行选择。
loss_type = "sigmoid" # 默认,标准 DPO
# 其他可选值:
# "hinge" - 来自 RSO 论文
# "ipo" - 来自 IPO 论文,解决过拟合
# "robust" - 鲁棒 DPO,处理噪声标签
4.4 创建 DPOTrainer
from trl import DPOTrainer
from datasets import load_dataset
# 加载偏好数据集
dataset = load_dataset("trl-lib/ultrafeedback_binarized")
# 创建 DPOTrainer
trainer = DPOTrainer(
model=model, # 策略模型
ref_model=None, # 参考模型(None 则自动创建)
args=training_args, # DPO 配置
train_dataset=dataset["train"],
processing_class=tokenizer, # 分词器
peft_config=lora_config, # LoRA 配置
)
# 开始训练
trainer.train()
5. 参考模型的处理
5.1 参考模型的作用
DPO 需要两个模型:
- 策略模型
- 我们要训练的模型
- 参数会被更新
- 参考模型
- 用于计算 KL 散度正则化
- 通常是 SFT 后的模型副本
- 参数冻结,不更新
5.2 三种参考模型策略
flowchart TB
subgraph Strategies["参考模型配置策略"]
direction TB
subgraph S1["策略1: 自动创建 推荐"]
S1_Code["ref_model = None"]
S1_F1["→ DPOTrainer 自动创建参考模型副本"]
S1_F2["→ 使用 PEFT 时,会卸载适配器进行参考推理"]
S1_F3["→ 最省显存"]
S1_Code --> S1_F1
S1_F1 --> S1_F2
S1_F2 --> S1_F3
end
subgraph S2["策略2: 手动创建"]
S2_Code["ref_model = AutoModelForCausalLM.from_pretrained(...)"]
S2_F1["→ 完全独立的模型副本"]
S2_F2["→ 显存占用翻倍"]
S2_Code --> S2_F1
S2_F1 --> S2_F2
end
subgraph S3["策略3: 适配器切换 (使用 PEFT 时)"]
S3_F1["→ 同一个基础模型加载两个适配器"]
S3_F2["→ 训练时切换适配器"]
S3_F3["→ 显存占用较低"]
S3_F1 --> S3_F2
S3_F2 --> S3_F3
end
S1 -.-> S2
S2 -.-> S3
end
5.3 代码示例
# 策略1: 自动创建(推荐)
trainer = DPOTrainer(
model=model,
ref_model=None, # ← 自动处理
...
)
# 策略2: 手动创建
ref_model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-8B",
dtype=torch.bfloat16,
device_map="auto",
)
ref_model.eval() # 冻结
trainer = DPOTrainer(
model=model,
ref_model=ref_model,
...
)
6. 训练监控指标
6.1 关键指标
| 指标 | 含义 | 期望趋势 |
|---|---|---|
rewards/chosen |
chosen 回答的平均奖励 | 上升 |
rewards/rejected |
rejected 回答的平均奖励 | 下降 |
rewards/margins |
chosen - rejected 的差值 | 上升 |
rewards/accuracies |
chosen 奖励 > rejected 奖励的比例 | 接近 1.0 |
6.2 监控示例
训练日志:
Step 100: rewards/margins = 0.5, rewards/accuracies = 0.65
Step 200: rewards/margins = 1.2, rewards/accuracies = 0.78
Step 300: rewards/margins = 2.0, rewards/accuracies = 0.89
...
分析:
margins 在上升 → 模型学会区分 chosen 和 rejected
accuracies 接近 1.0 → 模型几乎总能正确排序偏好
7. DPO 与 SFT 的训练流程对比
完整训练流程对比
| 阶段 | SFT (第一、二章) | DPO (本章) |
|---|---|---|
| 目标 | 让模型学会指令遵循 | 让模型学会偏好 |
| 数据 | (prompt, response) 二元对 | (prompt, chosen, rejected) 三元组 |
| 结果 | 能回答问题,但质量不一定最优 | 回答质量显著提升 |
| 时间 | 1-3 epochs | 1-3 epochs |
| 顺序 | 先进行 | 在 SFT 之后进行 |
重要: DPO 应该在 SFT 之后进行!
8. 运行代码
8.1 配置参数
在 dpotrainer.py 中可以修改:
MODEL_NAME = "Qwen/Qwen3-8B" # 模型
DATASET_NAME = "trl-lib/ultrafeedback_binarized" # 偏好数据集
BETA = 0.1 # KL 正则化强度
MAX_LENGTH = 2048 # 最大序列长度
8.2 运行训练
python dpotrainer.py
8.3 评估训练效果
训练后,观察:
rewards/margins是否在上升rewards/accuracies是否接近 1.0- 生成测试:模型回答是否更符合预期
9. 本章小结
| 概念 | 说明 |
|---|---|
| 偏好对齐 | 让模型学习”什么回答更好” |
| DPO | 直接偏好优化,简化的 RLHF 替代方案 |
| 偏好数据 | (prompt, chosen, rejected) 三元组 |
| 参考模型 | 用于 KL 正则化,防止模型偏离太远 |
| beta | 控制保守/激进程度的关键超参数 |
10. DPO 的局限性与展望
10.1 DPO 的局限性
依赖离线数据
- 只能使用预先收集的偏好数据
- 无法在线探索和学习
奖励函数隐式化
- DPO 将奖励函数通过数学变换隐藏了
- 无法直接控制奖励信号
- 难以实现复杂的奖励设计
只适用于偏好对齐
- 无法处理需要明确奖励信号的任务
- 如:数学推理、代码生成等需要可验证正确性的任务
10.2 下一步:GRPO 和可验证奖励
问题:如果我们的任务有明确的正确/错误判断(如数学题),能否设计更好的奖励?
答案:可以!这就是 GRPO 等高级 RL 方法的用武之地。
附录:常见问题
Q: DPO 训练后模型变差了?
A: 1) 检查 beta 值,可能太小导致过度偏离 2) 检查偏好数据质量 3) 减少训练 epochsQ: rewards/accuracies 一直不提升?
A: 1) 检查数据格式是否正确 2) 尝试增大学习率 3) 确保 chosen 确实比 rejected 好Q: 显存不够怎么办?
A: 1) 使用ref_model=None让 DPOTrainer 自动处理 2) 减小 batch_size 3) 启用 gradient_checkpointing