DPO 直接偏好优化 #

DPO 概述 #

DPO(Direct Preference Optimization,直接偏好优化)是 2023 年提出的一种简化 RLHF 流程的方法。它跳过了奖励模型训练和 PPO 优化,直接使用偏好数据优化语言模型。

DPO vs RLHF #

text
┌─────────────────────────────────────────────────────────────┐
│                    RLHF vs DPO 流程对比                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  RLHF 流程:                                                 │
│  ┌───────────┐   ┌───────────┐   ┌───────────┐             │
│  │  SFT 模型  │ → │ 奖励模型  │ → │  PPO 训练  │             │
│  └───────────┘   └───────────┘   └───────────┘             │
│       ↑               ↑               ↑                     │
│   SFT 数据       偏好数据        偏好数据                    │
│                                                             │
│  DPO 流程:                                                  │
│  ┌───────────┐   ┌───────────┐                              │
│  │  SFT 模型  │ → │  DPO 训练  │                              │
│  └───────────┘   └───────────┘                              │
│       ↑               ↑                                      │
│   SFT 数据       偏好数据                                    │
│                                                             │
│  DPO 优势:                                                  │
│  ├── 无需训练奖励模型                                        │
│  ├── 无需复杂的 PPO 训练                                     │
│  ├── 训练流程更简单                                          │
│  ├── 计算资源需求更低                                        │
│  └── 更容易实现和调试                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

DPO 核心原理 #

从 RLHF 到 DPO #

text
RLHF 目标:
────────────────────────
max E[r(x, y)] - β * KL(π_θ || π_ref)

其中:
├── r(x, y) 是奖励函数
├── π_θ 是待优化策略
├── π_ref 是参考策略(SFT 模型)
└── β 是 KL 约束系数

关键洞察:
────────────────────────
最优策略可以表示为:

π*(y|x) = (1/Z(x)) * π_ref(y|x) * exp(r(x,y)/β)

其中 Z(x) 是配分函数。

由此可得奖励函数:

r(x,y) = β * log(π*(y|x) / π_ref(y|x)) + β * log Z(x)

DPO 损失函数 #

text
将奖励函数代入 Bradley-Terry 模型:

P(y_w > y_l | x) = σ(r(x, y_w) - r(x, y_l))
                 = σ(β * log(π_θ(y_w|x) / π_ref(y_w|x)) 
                   - β * log(π_θ(y_l|x) / π_ref(y_l|x)))

DPO 损失函数:
────────────────────────
L_DPO = -E[log σ(β * (log(π_θ(y_w|x) / π_ref(y_w|x)) 
                     - log(π_θ(y_l|x) / π_ref(y_l|x))))]

直观理解:
────────────────────────
├── 增加被选择回复的概率(相对于参考模型)
├── 降低被拒绝回复的概率(相对于参考模型)
├── β 控制优化强度
└── 无需显式的奖励模型

DPO 目标函数图解 #

text
┌─────────────────────────────────────────────────────────────┐
│                    DPO 优化方向                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  对被选择的回复 y_w:                                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  目标:增加 π_θ(y_w|x) / π_ref(y_w|x)               │   │
│  │                                                     │   │
│  │  π_θ(y_w|x)                                        │   │
│  │     ↑                                              │   │
│  │     │      ────────                                │   │
│  │     │     /                                        │   │
│  │     │    /  优化方向                               │   │
│  │     │   /                                          │   │
│  │     └──┼───────────────→ 训练步数                  │   │
│  │        π_ref(y_w|x)                                │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  对被拒绝的回复 y_l:                                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  目标:降低 π_θ(y_l|x) / π_ref(y_l|x)               │   │
│  │                                                     │   │
│  │  π_θ(y_l|x)                                        │   │
│  │     ↑                                              │   │
│  │     │  \                                           │   │
│  │     │   \  优化方向                                │   │
│  │     │    \                                         │   │
│  │     │     ────────                                 │   │
│  │     └──┼───────────────→ 训练步数                  │   │
│  │        π_ref(y_l|x)                                │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

DPO 实现 #

基础实现 #

python
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer

