Chapter 08

RLHF & DPO:偏好对齐训练

SFT 让模型学会任务,对齐让模型知道什么是"好回答"。DPO 用更简洁的方式实现了 RLHF 的核心效果。

为什么需要对齐训练

SFT(监督微调)训练后的模型知道"怎么回答",但不一定知道"哪种回答更好"。对同一个问题,模型可能产生多个"都还可以"的回答,但它们的质量差异很大——一个简洁准确,另一个冗长甚至有误导性。对齐训练(Alignment Training)通过让模型学习人类偏好,使其在多个候选回答中选择更符合期望的那个。

对齐问题(Alignment Problem)
模型优化的目标(预测下一个 token 的概率)与我们真正想要的(有用、无害、诚实的回答)之间存在差距。SFT 只能让模型学会模仿示例,但人类真正的偏好难以用"模仿哪些示例"来完整表达——有时候"哪个更好"比"给出示例"更容易判断。对齐训练就是直接从"哪个更好"的比较数据中学习。
RLHF(Reinforcement Learning from Human Feedback)
人类反馈强化学习。InstructGPT(ChatGPT 的前身)使用的方法,由 OpenAI 在 2022 年提出。流程分三阶段:① SFT 初始化模型 → ② 用人类偏好数据训练奖励模型(Reward Model)→ ③ 用 PPO(近端策略优化)强化学习算法将策略模型朝向高奖励方向优化。效果显著但流程复杂,对调参要求极高。
DPO(Direct Preference Optimization)
直接偏好优化。Rafailov et al. 2023 年提出,发表于 NeurIPS 2023。其核心思想是:跳过奖励模型,直接从偏好数据(chosen/rejected 对)中推导最优策略的闭式解。数学上证明 DPO 与 RLHF 的优化目标等价,但实现更简单、更稳定。目前是最广泛使用的对齐方法。

RLHF 的三阶段流程

阶段一:SFT(监督微调) - 在高质量指令-回复数据上微调 - 得到能遵循指令的基础模型 π_SFT ↓ 阶段二:奖励模型训练 - 收集人类偏好对:(prompt, chosen, rejected) - 训练奖励模型 r(x, y) = "这个回答有多好" - 奖励模型本质是个分类器:chosen 应得高分,rejected 应得低分 ↓ 阶段三:PPO 强化学习 - 用当前策略生成回答,奖励模型打分 - PPO 梯度更新:提升高奖励回答的概率 - KL 惩罚:限制策略不能偏离 SFT 模型太远(防止奖励黑客攻击) ↓ 最终得到对齐后的模型 π_RL
奖励黑客攻击(Reward Hacking)
在 PPO 阶段,模型有时会学到"钻奖励模型空子"的策略——例如发现奖励模型偏爱更长的回答,于是生成大量无意义的填充内容来提高得分。这些回答在奖励模型看来是"好的",但对真实用户毫无价值。KL 散度惩罚项(β × KL(π_RL || π_SFT))限制了策略偏离 SFT 基础的程度,是防止奖励黑客攻击的核心机制。
PPO(Proximal Policy Optimization)
近端策略优化,OpenAI 2017 年提出的强化学习算法。"近端"的含义是每次更新时,新策略不能与旧策略偏差太大(通过 clip 操作或 KL 约束实现),这保证了训练稳定性。PPO 是 RL 中的"信任域方法"的简化版,比 TRPO 更容易实现,但参数调节仍然相当困难。

DPO 的数学原理

DPO 的关键洞察是:RLHF 的最优策略有一个闭式解。给定任意奖励函数 r(x, y) 和 KL 约束,最优策略满足:

RLHF 目标(有 KL 约束): max E[r(x, y)] - β × KL(π(y|x) || π_ref(y|x)) 最优策略的闭式解: π*(y|x) ∝ π_ref(y|x) × exp(r(x,y) / β) 将奖励函数用策略表示(反推): r(x, y) = β × log[π*(y|x) / π_ref(y|x)] + β × log Z(x) 代入 Bradley-Terry 偏好模型(人类选 chosen 的概率): P(y_w > y_l | x) = σ(r(x, y_w) - r(x, y_l)) DPO 损失函数(最终形式): L_DPO = -E[log σ(β × log(π_θ(y_w|x)/π_ref(y_w|x)) - β × log(π_θ(y_l|x)/π_ref(y_l|x)))] 直觉解读: - 增大 chosen 相对于参考模型的对数概率比值 - 减小 rejected 相对于参考模型的对数概率比值 - β 控制偏离参考策略的程度
为什么 DPO 比 RLHF 简单?

