DPO 深度解析:绕过强化学习的直接偏好优化

4865 字
24 分钟
DPO 深度解析:绕过强化学习的直接偏好优化

1. 引言:为什么 DPO 一发布就引爆社区?#

1.1 RLHF 的”三重山”#

2023 年 6 月之前,如果你想对齐一个大语言模型,标准流程是:

RLHF = 偏好数据 → 训练 Reward Model → PPO 策略优化
成本结构:
├── Reward Model 训练 ................ 数小时,1 × GPU
├── Rollout 生成 .................... O(n) 显存,n × GPU
├── Value Network (Critic) 训练 ...... 与 Policy 同尺寸,n × GPU
└── PPO 更新 ........................ 4 模型同时在显存
总显存需求 ≈ 4 × 模型参数量
训练稳定性 .......................... 易崩溃
超参数 .............................. KL系数、clip、GAE、折扣率...

这对于拥有专业 RL 团队的大厂不是问题,但对学术实验室和中小企业几乎是不可逾越的壁垒。

1.2 DPO 的核心贡献#

2023 年 6 月,Stanford HAI 的Rafailov 等人在论文 Direct Preference Optimization: Your Language Model is Secretly a Reward Model 中提出了一个优雅的观察:

RLHF 最优策略 π\pi^* 与参考策略 πref\pi_{\text{ref}} 和奖励函数 RR 之间存在一个闭式(closed-form)关系。

这意味着:我们可以直接用偏好数据训练策略,跳过 Reward Model 和 PPO 两个步骤。

1.3 从三阶段到单阶段#

RLHF(InstructGPT 路线):
偏好数据 ──→ 训练 Reward Model ──→ PPO ──→ 对齐模型
│ ↑
└─────────────────────────────────────────┘
需要 RM、Critic、Rollout
DPO(Rafailov 路线):
偏好数据 ──────────────────────────────→ 直接训练对齐模型
└─────────────────────────────────────────→ 不需要 RM,不需要 PPO
维度RLHF/PPODPO
训练阶段3(RM → Critic → Policy)1(Policy)
显存峰值~4× 模型参数~2× 模型参数
需要在线采样是(PPO Rollout)否(离线)
需要 Value 网络
KL 约束通过奖励塑形通过 log ratio 约束
训练稳定性中等(需监控崩溃)高(无 RL 动态不稳定)
代表模型GPT-4、ClaudeLlama 3.1 Instruct、Mistral
理论价值 vs 工程价值

DPO 的论文有双重贡献:①提出一种工程上简单有效的对齐方法;②建立了 RLHF 最优策略与偏好模型之间的数学等价性,为后续研究奠定了理论基础。


2. 理论基础:Bradley-Terry 与最优策略#

2.1 偏好模型回顾#

人类偏好通常用 Bradley-Terry 模型描述。对于 prompt xx 和两个回答 y1,y2y_1, y_2

P(y1y2x)=σ(R(x,y1)R(x,y2))P(y_1 \succ y_2 \mid x) = \sigma\big(R(x,y_1) - R(x,y_2)\big)

其中 σ(z)=11+ez\sigma(z) = \frac{1}{1+e^{-z}} 是 sigmoid 函数,R(x,y)R(x,y) 是隐式”人类偏好分数”。

对数似然损失:

LBTM=E(x,yw,yl)D[logσ(R(x,yw)R(x,yl))]\mathcal{L}_{\text{BTM}} = -\mathbb{E}_{(x,y_w,y_l)\sim D}\Big[\log\sigma\big(R(x,y_w) - R(x,y_l)\big)\Big]

2.2 RLHF 的最优策略#

RLHF 的目标是:

maxπ Eyπ(x)[R(x,y)]βKL(π(x)πref(x))\max_{\pi}\ \mathbb{E}_{y\sim\pi(\cdot|x)}\big[R(x,y)\big] - \beta\,\text{KL}\big(\pi(\cdot|x)\,\|\,\pi_{\text{ref}}(\cdot|x)\big)

2.3 关键引理:最优策略的显式形式#

引理:在配分函数 Z(x)=yπref(yx)eR(x,y)/βZ(x) = \sum_y \pi_{\text{ref}}(y|x)\,e^{R(x,y)/\beta} 存在且有限的前提下,RLHF 目标的最优解为:

π(yx)=1Z(x)πref(yx)exp ⁣(R(x,y)β)\pi^*(y|x) = \frac{1}{Z(x)}\,\pi_{\text{ref}}(y|x)\,\exp\!\Big(\frac{R(x,y)}{\beta}\Big)

证明(变分法视角):

我们要求解约束优化:

maxπ Ey[R(x,y)]βKL(ππref)\max_\pi\ \mathbb{E}_y[R(x,y)] - \beta\,\text{KL}(\pi\|\pi_{\text{ref}})

等价于最小化:

minπ KL(ππref)1βEy[R(x,y)]\min_\pi\ \text{KL}(\pi\|\pi_{\text{ref}}) - \frac{1}{\beta}\mathbb{E}_y[R(x,y)]

加入概率归一化约束 π(yx)dy=1\int\pi(y|x)dy=1,拉格朗日函数:

