DPO笔记

交叉熵

DKL(P∥Q)+H(P)=−∑yP(y)log⁡Q(y)  =  Ey∼P[−log⁡Q(y)]=H(P,Q) D_{KL}(P\|Q) + H(P) = -\sum_y P(y) \log Q(y) \;=\; \mathbb{E}_{y\sim P}\big[-\log Q(y)\big] =H(P,Q) DKL(P∥Q)+H(P)=−y∑P(y)logQ(y)=Ey∼P[−logQ(y)]=H(P,Q)

KL 散度

DKL(P∥Q)=∑yP(y)log⁡P(y)Q(y)=∑yP(y)log⁡P(y)−∑yP(y)log⁡Q(y)=−H(P)+H(P,Q) D_{KL}(P\|Q) = \sum_y P(y)\log\frac{P(y)}{Q(y)} = \sum_y P(y)\log P(y) - \sum_y P(y)\log Q(y) = -H(P) + H(P,Q) DKL(P∥Q)=y∑P(y)logQ(y)P(y)=y∑P(y)logP(y)−y∑P(y)logQ(y)=−H(P)+H(P,Q)
其中
H(P)=−∑yP(y)log⁡P(y) H(P) = -\sum_y P(y)\log P(y) H(P)=−y∑P(y)logP(y)

在监督学习中,我们通常写 DKL(Pdata∥Pmodel)D_{KL}(P_{\text{data}} \| P_{\text{model}})DKL(Pdata∥Pmodel),这时 PdataP_{\text{data}}Pdata 是训练数据的真实分布(固定),而 PmodelP_{\text{model}}Pmodel 是模型近似分布(优化)。所以第一个位置是真实分布

但在 RLHF 和 DPO 的场景里:

DKL(πθ∥πref) D_{KL}(\pi_\theta \| \pi_{\text{ref}}) DKL(πθ∥πref)

  • πθ\pi_\thetaπθ:当前正在优化的策略(就是我们想让 AI 学会的策略)。它并不是"真实"分布,而是我们要调整的。
  • πref\pi_{\text{ref}}πref:固定的参考策略(比如 SFT 后的初始模型)。它作为基准,相当于一个"锚点"。

这里的顺序是反过来的 :优化变量在第一个位置,固定基准在第二个位置。这和分类任务中"真实分布固定,模型分布在后"的习惯正好相反。原因在于 KL 散度是用来惩罚策略偏离参考模型 的:当 πθ\pi_\thetaπθ 离开 πref\pi_{\text{ref}}πref 太远时,KL 值变大,从而给优化目标增加惩罚。如果把参数顺序反过来写成 DKL(πref∥πθ)D_{KL}(\pi_{\text{ref}} \| \pi_\theta)DKL(πref∥πθ),它的行为会完全不同(它会惩罚 πref\pi_{\text{ref}}πref 远离 πθ\pi_\thetaπθ,这没有意义,因为 πref\pi_{\text{ref}}πref 是固定的)。

总结:在 DKL(P∥Q)D_{KL}(P \| Q)DKL(P∥Q) 中,第一个参数 PPP 不一定是"真实分布",而是"你想测量其与 QQQ 差异的分布"。在 RLHF 里,第一个是 正在优化的策略 ,第二个是 固定的参考策略


FAQ

为什么 DPO 必须用 KL 散度,而不能直接用交叉熵?

在 RLHF 中,优化目标是:
max⁡πθE[r]−βDKL(πθ∥πref) \max_{\pi_\theta} \mathbb{E}[r] - \beta D_{KL}(\pi_\theta \| \pi_{\text{ref}}) πθmaxE[r]−βDKL(πθ∥πref)

  • 这里 DKL(πθ∥πref)D_{KL}(\pi_\theta \| \pi_{\text{ref}})DKL(πθ∥πref) 的 第一个位置是 πθ\pi_\thetaπθ (正在优化的策略),第二个位置是固定的 πref\pi_{\text{ref}}πref。