RLHF 需要单独训练一个奖励模型,再用 PPO 进行强化学习(4个神经网络同时在 GPU 内存中:actor、critic、reward model、reference model)。DPO 将奖励模型隐式地嵌入了策略模型本身,只需要策略模型和一个固定的参考模型,训练过程与 SFT 几乎相同,稳定性大幅提升。代价是 DPO 在某些场景(特别是需要复杂的长期奖励)上效果略逊于精调好的 PPO。

DPO 数据格式

{
  "prompt": "如何提高代码质量?",
  "chosen": "提高代码质量的关键方法:\n1. 遵循 SOLID 原则(单一职责、开放封闭等)\n2. 编写单元测试(覆盖率 >80%)\n3. 代码审查(每个 PR 至少一位审阅者)\n4. 使用 Linter 和静态分析工具...",
  "rejected": "你可以多写注释,命名要规范,还有就是多练习。"
}
偏好数据格式中的常见错误

DPO 训练代码(TRL 库)

from trl import DPOTrainer, DPOConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
import torch

# 加载 SFT 训练后的模型(DPO 的起点必须是 SFT 模型)
# 注意:用预训练基座直接做 DPO 效果差,必须先做 SFT
model = AutoModelForCausalLM.from_pretrained(
    "./sft-checkpoint",
    torch_dtype=torch.bfloat16,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained("./sft-checkpoint")

# DPO 配合 LoRA 使用(节省显存,参考模型自动从 LoRA 适配器推导)
lora_config = LoraConfig(
    r=16,                  # DPO 通常用较小的 rank
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05,
    task_type="CAUSAL_LM"
)

# DPO 配置关键参数
dpo_config = DPOConfig(
    output_dir="./dpo-output",
    beta=0.1,               # KL 散度惩罚系数(核心超参数)
    learning_rate=5e-5,     # 通常比 SFT 小 2-5 倍
    num_train_epochs=1,      # DPO 通常只需 1-2 epoch,过多会过拟合偏好数据
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,
    max_length=2048,         # prompt + chosen/rejected 的总长度上限
    max_prompt_length=512,   # prompt 部分的最大长度
    remove_unused_columns=False,  # DPO 需要保留 prompt/chosen/rejected 列
    warmup_ratio=0.1,        # 10% steps 用于 warm-up
    logging_steps=10,
    save_strategy="steps",
    save_steps=100,
    fp16=False,
    bf16=True,               # 使用 BF16 更稳定
)

trainer = DPOTrainer(
    model=model,
    args=dpo_config,
    train_dataset=dpo_dataset,  # 需有 prompt/chosen/rejected 三列
    tokenizer=tokenizer,
    peft_config=lora_config,    # 传入 LoRA 配置,自动处理参考模型
)

trainer.train()
trainer.save_model("./dpo-final")

构建偏好数据集

偏好数据的质量比数量更重要。一般来说,500-2000 条高质量偏好对可以显著改善模型行为,但劣质偏好数据会损害模型。

方法 1:人工标注
对同一问题让模型生成多个回答,由领域专家选择最佳和最差的。优点:标注质量最高;缺点:成本最高(需要专业标注人员),难以大规模扩展。适用于高价值专业场景(医疗、法律、金融)。
方法 2:AI 辅助标注(Constitutional AI)
用强力模型(GPT-4o 或 Claude)评判哪个回答更好,并给出判断原因(以便事后审核)。成本较低,可大规模扩展。质量取决于裁判模型的能力——如果被标注模型与裁判模型能力接近,标注质量会下降。
方法 3:规则生成
用规则自动标注偏好对:例如"有引用来源 = better"、"使用专业术语 = better"、"长度在范围内 = better"。成本最低,但规则可能不完全对齐真实人类偏好,需要与其他方法结合验证。
方法 4:拒绝采样(Rejection Sampling)
对每个 prompt,用当前模型采样 N 个回答(N=8 到 16),选择质量最高的作为 chosen,随机选择一个较差的作为 rejected。随着模型迭代改进,新轮次的 chosen 质量越来越高,形成正向循环。这是 Llama 3 等模型使用的方法。
import json
import anthropic

client = anthropic.Anthropic()

def judge_preference(prompt: str, response_a: str, response_b: str) -> tuple[str, str]:
    """使用 Claude 判断两个回答的优劣,返回 (chosen, rejected)"""
    judgment = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=512,
        system="""你是一个 AI 回答质量评判者。
从以下维度评判哪个回答更好:
1. 准确性(信息是否正确)
2. 完整性(是否覆盖关键点)
3. 清晰度(是否易于理解)
4. 实用性(是否对用户有实际帮助)

必须输出 JSON 格式:
{"winner": "A" or "B", "reason": "一句话原因", "score_a": 1-5, "score_b": 1-5}""",
        messages=[{
            "role": "user",
            "content": f"问题:{prompt}\n\n回答A:{response_a}\n\n回答B:{response_b}"
        }]
    )

    result = json.loads(judgment.content[0].text)

    # 仅保留差距明显的偏好对(避免噪声)
    score_diff = abs(result["score_a"] - result["score_b"])
    if score_diff < 2:                  # 差距小于 2 分,丢弃
        return None, None

    if result["winner"] == "A":
        return response_a, response_b   # (chosen, rejected)
    else:
        return response_b, response_a