class DPOTrainer:
    def __init__(
        self,
        model,
        ref_model,
        tokenizer,
        beta=0.1,
        learning_rate=1e-6,
    ):
        self.model = model
        self.ref_model = ref_model
        self.tokenizer = tokenizer
        self.beta = beta
        
        self.optimizer = torch.optim.AdamW(
            model.parameters(),
            lr=learning_rate
        )
        
        self.ref_model.eval()
        for param in self.ref_model.parameters():
            param.requires_grad = False
    
    def get_log_probs(self, model, input_ids, attention_mask):
        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            labels=input_ids,
        )
        
        logits = outputs.logits[:, :-1, :]
        labels = input_ids[:, 1:]
        
        log_probs = F.log_softmax(logits, dim=-1)
        per_token_log_probs = log_probs.gather(
            2, labels.unsqueeze(-1)
        ).squeeze(-1)
        
        attention_mask_shifted = attention_mask[:, 1:]
        per_token_log_probs = per_token_log_probs * attention_mask_shifted
        
        return per_token_log_probs.sum(dim=-1)
    
    def compute_dpo_loss(
        self,
        input_ids_chosen,
        attention_mask_chosen,
        input_ids_rejected,
        attention_mask_rejected,
    ):
        policy_log_probs_chosen = self.get_log_probs(
            self.model, input_ids_chosen, attention_mask_chosen
        )
        policy_log_probs_rejected = self.get_log_probs(
            self.model, input_ids_rejected, attention_mask_rejected
        )
        
        with torch.no_grad():
            ref_log_probs_chosen = self.get_log_probs(
                self.ref_model, input_ids_chosen, attention_mask_chosen
            )
            ref_log_probs_rejected = self.get_log_probs(
                self.ref_model, input_ids_rejected, attention_mask_rejected
            )
        
        policy_log_ratio_chosen = policy_log_probs_chosen - ref_log_probs_chosen
        policy_log_ratio_rejected = policy_log_probs_rejected - ref_log_probs_rejected
        
        logits = self.beta * (
            policy_log_ratio_chosen - policy_log_ratio_rejected
        )
        
        loss = -F.logsigmoid(logits).mean()
        
        chosen_rewards = self.beta * policy_log_ratio_chosen
        rejected_rewards = self.beta * policy_log_ratio_rejected
        
        accuracy = (chosen_rewards > rejected_rewards).float().mean()
        
        return loss, {
            "loss": loss.item(),
            "chosen_rewards": chosen_rewards.mean().item(),
            "rejected_rewards": rejected_rewards.mean().item(),
            "accuracy": accuracy.item(),
        }
    
    def train_step(self, batch):
        loss, metrics = self.compute_dpo_loss(
            batch["input_ids_chosen"],
            batch["attention_mask_chosen"],
            batch["input_ids_rejected"],
            batch["attention_mask_rejected"],
        )
        
        self.optimizer.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
        self.optimizer.step()
        
        return metrics

数据集实现 #

python
from torch.utils.data import Dataset

class DPODataset(Dataset):
    def __init__(self, data_path, tokenizer, max_length=512):
        self.tokenizer = tokenizer
        self.max_length = max_length
        self.data = self._load_data(data_path)
    
    def _load_data(self, data_path):
        import json
        data = []
        with open(data_path, 'r') as f:
            for line in f:
                data.append(json.loads(line))
        return data
    
    def __len__(self):
        return len(self.data)
    
    def __getitem__(self, idx):
        item = self.data[idx]
        prompt = item["prompt"]
        chosen = item["chosen"]
        rejected = item["rejected"]
        
        chosen_text = prompt + chosen
        rejected_text = prompt + rejected
        
        chosen_enc = self.tokenizer(
            chosen_text,
            max_length=self.max_length,
            padding="max_length",
            truncation=True,
            return_tensors="pt"
        )
        
        rejected_enc = self.tokenizer(
            rejected_text,
            max_length=self.max_length,
            padding="max_length",
            truncation=True,
            return_tensors="pt"
        )
        
        return {
            "input_ids_chosen": chosen_enc["input_ids"].squeeze(0),
            "attention_mask_chosen": chosen_enc["attention_mask"].squeeze(0),
            "input_ids_rejected": rejected_enc["input_ids"].squeeze(0),
            "attention_mask_rejected": rejected_enc["attention_mask"].squeeze(0),
        }

完整训练脚本 #

python
from torch.utils.data import DataLoader
from transformers import get_linear_schedule_with_warmup
from tqdm import tqdm

def train_dpo(
    model_name,
    train_data_path,
    output_dir,
    beta=0.1,
    learning_rate=1e-6,
    num_epochs=3,
    batch_size=4,
    gradient_accumulation_steps=4,
):
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    tokenizer.pad_token = tokenizer.eos_token
    
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        torch_dtype=torch.bfloat16,
        device_map="auto",
    )
    
    ref_model = AutoModelForCausalLM.from_pretrained(
        model_name,
        torch_dtype=torch.bfloat16,
        device_map="auto",
    )
    
    train_dataset = DPODataset(train_data_path, tokenizer)
    train_dataloader = DataLoader(
        train_dataset,
        batch_size=batch_size,
        shuffle=True,
    )
    
    trainer = DPOTrainer(
        model=model,
        ref_model=ref_model,
        tokenizer=tokenizer,
        beta=beta,
        learning_rate=learning_rate,
    )
    
    num_training_steps = len(train_dataloader) * num_epochs // gradient_accumulation_steps
    scheduler = get_linear_schedule_with_warmup(
        trainer.optimizer,
        num_warmup_steps=int(0.03 * num_training_steps),
        num_training_steps=num_training_steps,
    )
    
    for epoch in range(num_epochs):
        print(f"Epoch {epoch + 1}/{num_epochs}")
        
        for step, batch in enumerate(tqdm(train_dataloader)):
            batch = {k: v.to(model.device) for k, v in batch.items()}
            
            metrics = trainer.train_step(batch)
            
            if (step + 1) % gradient_accumulation_steps == 0:
                scheduler.step()
            
            if step % 10 == 0:
                print(f"Step {step}: loss={metrics['loss']:.4f}, "
                      f"accuracy={metrics['accuracy']:.4f}")
        
        model.save_pretrained(f"{output_dir}/epoch-{epoch + 1}")
        tokenizer.save_pretrained(f"{output_dir}/epoch-{epoch + 1}")
    
    model.save_pretrained(output_dir)
    tokenizer.save_pretrained(output_dir)