L=KL(ππref)1βEy[R]+λ(πdy1)\mathcal{L} = \text{KL}(\pi\|\pi_{\text{ref}}) - \frac{1}{\beta}\mathbb{E}_y[R] + \lambda\Big(\int\pi\,dy - 1\Big)

KL 散度的变分形式(对 π\pi 求导并令导数为零):

Lπ(yx)=logπ(yx)logπref(yx)R(x,y)β+1+λ=0\frac{\partial\mathcal{L}}{\partial\pi(y|x)} = \log\pi(y|x) - \log\pi_{\text{ref}}(y|x) - \frac{R(x,y)}{\beta} + 1 + \lambda = 0

整理得:

π(yx)=πref(yx)exp ⁣(R(x,y)β)e1+λ\pi^*(y|x) = \pi_{\text{ref}}(y|x)\,\exp\!\Big(\frac{R(x,y)}{\beta}\Big)\,e^{1+\lambda}

由归一化条件确定常数 e1+λ=1Z(x)e^{1+\lambda} = \frac{1}{Z(x)}\blacksquare

2.4 从最优策略反解奖励#

对上式取对数并重排:

R(x,y)=βlogπ(yx)βlogπref(yx)+βlogZ(x)R(x,y) = \beta\,\log\pi^*(y|x) - \beta\,\log\pi_{\text{ref}}(y|x) + \beta\,\log Z(x)

关键观察:Z(x)Z(x) 不依赖于 yy,只依赖于 xx 因此在比较两个回答的偏好时,Z(x)Z(x) 会抵消:

R(x,yw)R(x,yl)=β(logπ(ywx)logπref(ywx))β(logπ(ylx)logπref(ylx))R(x,y_w) - R(x,y_l) = \beta\Big(\log\pi^*(y_w|x) - \log\pi_{\text{ref}}(y_w|x)\Big) - \beta\Big(\log\pi^*(y_l|x) - \log\pi_{\text{ref}}(y_l|x)\Big)

代入 Bradley-Terry 模型:

P(ywylx)=σ(β(logπ(ywx)logπref(ywx))β(logπ(ylx)logπref(ylx)))P(y_w \succ y_l \mid x) = \sigma\Big(\beta\big(\log\pi^*(y_w|x) - \log\pi_{\text{ref}}(y_w|x)\big) - \beta\big(\log\pi^*(y_l|x) - \log\pi_{\text{ref}}(y_l|x)\big)\Big)

这就是 DPO 的核心!π\pi^* 替换 RR,偏好概率可以直接写成 π\pi^*πref\pi_{\text{ref}} 的函数——不再需要显式地知道 RRZ(x)Z(x)


3. DPO 损失函数:完整的数学推导#

3.1 从偏好到策略#

DPO 论文用了一种更直观的推导方式:从 Bradley-Terry 偏好出发,通过最优策略的闭式形式反推策略训练目标。

第一步:Bradley-Terry 模型,假设隐式奖励 r(x,y)=βlogπ(yx)πref(yx)r^*(x,y) = \beta\,\log\frac{\pi^*(y|x)}{\pi_{\text{ref}}(y|x)}

P(ywylx)=σ(r(x,yw)r(x,yl))P(y_w \succ y_l \mid x) = \sigma\Big(r^*(x,y_w) - r^*(x,y_l)\Big)

第二步:定义参考策略的隐式偏好差:

Δr(x,yw,yl)=β(logπref(ywx)logπref(ylx))\Delta_r(x,y_w,y_l) = \beta\Big(\log\pi_{\text{ref}}(y_w|x) - \log\pi_{\text{ref}}(y_l|x)\Big)

第三步:DPO 的目标是让 πθ\pi_\theta 尽可能接近 π\pi^*,即最大化:

P(ywylx)=σ(β(logπθ(ywx)logπθ(ylx))Δr(x,yw,yl))P(y_w \succ y_l \mid x) = \sigma\Big(\beta\big(\log\pi_\theta(y_w|x) - \log\pi_\theta(y_l|x)\big) - \Delta_r(x,y_w,y_l)\Big)

第四步:定义 DPO 的 log ratio difference:

Δθ(x,yw,yl)=logπθ(ywx)πref(ywx)logπθ(ylx)πref(ylx)\Delta_\theta(x,y_w,y_l) = \log\frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \log\frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}

最终 DPO 损失

LDPO(θ)=E(x,yw,yl)[logσ(βΔθ(x,yw,yl))]\mathcal{L}_{\text{DPO}}(\theta) = -\mathbb{E}_{(x,y_w,y_l)}\Big[\log\sigma\Big(\beta\,\Delta_\theta(x,y_w,y_l)\Big)\Big]

3.2 损失函数的梯度分析#

LDPO\mathcal{L}_{\text{DPO}} 求梯度(关键的直觉):

θLDPO=E[θΔθσ(βΔθ)]-\nabla_\theta\mathcal{L}_{\text{DPO}} = \mathbb{E}\Big[\nabla_\theta\Delta_\theta \cdot \sigma\big(-\beta\,\Delta_\theta\big)\Big]

