为什么需要对齐
大模型预训练后学会了"续写文本",但不会"按要求回答"。对齐(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 | 成对偏好 | 无需参考模型 | 省显存 |
| ORPO | SFT+偏好 | 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 only | 1 | 1× | 6.0 | 1h | ⭐⭐⭐ |
| RLHF | 3 | 4× | 7.3 | 10h+ | ⭐ |
| DPO | 2 | 2× | 7.1 | 3h | ⭐⭐⭐ |
| KTO | 2 | 2× | 7.0 | 3h | ⭐⭐⭐ |
| SimPO | 2 | 1× | 7.2 | 2h | ⭐⭐⭐ |
| ORPO | 1 | 1× | 6.9 | 1.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论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
