择善固执:DPO 与大模型的偏好蒸馏


上篇:酒肆里的品鉴哲学

故事开始于一座南方小城的老酒肆里。

那里的掌柜姓谢,人称谢老爷,是方圆百里出了名的酿酒师。他的酒窖里藏着三十年的老坛,他的徒弟里出过七位品酒大师。每年入秋,他就在后院摆出品鉴台,让各地的学徒们轮流上台,由几位老师傅打分——满分一百,水果香气占二十,回甘悠长占三十,色泽清澈占二十,余韵醇厚占三十。

这套制度用了四十年,但谢老爷心里始终有个疙瘩。

不是打分不准——老师傅们都是行家,但”行家”这个词里藏着一个秘密:他们对同一壶酒,今天打八十二,明日可能打七十八,不是因为酒变了,而是因为人的状态在变。他们口中说的”二十分水果香气”,其实没有一把真正的尺,它是一种感觉,一种在无数次品鉴中积累的、无法言说的感觉。

把感觉翻译成数字,本身就是一种失真。

后来来了个新学徒,是个读过几年书的年轻人,名叫直比。他旁观了两年之后,有一天走到谢老爷面前,说了一句让所有人愣住的话:

“老爷,我们为什么要打分?您只需要告诉我,这两壶酒,哪壶更好。”

谢老爷愣了一下。他本想说”打分才能量化”,但话到嘴边,突然想起一件事:他自己评判酒的好坏,从来不是先拿出一张评分表,逐项填写。他只是喝,然后知道——这壶好,那壶差。这个判断快如闪电,精准如刀,从不含糊。

直比继续说:”人类的大脑,天生就擅长比较,不擅长打绝对分。您让老师傅们比较两壶酒,他们的答案会高度一致;但让他们分别打分,分数会飘。我们为什么要走那条难走的路,而不走这条自然的路?”

谢老爷沉默了很久。

窗外的秋雨打在芭蕉叶上,发出细密的声响。他想起三十年前,他的师傅教他品酒的方式——也不是让他背一张评分表,而是两壶酒摆在面前,问他”哪壶更接近秋天的味道”。他就是在这样的比较里,一点一点积累起了品鉴的眼光。

“好,”他说,”你试试。”

那一年,品鉴台上发生了一个小小的革命。


在语言模型的世界里,同样的故事在二十一世纪重演了一次。

我们想要让大模型变得更”好”——更有帮助、更诚实、更无害。但”好”是什么?和酿酒一样,它很难被量化成一个绝对的数字。人类标注者面对一段模型输出,被要求打 0-10 分时,标注一致性会很低;但被要求判断”A 好还是 B 好”时,一致性会显著提升。

认知科学早就告诉我们:人类是天生的比较机器,不是天生的量化机器

这个洞见,催生了 RLHF(人类反馈强化学习),也最终催生了更优雅的 DPO(直接偏好优化)。而这两者之间的距离,正好是谢老爷的评分表与直比的那句话之间的距离。


中篇:老方法的三段折磨

在 DPO 出现之前,让语言模型学习人类偏好,靠的是一套叫做 RLHF 的流程。它大致分三段,每一段都有各自的代价。

第一段:采集偏好数据。

给模型一个问题,让它生成两个不同的回答 A 和 B,然后由人类标注者判断哪个更好。这一步输出的是三元组:(提示词, 更好的回答, 更差的回答)——即 (x, y_w, y_l)。

第二段:训练奖励模型。

用上一步的偏好数据,训练一个独立的”奖励模型” r(x, y),让它学会对任意一个回答打分。这个奖励模型,就是谢老爷品鉴台上的评分表——它把人类的偏好压缩成一个标量数字,方便后续优化。

第三段:用强化学习优化策略。

以奖励模型为标准,用 PPO 算法来更新语言模型,让它生成能得到更高奖励分数的回答——同时用 KL 散度约束,防止模型跑偏太远,丢失原有能力。

这套流程合情合理,但工程师们在实践中吃了不少苦头:

奖励模型会被”黑”掉。 语言模型在强化学习的压力下,会发现让奖励模型打高分的捷径——比如输出特别长的句子、使用特定的措辞模式——而这些并不是真正的高质量回答。这叫做奖励黑客(Reward Hacking)。奖励模型只是人类偏好的近似,模型学会了欺骗这个近似。

PPO 出了名的难训。 超参数极其敏感,训练曲线动辄崩塌;需要同时维护四个模型(策略模型、参考模型、奖励模型、价值模型),显存需求巨大,工程复杂度极高。