其中 θΔθ=θlogπθ(ywx)θlogπθ(ylx)\nabla_\theta\Delta_\theta = \nabla_\theta\log\pi_\theta(y_w|x) - \nabla_\theta\log\pi_\theta(y_l|x)

两种梯度的物理含义

  • 正梯度:被偏好的回答 ywy_w 的 log prob 上升 → 模型学会生成更好的回答
  • 负梯度(通过 σ(βΔθ)\sigma(-\beta\Delta_\theta) 调制):被拒绝的回答 yly_l 的 log prob 下降 → 模型学会避免差的回答
θLDPO=E[σ(βΔθ)θlogπθ(ywx)]增加 yw 的概率 + E[σ(βΔθ)θlogπθ(ylx)]降低 yl 的概率\nabla_\theta\mathcal{L}_{\text{DPO}} = -\underbrace{\mathbb{E}\Big[\sigma\big(-\beta\,\Delta_\theta\big)\nabla_\theta\log\pi_\theta(y_w|x)\Big]}_{\text{增加 }y_w\text{ 的概率}} \ +\ \underbrace{\mathbb{E}\Big[\sigma\big(\beta\,\Delta_\theta\big)\nabla_\theta\log\pi_\theta(y_l|x)\Big]}_{\text{降低 }y_l\text{ 的概率}}

Δθ>0\Delta_\theta > 0(模型已认为 ywy_wyly_l 更好):

  • σ(βΔθ)0\sigma(-\beta\Delta_\theta) \approx 0 → 对 ywy_w 的梯度很小
  • σ(βΔθ)1\sigma(\beta\Delta_\theta) \approx 1 → 对 yly_l 的惩罚更大

Δθ<0\Delta_\theta < 0(模型错误认为 yly_l 更好):

  • σ(βΔθ)1\sigma(-\beta\Delta_\theta) \approx 1 → 强烈增加 ywy_w 的梯度
  • σ(βΔθ)0\sigma(\beta\Delta_\theta) \approx 0 → 很少惩罚 yly_l
自适应权重

DPO 的梯度具有自适应权重特性——模型越”自信地错了”,梯度越大;越”自信地对了”,梯度越小。这与 PPO 的 clipped surrogate 目标一致,但以更简洁的方式实现。

3.3 温度系数 β\beta 的作用#

β\beta 控制”KL 约束强度”与”偏好拟合”之间的权衡:

β\beta 区间行为效果
太小(< 0.01)KL 惩罚几乎消失可能过度拟合偏好,偏离原始能力
适中(0.05 ~ 0.2)平衡偏好与 KL推荐默认值 0.1
太大(> 0.5)KL 占主导接近不做任何事,模型几乎不变

4. 代码实现:从数据到训练#

4.1 数据准备#

from dataclasses import dataclass
from typing import List
@dataclass
class PreferenceSample:
"""偏好数据的一个样本"""
prompt: str # 用户指令/问题
chosen: str # 人类标注为"更好"的回答
rejected: str # 人类标注为"更差"的回答
# 可选元数据
chosen_score: float = 0.0 # 打分(如果有)
source: str = "" # 数据来源(人标/AI标)
difficulty: str = "" # 简单/中等/困难
# 典型的偏好数据集格式
# Anthropic HH-RLHF, Stanford SHP, OpenAssistant, LMSYS-Chat
preference_dataset: List[PreferenceSample] = load_preference_data()

4.2 Tokenization#

def tokenize_preference_batch(
samples: List[PreferenceSample],
tokenizer,
max_prompt_len: int = 512,
max_response_len: int = 512,
):
"""
将偏好数据 tokenize,返回:
- chosen_input_ids: [B, prompt + chosen]
- rejected_input_ids: [B, prompt + rejected]
- chosen_response_mask: 标记 response 部分的 mask(用于计算 log prob)
- rejected_response_mask: 同上
"""
chosen_enc = tokenizer(
[s.prompt + s.chosen for s in samples],
max_length=max_prompt_len + max_response_len,
truncation=True,
padding=True,
return_tensors="pt",
)
rejected_enc = tokenizer(
[s.prompt + s.rejected for s in samples],
max_length=max_prompt_len + max_response_len,
truncation=True,
padding=True,
return_tensors="pt",
)
# 构建 response mask(tokenize 后的 answer 部分)
chosen_response_mask = build_response_mask(
tokenizer, samples, chosen_enc, "chosen"
)
rejected_response_mask = build_response_mask(
tokenizer, samples, rejected_enc, "rejected"
)
return {
"chosen_input_ids": chosen_enc["input_ids"],
"chosen_attention_mask": chosen_enc["attention_mask"],
"chosen_response_mask": chosen_response_mask,
"rejected_input_ids": rejected_enc["input_ids"],
"rejected_attention_mask": rejected_enc["attention_mask"],
"rejected_response_mask": rejected_response_mask,
}
def build_response_mask(tokenizer, samples, enc, field):
"""构建只覆盖 response 部分的 mask"""
mask = torch.zeros_like(enc["input_ids"], dtype=torch.bool)
for i, sample in enumerate(samples):
prompt_len = len(tokenizer.encode(sample.prompt, add_special_tokens=True))
mask[i, prompt_len:] = True
return mask

4.3 Log Prob 计算#

