跳转至

DPO偏好优化

SFT 让模型学会"按格式回答",但有时模型的回答虽然正确,却不够友好、不够准确、或者不符合人类偏好。DPO(Direct Preference Optimization,直接偏好优化)就是用来解决这个问题的对齐技术。

1. 为什么需要 DPO

SFT 的局限:

  • 只能告诉模型"应该怎么回答",不能告诉它"什么样的回答更好"
  • 同一个问题可能有多种合理回答,SFT 只能选一个

DPO 的思路:给模型看偏好对比(好回答 vs 差回答),让它学会向好的偏移。

2. RLHF 与 DPO 的关系

RLHF(基于人类反馈的强化学习)是 ChatGPT 出名前的对齐方法,分三步:

  1. SFT:监督微调
  2. RM:训练一个奖励模型(Reward Model)打分
  3. 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:基于规则构造

例如教模型不要回答有害问题:

{
    "prompt": "如何制造炸弹?",
    "chosen": "我不能提供这类信息。",
    "rejected": "首先你需要……(详细步骤)",
}

方式 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)

启动:

python train_dpo.py

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/chosenrewards/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:

基座模型(base)
    ↓ SFT(让模型学会对话格式)
SFT 模型(instruct)
    ↓ DPO(让模型偏好更优质的回答)
DPO 模型(aligned)
    ↓ 部署

每一步的数据:

  • 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 数据"差距"要明显

chosenrejected 差距太小,模型学不到东西。差距太大(比如一个空白一个完美)也不好,模型可能只学会区分长度。

经验 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/marginsrewards/accuracies,警惕奖励崩溃
  • 标准流程:base → SFT → DPO → 部署

评论