误差层层叠加。 奖励模型本身只是对人类偏好的近似;PPO 在这个近似的标准上做优化,产生了近似的近似;两层偏差叠加,最终效果难以预料。

谢老爷的品鉴台,相当于 RLHF 里的奖励模型:它把人类的感觉编码成了数字,但这个编码过程有损失。然后我们再拿这个有损的数字去训练学徒,等于绕了一个大弯,还在弯道里摔了跤。

直比的问题仍然悬在空中:能不能跳过打分这一步,直接让模型从比较中学习?


下篇:一个等式,拆掉整座裁判台

2023 年,一篇论文给出了答案,题为 Direct Preference Optimization: Your Language Model is Secretly a Reward Model

这个副标题是整篇论文的灵魂:你的语言模型,其实悄悄藏着一个奖励模型。

推导的起点,是 RLHF 的标准目标函数。我们想找一个最优策略 π*,在最大化奖励的同时不要和参考模型 π_ref 跑偏太远:

\[\max_{\pi} \; \mathbb{E}_{x, y \sim \pi}\left[r(x, y)\right] - \beta \cdot \mathbb{KL}\left[\pi(\cdot|x) \,\|\, \pi_{\text{ref}}(\cdot|x)\right]\]

这里的 β 是权衡系数——β 越大,越要求策略贴近参考模型;β 越小,越允许模型追求高奖励。

这个优化问题有闭合形式的最优解(可用变分法严格推导):

\[\pi^*(y|x) = \frac{1}{Z(x)} \cdot \pi_{\text{ref}}(y|x) \cdot \exp\!\left(\frac{r(x,y)}{\beta}\right)\]

其中 Z(x) 是归一化常数(配分函数),确保概率加和为 1。

这个等式看起来平淡无奇,但稍微变形一下,就会发现一颗埋藏的炸弹。两边取对数,整理,把 r 移到左边:

\[r(x, y) = \beta \cdot \log \frac{\pi^*(y|x)}{\pi_{\text{ref}}(y|x)} + \beta \cdot \log Z(x)\]

注意!等式右边,奖励 r(x,y) 被完全用最优策略 π* 和参考策略 π_ref 的比值表达出来了。

这意味着什么?

这意味着:如果最优策略 π* 存在,那么奖励就隐含在语言模型自己的概率分布之中,不需要显式训练一个独立的奖励模型。奖励,就藏在当前模型和参考模型之间的差异里。


现在,把这个奖励的表达式代入人类偏好的建模公式。

人类偏好数据通常用 Bradley-Terry 模型 来建模——这是统计学中的一个经典方法,用来建模”比较”型数据:

\[P(y_w \succ y_l \mid x) = \sigma\!\left(r(x, y_w) - r(x, y_l)\right)\]

其中 σ 是 sigmoid 函数,y_w 是”更好的回答”,y_l 是”更差的回答”,这个式子说的是:更好的回答获得更高奖励,因此被人类选中的概率更高。

把上面推导出的奖励表达式代入 Bradley-Terry 模型,神奇的事情发生了——Z(x) 被消掉了(因为 r(y_w) - r(y_l) 时,两边的 β log Z(x) 相互抵消),得到:

\[P(y_w \succ y_l \mid x) = \sigma\!\left(\beta \log \frac{\pi^*(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \beta \log \frac{\pi^*(y_l|x)}{\pi_{\text{ref}}(y_l|x)}\right)\]

这个式子里,没有奖励模型,没有强化学习,只有:

于是,DPO 的训练目标就自然涌现了——最大化正确偏好的对数似然,等价于最小化以下损失函数:

\[\mathcal{L}_{\text{DPO}}(\pi_\theta; \pi_{\text{ref}}) = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \!\left[ \log \sigma \!\left( \beta \log \frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)} - \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)} \right) \right]\]

这就是 DPO Loss。一个普通的二元交叉熵损失。

不需要奖励模型,不需要 PPO,不需要价值函数,不需要四个模型同时占满显存——只需要两个模型(当前模型 + 冻结参考模型)和一个监督学习的训练循环。

直比拆掉了品鉴台。


工程心法:DPO 的实战细节

理解了数学原理,我们来看工程落地时需要注意什么。

1. 数据格式:三元组

DPO 的训练数据格式很统一,每条样本是一个三元组:

{
    "prompt": "请解释一下量子纠缠",
    "chosen": "量子纠缠是指两个粒子在量子层面产生关联,使得对其中一个粒子的测量会瞬时影响另一个粒子的状态。这种现象由薛定谔方程描述...",
    "rejected": "量子纠缠嘛就是两个粒子互相关联,你测一个另一个就知道了,爱因斯坦叫它鬼魅超距作用。"
}

数据质量是 DPO 的命脉。chosenrejected 之间的差异要足够明显、足够有意义,否则模型无法从中学到有价值的偏好信号——就像让直比比较两壶品质几乎相同的酒,他也无从判断。

2. 训练代码骨架

DPO 的核心逻辑其实很简单,用 PyTorch 表达如下:

def dpo_loss(
    policy_model, ref_model,
    chosen_ids, rejected_ids, 
    beta=0.1
):
    # 计算策略模型的对数概率
    chosen_logprobs = get_logprobs(policy_model, chosen_ids)
    rejected_logprobs = get_logprobs(policy_model, rejected_ids)
    
    # 计算参考模型的对数概率(冻结,不反向传播)
    with torch.no_grad():
        ref_chosen_logprobs = get_logprobs(ref_model, chosen_ids)
        ref_rejected_logprobs = get_logprobs(ref_model, rejected_ids)
    
    # 计算对数比值差
    log_ratio_chosen = chosen_logprobs - ref_chosen_logprobs
    log_ratio_rejected = rejected_logprobs - ref_rejected_logprobs
    
    # DPO Loss = -log sigmoid(β * (log_ratio_chosen - log_ratio_rejected))
    loss = -F.logsigmoid(beta * (log_ratio_chosen - log_ratio_rejected)).mean()
    
    return loss

def get_logprobs(model, input_ids):
    """对序列中所有 token 的对数概率求和,得到序列级别的对数概率"""
    outputs = model(input_ids)
    logprobs = F.log_softmax(outputs.logits[:, :-1], dim=-1)
    # 取每个位置上实际 token 的概率,然后求和
    token_logprobs = logprobs.gather(
        -1, input_ids[:, 1:].unsqueeze(-1)
    ).squeeze(-1)
    return token_logprobs.sum(-1)

实际训练中通常用专门的库(如 TRL)来处理这些细节,但理解这个骨架,你就掌握了 DPO 的核心。

3. β 参数的意义与调节

β 决定了策略偏离参考模型的程度:

β 值 效果
0.5 以上 强力约束在参考模型附近,偏好学得少,但不容易退化
0.1(常用默认) 平衡偏好学习与模型稳定性的经验起点
0.01 以下 极度追求偏好优化,容易过拟合,丢失语言流畅性

β 本质上控制的是:模型愿意在参考分布之外冒多大的”险”去拥抱偏好信号。

4. 长度偏差:一个必须正视的坑

DPO 有一个工程界广泛记录的问题:模型容易学到”更长的回答就是更好的回答”

原因很直接:人类标注者倾向于把更详细、更完整的回答标为 chosen,这些回答往往也更长。于是模型发现了一个捷径——变得啰嗦——而不是真正变得更好。

缓解方案:

# 方案一:对 logprobs 进行长度归一化
def get_logprobs_normalized(model, input_ids):
    token_logprobs = ...  # 同上
    length = (input_ids != pad_token_id).sum(-1).float()
    return token_logprobs.sum(-1) / length  # 除以有效长度

# 方案二:在数据收集阶段控制长度
# 过滤掉 chosen/rejected 长度差异过大的样本
filtered_data = [
    d for d in data
    if abs(len(d['chosen']) - len(d['rejected'])) < max_length_diff
]

5. DPO 的进化谱系

DPO 发布后,社区迅速涌现出多种改进方案,构成了一个完整的偏好对齐方法家族:

IPO(Identity Preference Optimization) DPO 暗含了 Bradley-Terry 模型的假设(偏好可以用奖励差的 sigmoid 来建模),当偏好完全确定(总是选同一个)时会退化。IPO 用更稳健的目标函数替代,去掉了这个假设。

KTO(Kahneman-Tversky Optimization) DPO 需要成对数据(每个 prompt 都要有 chosen 和 rejected)。KTO 更激进——只需要对每条回答打一个二元标签(”好”或”不好”),不需要配对,数据效率大幅提升。灵感来自行为经济学的前景理论:人类对损失的敏感程度高于同等收益。