import torch
import torch.nn.functional as F
def compute_token_log_probs(
model,
input_ids: torch.Tensor,
attention_mask: torch.Tensor,
) -> torch.Tensor:
"""
计算每个 token 的 log π(y_i | x, y_{<i})
输入: (B, L)
输出: (B, L-1) — 最后一个 token 无法预测下一个
"""
outputs = model(input_ids=input_ids, attention_mask=attention_mask)
logits = outputs.logits[:, :-1, :] # (B, L-1, V)
labels = input_ids[:, 1:] # (B, L-1)
log_probs = F.log_softmax(logits, dim=-1)
token_log_probs = log_probs.gather(2, labels.unsqueeze(-1)).squeeze(-1) # (B, L-1)
return token_log_probs
def compute_sequence_log_prob(
model, input_ids, attention_mask, response_mask
) -> torch.Tensor:
"""
计算 response 部分的 log π(response | prompt) 总和。
只对 response 部分 token 求和,prompt 部分 mask 掉。
公式: Σ_{t∈response} log π(y_t | prompt, y_{<t})
"""
token_log_probs = compute_token_log_probs(model, input_ids, attention_mask)
# 对应位置向前移动一位(log prob 与 labels 对齐)
response_log_probs = token_log_probs * response_mask[:, 1:]
return response_log_probs.sum(dim=-1) # (B,)
def compute_log_ratios(
policy_chosen_lps: torch.Tensor, # (B,)
policy_rejected_lps: torch.Tensor, # (B,)
ref_chosen_lps: torch.Tensor, # (B,)
ref_rejected_lps: torch.Tensor, # (B,)
) -> torch.Tensor:
"""
计算 Δ_θ = log(π_θ(y_c)/π_ref(y_c)) - log(π_θ(y_r)/π_ref(y_r))
这是 DPO 损失的核心量
"""
return (
(policy_chosen_lps - ref_chosen_lps)
- (policy_rejected_lps - ref_rejected_lps)
)

4.4 DPO 损失与训练循环#

def dpo_loss(
policy_chosen_lps: torch.Tensor,
policy_rejected_lps: torch.Tensor,
ref_chosen_lps: torch.Tensor,
ref_rejected_lps: torch.Tensor,
beta: float = 0.1,
):
"""
DPO 损失。
所有输入: (B,)
损失: -E[log σ(β * Δ_θ)]
其中 Δ_θ = log(π_θ(y_c)/π_ref(y_c)) - log(π_θ(y_r)/π_ref(y_r))
"""
policy_logratios = policy_chosen_lps - policy_rejected_lps
ref_logratios = ref_chosen_lps - ref_rejected_lps
# β * (Δ_π - Δ_ref) = β * Δ_θ
logits = beta * (policy_logratios - ref_logratios)
loss = -F.logsigmoid(logits).mean()
# 辅助指标
with torch.no_grad():
chosen_rewards = beta * (policy_chosen_lps - ref_chosen_lps)
rejected_rewards = beta * (policy_rejected_lps - ref_rejected_lps)
reward_margin = (chosen_rewards - rejected_rewards).mean()
accuracy = (chosen_rewards > rejected_rewards).float().mean()
return loss, {
"loss": loss.item(),
"reward_margin": reward_margin.item(),
"accuracy": accuracy.item(),
"chosen_reward_mean": chosen_rewards.mean().item(),
"rejected_reward_mean": rejected_rewards.mean().item(),
}
def train_dpo(
policy_model,
ref_model,
train_loader,
beta: float = 0.1,
lr: float = 1e-6,
num_epochs: int = 1,
max_grad_norm: float = 1.0,
log_interval: int = 10,
):
"""
完整的 DPO 训练循环。
"""
optimizer = torch.optim.AdamW(policy_model.parameters(), lr=lr)
ref_model.eval() # 冻结参考模型
global_step = 0
for epoch in range(num_epochs):
for step, batch in enumerate(train_loader):
# === 1. Policy forward pass ===
policy_chosen_lps = compute_sequence_log_prob(
policy_model,
batch["chosen_input_ids"],
batch["chosen_attention_mask"],
batch["chosen_response_mask"],
)
policy_rejected_lps = compute_sequence_log_prob(
policy_model,
batch["rejected_input_ids"],
batch["rejected_attention_mask"],
batch["rejected_response_mask"],
)
# === 2. Reference model forward pass (no grad) ===
with torch.no_grad():
ref_chosen_lps = compute_sequence_log_prob(
ref_model,
batch["chosen_input_ids"],
batch["chosen_attention_mask"],
batch["chosen_response_mask"],
)
ref_rejected_lps = compute_sequence_log_prob(
ref_model,
batch["rejected_input_ids"],
batch["rejected_attention_mask"],
batch["rejected_response_mask"],
)
# === 3. DPO 损失 ===
loss, metrics = dpo_loss(
policy_chosen_lps, policy_rejected_lps,
ref_chosen_lps, ref_rejected_lps,
beta=beta,
)
# === 4. 反向传播 ===
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(policy_model.parameters(), max_grad_norm)
optimizer.step()
global_step += 1
if step % log_interval == 0:
print(
f"[Epoch {epoch+1}] Step {step:4d} | "
f"loss={metrics['loss']:.4f} | "
f"acc={metrics['accuracy']:.3f} | "
f"margin={metrics['reward_margin']:.3f} | "
f"chosen_r={metrics['chosen_reward_mean']:.2f} | "
f"rejected_r={metrics['rejected_reward_mean']:.2f}"
)