如果直接用交叉熵 H(πθ,πref)H(\pi_\theta, \pi_{\text{ref}})H(πθ,πref) 替换 KL 散度会发生什么?

因为:
H(πθ,πref)=DKL(πθ∥πref)+H(πθ) H(\pi_\theta, \pi_{\text{ref}}) = D_{KL}(\pi_\theta \| \pi_{\text{ref}}) + H(\pi_\theta) H(πθ,πref)=DKL(πθ∥πref)+H(πθ)

所以替换后目标函数变成:
E[r]−βH(πθ,πref)=E[r]−βDKL(πθ∥πref)−βH(πθ) \mathbb{E}[r] - \beta H(\pi_\theta, \pi_{\text{ref}}) = \mathbb{E}[r] - \beta D_{KL}(\pi_\theta \| \pi_{\text{ref}}) - \beta H(\pi_\theta) E[r]−βH(πθ,πref)=E[r]−βDKL(πθ∥πref)−βH(πθ)

多出了一项 −βH(πθ)-\beta H(\pi_\theta)−βH(πθ),这会让优化过程同时最大化 πθ\pi_\thetaπθ 的熵(即让策略变得更均匀、更随机),这与我们"让策略不要偏离参考模型太远"的初衷不符。

因此,必须保留原始的 KL 散度形式,不能简化为交叉熵。

但在 DPO 中,我们面临的是 DKL(πθ∥πref)D_{KL}(\pi_\theta \| \pi_{\text{ref}})DKL(πθ∥πref),其中 πθ\pi_\thetaπθ 在第一位且会变化,此时 H(πθ)H(\pi_\theta)H(πθ) 不固定,因此不能偷换成交叉熵。


假设有一个简单分布:

  • PPP(真实标签)固定:P=[1,0]P=[1,0]P=[1,0](只有第一个类别)。

  • QQQ(模型预测)我们要优化。

  • KL 散度 DKL(P∥Q)=1⋅log⁡1Q1+0=−log⁡Q1D_{KL}(P\|Q) = 1\cdot\log\frac{1}{Q_1} + 0 = -\log Q_1DKL(P∥Q)=1⋅logQ11+0=−logQ1,最小化它等价于让 Q1→1Q_1 \to 1Q1→1。

  • 交叉熵 H(P,Q)=−log⁡Q1H(P,Q) = -\log Q_1H(P,Q)=−logQ1,在这个例子中数值恰好与 KL 散度相等 ,因为 H(P)=0H(P)=0H(P)=0。所以当 PPP 是 one‑hot 时,两者等价。

但若 PPP 不是 one‑hot(比如 P=[0.6,0.4]P=[0.6,0.4]P=[0.6,0.4]),则:
DKL(P∥Q)=0.6log⁡0.6Q1+0.4log⁡0.4Q2 D_{KL}(P\|Q) = 0.6\log\frac{0.6}{Q_1}+0.4\log\frac{0.4}{Q_2} DKL(P∥Q)=0.6logQ10.6+0.4logQ20.4
H(P,Q)=−0.6log⁡Q1−0.4log⁡Q2 H(P,Q) = -0.6\log Q_1 -0.4\log Q_2 H(P,Q)=−0.6logQ1−0.4logQ2

两者相差一个常数 H(P)=−0.6log⁡0.6−0.4log⁡0.4H(P)= -0.6\log0.6-0.4\log0.4H(P)=−0.6log0.6−0.4log0.4。此时最小化 H(P,Q)H(P,Q)H(P,Q) 等价于最小化 DKL(P∥Q)D_{KL}(P\|Q)DKL(P∥Q),因为 H(P)H(P)H(P) 固定。



DPO计算流程

