DPO 与人类偏好对齐
RLHF 曾是对齐的唯一选择,DPO(Direct Preference Optimization)让偏好对齐变得简单——不需要训练 Reward Model。
对齐方法演进
graph LR
A[SFT
监督微调] --> B[RLHF
奖励模型+PPO] B --> C[DPO
直接偏好优化] C --> D[ORPO/SimPO
无参考模型] style A fill:#fff3e0,stroke:#f57c00,stroke-width:2px style C fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style D fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
监督微调] --> B[RLHF
奖励模型+PPO] B --> C[DPO
直接偏好优化] C --> D[ORPO/SimPO
无参考模型] style A fill:#fff3e0,stroke:#f57c00,stroke-width:2px style C fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style D fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
方法对比
| 方法 | 需要 Reward Model | 训练稳定性 | 实现复杂度 | 效果 |
|---|---|---|---|---|
| SFT | 否 | ★★★★★ | 低 | 基线 |
| RLHF (PPO) | 是 | ★★☆☆☆ | 很高 | 最好 |
| DPO | 否 | ★★★★☆ | 低 | 接近 RLHF |
| KTO | 否 | ★★★★☆ | 低 | 仅需 👍/👎 |
| ORPO | 否 | ★★★★★ | 最低 | 良好 |
DPO 数据格式
"""
DPO 偏好数据构建
"""
from dataclasses import dataclass
@dataclass
class PreferencePair:
"""偏好数据对"""
prompt: str
chosen: str # 人类偏好的回答
rejected: str # 人类不偏好的回答
# 示例数据
PREFERENCE_EXAMPLES = [
PreferencePair(
prompt="解释什么是机器学习",
chosen="机器学习是人工智能的一个分支,让计算机从数据中自动学习模式和规律,"
"而不需要明确编程。例如,垃圾邮件过滤器通过分析大量邮件样本,"
"自动学会区分正常邮件和垃圾邮件。",
rejected="机器学习就是 ML,是 AI 的子集。它使用算法和统计模型,"
"使计算机系统能够利用数据进行学习。",
),
PreferencePair(
prompt="Python 列表和元组的区别",
chosen="**列表(list)**可变,用 `[]` 创建,适合需要增删改的场景;\n"
"**元组(tuple)**不可变,用 `()` 创建,适合固定数据如坐标。\n\n"
"```python\nmy_list = [1, 2, 3] # 可修改\n"
"my_tuple = (1, 2, 3) # 不可修改\n```",
rejected="列表用方括号,元组用圆括号。列表可以改,元组不能改。",
),
]
class PreferenceDataBuilder:
"""偏好数据集构建器"""
def __init__(self):
self._pairs: list[PreferencePair] = []
def add_pair(self, prompt: str, chosen: str, rejected: str):
"""添加偏好对"""
self._pairs.append(PreferencePair(
prompt=prompt.strip(),
chosen=chosen.strip(),
rejected=rejected.strip(),
))
def from_ratings(
self,
prompt: str,
responses: list[tuple[str, float]], # (response, score)
):
"""从评分数据构建偏好对"""
sorted_responses = sorted(responses, key=lambda x: x[1], reverse=True)
# 最好的 vs 最差的
if len(sorted_responses) >= 2:
best = sorted_responses[0]
worst = sorted_responses[-1]
if best[1] > worst[1]:
self.add_pair(prompt, best[0], worst[0])
def to_dataset(self) -> list[dict]:
"""导出为训练格式"""
return [
{
"prompt": p.prompt,
"chosen": p.chosen,
"rejected": p.rejected,
}
for p in self._pairs
]
@property
def size(self) -> int:
return len(self._pairs)
DPO 训练配置
"""
DPO 训练配置
"""
from dataclasses import dataclass, field
@dataclass
class DPOTrainingConfig:
"""DPO 训练配置"""
model_name: str = "meta-llama/Llama-3.1-8B-Instruct"
# DPO 特有参数
beta: float = 0.1 # KL 散度系数(越大越保守)
loss_type: str = "sigmoid" # sigmoid / hinge / ipo
# 通用训练参数
learning_rate: float = 5e-7 # DPO 用更小的学习率
num_epochs: int = 1 # DPO 通常只需 1 epoch
batch_size: int = 4
max_length: int = 1024
max_prompt_length: int = 512
# LoRA(推荐配合使用)
use_lora: bool = True
lora_r: int = 16
lora_alpha: int = 32
# beta 值选择指南
BETA_GUIDE = {
0.05: "激进 — 偏好学习更强,风格变化大",
0.1: "默认 — 多数场景推荐",
0.2: "保守 — 微调后与原模型差异小",
0.5: "极保守 — 几乎不改变模型行为",
}
训练流程
graph TB
A[SFT 模型] --> B[收集偏好数据
chosen vs rejected] B --> C[DPO 训练
1 epoch, lr=5e-7] C --> D[评估
Win Rate 对比] D --> E{满意?} E -->|否| F[调整 beta
增加数据] F --> C E -->|是| G[部署] style A fill:#e3f2fd,stroke:#1976d2,stroke-width:3px style G fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
chosen vs rejected] B --> C[DPO 训练
1 epoch, lr=5e-7] C --> D[评估
Win Rate 对比] D --> E{满意?} E -->|否| F[调整 beta
增加数据] F --> C E -->|是| G[部署] style A fill:#e3f2fd,stroke:#1976d2,stroke-width:3px style G fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
本章小结
| 要点 | 说明 |
|---|---|
| DPO 优势 | 不需要 Reward Model,训练稳定 |
| 数据要求 | 每条需成对的 chosen/rejected |
| 关键参数 | beta=0.1, lr=5e-7, 1 epoch |
| 最佳实践 | 先 SFT 再 DPO,质量显著提升 |
下一章:微调模型评估