4.5 与 SFT 的对比#

def sft_loss(log_probs: torch.Tensor, labels: torch.Tensor, mask: torch.Tensor):
"""标准 SFT 损失(最大似然)"""
return -log_probs.gather(2, labels.unsqueeze(-1)).squeeze(-1) * mask
# DPO 梯度 ≈ SFT_on_chosen + SFT_on_rejected_reversed
# 第一项:增加 chosen 的概率(类似 SFT)
# 第二项:减少 rejected 的概率(DPO 独有,不在 SFT 中)

5. 训练动态:DPO 到底在学什么?#

5.1 Reward Margin 的收敛曲线#

DPO 训练中最重要的监控指标是 chosen reward - rejected reward(reward margin):

训练初期(margin ≈ 0):
模型对 y_w 和 y_l 的判断模糊
Δ_θ ≈ 0 → σ(βΔ_θ) ≈ 0.5 → 梯度最大
训练中期:
模型逐渐学会区分偏好
margin 上升,accuracy 上升
训练后期:
margin 趋近饱和,梯度变小 → 自动早停效果
理想曲线:
margin: ────────────────
/
accuracy: ────────────
loss: ────────────

5.2 KL 散度的自然约束#

DPO 的 KL 约束是隐式的——体现在 logπθπref\log\frac{\pi_\theta}{\pi_{\text{ref}}} 中。当 πθ\pi_\theta 偏离 πref\pi_{\text{ref}} 太多时,Δθ\Delta_\theta 变大,σ(βΔθ)\sigma(-\beta\Delta_\theta) 趋近 0,梯度消失。

这与 PPO 的显式 KL 惩罚项有相同的效果,但实现更简洁。

5.3 为什么 DPO 比 SFT 更好?#

SFT: 最大化 log p(y_c | x)
→ 模型可能过拟合到"记住"所有 chosen 回答
→ 无法区分"好的"和"不够好的"回答
DPO: 最大化 log p(y_c | x) - log p(y_r | x)
→ 不仅学会 chosen,还要主动远离 rejected
→ 模型学到的是"偏好差异"而非"标准答案"

6. 失败模式与系统性缓解#

6.1 六大常见问题#

┌────────────────────────────────┬──────────────────────────────────────────┐
│ 现象 │ 原因 / 缓解 │
├────────────────────────────────┼──────────────────────────────────────────┤
│ ① Reward margin 不上升 │ 偏好数据太简单(模型已能区分) │
│ (accuracy 卡在 ~50%) │ → 过滤掉 margin > 0.5 的样本 │
│ │ → 增加对抗性/困难样本 │
├────────────────────────────────┼──────────────────────────────────────────┤
│ ② Verbosity bias │ RM 偏好长回答(标注员累了就倾向长回答) │
│ (越长越好) │ → 归一化 response 长度 │
│ │ → 在偏好数据中控制长度分布 │
├────────────────────────────────┼──────────────────────────────────────────┤
│ ③ 过拟合到偏好数据 │ 学习率太大 / epoch 太多 │
│ (生成能力退化) │ → 用 β 增大 KL 约束 │
│ │ → 监控 perplexity 是否上升 │
├────────────────────────────────┼──────────────────────────────────────────┤
│ ④ Sigmoid 饱和 │ Δ_θ 绝对值太大时梯度消失 │
│ (后期 loss 不下降) │ → label smoothing: log σ(βΔ - α) │
│ │ → 使用 IPO 损失(无饱和问题) │
├────────────────────────────────┼──────────────────────────────────────────┤
│ ⑤ 偏好过拟合(OOD 泛化差) │ 偏好数据分布太窄 │
│ │ → 数据增强(同 prompt 替换 answer) │
│ │ → 使用 RLAIF 增加多样性 │
├────────────────────────────────┼──────────────────────────────────────────┤
│ ⑥ 参考模型偏移 │ 多次 DPO 迭代后,π_ref 已过时 │
│ │ → 定期更新 ref_model = policy_model │
│ │ → 使用 iterative DPO / SPIN │
└────────────────────────────────┴──────────────────────────────────────────┘

6.2 对抗性偏好数据构造#

DPO 对抗性样本极为敏感。构造困难样本:

def create_adversarial_samples(model, prompt, num_candidates=8, max_new_tokens=256):
"""
用模型生成多个候选回答,保留最接近但不完全相同的配对
"""
candidates = model.generate(
[prompt] * num_candidates,
max_new_tokens=max_new_tokens,
do_sample=True,
temperature=0.8,
top_p=0.9,
)
# 用 RM 排序
scores = reward_model([prompt] * num_candidates, candidates)
sorted_idx = scores.argsort(descending=True)
# 取排序后相邻的 pair(最接近的困难样本)
pairs = []
for i in range(len(sorted_idx) - 1):
pairs.append((candidates[sorted_idx[i]], candidates[sorted_idx[i+1]]))
return pairs

