"微调"、"RLHF"、"DPO"——这三个词在大模型圈被频繁提及,但很多工程师对它们的本质区别和适用场景并不清晰。本文从工程视角系统梳理三种对齐技术,帮你做出正确的技术选型。
一、三种技术解决的是不同问题
先明确一个重要认知:Fine-Tuning、RLHF、DPO 并不是替代关系,而是针对不同问题的不同工具。
大模型能力提升路径(简化):
┌─────────────────────────────────────────┐
│ 预训练(Pre-training) │
│ 目标:让模型学习语言能力和世界知识 │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 指令微调(SFT/Instruction Tuning) │
│ 目标:让模型学会"听指令" │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 对齐训练(RLHF / DPO / 其他) │
│ 目标:让模型"说出符合偏好的答案" │
└─────────────────┬───────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 领域微调(Domain Fine-Tuning) │
│ 目标:让模型精通特定垂直领域 │
└─────────────────────────────────────────┘
当我们说"用 Fine-Tuning 还是 RLHF"时,往往是在问两个不同层次的问题没有对齐。
二、监督微调(Supervised Fine-Tuning,SFT)
2.1 核心原理
SFT 是最直接的方法:给模型看大量(输入, 输出)配对数据,用交叉熵损失训练模型,让它学会对特定输入产生特定输出。
# SFT 的训练逻辑(高度简化)
def sft_training_step(model, batch):
input_ids = batch["input_ids"] # 用户指令 + 开头
labels = batch["labels"] # 期望的助手回复
# 前向传播
outputs = model(input_ids=input_ids, labels=labels)
# 只对助手回复部分计算 loss
loss = outputs.loss
# 反向传播
loss.backward()
optimizer.step()
return loss.item()
2.2 数据格式标准
# SFT 训练数据格式(ChatML 格式)
training_example = {
"messages": [
{
"role": "system",
"content": "你是一个专业的代码助手,擅长 Python 开发。"
},
{
"role": "user",
"content": "写一个快速排序函数"
},
{
"role": "assistant",
"content": """def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)"""
}
]
}
2.3 SFT 的工程实现
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from trl import SFTTrainer, DataCollatorForCompletionOnlyLM
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B",
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B")
# 只计算 response 部分的 loss
response_template = "<|im_start|>assistant\n"
collator = DataCollatorForCompletionOnlyLM(
response_template=response_template,
tokenizer=tokenizer
)
training_args = TrainingArguments(
output_dir="./sft_output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-5,
warmup_ratio=0.1,
lr_scheduler_type="cosine",
fp16=True,
save_strategy="epoch",
logging_steps=50,
)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
data_collator=collator,
training_args=training_args,
max_seq_length=2048,
)
trainer.train()
2.4 SFT 的局限性
SFT 的核心问题是:它只能让模型学会"模仿",而无法真正理解什么是"好的回答"。
具体表现:
- 分布外泛化差:训练集没覆盖的指令类型,效果明显下降
- 复制偏差:倾向于模仿训练数据的写作风格,即使有更好的表达方式
- 无法表达不确定性:不知道什么时候应该说"我不确定"
三、基于人类反馈的强化学习(RLHF)
3.1 RLHF 的完整流程
RLHF 是让模型学习人类偏好的革命性技术,包含三个阶段:
RLHF 训练三阶段:
Phase 1: 收集偏好数据
- 让 SFT 模型对同一问题生成 2+ 个不同回答
- 人工标注哪个回答更好(偏好对)
- 格式:(question, response_A, response_B, preference)
Phase 2: 训练奖励模型(Reward Model)
- 用偏好对数据训练一个评分模型
- 输入:(question, response)
- 输出:一个标量分数(表示人类偏好程度)
Phase 3: PPO 强化学习优化
- 用奖励模型的评分作为奖励信号
- 用 PPO 算法优化 SFT 模型
- 同时加 KL 散度约束,防止模型偏离太远
3.2 奖励模型训练
from transformers import AutoModelForSequenceClassification
import torch
import torch.nn as nn
class RewardModel(nn.Module):
def __init__(self, base_model_name: str):
super().__init__()
# 在预训练模型上加一个线性层输出标量奖励
self.backbone = AutoModelForSequenceClassification.from_pretrained(
base_model_name,
num_labels=1, # 输出一个标量
)
def forward(self, input_ids, attention_mask):
outputs = self.backbone(
input_ids=input_ids,
attention_mask=attention_mask
)
return outputs.logits # shape: (batch_size, 1)
def reward_model_loss(chosen_reward, rejected_reward):
"""
对比损失:让 chosen 的奖励高于 rejected
"""
return -torch.log(torch.sigmoid(chosen_reward - rejected_reward)).mean()
# 训练循环
for batch in preference_dataloader:
chosen_inputs = tokenizer(batch["chosen"], return_tensors="pt", padding=True)
rejected_inputs = tokenizer(batch["rejected"], return_tensors="pt", padding=True)
chosen_reward = reward_model(**chosen_inputs)
rejected_reward = reward_model(**rejected_inputs)
loss = reward_model_loss(chosen_reward, rejected_reward)
loss.backward()
optimizer.step()
3.3 RLHF 的工程实现(使用 TRL)
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
# PPO 配置
ppo_config = PPOConfig(
model_name="your_sft_model",
learning_rate=1.41e-5,
log_with="wandb",
mini_batch_size=4,
batch_size=16,
gradient_accumulation_steps=1,
optimize_cuda_cache=True,
early_stopping=False,
target_kl=0.1, # KL 散度约束上限
kl_penalty="kl",
seed=0,
steps=20000,
init_kl_coef=0.2, # 初始 KL 系数
)
# 加载带 value head 的模型
model = AutoModelForCausalLMWithValueHead.from_pretrained(
ppo_config.model_name
)
ppo_trainer = PPOTrainer(
config=ppo_config,
model=model,
ref_model=ref_model, # 参考模型(SFT checkpoint,frozen)
tokenizer=tokenizer,
dataset=dataset,
data_collator=collator
)
for epoch in range(ppo_config.steps):
for batch in ppo_trainer.dataloader:
# 生成回复
query_tensors = batch["input_ids"]
response_tensors = ppo_trainer.generate(
query_tensors,
max_new_tokens=512,
)
# 用奖励模型评分
rewards = [reward_model(q, r) for q, r in zip(query_tensors, response_tensors)]
# PPO 更新
stats = ppo_trainer.step(query_tensors, response_tensors, rewards)
3.4 RLHF 的工程难点
RLHF 的强大伴随着极高的工程复杂度:
| 难点 | 描述 | 缓解方案 |
|---|---|---|
| 奖励 Hack | 模型发现奖励模型的盲点,给出高分但质量差的回答 | 定期更新奖励模型,加入多样性约束 |
| 训练不稳定 | PPO 的超参数极其敏感 | 仔细调 KL 系数,使用 adaptive KL |
| 人力成本 | 偏好数据标注成本极高 | 用 AI 辅助标注,减少人工标注量 |
| 计算成本 | 需要同时维护 4 个模型 | 使用 LoRA,降低计算开销 |
四、直接偏好优化(Direct Preference Optimization,DPO)
4.1 DPO 的核心创新
DPO 是斯坦福团队在 2023 年提出的工作,核心发现:
RLHF 中奖励模型训练 + PPO 优化可以被数学等价地合并为一个简单的分类损失函数。
这意味着 DPO 可以直接从偏好数据训练语言模型,完全跳过奖励模型和 PPO 阶段。
RLHF 流程:
SFT 数据 → SFT 模型 → 偏好数据 → 奖励模型 → PPO 强化学习 → 对齐模型
DPO 流程:
SFT 数据 → SFT 模型 → 偏好数据 ─────────────────────────→ 对齐模型
4.2 DPO 损失函数
import torch
import torch.nn.functional as F
def dpo_loss(
policy_chosen_logps: torch.Tensor, # 策略模型对 chosen 的 log prob
policy_rejected_logps: torch.Tensor, # 策略模型对 rejected 的 log prob
reference_chosen_logps: torch.Tensor, # 参考模型对 chosen 的 log prob
reference_rejected_logps: torch.Tensor, # 参考模型对 rejected 的 log prob
beta: float = 0.1, # 温度参数,控制对齐强度
) -> torch.Tensor:
"""
DPO 损失函数
直觉理解:
- 让策略模型在 chosen 上的偏好(相对于参考模型)> rejected 上的偏好
- beta 控制偏离参考模型的程度:beta 越小,允许偏离越多
"""
# 计算相对 log 概率比
chosen_ratio = policy_chosen_logps - reference_chosen_logps
rejected_ratio = policy_rejected_logps - reference_rejected_logps
# DPO 损失:最大化 chosen 与 rejected 之间的偏好差距
logits = beta * (chosen_ratio - rejected_ratio)
loss = -F.logsigmoid(logits).mean()
return loss
4.3 DPO 工程实现
from trl import DPOTrainer, DPOConfig
from transformers import AutoModelForCausalLM
# DPO 所需的数据格式
dpo_dataset = [
{
"prompt": "用户:请解释什么是量子纠缠?\n助手:",
"chosen": "量子纠缠是量子力学中的一个奇特现象,指两个或多个粒子的量子态无法独立描述...",
"rejected": "量子纠缠就是两个粒子之间有神秘联系,爱因斯坦也不理解它..."
},
# ... 更多偏好对
]
model = AutoModelForCausalLM.from_pretrained("your_sft_model")
ref_model = AutoModelForCausalLM.from_pretrained("your_sft_model") # frozen
dpo_config = DPOConfig(
output_dir="./dpo_output",
beta=0.1, # 关键超参数
num_train_epochs=1,
per_device_train_batch_size=4,
learning_rate=5e-7, # DPO 通常比 SFT 学习率更低
warmup_ratio=0.1,
gradient_accumulation_steps=4,
fp16=True,
max_length=1024,
max_prompt_length=512,
)
dpo_trainer = DPOTrainer(
model=model,
ref_model=ref_model,
args=dpo_config,
train_dataset=dpo_dataset,
tokenizer=tokenizer,
)
dpo_trainer.train()
4.4 DPO 的改进变体
DPO 发布后,涌现出大量改进版本:
| 变体 | 创新点 | 适用场景 |
|---|---|---|
| DPO | 原始版本,简单直接 | 一般对齐任务 |
| IPO(Identity Preference Optimization) | 解决 DPO 过拟合问题 | 偏好数据较少时 |
| KTO(Kahneman-Tversky Optimization) | 使用二元反馈(好/差)而非配对 | 难以获取配对数据时 |
| ORPO(Odds Ratio Preference Optimization) | 合并 SFT 和对齐训练 | 一步完成微调和对齐 |
| SimPO(Simple Preference Optimization) | 去掉参考模型,更简单 | 计算资源有限时 |
# KTO 示例:使用非配对的偏好数据
# 比 DPO 更适合只有"好/差"标签,没有配对对比的场景
kto_dataset = [
{"prompt": "什么是机器学习?", "completion": "机器学习是...", "label": True}, # 好回答
{"prompt": "什么是深度学习?", "completion": "深度学习就是神经网络...", "label": False}, # 差回答
# 不需要配对,每条数据独立标注
]
五、三种技术的选型决策框架
5.1 对比矩阵
| 维度 | SFT | RLHF | DPO |
|---|---|---|---|
| 技术复杂度 | 低 | 高 | 中 |
| 计算资源需求 | 低-中 | 很高 | 低-中 |
| 数据需求 | 配对指令数据 | 偏好对数据 | 偏好对数据 |
| 数据获取成本 | 中 | 高(需人工标注) | 高 |
| 训练稳定性 | 高 | 低(超参数敏感) | 中 |
| 效果上限 | 中 | 高 | 高 |
| 社区支持 | 非常完善 | 完善 | 完善且快速增长 |
5.2 场景导向的选型建议
场景 A:让通用模型适应特定领域
(医疗/法律/金融/客服等)
→ 推荐:领域 SFT
→ 原因:有大量领域文档可以转换为指令数据
场景 B:让模型说话更符合品牌风格和价值观
→ 推荐:DPO(优于 RLHF)
→ 原因:风格偏好数据容易收集,DPO 训练稳定
场景 C:让模型的回答质量对齐人类标注员偏好
(需要模型像 ChatGPT 一样"好用")
→ 推荐:RLHF 或 DPO
→ 原因:需要从多维度(有帮助/无害/诚实)对齐
场景 D:预算有限,GPU 资源紧张
→ 推荐:DPO(甚至 SimPO)
→ 原因:不需要奖励模型,计算成本低 50% 以上
场景 E:需要持续优化,快速迭代
→ 推荐:DPO + 线上反馈数据收集
→ 原因:DPO 训练周期短,可以快速响应新反馈
5.3 生产部署的组合策略
实践中,最佳方案往往是组合使用:
大规模生产系统的对齐训练流水线:
1. 基础 SFT(1-2 个 epoch)
→ 让模型学会遵循指令格式
2. DPO 对齐(1 个 epoch)
→ 基于偏好数据提升回答质量
3. 持续在线学习(每 1-2 周)
→ 收集用户反馈 → DPO 增量更新
→ 处理模型的弱点区域
4. 定期全量 RLHF(季度级别)
→ 大规模偏好数据 + 深度对齐优化
六、数据工程:对齐的隐藏关键
技术选型固然重要,但数据质量往往决定了对齐的上限。
class PreferenceDataQualityChecker:
"""
偏好数据质量检查器
"""
def check_dataset(self, dataset: list) -> dict:
issues = {
"annotation_disagreement": [], # 标注者意见分歧
"trivial_rejections": [], # 被拒绝的回答质量差距不够
"length_bias": [], # 仅因为更长就被选择
"format_bias": [], # 仅因为格式更好被选择
}
for item in dataset:
chosen = item["chosen"]
rejected = item["rejected"]
# 检测长度偏差
len_ratio = len(chosen) / (len(rejected) + 1)
if len_ratio > 1.5 or len_ratio < 0.67:
if self._content_similar(chosen, rejected):
issues["length_bias"].append(item)
# 检测质量差距不够大
quality_gap = self._estimate_quality_gap(chosen, rejected)
if quality_gap < 0.2:
issues["trivial_rejections"].append(item)
return {
"total": len(dataset),
"issues": {k: len(v) for k, v in issues.items()},
"quality_score": 1 - sum(len(v) for v in issues.values()) / (len(dataset) * len(issues))
}
七、总结
三种技术各司其职,没有"最好的",只有"最合适的":
- SFT:能力注入的基础,让模型学会做新任务
- RLHF:当你有大量标注资源,需要深度对齐时的最强选择
- DPO:大多数团队的最佳起点,兼顾效果和工程简洁性
2026 年的趋势:DPO 及其变体(SimPO、ORPO)正在成为主流,因为它们在 90% 的场景下能以更低成本达到 RLHF 的 80-90% 效果。但理解三种技术的原理,才能在关键场景做出正确的技术决策。