对于一对偏好数据 (x,yw,yl)(x, y_w, y_l)(x,yw,yl),DPO 损失为:
LDPO=−log⁡σ(β(log⁡πθ(yw∣x)πref(yw∣x)−log⁡πθ(yl∣x)πref(yl∣x))) \mathcal{L}{\text{DPO}} = -\log \sigma\left( \beta \left( \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)} \right) \right) LDPO=−logσ(β(logπref(yw∣x)πθ(yw∣x)−logπref(yl∣x)πθ(yl∣x)))

其中:

  • πθ\pi_\thetaπθ:当前策略(被训练的模型)
  • πref\pi_{\text{ref}}πref:参考策略(固定)
  • β\betaβ:温度参数
  • σ\sigmaσ:sigmoid 函数

代码中的 chosen_logps 对应 log⁡πθ(yw∣x)\log\pi_\theta(y_w|x)logπθ(yw∣x),rejected_logps 对应 log⁡πθ(yl∣x)\log\pi_\theta(y_l|x)logπθ(yl∣x),ref_chosen_logps 对应 log⁡πref(yw∣x)\log\pi_{\text{ref}}(y_w|x)logπref(yw∣x),ref_rejected_logps 对应 log⁡πref(yl∣x)\log\pi_{\text{ref}}(y_l|x)logπref(yl∣x)。


计算log⁡π(y∣x)\log \pi(y \mid x)logπ(y∣x) logp_response

python 复制代码
# 拼接 prompt + response 获取 input_ids
full_input = prompt + response
encodings = tokenizer(full_input, ...)
input_ids = encodings["input_ids"] #  将full_input切分成 token 后,每个 token 在词表中对应的唯一数字 ID

# 获取 response 起始位置
prompt_ids = tokenizer(prompt, ...)["input_ids"]
response_start = prompt_ids.shape[-1]

# 模型前向传播
with torch.no_grad():
    outputs = model(**encodings)  # input_ids shape: (batch_size, seq_len, vocab_size)
    logits = outputs.logits

# 前向得到 logits,转为 log_probs
log_probs = F.log_softmax(logits, dim=-1) # \log\frac{\pi_(z|x)} ,z=prompr+token_predict
# shift 对齐:取预测 response 各 token 的 log 概率分布
response_logits = log_probs[:, response_start-1:-1, :] # \log\frac{\pi_(y|x)} ,y=token_predict
response_token_ids = input_ids[:, response_start:]
# gather 取出"真实" token 的 log 概率
response_logp = torch.gather(response_logits, 2, response_token_ids.unsqueeze(-1)).squeeze(-1)
# 平均(或求和)得到整个 response 的 log 概率
logp_response = response_logp.mean()

作用 :这是实现 log⁡π(y∣x)\log\pi(y|x)logπ(y∣x) 计算的核心,也是 DPO 所需的基础数值。

与 DPO 公式的关系logp_response 就是公式中的 log⁡π(y∣x)\log\pi(y|x)logπ(y∣x)(可能是均值而非总和,但差别不大)。

后续你通过 compute_logp 函数分别得到:

  • logps_chosen (actor 对 chosen)
  • logps_rejected (actor 对 rejected)
  • logps_ref_chosen (ref 对 chosen)
  • logps_ref_rejected (ref 对 rejected)

这四个值直接对应 DPO 公式中的四个对数概率。


一步步拆解这行代码:

python 复制代码
response_logp = torch.gather(response_logits, 2, response_token_ids.unsqueeze(-1)).squeeze(-1)

假设变量内容

response_logits 形状 [1, 3, 5](batch_size=1,response 长度=3,vocab_size=5)

数值示例(log 概率,随意编的):

复制代码
[ [ [0.1, 0.5, 0.2, 0.1, 0.1],    # 位置0(预测 response 第1个token)
    [0.3, 0.1, 0.4, 0.1, 0.1],    # 位置1(预测 response 第2个token)
    [0.2, 0.2, 0.1, 0.4, 0.1] ] ] # 位置2(预测 response 第3个token)