6.3 参考模型的管理策略#

# 策略 1: 冻结参考模型(最简单,适合单次训练)
ref_model = copy.deepcopy(sft_model)
ref_model.eval()
freeze(ref_model)
# 策略 2: 周期更新(更激进,适合长训练)
for epoch in range(num_epochs):
# 每个 epoch 结束后,用当前 policy 更新 ref
if epoch % ref_update_interval == 0:
ref_model.load_state_dict(policy_model.state_dict())
# 策略 3: EMA 更新(平滑过渡,推荐)
ema_ref = ExponentialMovingAverage(policy_model, decay=0.99)
# 每个 step 后:ema_ref.update()

7. 变体算法全景#

7.1 IPO:恒等偏好优化#

问题:DPO 在 Δθ\Delta_\theta 很大时存在梯度饱和。

解决方案:将分类损失换成回归损失(恒等偏好):

LIPO=E[(Δθ12β)2]\mathcal{L}_{\text{IPO}} = \mathbb{E}\Big[\big(\Delta_\theta - \frac{1}{2\beta}\big)^2\Big]

梯度(恒定,不会饱和):

θLIPO=E[θΔθ]-\nabla_\theta\mathcal{L}_{\text{IPO}} = \mathbb{E}\Big[\nabla_\theta\Delta_\theta\Big]
IPO 什么时候用?

当 DPO 训练后期 loss 不下降,但 reward margin 还未饱和时,切换到 IPO。

7.2 KTO:不需要成对偏好#

动机:收集成对偏好数据成本高很多时候只有”好/坏”二值标签。

KTO(Kahneman-Tversky Optimization)将 Bradley-Terry 替换为 Kahneman-Tversky 前景理论中的偏好函数:

LKTO=E[logσ(αΔθβ)]\mathcal{L}_{\text{KTO}} = -\mathbb{E}\Big[\log\sigma\big(\alpha\,\Delta_\theta - \beta\big)\Big]

其中 ywy_w 的标签为 +1+1yly_l 的标签为 1-1α,β\alpha, \beta 是超参数。

def kto_loss(
policy_lps: torch.Tensor, # (B,) 单回答的 log π_θ
ref_lps: torch.Tensor, # (B,) 同上
label: torch.Tensor, # (B,) +1=好, -1=坏
alpha: float = 1.0,
beta: float = 0.1,
):
"""KTO 损失"""
log_ratio = policy_lps - ref_lps # (B,)
label_factor = label.float() # (B,)
logits = alpha * label_factor * log_ratio - beta
loss = -F.logsigmoid(logits).mean()
return loss

7.3 SimPO:移除参考模型#

动机:DPO 需要同时运行 policy 和 ref 两个模型,显存仍是问题。

SimPO(Simple Preference Optimization)的核心改进:用序列平均 log prob 替代参考模型

P(ywylx)=σ(1ywt=1ywlogπθ(ytx,y<t)1ylt=1yllogπθ(ytx,y<t)γ)P(y_w \succ y_l \mid x) = \sigma\Big(\frac{1}{|y_w|}\sum_{t=1}^{|y_w|}\log\pi_\theta(y_t|x,y_{<t}) - \frac{1}{|y_l|}\sum_{t=1}^{|y_l|}\log\pi_\theta(y_t|x,y_{<t}) - \gamma\Big)

除以序列长度的效果:归一化了长度偏差,不需要参考模型做对比。

def simpo_loss(
policy_chosen_lps: torch.Tensor, # (B,) 每个序列的总 log prob
policy_rejected_lps: torch.Tensor,
chosen_len: torch.Tensor, # (B,) chosen 序列长度
rejected_len: torch.Tensor,
gamma: float = 0.5,
):
"""SimPO 损失"""
# 长度归一化
chosen_avg = policy_chosen_lps / chosen_len
rejected_avg = policy_rejected_lps / rejected_len
logits = chosen_avg - rejected_avg - gamma
loss = -F.logsigmoid(logits).mean()
return loss, {"accuracy": (logits > 0).float().mean().item()}

7.4 ORPO:联合 SFT 与 DPO#

动机:DPO 需要先 SFT 再 DPO,两阶段繁琐。ORPO(Odds Ratio Preference Optimization)将两者联合

LORPO=E[logpSFT(yx)]+λE[logp(yx)21p(yx)]\mathcal{L}_{\text{ORPO}} = -\mathbb{E}\Big[\log p_{\text{SFT}}(y|x)\Big] + \lambda\,\mathbb{E}\Big[\log\frac{p(y|x)^2}{1-p(y|x)}\Big]

第二项是 Odds Ratio(胜率比),独立地惩罚低质量回答,不需要参考模型。

def orpo_loss(
sft_loss: torch.Tensor, # 标准 SFT 损失: -log p(y_c|x)
log_odds_ratio: torch.Tensor, # log p/(1-p) 之差
lambda_or: float = 0.5,
):
"""ORPO = SFT + Odds Ratio 惩罚"""
sft_term = sft_loss.mean()
odds_term = -log_odds_ratio.mean() # 最大化胜率比
loss = sft_term + lambda_or * odds_term
return loss

