DPO偏好优化
SFT 让模型学会"按格式回答",但有时模型的回答虽然正确,却不够友好、不够准确、或者不符合人类偏好。DPO(Direct Preference Optimization,直接偏好优化)就是用来解决这个问题的对齐技术。
1. 为什么需要 DPO
SFT 的局限:
- 只能告诉模型"应该怎么回答",不能告诉它"什么样的回答更好"
- 同一个问题可能有多种合理回答,SFT 只能选一个
DPO 的思路:给模型看偏好对比(好回答 vs 差回答),让它学会向好的偏移。
2. RLHF 与 DPO 的关系
RLHF(基于人类反馈的强化学习)是 ChatGPT 出名前的对齐方法,分三步:
- SFT:监督微调
- RM:训练一个奖励模型(Reward Model)打分
- PPO:用强化学习根据奖励优化策略
RLHF 实现复杂、训练不稳定、资源需求大。
DPO(2023 年提出)用数学推导证明:可以跳过奖励模型和 PPO,直接用偏好数据优化模型。简单、稳定、效果接近 RLHF,迅速成为主流。
3. DPO 数据格式
每条数据包含三部分:
{
"prompt": "什么是机器学习?",
"chosen": "机器学习是人工智能的一个分支,它让计算机系统能够从数据中学习模式并做出预测,而无需明确编程。常见的机器学习类型包括监督学习、无监督学习和强化学习。",
"rejected": "机器学习就是机器在学习东西。"
}
prompt:问题或指令chosen:期望的、更好的回答rejected:较差的回答
4. 如何获得偏好数据
方式 1:人工标注
最准确,但成本高。每条数据让标注员对两个回答二选一。
方式 2:用更强的模型生成 chosen
用当前模型生成 rejected,用 GPT-4 / Claude 生成 chosen:
# 伪代码
for prompt in prompts:
rejected = small_model.generate(prompt)
chosen = gpt4.generate(prompt)
save({"prompt": prompt, "chosen": chosen, "rejected": rejected})
方式 3:基于规则构造
例如教模型不要回答有害问题:
方式 4:使用公开偏好数据集
| 数据集 | 说明 |
|---|---|
Anthropic/hh-rlhf |
Anthropic 发布的有用性 + 无害性偏好数据 |
argilla/dpo-mix-7k |
混合多种来源的高质量偏好数据 |
openbmb/UltraFeedback |
大规模 GPT-4 标注偏好数据 |
5. DPO 训练完整脚本
新建 train_dpo.py:
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
from peft import LoraConfig, PeftModel
from trl import DPOConfig, DPOTrainer
# === DPO 通常基于 SFT 后的模型继续训练 ===
SFT_MODEL = "./output_sft" # 上一步 SFT 训练得到的模型
BASE_MODEL = "Qwen/Qwen2.5-7B-Instruct" # 原始基座
DATA_PATH = "dpo_data.jsonl"
OUTPUT_DIR = "./output_dpo"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# === 加载模型(带 SFT LoRA)===
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
quantization_config=bnb_config,
device_map="auto",
)
# 加载 SFT 阶段的 LoRA 权重
model = PeftModel.from_pretrained(model, SFT_MODEL, is_trainable=True)
# === 新的 LoRA 用于 DPO ===
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
)
# === 加载数据集 ===
dataset = load_dataset("json", data_files=DATA_PATH, split="train")
dataset = dataset.train_test_split(test_size=0.05, seed=42)
# === DPO 训练参数 ===
training_args = DPOConfig(
output_dir=OUTPUT_DIR,
num_train_epochs=1, # DPO 通常只训 1 epoch
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=5e-6, # DPO 学习率比 SFT 小很多
warmup_ratio=0.05,
lr_scheduler_type="cosine",
bf16=True,
gradient_checkpointing=True,
optim="paged_adamw_8bit",
logging_steps=10,
eval_strategy="epoch",
save_strategy="epoch",
save_total_limit=2,
report_to="tensorboard",
# DPO 专属参数
beta=0.1, # KL 散度权重,控制偏离参考模型的程度
max_length=2048,
max_prompt_length=1024,
)
# === 创建 Trainer 并训练 ===
trainer = DPOTrainer(
model=model,
ref_model=None, # 用 PEFT 时不需要单独传参考模型
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
tokenizer=tokenizer,
peft_config=peft_config,
)
trainer.train()
trainer.save_model(OUTPUT_DIR)
启动:
6. 关键参数解读
beta(最重要)
控制 DPO 偏离参考模型的程度:
beta小(如 0.01-0.05)→ 偏移大,效果激进,可能过拟合beta中(0.1-0.2)→ 最常用,平衡偏好和稳定性beta大(如 0.5-1.0)→ 偏移小,效果保守
可以理解为"温度":beta 越大,模型越保守,越接近 SFT 模型。
学习率
DPO 学习率比 SFT 要小一个量级:
- 全量 DPO:
5e-7 ~ 1e-6 - LoRA DPO:
5e-6 ~ 1e-5
学习率太大会导致奖励崩溃(reward hacking)——模型走极端去最大化 chosen 和 rejected 的差距,但生成质量下降。
Epoch
DPO 通常只训 1 个 epoch,最多 2 个。继续训练容易过拟合。
参考模型(ref_model)
DPO 需要一个"参考模型"来计算 KL 散度,防止偏移过大:
- 使用 PEFT 时:不需要显式传
ref_model,TRL 会自动用 LoRA 关闭后的基座作为参考 - 全量训练时:需要传一份初始模型作为
ref_model
7. 监控 DPO 训练
DPO 训练时关注几个特殊指标:
| 指标 | 含义 | 期望趋势 |
|---|---|---|
loss |
DPO 损失 | 下降 |
rewards/chosen |
chosen 的隐式奖励 | 上升 |
rewards/rejected |
rejected 的隐式奖励 | 下降 |
rewards/margins |
chosen - rejected | 上升 |
rewards/accuracies |
chosen > rejected 的比例 | 接近 1 |
如果 rewards/chosen 和 rewards/rejected 都下降(margin 不变),说明模型生成质量整体下降——这是奖励崩溃信号,需要降低学习率或调大 beta。
8. DPO 后的推理
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
BASE_MODEL = "Qwen/Qwen2.5-7B-Instruct"
DPO_DIR = "./output_dpo"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL, quantization_config=bnb_config, device_map="auto"
)
model = PeftModel.from_pretrained(model, DPO_DIR)
model.eval()
messages = [{"role": "user", "content": "推荐一本科幻小说"}]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=512, do_sample=True, temperature=0.7)
print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))
9. SFT + DPO 的标准流程
工业界常见的对齐 pipeline:
每一步的数据:
- SFT:~1万-10万条 (instruction, response) 数据
- DPO:~5千-5万条 (prompt, chosen, rejected) 数据
10. DPO 的变种
DPO 火了之后出现了不少变种,简单了解一下:
| 方法 | 改进点 |
|---|---|
| IPO | 改进 DPO 的损失函数,缓解过拟合 |
| KTO | 不需要成对数据,单条数据加正负标签即可 |
| ORPO | 把 SFT 和偏好优化合并到一步训练 |
| SimPO | 不需要参考模型,更省显存 |
TRL 都已经支持,使用方式类似 DPOTrainer,可以根据数据情况选择。
11. 经验和坑
经验 1:DPO 数据"差距"要明显
chosen 和 rejected 差距太小,模型学不到东西。差距太大(比如一个空白一个完美)也不好,模型可能只学会区分长度。
经验 2:先做好 SFT 再 DPO
直接在 base 模型上做 DPO 效果很差。必须先用 SFT 让模型"会说话"再用 DPO "调教偏好"。
经验 3:检查数据质量
人工抽查 100 条偏好数据,确认 chosen 真的比 rejected 好。GPT 生成的偏好数据质量参差不齐。
坑 1:DPO 后模型重复输出
通常是学习率太大或训练过头。降学习率、减 epoch、调大 beta。
坑 2:DPO 后通用能力下降
灾难性遗忘。混入一些通用偏好数据(如 hh-rlhf)保持平衡。
总结
- DPO 是 RLHF 的简化版,直接用偏好数据训练,无需奖励模型和 PPO
- 数据格式:
{prompt, chosen, rejected} - DPO 通常基于 SFT 模型继续训练,学习率小 10 倍、epoch 只 1 个
- 关键参数
beta(默认 0.1)控制偏离参考模型的程度 - 监控
rewards/margins和rewards/accuracies,警惕奖励崩溃 - 标准流程:base → SFT → DPO → 部署