在大模型对齐任务中,基于人类反馈的强化学习一直是主流路线。它通常先训练一个奖励模型来拟合人类对回答的偏好,再用PPO等算法优化语言模型。这条链路效果不错,但奖励模型偏差、PPO训练脆弱以及多阶段工程复杂度,让很多团队在落地时感到吃力。DPO直接偏好优化尝试换一种思路:不再显式训练奖励模型,而是从偏好数据中直接学习策略,把对齐过程简化为一个分类任务。这个转变不仅降低了训练门槛,也在不少基准上取得了与RLHF相当甚至更好的表现。

奖励模型在RLHF中的角色与问题
传统RLHF可以拆成几个阶段:先做监督微调得到SFT模型,再收集人工偏好数据训练奖励模型,最后用PPO在奖励信号引导下继续更新策略。奖励模型本质上是一个排序器或评分器,它对同一个问题下的不同回答给出分数,PPO根据分数差异调整生成策略。这个中间产物看似自然,却带来了额外的复杂性和不确定性。
奖励模型的问题首先体现在标注成本上。训练一个可靠的奖励模型需要大量高质量偏好对比,而人类标注本身存在噪声,不同标注者对好回答的判断并不完全一致。其次,奖励模型容易学习到长度、格式等表面特征,而不是真实的有用性和准确性。PPO阶段会进一步放大这些问题,策略模型可能找到奖励模型的漏洞,输出冗长但信息量低的回答,也就是常说的reward hacking。除此之外,PPO需要同时维护策略网络、价值网络、奖励模型和参考模型,训练过程中还要处理优势估计、裁剪范围和KL散度约束,调试成本很高。
DPO的出发点正是绕过这个脆弱的中间层。它并不否认偏好学习的目标,而是通过公式推导,将奖励模型从优化目标中直接消掉。这样得到的训练损失只依赖当前策略和参考策略的输出概率,不再需要单独训练奖励模型,也不需要在线采样和优势估计。
DPO推导:把奖励函数从损失中消掉
DPO的数学基础是Bradley-Terry偏好模型。给定输入x和两个回答y_w、y_l,人类选择y_w的概率可以写成sigmoid(r(x,y_w)-r(x,y_l)),其中r是未被观察到的奖励函数。传统RLHF会先用数据估计r,再做强化学习。而DPO观察到,在带有KL约束的奖励最大化目标中,最优策略与奖励函数之间存在闭式关系。
具体来说,RLHF的优化目标是最大化奖励并最小化策略与参考策略的KL散度。对这个目标求变分最优解,可以得到奖励函数关于策略的表达式:r(x,y)等于β乘以log(π(y|x)/π_ref(y|x)),再加上一个只与x有关的配分项log Z(x)。把这一表达式代入Bradley-Terry模型后,配分项会在chosen与rejected概率的差值中抵消。于是人类偏好概率可以直接用当前策略和参考策略的对数概率比来表示。
这样就得到了DPO的核心损失:-E[log σ(β log πθ(y_w|x)/π_ref(y_w|x) - β log πθ(y_l|x)/π_ref(y_l|x))]。其中πθ是当前策略,π_ref是冻结的参考模型,β控制偏离参考模型的程度。这个损失的含义很直接:如果模型给chosen回答的修正概率越大,同时给rejected回答的修正概率越小,损失就越低。训练过程不再有独立的奖励头,也没有PPO中的价值函数和优势估计。奖励函数被隐式地表示为策略概率与参考策略概率的比值,这让偏好优化变得更简洁。
DPO训练示例与代码实现
在工程实现上,DPO每次更新只需要两个模型:当前策略模型和冻结的参考模型。参考模型通常使用SFT后的权重,并且在DPO训练期间不参与梯度更新。每个训练样本是一个三元组,包含prompt、chosen回答和rejected回答。对同一个prompt,分别计算当前策略和参考模型在chosen与rejected序列上的对数概率,然后代入上面的损失函数。
import torch
import torch.nn.functional as F
def dpo_loss(pi_logits, ref_logits, chosen_ids, rejected_ids, beta=0.1):
"""
pi_logits: 当前策略模型对完整序列的输出 logits
ref_logits: 参考模型对完整序列的输出 logits
chosen_ids/rejected_ids: 正负样本的 token id 序列
"""
pi_chosen_logps = logprob_from_logits(pi_logits, chosen_ids)
ref_chosen_logps = logprob_from_logits(ref_logits, chosen_ids)
pi_rejected_logps = logprob_from_logits(pi_logits, rejected_ids)
ref_rejected_logps = logprob_from_logits(ref_logits, rejected_ids)
chosen_ratio = (pi_chosen_logps - ref_chosen_logps) * beta
rejected_ratio = (pi_rejected_logps - ref_rejected_logps) * beta
loss = -F.logsigmoid(chosen_ratio - rejected_ratio).mean()
return loss
def logprob_from_logits(logits, token_ids):
log_probs = F.log_softmax(logits, dim=-1)
selected = torch.gather(log_probs, dim=-1, index=token_ids.unsqueeze(-1)).squeeze(-1)
return selected.sum(dim=-1)
这段实现的核心是logprob_from_logits函数。它先对logits做log_softmax,再根据token id取出每个位置的对数概率,最后对序列求和。实际训练时应当只对答案部分计算对数概率,忽略prompt部分,否则损失会被不同长度的prompt所干扰。另一个常见做法是对序列长度做归一化,避免模型因为回答长度不同而产生不稳定的梯度。
需要注意的是,DPO中的参考模型必须保持冻结,并且通常不放在DataParallel或混合精度下产生数值差异。若参考模型使用半精度计算,建议与策略模型保持一致,否则概率比会出现系统性偏差。对于大规模训练,可以将参考模型预计算并缓存概率,减少一次完整前向带来的显存和时间开销。
与RLHF的对比及工程调参建议
从训练资源角度看,DPO的简化优势非常明显。RLHF通常需要同时维护策略网络、价值网络、奖励模型和参考模型,PPO更新还需要处理大量采样样本和优势计算。DPO则只要求策略模型和参考模型,训练过程是完全离线的监督式更新,不需要在线生成回答,也不需要价值函数估计。这个差异在显存占用、通信开销和调试成本上都很突出。
| 对比维度 | RLHF | DPO |
|---|---|---|
| 奖励模型 | 需要单独训练 | 不需要 |
| 在线交互 | 通常需要采样与价值估计 | 完全离线 |
| 训练模型数量 | 策略、价值、奖励、参考 | 策略、参考 |
| 超参数敏感度 | 对PPO裁剪、KL系数等较敏感 | 主要对β和批大小敏感 |
| 实现难度 | 较高 | 较低 |
不过DPO也并非没有代价。它直接依赖已有的偏好数据,如果数据中chosen与rejected差异不明显,或者标注噪声较大,模型学到的策略可能不稳定。DPO的离线性质也意味着它不能像在线RL那样持续利用新生成的样本修正策略。对于一些需要细粒度评分或连续反馈的任务,Bradley-Terry二元偏好模型可能表达力不足。
实际调参时,β是最关键的参数之一。β越小,模型越激进地利用偏好信号,但也更容易过拟合到训练数据;β越大,策略越贴近参考模型,训练越保守。常见取值在0.05到0.3之间,具体可以根据验证集上的生成质量进行调整。学习率通常比SFT阶段低一个数量级,例如SFT使用5e-6时,DPO可以尝试5e-7到1e-6。批量大小建议保持适中,过小的batch会让偏好对之间的梯度方差变大,过大的batch又可能掩盖掉有区分度的偏好信息。训练轮数一般控制在1到3轮,过度训练容易导致语言能力退化或重复输出。
数据质量方面,应优先清洗掉chosen与rejected几乎相同、只是标点或长度存在差异的样本。这类样本无法提供有效的学习信号,还会增加损失中的噪声。对于中文任务,可以额外加入长度惩罚或对回答长度做归一化,避免模型偏向生成更长的文本。总体来看,DPO适合作为SFT之后的第一阶段对齐,如果后续需要更强的人类偏好拟合,再考虑引入在线策略优化或更复杂的偏好建模方式。