Fine-Tuning vs RLHF vs DPO:大模型对齐技术深度选型指南

3 阅读1分钟

"微调"、"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 对比矩阵

维度SFTRLHFDPO
技术复杂度
计算资源需求低-中很高低-中
数据需求配对指令数据偏好对数据偏好对数据
数据获取成本高(需人工标注)
训练稳定性低(超参数敏感)
效果上限
社区支持非常完善完善完善且快速增长

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% 效果。但理解三种技术的原理,才能在关键场景做出正确的技术决策。