DPO 变体 #

IPO(Identity Preference Optimization) #

python
def compute_ipo_loss(
    policy_log_ratio_chosen,
    policy_log_ratio_rejected,
    beta=0.1,
):
    diff = policy_log_ratio_chosen - policy_log_ratio_rejected
    loss = (diff - 1 / (2 * beta)) ** 2
    return loss.mean()

KTO(Kahneman-Tversky Optimization) #

python
def compute_kto_loss(
    policy_log_probs,
    ref_log_probs,
    is_preferred,
    beta=0.1,
    desirable_weight=1.0,
    undesirable_weight=0.5,
):
    log_ratio = policy_log_probs - ref_log_probs
    reward = beta * log_ratio
    
    if is_preferred:
        loss = desirable_weight * (1 - torch.sigmoid(reward))
    else:
        loss = undesirable_weight * torch.sigmoid(reward)
    
    return loss.mean()

ORPO(Odds Ratio Preference Optimization) #

python
def compute_orpo_loss(
    policy_log_probs_chosen,
    policy_log_probs_rejected,
    ref_log_probs_chosen,
    ref_log_probs_rejected,
    beta=0.1,
):
    odds_ratio = torch.exp(
        policy_log_probs_chosen - policy_log_probs_rejected
    )
    
    log_odds_ratio = torch.log(odds_ratio)
    sig_ratio = torch.sigmoid(log_odds_ratio)
    
    preference_loss = -torch.log(sig_ratio)
    
    sft_loss = -policy_log_probs_chosen.mean()
    
    total_loss = sft_loss + beta * preference_loss.mean()
    
    return total_loss

DPO vs RLHF 详细对比 #

性能对比 #

text
┌─────────────────────────────────────────────────────────────┐
│                    DPO vs RLHF 性能对比                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  训练效率:                                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  指标         │  RLHF      │  DPO                  │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  训练时间     │  长        │  短                   │   │
│  │  计算资源     │  高        │  低                   │   │
│  │  实现复杂度   │  高        │  低                   │   │
│  │  调参难度     │  高        │  低                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  模型效果:                                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  指标         │  RLHF      │  DPO                  │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  对齐效果     │  优秀      │  良好                 │   │
│  │  灵活性       │  高        │  中                   │   │
│  │  迭代优化     │  支持      │  支持                 │   │
│  │  大规模模型   │  成熟      │  可行                 │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

适用场景 #

text
选择 DPO 的场景:
────────────────────────
├── 计算资源有限
├── 快速原型开发
├── 中小规模模型
├── 简单对齐需求
└── 团队经验有限

选择 RLHF 的场景:
────────────────────────
├── 追求最佳效果
├── 大规模模型
├── 复杂偏好学习
├── 需要精细控制
└── 有充足资源

DPO 最佳实践 #

超参数选择 #

text
关键超参数:
────────────────────────
β(KL 约束系数):
├── 典型值:0.1 ~ 0.5
├── 较小:优化更激进,可能过拟合
├── 较大:优化更保守,更稳定
└── 建议:从 0.1 开始,根据效果调整

学习率:
├── 典型值:1e-7 ~ 1e-5
├── 较大:训练快,可能不稳定
├── 较小:稳定,但收敛慢
└── 建议:1e-6 是常用起点

批次大小:
├── 典型值:16 ~ 128
├── 较大:更稳定,需要更多显存
├── 较小:梯度噪声大
└── 建议:根据显存选择最大可能值

数据质量 #

text
高质量偏好数据要求:
────────────────────────
├── 明显的偏好差异
│   └── chosen 和 rejected 应该有清晰的质量差距
│
├── 多样化的提示
│   └── 覆盖不同任务类型和难度
│
├── 一致的标注标准
│   └── 标注者对偏好标准有共识
│
└── 足够的数据量
    └── 通常需要数万条偏好数据

训练技巧 #

python
class ImprovedDPOTrainer(DPOTrainer):
    def __init__(self, *args, label_smoothing=0.0, **kwargs):
        super().__init__(*args, **kwargs)
        self.label_smoothing = label_smoothing
    
    def compute_dpo_loss(self, *args, **kwargs):
        loss, metrics = super().compute_dpo_loss(*args, **kwargs)
        
        if self.label_smoothing > 0:
            loss = loss * (1 - self.label_smoothing) + 0.5 * self.label_smoothing
        
        return loss, metrics
    
    def train_step(self, batch):
        metrics = super().train_step(batch)
        
        if metrics["accuracy"] < 0.5:
            print("Warning: Low accuracy, consider adjusting beta or data quality")
        
        return metrics

下一步 #

现在你已经掌握了 DPO 的原理和实现,接下来学习 高级技术,了解更多 RLHF 变体和前沿方法!

最后更新:2026-04-05