response_token_ids 形状 [1, 3],假设真实 token ID 为 [2, 0, 4](即第1个token是ID2,第2个是ID0,第3个是ID4)。

执行 response_token_ids.unsqueeze(-1)

  • unsqueeze(-1) 在最后一个维度加一维,形状从 [1, 3] 变为 [1, 3, 1]
  • 数值:[[[2], [0], [4]]]。这个序列的 ID 是按照 token 在句子中出现的顺序排列的,但 ID 的数值是词汇表里固定的编号,不一定连续。

这个形状与 response_logits 的前两个维度一致,第三个维度为1(索引值)。

执行 torch.gather(response_logits, 2, index)

  • gather 的规则:对于 dim=2(词汇表维度),在 response_logits 的每个 [batch, seq] 位置,用 index 中对应的值作为下标,取出该位置的元素。
  • 具体操作:
    • response_logits[0][0](第1个位置的5个值),index[0][0]=2,取出第2个值(0.2) → 得到 0.2
    • response_logits[0][1]index[0][1]=0,取出第0个值(0.3) → 0.3
    • response_logits[0][2]index[0][2]=4,取出第4个值(0.1) → 0.1
  • 输出形状:[1, 3, 1],数值为 [[[0.2], [0.3], [0.1]]]

执行 .squeeze(-1)

去掉最后一维(尺寸为1),得到形状 [1, 3],数值 [[0.2, 0.3, 0.1]]

这就是每个 token 的对数概率:

  • log π(y1) = 0.2
  • log π(y2) = 0.3
  • log π(y3) = 0.1

对应 DPO 公式中的位置

[0.2, 0.3, 0.1] 就是:
log⁡πθ(y1∣x),log⁡πθ(y2∣x,y1),log⁡πθ(y3∣x,y1,y2) \log \pi_\theta(y_1 \mid x), \quad \log \pi_\theta(y_2 \mid x, y_1), \quad \log \pi_\theta(y_3 \mid x, y_1, y_2) logπθ(y1∣x),logπθ(y2∣x,y1),logπθ(y3∣x,y1,y2)

后续会对它们求和(或平均)得到 log⁡πθ(y∣x)\log \pi_\theta(y \mid x)logπθ(y∣x),用于 DPO 损失中。


reward log⁡πθ(y∣x)πref(y∣x)\log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)}logπref(y∣x)πθ(y∣x) 计算方法

python 复制代码
chosen_rewards = self.beta * (chosen_logps - ref_chosen_logps).detach()
rejected_rewards = self.beta * (rejected_logps - ref_rejected_logps).detach()

解释

  • 根据 Bradley-Terry 模型,最优策略下的奖励函数可表示为
    r(x,y) = \\beta \\log \\frac{\\pi_\\theta(y\|x)}{\\pi_{\\text{ref}}(y\|x)} + \\text{const}
    这里 chosen_rewards 正是这个奖励值(忽略常数)。

DPO与KL散度的差异

对于一对偏好数据 (x,yw,yl)(x, y_w, y_l)(x,yw,yl),DPO 损失为:
LDPO=−log⁡σ(β(log⁡πθ(yw∣x)πref(yw∣x)−log⁡πθ(yl∣x)πref(yl∣x))) \mathcal{L}{\text{DPO}} = -\log \sigma\left( \beta \left( \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)} \right) \right) LDPO=−logσ(β(logπref(yw∣x)πθ(yw∣x)−logπref(yl∣x)πθ(yl∣x)))

DKL(πθ∥πref)=∑yπθ(y∣x)log⁡πθ(y∣x)πref(y∣x) D_{KL}(\pi_\theta \| \pi_{\text{ref}}) = \sum_y \pi_\theta(y|x)\log\frac{\pi_\theta(y|x)}{\pi_{ref}(y|x)} DKL(πθ∥πref)=y∑πθ(y∣x)logπref(y∣x)πθ(y∣x)