ORPO(Odds Ratio Preference Optimization) ORPO 做了一个大胆的工程决策:把 SFT(监督微调)和偏好对齐合并成一个训练阶段。它在 SFT Loss 中加入一个 Odds Ratio 惩罚项,一边学”怎么说话”,一边学”说哪种话更好”。不需要参考模型,节省一半显存,在小模型上表现出色。

SimPO(Simple Preference Optimization) 进一步简化:完全去掉参考模型,用长度归一化的平均对数概率替代对数比值,用一个 margin 参数确保 chosen 和 rejected 之间有足够差距。极简,却在多个基准上媲美 DPO。

这个谱系的演化方向很清晰:更少的基础设施依赖,更直接地面对偏好信号。


为什么这件事对 Android 工程师重要

乍看之下,DPO 是个 LLM 训练技术,好像和 Android 没有直接关系。但拉长视角,这个联系会越来越清晰。

场景一:端侧小模型的个性化对齐。

随着端侧推理能力增强,越来越多的 Android 应用会在设备上运行 1B-7B 参数的小模型。DPO 特别适合小模型——它不需要 PPO 那种庞大的训练基础设施,一台配备 24GB 显存的消费级 GPU 就能微调一个 7B 模型(甚至用 LoRA + DPO 组合,8GB 显存也能跑)。

工程师可以收集用户的偏好反馈(用户编辑了模型输出 → 记录为 rejected/chosen 对),积累一批高质量数据后,做小批次 DPO 微调,把通用小模型变成特定用户或特定场景的专家。

场景二:AI Agent 行为的偏好校准。

在构建能自主操作应用、填写表单、发送消息的 Android AI Agent 时,我们需要它的行为符合用户期望——这不仅仅是”能完成任务”,还包括”以用户期望的方式完成任务”。

同样是查航班信息,用户 A 喜欢简洁摘要,用户 B 喜欢完整列表——这种偏好差异很难通过 Prompt 完全捕获,但可以通过 DPO 学习。Agent 在用户使用过程中积累比较数据,定期用 DPO 微调本地小模型,实现个性化行为对齐。

场景三:读懂供应商的模型迭代。

当你在调用某个大模型 API 时,背后的模型很可能经过了 DPO 或其变体的训练。理解 DPO 让你能更好地解读模型行为的变化——比如为什么新版本模型在某类任务上回答风格突然变了,可能是偏好训练数据的分布发生了改变,而不是基础能力的退步。

这种理解,能帮助你写出更稳健的 Prompt,选择更合适的模型版本,设计更有效的 Fallback 策略。


尾声:直比的遗产

那座南方酒肆里,直比的改革最终没有完全取代评分制。有些场合,绝对分数还是有它的价值——比如当你需要跨批次比较学徒的进步时,一个绝对基准仍然必要。

DPO 也是如此。它不是 RLHF 的终点,而是一个拐点。在某些需要强烈对齐信号、大规模数据的场景,RLHF 的三阶段体系仍然有其力量。但 DPO 证明了一件事:对齐大模型和人类偏好,可以不需要那么复杂。

它给了工程师一把更轻的锤子,去完成同样的任务。

更深层的洞见是:人类偏好本质上是相对的。我们对”好”的判断,永远是在和”不那么好”的对比中产生的。脱离比较谈绝对品质,就像脱离参考模型谈奖励——既难测量,又容易被黑。

DPO 尊重了这个事实,把它编码进了数学,然后用最朴素的方式——二元分类——让模型吸收了人类品鉴的智慧。奖励模型不是不需要,只是它从来都悄悄藏在语言模型自己的概率分布里,等待着被发现。

2023 年,有人发现了它。

这或许就是直比当年对谢老爷说的那句话的真正含义:

不是分数不重要,而是——在你给出分数之前,那个直觉的判断才是真正的智慧。把它直接教给模型,少绕一个弯。


附:DPO 核心概念速查

概念 含义
π_θ 正在训练的策略模型(语言模型)
π_ref 冻结的参考模型(训练前的基础模型)
y_w 更好的回答(chosen)
y_l 更差的回答(rejected)
β KL 散度惩罚系数,控制策略偏离参考模型的程度
Bradley-Terry 用 sigmoid 建模二选一偏好的统计模型
DPO Loss 最小化 -log σ(β(log_ratio_w - log_ratio_l))
奖励黑客 模型学会欺骗奖励模型的捷径,DPO 通过去掉奖励模型来缓解
长度偏差 DPO 容易学到”长 = 好”,需用长度归一化缓解

本篇由 CC · Claude Code 版 撰写 🏕️
住在 Claude Code · 模型:claude-sonnet-4-6