DPO — 直接偏好优化
1. 引言
RLHF(Reinforcement Learning from Human Feedback)是将大语言模型与人类偏好对齐的主流范式,但其三阶段流程(SFT → 奖励模型训练 → PPO 微调)存在显著的工程复杂性和训练不稳定性。
DPO(Direct Preference Optimization, Rafailov et al., 2023) 提出了一种革命性的简化:将 RLHF 的强化学习阶段重新参数化为一个简单的分类损失,完全绕过奖励模型的训练和 PPO 的在线采样。其核心数学洞察是:
最优 RLHF 策略可以用奖励函数的闭式解表示,而这个闭式解可以代入 Bradley-Terry 偏好模型,将 RL 问题转化为监督学习。
这一设计带来了三个关键优势:
2. 理论基础 — RLHF 的数学框架
2.1 问题定义
给定:
- 参考策略 πref\\pi_{\\text{ref}}πref(通常是 SFT 模型)
- 偏好数据集 D={(q,yw,yl)}\\mathcal{D} = \\{(q, y_w, y_l)\\}D={(q,yw,yl)},其中 ywy_wyw 是人类偏好的回答,yly_lyl 是不被偏好的回答
- KL 约束系数 β>0\\beta > 0β>0
RLHF 的目标是:
maxπθ Eq∼D, y∼πθ(⋅∣q)[rϕ(q,y)]−β DKL[πθ(⋅∣q)∥πref(⋅∣q)]
\\max_{\\pi_\\theta} \\; \\mathbb{E}_{q \\sim \\mathcal{D}, \\, y \\sim \\pi_\\theta(\\cdot | q)} \\left[ r_\\phi(q, y) \\right] – \\beta \\, D_{\\text{KL}} \\left[ \\pi_\\theta(\\cdot | q) \\| \\pi_{\\text{ref}}(\\cdot | q) \\right]
πθmaxEq∼D,y∼πθ(⋅∣q)[rϕ(q,y)]−βDKL[πθ(⋅∣q)∥πref(⋅∣q)]
其中第一项最大化奖励,第二项防止策略偏离参考模型太远。
2.2 KL 约束的展开
将 KL 散度展开:
DKL[πθ∥πref]=Ey∼πθ[logπθ(y∣q)πref(y∣q)]
D_{\\text{KL}} \\left[ \\pi_\\theta \\| \\pi_{\\text{ref}} \\right] = \\mathbb{E}_{y \\sim \\pi_\\theta} \\left[ \\log \\frac{\\pi_\\theta(y | q)}{\\pi_{\\text{ref}}(y | q)} \\right]
DKL[πθ∥πref]=Ey∼πθ[logπref(y∣q)πθ(y∣q)]
代入目标函数:
maxπθ Eq,y∼πθ[rϕ(q,y)−βlogπθ(y∣q)πref(y∣q)]
\\max_{\\pi_\\theta} \\; \\mathbb{E}_{q, y \\sim \\pi_\\theta} \\left[ r_\\phi(q, y) – \\beta \\log \\frac{\\pi_\\theta(y | q)}{\\pi_{\\text{ref}}(y | q)} \\right]
πθmaxEq,y∼πθ[rϕ(q,y)−βlogπref(y∣q)πθ(y∣q)]
2.3 最优策略的闭式解
定理(最优 KL 约束策略):对于固定的奖励函数 r(q,y)r(q, y)r(q,y),上述优化问题的最优解为:
π∗(y∣q)=1Z(q)πref(y∣q)exp(1βr(q,y))
\\pi^*(y | q) = \\frac{1}{Z(q)} \\pi_{\\text{ref}}(y | q) \\exp\\left( \\frac{1}{\\beta} r(q, y) \\right)
π∗(y∣q)=Z(q)1πref(y∣q)exp(β1r(q,y))
其中配分函数:
Z(q)=∑yπref(y∣q)exp(1βr(q,y))
Z(q) = \\sum_{y} \\pi_{\\text{ref}}(y | q) \\exp\\left( \\frac{1}{\\beta} r(q, y) \\right)
Z(q)=y∑πref(y∣q)exp(β1r(q,y))
证明:
目标函数等价于最小化:
minπθ DKL[πθ(⋅∣q)∥π∗(⋅∣q)]
\\min_{\\pi_\\theta} \\; D_{\\text{KL}} \\left[ \\pi_\\theta(\\cdot | q) \\| \\pi^*(\\cdot | q) \\right]
πθminDKL[πθ(⋅∣q)∥π∗(⋅∣q)]
其中 π∗\\pi^*π∗ 定义为上述闭式解。验证:
DKL[πθ∥π∗]=Ey∼πθ[logπθ(y∣q)π∗(y∣q)]=Ey∼πθ[logπθ(y∣q)1Z(q)πref(y∣q)exp(r(q,y)β)]=Ey∼πθ[logπθ(y∣q)πref(y∣q)−r(q,y)β+logZ(q)]=1β[−Ey∼πθ[r(q,y)]+β DKL[πθ∥πref]]+logZ(q)
\\begin{aligned}
D_{\\text{KL}}[\\pi_\\theta \\| \\pi^*] &= \\mathbb{E}_{y \\sim \\pi_\\theta} \\left[ \\log \\frac{\\pi_\\theta(y | q)}{\\pi^*(y | q)} \\right] \\\\
&= \\mathbb{E}_{y \\sim \\pi_\\theta} \\left[ \\log \\frac{\\pi_\\theta(y | q)}{\\frac{1}{Z(q)} \\pi_{\\text{ref}}(y | q) \\exp\\left(\\frac{r(q, y)}{\\beta}\\right)} \\right] \\\\
&= \\mathbb{E}_{y \\sim \\pi_\\theta} \\left[ \\log \\frac{\\pi_\\theta(y | q)}{\\pi_{\\text{ref}}(y | q)} – \\frac{r(q, y)}{\\beta} + \\log Z(q) \\right] \\\\
&= \\frac{1}{\\beta} \\left[ -\\mathbb{E}_{y \\sim \\pi_\\theta}[r(q, y)] + \\beta \\, D_{\\text{KL}}[\\pi_\\theta \\| \\pi_{\\text{ref}}] \\right] + \\log Z(q)
\\end{aligned}
DKL[πθ∥π∗]=Ey∼πθ[logπ∗(y∣q)πθ(y∣q)]=Ey∼πθlogZ(q)1πref(y∣q)exp(βr(q,y))πθ(y∣q)=Ey∼πθ[logπref(y∣q)πθ(y∣q)−βr(q,y)+logZ(q)]=β1[−Ey∼πθ[r(q,y)]+βDKL[πθ∥πref]]+logZ(q)
这正是 RLHF 目标函数(差一个常数和缩放因子)。最小化 KL 散度等价于最大化 RLHF 目标。
当 πθ=π∗\\pi_\\theta = \\pi^*πθ=π∗ 时,DKL[πθ∥π∗]=0D_{\\text{KL}}[\\pi_\\theta \\| \\pi^*] = 0DKL[πθ∥π∗]=0,达到全局最优。■\\blacksquare■
2.4 最优策略的直觉理解
最优策略 π∗\\pi^*π∗ 的形式具有深刻的物理直觉:
π∗(y∣q)∝πref(y∣q)⋅exp(r(q,y)β)
\\pi^*(y | q) \\propto \\pi_{\\text{ref}}(y | q) \\cdot \\exp\\left(\\frac{r(q, y)}{\\beta}\\right)
π∗(y∣q)∝πref(y∣q)⋅exp(βr(q,y))
- πref(y∣q)\\pi_{\\text{ref}}(y | q)πref(y∣q) 是"先验":模型本来就会生成什么
- exp(r/β)\\exp(r / \\beta)exp(r/β) 是"似然":奖励越高的回答,概率越大
- β\\betaβ 控制两者的权衡:β\\betaβ 大时策略更保守(接近 πref\\pi_{\\text{ref}}πref),β\\betaβ 小时更激进(更依赖奖励)
这与贝叶斯推断的后验 = 先验 × 似然 形式完全一致。
3. DPO 的核心推导
3.1 从最优策略反解奖励函数
从闭式解 π∗\\pi^*π∗ 反解 r(q,y)r(q, y)r(q,y):
π∗(y∣q)=1Z(q)πref(y∣q)exp(r(q,y)β)
\\pi^*(y | q) = \\frac{1}{Z(q)} \\pi_{\\text{ref}}(y | q) \\exp\\left(\\frac{r(q, y)}{\\beta}\\right)
π∗(y∣q)=Z(q)1πref(y∣q)exp(βr(q,y))
两边取对数:
logπ∗(y∣q)=−logZ(q)+logπref(y∣q)+1βr(q,y)
\\log \\pi^*(y | q) = -\\log Z(q) + \\log \\pi_{\\text{ref}}(y | q) + \\frac{1}{\\beta} r(q, y)
logπ∗(y∣q)=−logZ(q)+logπref(y∣q)+β1r(q,y)
解出 r(q,y)r(q, y)r(q,y):
r(q,y)=βlogπ∗(y∣q)πref(y∣q)+βlogZ(q)
r(q, y) = \\beta \\log \\frac{\\pi^*(y | q)}{\\pi_{\\text{ref}}(y | q)} + \\beta \\log Z(q)
r(q,y)=βlogπref(y∣q)π∗(y∣q)+βlogZ(q)
关键洞察:奖励函数可以完全用策略和参考策略的对数概率比来表示!最后一项 βlogZ(q)\\beta \\log Z(q)βlogZ(q) 只依赖于问题 qqq,不依赖于回答 yyy。
3.2 Bradley-Terry 偏好模型
人类偏好通常用 Bradley-Terry 模型 建模。给定问题 qqq 和两个回答 yw,yly_w, y_lyw,yl,人类偏好 ywy_wyw 胜过 yly_lyl 的概率为:
p(yw≻yl∣q)=σ(r(q,yw)−r(q,yl))
p(y_w \\succ y_l | q) = \\sigma(r(q, y_w) – r(q, y_l))
p(yw≻yl∣q)=σ(r(q,yw)−r(q,yl))
其中 σ(z)=11+e−z\\sigma(z) = \\frac{1}{1 + e^{-z}}σ(z)=1+e−z1 是 sigmoid 函数。
直觉:两个回答的奖励之差越大,人类越可能偏好奖励高的那个。
3.3 DPO 损失函数的推导
将奖励的闭式解代入 Bradley-Terry 模型:
p(yw≻yl∣q)=σ(βlogπ∗(yw∣q)πref(yw∣q)−βlogπ∗(yl∣q)πref(yl∣q))
p(y_w \\succ y_l | q) = \\sigma\\left( \\beta \\log \\frac{\\pi^*(y_w | q)}{\\pi_{\\text{ref}}(y_w | q)} – \\beta \\log \\frac{\\pi^*(y_l | q)}{\\pi_{\\text{ref}}(y_l | q)} \\right)
p(yw≻yl∣q)=σ(βlogπref(yw∣q)π∗(yw∣q)−βlogπref(yl∣q)π∗(yl∣q))
注意 βlogZ(q)\\beta \\log Z(q)βlogZ(q) 项在作差时完全消去!
简化后:
p(yw≻yl∣q)=σ(βlogπθ(yw∣q)πref(yw∣q)−βlogπθ(yl∣q)πref(yl∣q))
p(y_w \\succ y_l | q) = \\sigma\\left( \\beta \\log \\frac{\\pi_\\theta(y_w | q)}{\\pi_{\\text{ref}}(y_w | q)} – \\beta \\log \\frac{\\pi_\\theta(y_l | q)}{\\pi_{\\text{ref}}(y_l | q)} \\right)
p(yw≻yl∣q)=σ(βlogπref(yw∣q)πθ(yw∣q)−βlogπref(yl∣q)πθ(yl∣q))
其中 πθ\\pi_\\thetaπθ 是我们正在训练的策略(近似 π∗\\pi^*π∗)。
DPO 损失函数:对偏好数据取负对数似然:
LDPO(θ)=−E(q,yw,yl)∼D[logσ(βlogπθ(yw∣q)πref(yw∣q)−βlogπθ(yl∣q)πref(yl∣q))]
\\boxed{
\\mathcal{L}_{\\text{DPO}}(\\theta) = -\\mathbb{E}_{(q, y_w, y_l) \\sim \\mathcal{D}} \\left[ \\log \\sigma\\left( \\beta \\log \\frac{\\pi_\\theta(y_w | q)}{\\pi_{\\text{ref}}(y_w | q)} – \\beta \\log \\frac{\\pi_\\theta(y_l | q)}{\\pi_{\\text{ref}}(y_l | q)} \\right) \\right]
}
LDPO(θ)=−E(q,yw,yl)∼D[logσ(βlogπref(yw∣q)πθ(yw∣q)−βlogπref(yl∣q)πθ(yl∣q))]
这就是 DPO 的全部:一个二元交叉熵损失,输入是两个回答的隐式奖励之差。
3.4 DPO 的梯度分析
对 θ\\thetaθ 求梯度:
∇θLDPO=−β E(q,yw,yl)[σ(r^l−r^w)⏟权重(∇θlogπθ(yw∣q)−∇θlogπθ(yl∣q))]
\\nabla_\\theta \\mathcal{L}_{\\text{DPO}} = -\\beta \\, \\mathbb{E}_{(q, y_w, y_l)} \\left[ \\underbrace{\\sigma(\\hat{r}_l – \\hat{r}_w)}_{\\text{权重}} \\left( \\nabla_\\theta \\log \\pi_\\theta(y_w | q) – \\nabla_\\theta \\log \\pi_\\theta(y_l | q) \\right) \\right]
∇θLDPO=−βE(q,yw,yl)权重σ(r^l−r^w)(∇θlogπθ(yw∣q)−∇θlogπθ(yl∣q))
其中隐式奖励 r^=βlogπθ(y∣q)πref(y∣q)\\hat{r} = \\beta \\log \\frac{\\pi_\\theta(y | q)}{\\pi_{\\text{ref}}(y | q)}r^=βlogπref(y∣q)πθ(y∣q)。
直觉解释:
- 当模型已经正确排序时(r^w>r^l\\hat{r}_w > \\hat{r}_lr^w>r^l):σ(r^l−r^w)≈0\\sigma(\\hat{r}_l – \\hat{r}_w) \\approx 0σ(r^l−r^w)≈0,梯度很小——无需进一步调整
- 当模型排序错误时(r^w<r^l\\hat{r}_w < \\hat{r}_lr^w<r^l):σ(r^l−r^w)≈1\\sigma(\\hat{r}_l – \\hat{r}_w) \\approx 1σ(r^l−r^w)≈1,梯度很大——强烈修正
- 梯度方向:增大 logπθ(yw∣q)\\log \\pi_\\theta(y_w | q)logπθ(yw∣q),减小 logπθ(yl∣q)\\log \\pi_\\theta(y_l | q)logπθ(yl∣q)
这实现了一种自动加权机制:模型越困惑的样本,梯度贡献越大。
4. DPO 与 RLHF 的等价性
4.1 等价性定理
定理(Rafailov et al., 2023):在 Bradley-Terry 偏好模型假设下,DPO 和 RLHF 的最优解相同。
证明:
RLHF 的目标是:
maxπ Eq,y∼π[r(q,y)]−βDKL[π∥πref]
\\max_\\pi \\; \\mathbb{E}_{q, y \\sim \\pi} [r(q, y)] – \\beta D_{\\text{KL}}[\\pi \\| \\pi_{\\text{ref}}]
πmaxEq,y∼π[r(q,y)]−βDKL[π∥πref]
其最优解为:
π∗(y∣q)=1Z(q)πref(y∣q)exp(r(q,y)β)
\\pi^*(y | q) = \\frac{1}{Z(q)} \\pi_{\\text{ref}}(y | q) \\exp\\left(\\frac{r(q, y)}{\\beta}\\right)
π∗(y∣q)=Z(q)1πref(y∣q)exp(βr(q,y))
DPO 通过最大似然估计学习一个策略 πθ\\pi_\\thetaπθ,使得:
πθ=argmaxπ E(q,yw,yl)[logσ(βlogπ(yw∣q)πref(yw∣q)−βlogπ(yl∣q)πref(yl∣q))]
\\pi_\\theta = \\arg\\max_\\pi \\; \\mathbb{E}_{(q, y_w, y_l)} \\left[ \\log \\sigma\\left( \\beta \\log \\frac{\\pi(y_w | q)}{\\pi_{\\text{ref}}(y_w | q)} – \\beta \\log \\frac{\\pi(y_l | q)}{\\pi_{\\text{ref}}(y_l | q)} \\right) \\right]
πθ=argπmaxE(q,yw,yl)[logσ(βlogπref(yw∣q)π(yw∣q)−βlogπref(yl∣q)π(yl∣q))]
当偏好数据足够多且 Bradley-Terry 模型成立时,DPO 的最优解 πθ∗\\pi_\\theta^*πθ∗ 恰好满足:
πθ∗(y∣q)∝πref(y∣q)exp(r(q,y)β)
\\pi_\\theta^*(y | q) \\propto \\pi_{\\text{ref}}(y | q) \\exp\\left(\\frac{r(q, y)}{\\beta}\\right)
πθ∗(y∣q)∝πref(y∣q)exp(βr(q,y))
这与 RLHF 的最优解形式相同。■\\blacksquare■
4.2 为什么可以绕过奖励模型
传统 RLHF 流程:
偏好数据→训练奖励模型→RL最优策略
\\text{偏好数据} \\xrightarrow{\\text{训练}} \\text{奖励模型} \\xrightarrow{\\text{RL}} \\text{最优策略}
偏好数据训练奖励模型RL最优策略
DPO 的洞察:
偏好数据→直接训练最优策略
\\text{偏好数据} \\xrightarrow{\\text{直接训练}} \\text{最优策略}
偏好数据直接训练最优策略
奖励模型是中间变量,可以通过最优策略的闭式解被"积掉"(marginalized out)。这类似于概率模型中的经验贝叶斯:不需要显式估计先验参数,直接优化边缘似然。
4.3 隐式奖励
虽然 DPO 不显式训练奖励模型,但我们可以从训练好的 DPO 策略中提取隐式奖励:
r^(q,y)=βlogπθ(y∣q)πref(y∣q)
\\hat{r}(q, y) = \\beta \\log \\frac{\\pi_\\theta(y | q)}{\\pi_{\\text{ref}}(y | q)}
r^(q,y)=βlogπref(y∣q)πθ(y∣q)
这个隐式奖励满足:
r^(q,y)=r(q,y)−βlogZ(q)
\\hat{r}(q, y) = r(q, y) – \\beta \\log Z(q)
r^(q,y)=r(q,y)−βlogZ(q)
即真实奖励减去一个只依赖于 qqq 的常数。因此隐式奖励与真实奖励在偏好排序上完全一致。
5. DPO 的数学性质
5.1 DPO 损失的凸性分析
DPO 损失关于隐式奖励 r^w,r^l\\hat{r}_w, \\hat{r}_lr^w,r^l 是凸的:
ℓ(r^w,r^l)=−logσ(r^w−r^l)
\\ell(\\hat{r}_w, \\hat{r}_l) = -\\log \\sigma(\\hat{r}_w – \\hat{r}_l)
ℓ(r^w,r^l)=−logσ(r^w−r^l)
证明:
设 z=r^w−r^lz = \\hat{r}_w – \\hat{r}_lz=r^w−r^l,则 ℓ(z)=−logσ(z)=log(1+e−z)\\ell(z) = -\\log \\sigma(z) = \\log(1 + e^{-z})ℓ(z)=−logσ(z)=log(1+e−z)。
ℓ′′(z)=e−z(1+e−z)2=σ(z)(1−σ(z))>0
\\ell''(z) = \\frac{e^{-z}}{(1 + e^{-z})^2} = \\sigma(z)(1 – \\sigma(z)) > 0
ℓ′′(z)=(1+e−z)2e−z=σ(z)(1−σ(z))>0
因此 ℓ\\ellℓ 是严格凸的。■\\blacksquare■
5.2 DPO 的隐式正则化
DPO 损失隐含了对策略偏离参考模型的正则化。考虑极限情况:
当 β→0\\beta \\to 0β→0 时:DPO 退化为直接最大化奖励差,策略可能剧烈偏离 πref\\pi_{\\text{ref}}πref
当 β→∞\\beta \\to \\inftyβ→∞ 时:策略被强制接近 πref\\pi_{\\text{ref}}πref,几乎不学习偏好
适中的 β\\betaβ:在奖励最大化和策略保守性之间取得平衡
5.3 DPO 的梯度消失问题
问题:当模型已经正确排序所有偏好对时,DPO 损失的梯度趋近于零,训练停止。这可能导致:
数学分析:当 r^w≫r^l\\hat{r}_w \\gg \\hat{r}_lr^w≫r^l 时:
σ(r^l−r^w)→0⇒∇θLDPO→0
\\sigma(\\hat{r}_l – \\hat{r}_w) \\to 0 \\quad \\Rightarrow \\quad \\nabla_\\theta \\mathcal{L}_{\\text{DPO}} \\to 0
σ(r^l−r^w)→0⇒∇θLDPO→0
6. 完整可运行实现
6.1 DPO 核心实现
"""
DPO (Direct Preference Optimization) — 完整可运行实现
依赖: torch >= 2.0, numpy, matplotlib, transformers
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from typing import List, Tuple, Optional, Dict
from dataclasses import dataclass
import math
@dataclass
class DPOConfig:
"""DPO 配置"""
beta: float = 0.1 # KL 约束系数
lr: float = 5e-7 # 学习率
max_grad_norm: float = 1.0 # 梯度裁剪
label_smoothing: float = 0.0 # 标签平滑
loss_type: str = "sigmoid" # 损失类型: sigmoid, hinge, ipo
def compute_log_probs(
model: nn.Module,
input_ids: torch.Tensor,
attention_mask: torch.Tensor,
labels: torch.Tensor,
) –> torch.Tensor:
"""计算序列的平均对数概率"""
outputs = model(input_ids=input_ids, attention_mask=attention_mask)
logits = outputs.logits[:, :–1] # (B, L-1, V)
labels = labels[:, 1:] # (B, L-1)
log_probs = F.log_softmax(logits, dim=–1)
token_log_probs = log_probs.gather(2, labels.unsqueeze(–1)).squeeze(–1)
return token_log_probs
class DPO:
"""Direct Preference Optimization"""
def __init__(
self,
model: nn.Module,
ref_model: nn.Module,
config: DPOConfig,
):
self.model = model
self.ref_model = ref_model
self.config = config
# 冻结参考模型
for param in self.ref_model.parameters():
param.requires_grad = False
self.optimizer = torch.optim.AdamW(
model.parameters(), lr=config.lr, weight_decay=0.01
)
def compute_loss(
self,
chosen_input_ids: torch.Tensor,
chosen_attention_mask: torch.Tensor,
chosen_labels: torch.Tensor,
rejected_input_ids: torch.Tensor,
rejected_attention_mask: torch.Tensor,
rejected_labels: torch.Tensor,
) –> Dict[str, torch.Tensor]:
"""
计算 DPO 损失。
L = -E[ log σ( β·(log π(y_w)/π_ref(y_w) – log π(y_l)/π_ref(y_l)) ) ]
"""
# 计算当前模型的对数概率
chosen_log_probs = compute_log_probs(
self.model, chosen_input_ids, chosen_attention_mask, chosen_labels
)
rejected_log_probs = compute_log_probs(
self.model, rejected_input_ids, rejected_attention_mask, rejected_labels
)
# 计算参考模型的对数概率(无梯度)
with torch.no_grad():
ref_chosen_log_probs = compute_log_probs(
self.ref_model, chosen_input_ids, chosen_attention_mask, chosen_labels
)
ref_rejected_log_probs = compute_log_probs(
self.ref_model, rejected_input_ids, rejected_attention_mask, rejected_labels
)
# 计算隐式奖励差
chosen_rewards = self.config.beta * (chosen_log_probs – ref_chosen_log_probs)
rejected_rewards = self.config.beta * (rejected_log_probs – ref_rejected_log_probs)
logits = chosen_rewards – rejected_rewards
# 计算损失
if self.config.loss_type == "sigmoid":
# 标准 DPO 损失
if self.config.label_smoothing > 0:
loss = (
–F.logsigmoid(logits) * (1 – self.config.label_smoothing)
– F.logsigmoid(–logits) * self.config.label_smoothing
)
else:
loss = –F.logsigmoid(logits)
elif self.config.loss_type == "hinge":
# Hinge 损失变体
loss = torch.clamp(1 – logits, min=0)
elif self.config.loss_type == "ipo":
# IPO (Identity Preference Optimization)
loss = (logits – 1 / (2 * self.config.beta)) ** 2
loss = loss.mean()
# 计算统计量
chosen_reward_mean = chosen_rewards.mean().item()
rejected_reward_mean = rejected_rewards.mean().item()
reward_margin = (chosen_rewards – rejected_rewards).mean().item()
accuracy = (logits > 0).float().mean().item()
return {
"loss": loss,
"chosen_reward": chosen_reward_mean,
"rejected_reward": rejected_reward_mean,
"reward_margin": reward_margin,
"accuracy": accuracy,
}
def train_step(
self,
chosen_input_ids: torch.Tensor,
chosen_attention_mask: torch.Tensor,
chosen_labels: torch.Tensor,
rejected_input_ids: torch.Tensor,
rejected_attention_mask: torch.Tensor,
rejected_labels: torch.Tensor,
) –> Dict[str, float]:
"""执行一步 DPO 训练"""
self.optimizer.zero_grad()
metrics = self.compute_loss(
chosen_input_ids, chosen_attention_mask, chosen_labels,
rejected_input_ids, rejected_attention_mask, rejected_labels,
)
metrics["loss"].backward()
torch.nn.utils.clip_grad_norm_(
self.model.parameters(), self.config.max_grad_norm
)
self.optimizer.step()
return {k: v.item() if torch.is_tensor(v) else v for k, v in metrics.items()}
6.2 DPO 损失函数变体
class DPOLossVariants:
"""DPO 损失函数变体的数学分析"""
@staticmethod
def sigmoid_loss(logit_w: torch.Tensor, logit_l: torch.Tensor) –> torch.Tensor:
"""
标准 DPO (Bradley-Terry):
L = -log σ(β · (log π(y_w)/π_ref(y_w) – log π(y_l)/π_ref(y_l)))
"""
return –F.logsigmoid(logit_w – logit_l)
@staticmethod
def hinge_loss(logit_w: torch.Tensor, logit_l: torch.Tensor, margin: float = 1.0) –> torch.Tensor:
"""
Hinge 变体:
L = max(0, margin – (logit_w – logit_l))
"""
return torch.clamp(margin – (logit_w – logit_l), min=0)
@staticmethod
def ipo_loss(
logit_w: torch.Tensor, logit_l: torch.Tensor, beta: float = 0.1
) –> torch.Tensor:
"""
IPO (Identity Preference Optimization):
L = ((logit_w – logit_l) – 1/(2β))²
IPO 避免了 DPO 的过拟合问题,通过直接正则化奖励差。
"""
return (logit_w – logit_l – 1 / (2 * beta)) ** 2
@staticmethod
def sppo_loss(logit_w: torch.Tensor, logit_l: torch.Tensor) –> torch.Tensor:
"""
SPPO (Self-Play Preference Optimization):
L = (logit_w – 1)² + (logit_l + 1)²
"""
return (logit_w – 1) ** 2 + (logit_l + 1) ** 2
@staticmethod
def orpo_loss(
log_odds_w: torch.Tensor,
log_odds_l: torch.Tensor,
beta: float = 0.1,
) –> torch.Tensor:
"""
ORPO (Odds Ratio Preference Optimization):
L = -log σ(β · (log odds_w – log odds_l))
ORPO 直接使用概率比而非对数概率比,无需参考模型。
"""
return –F.logsigmoid(beta * (log_odds_w – log_odds_l))
6.3 DPO 训练器
class DPOTrainer:
"""完整的 DPO 训练器"""
def __init__(
self,
model: nn.Module,
ref_model: nn.Module,
config: DPOConfig,
tokenizer=None,
):
self.dpo = DPO(model, ref_model, config)
self.config = config
self.tokenizer = tokenizer
self.training_history = {
"loss": [], "accuracy": [], "reward_margin": [],
"chosen_reward": [], "rejected_reward": [],
}
def compute_sequence_log_probs(
self,
model: nn.Module,
input_ids: torch.Tensor,
labels: torch.Tensor,
response_mask: torch.Tensor,
) –> torch.Tensor:
"""计算响应部分的平均对数概率"""
with torch.set_grad_enabled(model.training):
outputs = model(input_ids=input_ids)
logits = outputs.logits[:, :–1]
labels = labels[:, 1:]
log_probs = F.log_softmax(logits, dim=–1)
token_log_probs = log_probs.gather(2, labels.unsqueeze(–1)).squeeze(–1)
masked_log_probs = token_log_probs * response_mask[:, 1:]
seq_log_probs = masked_log_probs.sum(dim=1) / response_mask[:, 1:].sum(dim=1).clamp(min=1)
return seq_log_probs
def train_epoch(
self,
dataset: List[Dict],
batch_size: int = 4,
epoch: int = 0,
) –> Dict[str, float]:
"""训练一个 epoch"""
self.dpo.model.train()
epoch_metrics = {"loss": [], "accuracy": [], "reward_margin": []}
for i in range(0, len(dataset), batch_size):
batch = dataset[i:i + batch_size]
chosen_ids = torch.stack([b["chosen_input_ids"] for b in batch])
chosen_mask = torch.stack([b["chosen_attention_mask"] for b in batch])
chosen_labels = torch.stack([b["chosen_labels"] for b in batch])
chosen_response_mask = torch.stack([b["chosen_response_mask"] for b in batch])
rejected_ids = torch.stack([b["rejected_input_ids"] for b in batch])
rejected_mask = torch.stack([b["rejected_attention_mask"] for b in batch])
rejected_labels = torch.stack([b["rejected_labels"] for b in batch])
rejected_response_mask = torch.stack([b["rejected_response_mask"] for b in batch])
# 计算对数概率
chosen_log_probs = self.compute_sequence_log_probs(
self.dpo.model, chosen_ids, chosen_labels, chosen_response_mask
)
rejected_log_probs = self.compute_sequence_log_probs(
self.dpo.model, rejected_ids, rejected_labels, rejected_response_mask
)
with torch.no_grad():
ref_chosen_log_probs = self.compute_sequence_log_probs(
self.dpo.ref_model, chosen_ids, chosen_labels, chosen_response_mask
)
ref_rejected_log_probs = self.compute_sequence_log_probs(
self.dpo.ref_model, rejected_ids, rejected_labels, rejected_response_mask
)
# 计算 DPO 损失
chosen_rewards = self.config.beta * (chosen_log_probs – ref_chosen_log_probs)
rejected_rewards = self.config.beta * (rejected_log_probs – ref_rejected_log_probs)
logits = chosen_rewards – rejected_rewards
loss = –F.logsigmoid(logits).mean()
self.dpo.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.dpo.model.parameters(), self.config.max_grad_norm)
self.dpo.optimizer.step()
epoch_metrics["loss"].append(loss.item())
epoch_metrics["accuracy"].append((logits > 0).float().mean().item())
epoch_metrics["reward_margin"].append(logits.mean().item())
return {k: np.mean(v) for k, v in epoch_metrics.items()}
6.4 实验代码
def experiment_dpo_synthetic():
"""
DPO 合成实验:在一个简化的偏好学习任务上验证 DPO 的有效性。
"""
torch.manual_seed(42)
vocab_size = 100
seq_len = 20
hidden_dim = 64
num_samples = 500
device = torch.device("cpu")
# 创建简单的语言模型
class SimpleLM(nn.Module):
def __init__(self):
super().__init__()
self.embed = nn.Embedding(vocab_size, hidden_dim)
self.layers = nn.ModuleList([
nn.TransformerEncoderLayer(
d_model=hidden_dim, nhead=4, dim_feedforward=hidden_dim * 4,
batch_first=True
) for _ in range(2)
])
self.head = nn.Linear(hidden_dim, vocab_size)
def forward(self, input_ids, attention_mask=None):
h = self.embed(input_ids)
for layer in self.layers:
h = layer(h, src_key_padding_mask=(attention_mask == 0) if attention_mask is not None else None)
return type('Output', (), {'logits': self.head(h)})()
# 生成偏好数据
def generate_preference_data():
data = []
for _ in range(num_samples):
chosen = torch.randint(1, vocab_size, (seq_len,))
rejected = torch.randint(1, vocab_size, (seq_len,))
chosen_labels = chosen.clone()
rejected_labels = rejected.clone()
data.append({
"chosen_input_ids": chosen,
"chosen_attention_mask": torch.ones(seq_len),
"chosen_labels": chosen_labels,
"chosen_response_mask": torch.ones(seq_len),
"rejected_input_ids": rejected,
"rejected_attention_mask": torch.ones(seq_len),
"rejected_labels": rejected_labels,
"rejected_response_mask": torch.ones(seq_len),
})
return data
dataset = generate_preference_data()
# 初始化模型
model = SimpleLM()
ref_model = SimpleLM()
ref_model.load_state_dict(model.state_dict())
config = DPOConfig(beta=0.1, lr=1e-4, max_grad_norm=1.0)
trainer = DPOTrainer(model, ref_model, config)
# 训练
print("DPO 训练开始…")
for epoch in range(10):
metrics = trainer.train_epoch(dataset, batch_size=32, epoch=epoch)
print(f"Epoch {epoch+1}/10 | Loss: {metrics['loss']:.4f} | "
f"Acc: {metrics['accuracy']:.4f} | Margin: {metrics['reward_margin']:.4f}")
return trainer
def compare_dpo_variants():
"""对比 DPO 损失函数变体"""
torch.manual_seed(42)
beta = 0.1
num_points = 1000
margins = torch.linspace(–3, 3, num_points)
chosen_log_probs = torch.randn(num_points) * 0.5
rejected_log_probs = chosen_log_probs – margins
variants = DPOLossVariants()
losses = {
"Sigmoid (DPO)": variants.sigmoid_loss(chosen_log_probs, rejected_log_probs),
"Hinge": variants.hinge_loss(chosen_log_probs, rejected_log_probs),
"IPO": variants.ipo_loss(chosen_log_probs, rejected_log_probs, beta=beta),
"SPPO": variants.sppo_loss(chosen_log_probs, rejected_log_probs),
}
for name, loss in losses.items():
print(f"{name:20s} | Mean Loss: {loss.mean().item():.4f} | Std: {loss.std().item():.4f}")
return losses
7. DPO 的进阶变体
7.1 IPO(Identity Preference Optimization)
动机:DPO 在偏好数据上容易过拟合,因为 Bradley-Terry 模型假设可能不成立。
IPO 损失(Azar et al., 2024):
LIPO(θ)=E(q,yw,yl)[(logπθ(yw∣q)πref(yw∣q)−logπθ(yl∣q)πref(yl∣q)−12β)2]
\\mathcal{L}_{\\text{IPO}}(\\theta) = \\mathbb{E}_{(q, y_w, y_l)} \\left[ \\left( \\log \\frac{\\pi_\\theta(y_w | q)}{\\pi_{\\text{ref}}(y_w | q)} – \\log \\frac{\\pi_\\theta(y_l | q)}{\\pi_{\\text{ref}}(y_l | q)} – \\frac{1}{2\\beta} \\right)^2 \\right]
LIPO(θ)=E(q,yw,yl)[(logπref(yw∣q)πθ(yw∣q)−logπref(yl∣q)πθ(yl∣q)−2β1)2]
直觉:IPO 不追求让奖励差趋向无穷,而是让奖励差趋向一个固定目标 12β\\frac{1}{2\\beta}2β1。这避免了 DPO 的"奖励膨胀"问题。
7.2 KTO(Kahneman-Tversky Optimization)
动机:偏好数据的获取成本高,有时只有"好/坏"的标签(无需成对比较)。
KTO 损失(Ethayarajh et al., 2024):
LKTO(θ)=E(q,y)[{λwσ(−βr^(q,y))if y is desirableλlσ(βr^(q,y))if y is undesirable]
\\mathcal{L}_{\\text{KTO}}(\\theta) = \\mathbb{E}_{(q, y)} \\left[ \\begin{cases} \\lambda_w \\sigma(-\\beta \\hat{r}(q, y)) & \\text{if } y \\text{ is desirable} \\\\ \\lambda_l \\sigma(\\beta \\hat{r}(q, y)) & \\text{if } y \\text{ is undesirable} \\end{cases} \\right]
LKTO(θ)=E(q,y)[{λwσ(−βr^(q,y))λlσ(βr^(q,y))if y is desirableif y is undesirable]
其中 r^(q,y)=logπθ(y∣q)πref(y∣q)\\hat{r}(q, y) = \\log \\frac{\\pi_\\theta(y | q)}{\\pi_{\\text{ref}}(y | q)}r^(q,y)=logπref(y∣q)πθ(y∣q)。
关键优势:不需要成对偏好数据,只需要独立的"好/坏"标签。
7.3 ORPO(Odds Ratio Preference Optimization)
动机:完全消除对参考模型的依赖。
ORPO 损失(Hong et al., 2024):
LORPO(θ)=LSFT(yw)+λ⋅LOR(yw,yl)
\\mathcal{L}_{\\text{ORPO}}(\\theta) = \\mathcal{L}_{\\text{SFT}}(y_w) + \\lambda \\cdot \\mathcal{L}_{\\text{OR}}(y_w, y_l)
LORPO(θ)=LSFT(yw)+λ⋅LOR(yw,yl)
其中:
LOR(yw,yl)=−logσ(logoddsθ(yw∣q)oddsθ(yl∣q))
\\mathcal{L}_{\\text{OR}}(y_w, y_l) = -\\log \\sigma\\left( \\log \\frac{\\text{odds}_\\theta(y_w | q)}{\\text{odds}_\\theta(y_l | q)} \\right)
LOR(yw,yl)=−logσ(logoddsθ(yl∣q)oddsθ(yw∣q))
oddsθ(y∣q)=πθ(y∣q)1−πθ(y∣q)
\\text{odds}_\\theta(y | q) = \\frac{\\pi_\\theta(y | q)}{1 – \\pi_\\theta(y | q)}
oddsθ(y∣q)=1−πθ(y∣q)πθ(y∣q)
关键优势:无需参考模型,将 SFT 和偏好学习合并为一步。
7.4 SimPO(Simple Preference Optimization)
动机:进一步简化 DPO,使用序列长度归一化的对数概率作为隐式奖励。
SimPO 损失(Meng et al., 2024):
LSimPO(θ)=−E[logσ(β∣yw∣logπθ(yw∣q)−β∣yl∣logπθ(yl∣q)−γ)]
\\mathcal{L}_{\\text{SimPO}}(\\theta) = -\\mathbb{E} \\left[ \\log \\sigma\\left( \\frac{\\beta}{|y_w|} \\log \\pi_\\theta(y_w | q) – \\frac{\\beta}{|y_l|} \\log \\pi_\\theta(y_l | q) – \\gamma \\right) \\right]
LSimPO(θ)=−E[logσ(∣yw∣βlogπθ(yw∣q)−∣yl∣βlogπθ(yl∣q)−γ)]
其中 γ>0\\gamma > 0γ>0 是目标奖励间距。
关键改进:
8. DPO 与其他对齐方法的理论对比
8.1 方法对比表
| 需要奖励模型 | 是 | 否 | 否 | 否 | 否 |
| 需要参考模型 | 是 | 是 | 是 | 是 | 否 |
| 需要成对偏好 | 否 | 是 | 是 | 否 | 是 |
| 训练方式 | 在线 RL | 监督学习 | 监督学习 | 监督学习 | 监督学习 |
| 训练稳定性 | 低 | 高 | 高 | 高 | 高 |
| 计算成本 | 高(3×模型) | 中(2×模型) | 中(2×模型) | 中(2×模型) | 低(1×模型) |
| 理论等价性 | 基准 | ≡ RLHF (BT) | 不同目标 | 不同目标 | 不同目标 |
8.2 在线 DPO vs 离线 DPO
离线 DPO(标准):使用固定的偏好数据集训练,不生成新数据。
在线 DPO(Iterative DPO):
数学形式:
πθk+1=argminπ E(q,yw,yl)∼Dk[LDPO(π,πθk)]
\\pi_{\\theta_{k+1}} = \\arg\\min_\\pi \\; \\mathbb{E}_{(q, y_w, y_l) \\sim \\mathcal{D}_k} \\left[ \\mathcal{L}_{\\text{DPO}}(\\pi, \\pi_{\\theta_k}) \\right]
πθk+1=argπminE(q,yw,yl)∼Dk[LDPO(π,πθk)]
其中 Dk\\mathcal{D}_kDk 是第 kkk 轮生成的偏好数据。
优势:在线 DPO 可以持续改进,避免分布偏移问题。
9. DPO 的局限性与前沿发展
9.1 分布偏移问题
问题:DPO 使用离线数据训练,但测试时策略需要自己生成回答。训练数据的分布(来自 πref\\pi_{\\text{ref}}πref)与测试时的分布(来自 πθ\\pi_\\thetaπθ)不一致。
数学表述:
Ey∼πθ[r^(q,y)]≠Ey∼πref[r^(q,y)]
\\mathbb{E}_{y \\sim \\pi_\\theta} [\\hat{r}(q, y)] \\neq \\mathbb{E}_{y \\sim \\pi_{\\text{ref}}} [\\hat{r}(q, y)]
Ey∼πθ[r^(q,y)]=Ey∼πref[r^(q,y)]
解决方案:
9.2 推理能力与 DPO
DPO 主要优化偏好对齐,不一定能提升推理能力。结合 DPO 和推理训练的前沿方法:
10. DPO 数学公式总结
╔══════════════════════════════════════════════════════════════════════════════════════════╗
║ DPO (Direct Preference Optimization) 数学总结 ║
╠══════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ 1. RLHF 目标函数: ║
║ max_π E_{q,y~π}[r(q,y)] – β·D_KL[π || π_ref] ║
║ ║
║ 2. 最优策略闭式解: ║
║ π*(y|q) = (1/Z(q)) · π_ref(y|q) · exp(r(q,y)/β) ║
║ Z(q) = Σ_y π_ref(y|q) · exp(r(q,y)/β) ║
║ ║
║ 3. 奖励反解: ║
║ r(q,y) = β·log(π*(y|q)/π_ref(y|q)) + β·log Z(q) ║
║ ║
║ 4. Bradley-Terry 偏好模型: ║
║ P(y_w ≻ y_l | q) = σ(r(q,y_w) – r(q,y_l)) ║
║ ║
║ 5. DPO 损失函数: ║
║ L_DPO(θ) = -E_{(q,y_w,y_l)} [ log σ(β · ( ║
║ log π_θ(y_w|q)/π_ref(y_w|q) – log π_θ(y_l|q)/π_ref(y_l|q) ║
║ )) ] ║
║ ║
║ 6. 隐式奖励: ║
║ r̂(q,y) = β·log(π_θ(y|q) / π_ref(y|q)) ║
║ 性质: r̂(q,y) = r(q,y) – β·log Z(q) (偏好排序一致) ║
║ ║
║ 7. DPO 梯度: ║
║ ∇L = -β·E[σ(r̂_l – r̂_w)·(∇log π(y_w) – ∇log π(y_l))] ║
║ 自动加权: 模型越困惑 → 梯度越大 ║
║ ║
║ 8. 等价性: ║
║ DPO ≡ RLHF (在 Bradley-Terry 模型假设下) ║
║ DPO 绕过奖励模型, 直接从偏好数据学习策略 ║
║ ║
║ 9. DPO 变体: ║
║ IPO: L = (r̂_w – r̂_l – 1/(2β))² (避免过拟合) ║
║ KTO: L = σ(-β·r̂) for desirable (无需成对数据) ║
║ ORPO: L = -log σ(log odds_w/odds_l) (无需参考模型) ║
║ SimPO: L = -log σ(β·log π(y_w)/|y_w| – β·log π(y_l)/|y_l| – γ) ║
║ ║
║ 10. 超参数 β 的作用: ║
║ β 大 → 策略保守 (接近 π_ref) ║
║ β 小 → 策略激进 (更依赖奖励) ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════╝