DPO 没有对 yyy 求和,也没有乘上 πθ(y∣x)\pi_\theta(y|x)πθ(y∣x)原因如下:


  1. DPO 损失不是直接等于 KL 散度

KL 散度是整个 RLHF 优化目标的一部分,而 DPO 损失是那个优化目标在 偏好数据 下的等价替代形式。这个替代形式巧妙地避开了对全分布求和,只需要配对样本的比值。


  1. 为什么可以省略 πθ(y∣x)\pi_\theta(y|x)πθ(y∣x)?

因为 DPO 只关心 偏好对 ,而不是所有可能的输出。在 Bradley--Terry 模型下,两个回答 yw,yly_w, y_lyw,yl 的相对优劣只取决于它们的奖励差值。奖励又被表示为 βlog⁡πθ(y∣x)πref(y∣x)\beta \log\frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)}βlogπref(y∣x)πθ(y∣x)(忽略常数)。因此,要最大化偏好概率,我们只需要最大化 这个差值 ,而差值中天然不包含 πθ(y∣x)\pi_\theta(y|x)πθ(y∣x) 作为权重。

这里的"权重"概念只在全分布求和时才出现。对于单个样本,没有求和,自然就没有加权。


  1. 一个帮助理解的类比

假设你想最小化 DKL(P∥Q)D_{KL}(P \| Q)DKL(P∥Q),但你只有一对样本 (xw,xl)(x_w, x_l)(xw,xl) 告诉你"xwx_wxw 比 xlx_lxl 更真实"。你可以设计一个损失函数:

L=−log⁡σ(log⁡Q(xw)P(xw)−log⁡Q(xl)P(xl)) \mathcal{L} = -\log \sigma\left( \log\frac{Q(x_w)}{P(x_w)} - \log\frac{Q(x_l)}{P(x_l)} \right) L=−logσ(logP(xw)Q(xw)−logP(xl)Q(xl))

这个损失不包含 P(x)P(x)P(x) 权重,但它仍然倾向于使 QQQ 靠近 PPP。DPO 做的正是类似的事情:用偏好对比来隐式地拉近 πθ\pi_\thetaπθ 和 πref\pi_{\text{ref}}πref,而不需要显式计算整个 KL 散度。

DPO 损失是从原始 KL 约束推导出的 pairwise 对比形式,它不需要对全分布求和,只依赖两个具体样本,自然不需要权重πθ(y∣x)\pi_\theta(y|x)πθ(y∣x)。这实际上是 DPO 的高效之处:避免了在每个更新步骤中计算整个分布上的期望。

相关推荐
老纪的技术唠嗑局2 小时前
深度解析 LLM Wiki / Obsidian-Wiki / GBrain:Agent 时代知识的“自组织”与“自进化”
大数据·数据库·人工智能·算法
EnCi Zheng4 小时前
01d-前馈神经网络代码实现 [特殊字符]
人工智能·深度学习·神经网络
YXXY3135 小时前
模拟算法的介绍
算法
happymaker06266 小时前
简单LRU的实现(基于LinkedHashMap)
算法·leetcode·lru
deephub6 小时前
为什么 MCP 在协议层会有 prompt injection的问题:工具描述如何劫持 agent 上下文
人工智能·深度学习·大语言模型·ai-agent·mcp
会编程的土豆6 小时前
【数据结构与算法】空间复杂度从入门到面试:不仅会算,还要会解释
数据结构·c++·算法·面试·职场和发展
普通网友6 小时前
《算法面试必刷:15 个高频 LeetCode 题(附代码)》
算法·leetcode·面试
_深海凉_6 小时前
LeetCode热题100-搜索二维矩阵
算法·leetcode·矩阵
张槊哲6 小时前
C++ 进阶指南:如何丝滑地理解与实践多线程与多进程
开发语言·c++·算法