为什么需要对齐

大模型预训练后学会了"续写文本",但不会"按要求回答"。对齐(Alignment)让模型学会按人类偏好行事——有帮助、诚实、无害。

预训练模型:"如何制作蛋糕?" → 续写:"如何制作饼干?"(续写模式)
对齐后模型:"如何制作蛋糕?" → 回答:"制作蛋糕的步骤如下:1..."(问答模式)

RLHF:经典三阶段

阶段一:SFT(监督微调)

# 用人工标注的指令-回答对微调模型
sft_data = [
    {"instruction": "写一首关于春天的诗", "response": "春风拂面花开早..."},
    {"instruction": "解释什么是量子纠缠", "response": "量子纠缠是指..."},
]

# 标准的下一个 Token 预测训练
def sft_loss(model, batch):
    input_ids = batch["input_ids"]      # instruction + response
    labels = batch["labels"]            # 只对 response 部分计算 loss
    outputs = model(input_ids, labels=labels)
    return outputs.loss

阶段二:奖励模型(RM)

训练一个模型来预测人类偏好分数:

class RewardModel(nn.Module):
    def __init__(self, base_model):
        super().__init__()
        self.backbone = base_model      # 通常用 SFT 模型初始化
        self.reward_head = nn.Linear(hidden_size, 1)
    
    def forward(self, input_ids):
        hidden = self.backbone(input_ids).last_hidden_state[:, -1, :]
        reward = self.reward_head(hidden)  # 标量奖励
        return reward

# 训练数据:同一问题的两个回答,标注哪个更好
# {prompt, chosen_response, rejected_response}
def rm_loss(reward_model, batch):
    chosen_rewards = reward_model(batch["chosen_ids"])
    rejected_rewards = reward_model(batch["rejected_ids"])
    # Bradley-Terry 模型:chosen 应比 rejected 分数高
    loss = -torch.log(torch.sigmoid(chosen_rewards - rejected_rewards))
    return loss.mean()

阶段三:PPO(近端策略优化)

用奖励模型的分数作为信号,通过 RL 优化 SFT 模型:

import trl
from trl import PPOTrainer, PPOConfig

def ppo_train(sft_model, reward_model, dataset):
    config = PPOConfig(
        learning_rate=5e-7,
        batch_size=32,
        mini_batch_size=8,
        ppo_epochs=4,
        cliprange=0.2,
        kl_penalty="kl",          # KL 散度惩罚,防止偏离 SFT 太远
        target_kl=6.0,
        beta=0.1,                  # KL 惩罚系数
    )
    
    ppo_trainer = PPOTrainer(
        config=config,
        model=sft_model,
        ref_model=sft_model,        # 参考模型(冻结的 SFT)用于 KL 计算
        tokenizer=tokenizer,
        dataset=dataset,
    )
    
    for batch in dataloader:
        # 1. 生成回答
        response = ppo_trainer.generate(batch["prompt"])
        
        # 2. 奖励模型打分
        rewards = reward_model.score(batch["prompt"], response)
        
        # 3. PPO 更新
        stats = ppo_trainer.step(batch["prompt"], response, rewards)

RLHF 的痛点

# RLHF 的工程复杂度
rlhf_components = {
    "模型数量": 4,  # SFT + Reference + Reward + Actor(各需 GPU)
    "训练阶段": 3,  # SFT → RM → PPO
    "超参数": 20+,  # PPO 特有的 cliprange, kl_penalty, beta, target_kl...
    "稳定性问题": [
        "奖励模型过拟合",
        "PPO 训练发散",
        "模型产生 reward hacking(钻奖励模型空子)",
        "KL 约束失效导致模型崩溃",
    ],
    "显存需求": "4× 模型大小(SFT+Ref+RM+Actor)",
}

DPO:直接偏好优化

核心思想

DPO 的关键洞察:不需要显式的奖励模型。通过数学推导,将偏好优化问题转化为一个简单的分类问题。

数学原理

RLHF 的目标是最大化奖励同时不偏离参考模型太远:

max E[r(x,y)] - β·KL[π || π_ref]

DPO 证明这个问题的最优解可以写成:

r(x,y) = β·log(π(y|x)/π_ref(y|x)) + const

将这个解代入 Bradley-Terry 偏好模型,得到 DPO 损失:

def dpo_loss(policy_chosen_logps, policy_rejected_logps,
             ref_chosen_logps, ref_rejected_logps, beta=0.1):
    """
    DPO Loss: -log σ(β · [log(π_chosen/π_ref_chosen) - log(π_rejected/π_ref_rejected)])
    """
    chosen_logratio = policy_chosen_logps - ref_chosen_logps
    rejected_logratio = policy_rejected_logps - ref_rejected_logps
    
    logits = beta * (chosen_logratio - rejected_logratio)
    loss = -torch.nn.functional.logsigmoid(logits).mean()
    
    return loss

DPO 训练流程

from trl import DPOTrainer, DPOConfig

def dpo_train(sft_model_path, preference_data):
    # 1. 加载 SFT 模型(作为策略模型和参考模型)
    model = AutoModelForCausalLM.from_pretrained(sft_model_path)
    ref_model = AutoModelForCausalLM.from_pretrained(sft_model_path)  # 冻结
    
    # 2. DPO 配置
    config = DPOConfig(
        learning_rate=5e-7,
        batch_size=16,
        gradient_accumulation_steps=2,
        beta=0.1,                    # KL 约束强度
        loss_type="sigmoid",          # 标准 DPO
        max_length=1024,
        max_prompt_length=512,
        num_train_epochs=3,
        bf16=True,
    )
    
    # 3. 训练
    trainer = DPOTrainer(
        model=model,
        ref_model=ref_model,
        args=config,
        train_dataset=preference_data,
        tokenizer=tokenizer,
    )
    
    trainer.train()
    return model