beta 超参的影响

beta 值效果适用场景
β = 0.01 - 0.05激进对齐,偏好数据影响大,可能导致模型输出不流畅偏好数据质量很高、数量充足时
β = 0.1(默认)平衡对齐强度与保守程度,大多数情况稳定工作绝大多数场景的推荐起始值
β = 0.5 - 1.0保守对齐,更多保留 SFT 基础行为偏好数据噪声较大或数量不足时
DPO 常见失败模式

评估对齐效果

Reward Margin(奖励边际)
DPO 训练日志中的核心指标。定义为:模型对 chosen 和 rejected 的隐式奖励差值(chosen 的对数概率比值 - rejected 的对数概率比值)。理想情况下,训练过程中 reward margin 持续为正且增大。如果 reward margin 开始下降或变为负值,说明训练出了问题。
Win Rate(胜率)
用强力模型(GPT-4o 或 Claude)作为裁判,让 DPO 前后的模型对相同测试问题各生成一个回答,由裁判判断哪个更好。DPO 后模型的胜率(vs DPO 前)通常在 55%-75% 之间才算有效对齐。低于 55% 说明对齐效果有限;高于 75% 需要检查是否有长度偏差。
MT-Bench 评分
对话质量标准评测集,包含 80 个多轮对话问题,覆盖推理、数学、代码、写作等场景,使用 GPT-4 自动评分(1-10分)。可以用来量化对齐前后的质量变化,以及与其他开源模型的横向对比。
# 计算并记录 DPO 训练指标(在 TRL 的 callbacks 中)
from transformers import TrainerCallback

class DPOMonitorCallback(TrainerCallback):
    """监控 DPO 训练关键指标"""

    def on_log(self, args, state, control, logs=None, **kwargs):
        if logs is None:
            return

        # 提取关键 DPO 指标
        chosen_reward = logs.get("train/rewards/chosen", None)
        rejected_reward = logs.get("train/rewards/rejected", None)

        if chosen_reward and rejected_reward:
            margin = chosen_reward - rejected_reward
            if margin < 0:
                # 奖励边际为负是严重警告信号
                print(f"[警告] DPO reward margin 为负: {margin:.4f}")
                print("  可能原因:beta 太大 / 偏好数据有噪声 / 参考模型不匹配")

            # 记录到 W&B(如果启用)
            print(f"Step {state.global_step}: "
                  f"chosen={chosen_reward:.4f}, "
                  f"rejected={rejected_reward:.4f}, "
                  f"margin={margin:.4f}")

PPO vs DPO 选择指南

维度PPO(完整 RLHF)DPO
实现复杂度高(4 个模型同时在显存中)低(与 SFT 基本相同)
训练稳定性低(超参数敏感)高(类似 SFT 训练)
显存需求4× 模型显存2× 模型显存
长期奖励学习强(强化学习的优势)弱(单步偏好)
偏好数据要求在线数据(边训练边采样)离线数据(预先准备好)
调参难度极难(学习率、KL 系数、clip 范围等)简单(主要是 beta)
推荐场景顶级模型(需要极致对齐效果)绝大多数业务微调场景

完整的对齐训练流程

步骤 1:准备 SFT 基础模型 └─ 完成 SFT 训练,保存为检查点(./sft-checkpoint) ↓ 步骤 2:准备偏好数据 ├─ 人工标注 / AI 辅助标注 / 拒绝采样 ├─ 过滤:chosen == rejected 的样本 ├─ 检查:chosen 和 rejected 长度分布是否相近 └─ 格式化为 {prompt, chosen, rejected} 的 JSON ↓ 步骤 3:DPO 训练 ├─ 加载 SFT 检查点为策略模型 ├─ 固定 SFT 检查点为参考模型 ├─ beta=0.1,epochs=1,学习率=5e-5 └─ 监控 reward margin(应持续为正) ↓ 步骤 4:评估 ├─ 计算 Win Rate(vs DPO 前模型) ├─ MT-Bench 评分对比 └─ 人工抽检 50-100 条样本 ↓ 步骤 5:迭代(如需要) └─ 根据评估反馈调整偏好数据或 beta 值
本章核心要点