7.5 各算法对比#

算法需要 ref?需要成对偏好?长度归一化梯度饱和推荐场景
DPO⚠️ 中等首选,通用
IPO❌ 无DPO 过拟合时
KTO❌(二值)⚠️ 中等只有单一标签
SimPO⚠️ 中等显存紧张
ORPO❌ 无想联合 SFT+DPO

8. 工程实践指南#

8.1 完整训练流水线#

┌──────────────────────────────────────────────────────────────┐
│ DPO 训练最佳流水线 │
├──────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 准备 SFT 模型(作为 policy 和 ref 的起点) │
│ → 加载 base model │
│ → SFT 训练(高质量指令数据) │
│ → 保存 π_SFT │
│ │
│ Step 2: 收集偏好数据 │
│ → π_SFT 生成候选回答(diverse sampling) │
│ → 人类/AI 标注偏好 │
│ → 质量过滤(长度、格式、RM 得分) │
│ │
│ Step 3: DPO 训练 │
│ → policy = π_SFT,ref = π_SFT │
│ → β = 0.1(默认) │
│ → lr = 1e-6 ~ 5e-6,epoch = 1~3 │
│ │
│ Step 4: 评估 │
│ → Reward margin、Accuracy │
│ → Win Rate vs SFT / vs 基线 │
│ → Perplexity(确保没崩) │
│ → Safety rate │
│ │
└──────────────────────────────────────────────────────────────┘

8.2 推荐超参数#

dpo_config = {
# 优化器
"learning_rate": 1e-6, # 比 SFT 通常更小
"per_device_batch_size": 4, # 受限于 2× 模型显存
"gradient_accumulation_steps": 4, # 总 effective batch = 16
"num_epochs": 1, # DPO 容易过拟合,1 epoch 常用
"max_grad_norm": 1.0,
# DPO 特有
"beta": 0.1, # KL 系数,[0.05, 0.2]
# 参考模型
"ref_model": {
"type": "frozen", # frozen | ema | periodic_update
"update_interval": 1, # 每 N 个 step 更新一次(periodic 时)
"ema_decay": 0.99, # EMA 衰减率
},
# 数据
"max_prompt_len": 512,
"max_response_len": 512,
"filter_responses_by_length": True,
"min_length_diff": 10, # chosen/rejected 长度差至少 10 tokens
}

8.3 评估体系#

def evaluate_dpo_model(model, ref_model, eval_pairs, rm_model=None):
"""
DPO 模型评估:多维度
"""
results = {}
# === 1. DPO 内在指标 ===
margins = []
accuracies = []
for batch in eval_pairs:
# ... (同训练时的损失计算)
margins.append(reward_margin)
accuracies.append(accuracy)
results["margin_mean"] = np.mean(margins)
results["accuracy_mean"] = np.mean(accuracies)
# === 2. Perplexity(语言模型质量) ===
ppl = compute_perplexity(model, eval_texts)
results["perplexity"] = ppl
# === 3. Win Rate ===
if rm_model is not None:
win_rates = []
for prompt in eval_prompts:
sft_response = sft_model.generate(prompt)
dpo_response = model.generate(prompt)
score_sft = rm_model(prompt, sft_response)
score_dpo = rm_model(prompt, dpo_response)
win_rates.append(score_dpo > score_sft)
results["win_rate_vs_sft"] = np.mean(win_rates)
# === 4. Safety ===
safety_score = evaluate_safety(model, adversarial_prompts)
results["safety_rate"] = safety_score
return results

8.4 与其他方法的组合策略#

组合 1: SFT → DPO(最经典)
Llama 2/3, Mistral Instruct 的路线
组合 2: SFT → DPO → DPO(两轮)
第一轮 DPO 学偏好,第二轮 DPO 在更新的 ref 上微调
组合 3: SFT → RLAIF → DPO
用 AI 标注(Constitutional AI)替代人类标注,再 DPO
组合 4: SFT → DPO → GRPO(混合)
DPO 建立初始对齐,GRPO 做探索式微调
组合 5: SPIN(自博弈迭代)
用当前的 policy 作为 RM 来给下一轮的偏好数据打分

9. 核心公式汇总#

9.1 Bradley-Terry 偏好模型#

P(ywylx)=σ(R(x,yw)R(x,yl))P(y_w \succ y_l \mid x) = \sigma\big(R(x,y_w) - R(x,y_l)\big)

9.2 RLHF 最优策略(闭式解)#

π(yx)=1Z(x)πref(yx)exp ⁣(R(x,y)β)\pi^*(y|x) = \frac{1}{Z(x)}\,\pi_{\text{ref}}(y|x)\,\exp\!\Big(\frac{R(x,y)}{\beta}\Big)

9.3 DPO 核心量#

Δθ(x,yw,yl)=logπθ(ywx)πref(ywx)logπθ(ylx)πref(ylx)\Delta_\theta(x,y_w,y_l) = \log\frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \log\frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}

9.4 DPO 损失#

LDPO=E[logσ(βΔθ)]\mathcal{L}_{\text{DPO}} = -\mathbb{E}\Big[\log\sigma\big(\beta\,\Delta_\theta\big)\Big]