# 偏好数据格式
preference_data = [
    {
        "prompt": "解释什么是递归",
        "chosen": "递归是一种函数调用自身的编程技术。它通过将问题分解为更小的同类子问题来求解。例如阶乘:f(n) = n * f(n-1)...",
        "rejected": "递归就是函数调用自己。",
    },
    # ...
]

DPO 的优势

维度RLHF (PPO)DPO
模型数量4(SFT+Ref+RM+Actor)2(Policy+Ref)
训练阶段3(SFT→RM→PPO)2(SFT→DPO)
超参数20+~5
稳定性差(PPO 易发散)好(分类问题)
显存4× 模型2× 模型
实现复杂度
效果略好(有 RM 信号)接近 RLHF

DPO 变体

IPO(Identity Preference Optimization)

DPO 在数据无噪声时可能过拟合,IPO 添加正则化:

def ipo_loss(chosen_logratio, rejected_logratio, beta=0.1):
    logits = chosen_logratio - rejected_logratio
    # IPO: (logits - 1/2)² 代替 -log σ(logits)
    loss = (logits - 0.5) ** 2
    return loss.mean()

KTO(Kahneman-Tversky Optimization)

不需要成对偏好数据,只需二元反馈(赞/踩):

# KTO 数据格式:不需要 rejected 回答
kto_data = [
    {"prompt": "写首诗", "completion": "春风...", "label": True},   # 正例
    {"prompt": "写首诗", "completion": "不知道", "label": False},  # 负例
]

def kto_loss(policy_logps, ref_logps, labels, beta=0.1):
    # 正例:最大化 log(π/π_ref)
    # 负例:最小化 log(π/π_ref)
    logratios = policy_logps - ref_logps
    # Kahneman-Tversky 价值函数:损失厌恶
    losses = torch.where(
        labels,
        1 - torch.sigmoid(beta * logratios),           # 正例 loss
        1 - torch.sigmoid(-beta * logratios),          # 负例 loss
    )
    return losses.mean()

变体对比

方法数据需求核心改进适用场景
DPO成对偏好直接优化偏好通用首选
IPO成对偏好防过拟合正则数据量小
KTO二元反馈不需成对数据只有赞/踩反馈
SimPO成对偏好无需参考模型省显存
ORPOSFT+偏好SFT 和偏好合并省训练阶段
# SimPO:无需参考模型,省一半显存
from trl import DPOConfig

config = DPOConfig(
    loss_type="simpo",       # 使用 SimPO loss
    beta=2.0,                # SimPO 的 beta 通常更大
    ref_model=None,          # 不需要参考模型!
)

实验对比

在 Llama-2-7B 上的对齐效果(MT-Bench 分数):

方法阶段数显存MT-Bench训练时间稳定性
SFT only16.01h⭐⭐⭐
RLHF37.310h+
DPO27.13h⭐⭐⭐
KTO27.03h⭐⭐⭐
SimPO27.22h⭐⭐⭐
ORPO16.91.5h⭐⭐⭐

数据为参考值,实际效果取决于数据质量和超参数。

数据质量:对齐的真正瓶颈

# 偏好数据质量比方法选择更重要
# 高质量 DPO 数据 >> 低质量 RLHF

# 好的偏好数据特征:
good_preference_data = {
    "chosen_better_reason": "更准确、更详细、更安全",
    "rejected_not_bad": "不是垃圾回答,只是不如 chosen",
    "diversity": "覆盖多种任务类型",
    "consistency": "多个标注员一致性 > 0.8",
}

# 用 GPT-4 生成偏好数据(低成本方案)
def generate_preference_data(prompt, sft_model):
    # 生成两个不同温度的回答
    answer_a = sft_model.generate(prompt, temperature=0.3)
    answer_b = sft_model.generate(prompt, temperature=0.9)
    
    # 用 GPT-4 判断哪个更好
    judge_prompt = f"""选择更好的回答:
    问题:{prompt}
    回答A:{answer_a}
    回答B:{answer_b}
    输出 A 或 B:"""
    
    preference = gpt4.generate(judge_prompt)
    
    if preference == "A":
        return {"prompt": prompt, "chosen": answer_a, "rejected": answer_b}
    else:
        return {"prompt": prompt, "chosen": answer_b, "rejected": answer_a}

选型建议

def choose_alignment_method(requirements):
    if requirements.get("team_size", 0) <= 2:
        return "DPO"      # 小团队:简单稳定
    if requirements.get("data_type") == "binary_feedback":
        return "KTO"      # 只有赞/踩:用 KTO
    if requirements.get("gpu_memory") == "limited":
        return "SimPO"    # 显存有限:无需参考模型
    if requirements.get("max_quality"):
        return "RLHF"     # 追求极致质量:有资源上 RLHF
    if requirements.get("fast_iteration"):
        return "ORPO"     # 快速迭代:合并 SFT 和偏好
    return "DPO"          # 默认推荐

2025 年推荐路径:SFT → DPO(90% 的场景够用)。只有在 DPO 效果明确不够时才考虑 RLHF。数据质量决定上限,方法只是逼近这个上限的工具。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。