交叉熵
DKL(P∥Q)+H(P)=−∑yP(y)logQ(y) = Ey∼P[−logQ(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)logP(y)Q(y)=∑yP(y)logP(y)−∑yP(y)logQ(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)logP(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⋅log1Q1+0=−logQ1D_{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)=−logQ1H(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.6log0.6Q1+0.4log0.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.6logQ1−0.4logQ2 H(P,Q) = -0.6\log Q_1 -0.4\log Q_2 H(P,Q)=−0.6logQ1−0.4logQ2
两者相差一个常数 H(P)=−0.6log0.6−0.4log0.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.2log π(y2) = 0.3log π(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)原因如下:
- DPO 损失不是直接等于 KL 散度
KL 散度是整个 RLHF 优化目标的一部分,而 DPO 损失是那个优化目标在 偏好数据 下的等价替代形式。这个替代形式巧妙地避开了对全分布求和,只需要配对样本的比值。
- 为什么可以省略 πθ(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) 作为权重。
这里的"权重"概念只在全分布求和时才出现。对于单个样本,没有求和,自然就没有加权。
- 一个帮助理解的类比
假设你想最小化 DKL(P∥Q)D_{KL}(P \| Q)DKL(P∥Q),但你只有一对样本 (xw,xl)(x_w, x_l)(xw,xl) 告诉你"xwx_wxw 比 xlx_lxl 更真实"。你可以设计一个损失函数:
L=−logσ(logQ(xw)P(xw)−logQ(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 的高效之处:避免了在每个更新步骤中计算整个分布上的期望。