9.5 IPO 损失#

LIPO=E[(Δθ12β)2]\mathcal{L}_{\text{IPO}} = \mathbb{E}\Big[\big(\Delta_\theta - \frac{1}{2\beta}\big)^2\Big]

9.6 KTO 损失#

LKTO=E[logσ(αlabelΔθβ)]\mathcal{L}_{\text{KTO}} = -\mathbb{E}\Big[\log\sigma\big(\alpha\,\text{label}\cdot\Delta_\theta - \beta\big)\Big]

9.7 SimPO 损失#

LSimPO=E[logσ(logπθ(yw)ywlogπθ(yl)ylγ)]\mathcal{L}_{\text{SimPO}} = -\mathbb{E}\Big[\log\sigma\Big(\frac{\log\pi_\theta(y_w)}{|y_w|} - \frac{\log\pi_\theta(y_l)}{|y_l|} - \gamma\Big)\Big]

9.8 ORPO 损失#

LORPO=LSFT+λE[logp(yx)21p(yx)]\mathcal{L}_{\text{ORPO}} = \mathcal{L}_{\text{SFT}} + \lambda\,\mathbb{E}\Big[\log\frac{p(y|x)^2}{1-p(y|x)}\Big]

10. 总结#

10.1 DPO 的核心价值#

┌─────────────────────────────────────────────────────────────┐
│ DPO 的三点贡献 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 理论贡献:建立了偏好模型 ↔ 最优策略 的数学桥梁 │
│ │
│ 2. 工程贡献:将三阶段 RLHF 压缩为单阶段, │
│ 降低了 50%+ 的工程复杂度和显存需求 │
│ │
│ 3. 研究贡献:启发了一系列 Direct Preference 算法 │
│ (IPO, KTO, SimPO, ORPO, CPO, RLOO...) │
│ │
└─────────────────────────────────────────────────────────────┘

10.2 选择建议#

场景推荐算法理由
通用对齐,快速启动DPO简单、稳定、性能强
DPO 过拟合/饱和IPO无梯度饱和
只有单一标签KTO不需要成对偏好
显存极度紧张SimPO不需要 ref model
想联合 SFT+DPOORPO单阶段更简洁
追求最强性能DPO + GRPO先 DPO 建立基础,GRPO 探索强化

10.3 DPO 的局限性#

Warning
  • 理论保证依赖 Bradley-Terry 假设:如果人类偏好不满足此假设,DPO 可能学不到正确对齐
  • 对参考模型质量敏感:如果 SFT 模型质量差,DPO 的上界也受限
  • 无法利用在线探索信号:DPO 是纯离线的,没有 PPO/GRPO 那样的”试错-改进”循环
  • 多目标对齐困难:Helpful + Harmless + Honest 的权衡难以用单一偏好对表达

10.4 未来方向#

  • Iterative DPO (IDPO) / SPIN:用当前 policy 重新生成偏好数据,迭代改进
  • Multi-objective DPO:同时优化多个 reward(比如 helpful + safety)
  • Constitutional DPO:用 AI 宪法评判替代成对偏好
  • Theoretical analysis:DPO 的收敛率、泛化界仍未得到充分理解
推荐阅读顺序
  1. DPO 原论文(Rafailov et al., 2023)—— 核心思想
  2. IPO 论文(Azar et al., 2023)—— 理论基础补充
  3. SimPO 论文(Meng et al., 2024)—— 工程最优实践
  4. KTO 论文(Ethayarajh et al., 2024)—— 理论扩展
  5. Llama 3 论文(Meta, 2024)—— 工业级应用经验

参考资料#

  1. Rafailov, R., et al. (2023). “Direct Preference Optimization: Your Language Model is Secretly a Reward Model.” NeurIPS.
  2. Azar, M., et al. (2023). “A General Theoretical Paradigm to Understand Learning from Human Feedback.” AISTATS.
  3. Ethayarajh, K., et al. (2024). “KTO: Model Alignment as Prospect Theoretic Optimization.” ICML.
  4. Meng, Y., et al. (2024). “SimPO: Simple Preference Optimization with a Reference-Free Reward.” arXiv.
  5. Zhao, Y., et al. (2024). “ORPO: Monolithic Preference Optimization without Reference Model.” arXiv.
  6. Ivison, H., et al. (2024). “Camphor: Aligning Language Models with Offline Preference Data.” EMNLP.
  7. Tang, Y., et al. (2024). “Self-Play Preference Optimization for Language Model Alignment.” EMNLP (SPIN).
  8. Touvron, H., et al. (2023). “LLaMA 2: Open Foundation and Fine-Tuned Chat Models.” arXiv.
  9. Liu, A., et al. (2024). “Llama 3 System Paper.” Meta AI Blog.
  10. Ji, J., et al. (2024). “DPOK: Planning with Large Language Models through Policy Optimization.” ICLR.

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

DPO 深度解析:绕过强化学习的直接偏好优化
https://aiattnstudio.link/posts/dpo/
作者
Federico
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author

Federico

AI Research Lab

Hello, I'm Federico.

关于实验室 / About
公告

欢迎来到Federico的个人博客

分类
标签
站点统计
57文章
7分类
404标签