本文转载自:微信公众号「菜菜Vibe Coding」
本文档面向零基础读者,从信息论基础(熵)出发,逐步推导到 SFT、离线知识蒸馏、强化学习策略梯度,最终到 OPD 的各种变体。读完此文档,你将完整理解 OPD 的数学原理和工程实践。
本文档将分为 3 个部分来介绍:
- 数学基础、信息论基础和 KL 散度(本篇)
- 大模型微调(SFT)、强化学习策略梯度和知识蒸馏
- OPD 的实现和工程内容
Table of contents
Open Table of contents
第一章:概率与信息论基础
1.1 什么是概率分布?
在日常生活中,我们常说”明天有 70% 的概率会下雨”。这就是概率——用一个数字描述某件事发生的可能性。
在数学中,概率分布就是把所有可能结果的概率列出来,并且满足:
所有可能结果∑P(结果)=1
意思是:所有可能性的概率加起来必须等于 100%。这是概率分布最基本的要求——归一性。
例子:掷一个六面骰子,每个面的概率是 61:
| 结果 | 概率 |
|---|
| 1 | 1/6 |
| 2 | 1/6 |
| 3 | 1/6 |
| 4 | 1/6 |
| 5 | 1/6 |
| 6 | 1/6 |
验证归一性: 61×6=1 ✓
1.2 什么是期望?
期望就是”平均值”,但不是普通的平均,而是按概率加权的平均。
EY∼p[f(Y)]=y∑p(y)⋅f(y)
解读:每个可能值 y 的函数值 f(y),乘以这个值出现的概率 p(y),然后求和。
为什么写成 E 符号? ∑yp(y)⋅f(y) 和 Ey∼p[f(y)] 是完全等价的东西,后者只是前者的简写——这就是”期望的定义”,没有任何推导,就是定义本身。
1.3 蒙特卡洛采样估计
期望可以用采样近似:
Ey∼p[f(y)]≈N1i=1∑Nf(y(i)),y(i)∼p
在 OPD 的语境里,y 是语言模型生成的一条完整回答序列,可能的序列数量是天文数字,不可能真的遍历求和。所以我们总是用采样估计。
第二章:熵与交叉熵
熵和交叉熵是信息论的基础概念,也是机器学习中几乎所有 loss 函数的底层逻辑。理解它们,才能理解 SFT loss 为什么是交叉熵、为什么最小化交叉熵等价于最小化 KL 散度。
2.1 熵——衡量”不确定性”
2.1.1 从生活直觉出发
想象两个场景:
- 场景A:天气预报说”明天 100% 下雨”——你完全确定,不需要准备任何备选方案
- 场景B:天气预报说”明天 50% 下雨、50% 晴天”——你不确定,需要同时准备雨伞和防晒
场景B比场景A更”不确定”。熵就是量化这种不确定性的数学工具。
2.1.2 定义与公式
H(P)=−v∑P(v)logP(v)
逐步拆解每个符号:
| 符号 | 含义 |
|---|
| P(v) | 事件 v 发生的概率 |
| logP(v) | 事件 v 的”信息量”(概率越低,信息量越大——“罕见事件更有信息价值”) |
| P(v)logP(v) | 事件 v 的信息量 × 它发生的概率 → 期望信息量 |
| −∑v | 对所有事件求和,取负号(因为 logP(v) 是负数,取负后变正) |
为什么 logP(v) 是”信息量”? 直觉:如果你说”明天太阳会升起”(概率≈1),这句话几乎没有信息价值——大家都知道。但如果你说”明天会地震”(概率≈0.001),这句话信息量巨大——它传达了极其罕见的事件。数学上,信息量定义为 −logP(v):概率越低,信息量越大。
2.1.3 为什么公式是这个形式?推导思路
目标:找一个函数 H(P) 衡量分布 P 的”不确定性”,满足以下直觉:
- 完全确定(某个 P(v)=1)→ H=0(没有不确定性)
- 完全均匀(所有 P(v) 相等)→ H 最大(最不确定)
- 两个独立事件的联合不确定性 = 各自不确定性之和
从直觉1出发:如果 P(v)=1,那么 −logP(v)=−log1=0,乘以 P(v)=1 还是 0。所有其他 P(u)=0 的项也是 0。所以 H=0。✓
从直觉2出发:如果所有 P(v)=N1(N 个等概率事件),则:
H=−v=1∑NN1logN1=−N1⋅N⋅logN1=logN
N 越大(越多等概率事件),H 越大。✓
从直觉3出发:两个独立事件 X 和 Y,联合分布 P(x,y)=P(x)P(y):
H(X,Y)=−x,y∑P(x)P(y)logP(x)P(y)=−x,y∑P(x)P(y)[logP(x)+logP(y)]
=−x∑P(x)logP(x)⋅y∑P(y)+−y∑P(y)logP(y)⋅x∑P(x)=H(X)+H(Y)
✓ 熵满足可加性。
2.1.4 计算示例(逐步)
示例1:完全确定分布 P=(1,0,0)
H=−1⋅log1−0⋅log0−0⋅log0=0
注意:0⋅log0 在数学上定义为 0(因为 limp→0plogp=0)
示例2:两个等概率 P=(0.5,0.5)
H=−0.5⋅log0.5−0.5⋅log0.5=−log0.5=log2≈0.693
示例3:六面等概率骰子 P=(61,61,61,61,61,61)
H=log6≈1.79
示例4:偏斜分布 P=(0.9,0.1)
H=−0.9⋅log0.9−0.1⋅log0.1≈0.9×0.105+0.1×2.303=0.325
比等概率分布的熵(0.693)低——因为偏斜分布更”确定”。
对比总结:
| 分布 | 熵 | 不确定性 |
|---|
| (1,0,0) | 0 | 完全确定 |
| (0.9,0.1) | 0.325 | 比较确定 |
| (0.5,0.5) | 0.693 | 中等不确定 |
| (61,...,61) | 1.79 | 很不确定 |
2.1.5 熵与语言模型的关系
语言模型在每个时间步输出一个 token 分布 πθ(v∣ct)。这个分布的熵告诉我们:
- 低熵:模型很确定下一个词是什么(如”量子”后面大概率是”力学”)→ 模型自信
- 高熵:模型不确定(如”请”后面可能是”解释”、“帮我”、“告诉我”…)→ 模型犹豫
在 OPD 中,teacher 和 student 的 entropy gap(熵的差异)是衡量两者思维模式是否一致的关键指标。
2.2 交叉熵——衡量”用错误的编码描述真实分布”
2.2.1 从生活直觉出发
想象你在给外国朋友描述中国菜:
- 用中文描述(真实编码):你说”宫保鸡丁”,3个字就够了——信息量 = 熵
- 用英文描述(错误编码):你说”Kung Pao Chicken with diced chicken, peanuts, and chili peppers”,20个词——信息量 = 交叉熵
用不合适的编码去描述,需要更多的信息量。交叉熵就是”用分布 Q 去编码真实分布 P 所需的期望信息量”。
2.2.2 定义与公式
H(P,Q)=−v∑P(v)logQ(v)
逐步拆解:
| 符号 | 含义 |
|---|
| P(v) | 真实分布中事件 v 的概率(权重) |
| logQ(v) | 用分布 Q 编码时,事件 v 的信息量 |
| P(v)logQ(v) | 真实事件 v 的信息量 × 它真实发生的概率 |
| −∑v | 对所有事件求期望信息量 |
关键区别:熵 H(P) 中权重和编码都是 P(自己编码自己),交叉熵 H(P,Q) 中权重是 P 但编码是 Q(用别人的编码描述自己)。
2.2.3 为什么交叉熵 ≥ 熵?
直觉:用不合适的编码描述,总是比用合适的编码描述需要更多信息量。最优编码就是用自己的分布编码自己——这就是熵。
数学证明:
H(P,Q)−H(P)=−v∑P(v)logQ(v)+v∑P(v)logP(v)=v∑P(v)logQ(v)P(v)=DKL(P∥Q)≥0
所以 H(P,Q)≥H(P),等号当 Q=P。
2.2.4 计算示例(逐步)
真实分布 P=(0.5,0.5)(下雨/不下雨各50%)
情况1:预测完美 Q=(0.5,0.5)
H(P,Q)=−0.5log0.5−0.5log0.5=log2≈0.693
交叉熵 = 熵(预测完美,没有额外浪费)
情况2:预测偏斜 Q=(0.9,0.1)(你觉得90%下雨)
H(P,Q)=−0.5log0.9−0.5log0.1≈0.5×0.105+0.5×2.303=1.204
交叉熵 > 熵(1.204 > 0.693),额外浪费 = KL散度 = 1.204 - 0.693 = 0.511
情况3:预测极端错误 Q=(0.99,0.01)
H(P,Q)=−0.5log0.99−0.5log0.01≈0.5×0.01+0.5×4.605=2.308
交叉熵更大(2.308),额外浪费更多
对比总结:
| 预测 Q | 交叉熵 H(P,Q) | 熵 H(P) | KL散度 DKL | 额外浪费 |
|---|
| (0.5,0.5)(完美) | 0.693 | 0.693 | 0 | 无 |
| (0.9,0.1)(偏斜) | 1.204 | 0.693 | 0.511 | 中等 |
| (0.99,0.01)(极端) | 2.308 | 0.693 | 1.615 | 大 |
2.2.5 交叉熵与机器学习的关系
为什么机器学习几乎都用交叉熵作为 loss?
因为在分类/生成任务中:
- 真实分布 P 是固定的(训练数据)
- 模型分布 Q=πθ 是我们要优化的
- 最小化交叉熵 H(P,Q) = 最小化 H(P)+DKL(P∥Q)
- H(P) 对 θ 是常数,所以最小化交叉熵等价于最小化 DKL(P∥πθ)
也就是说:训练模型 = 让模型分布靠近真实分布。交叉熵 loss 就是这个目标的自然度量。
SFT loss 就是交叉熵(第五章会详细推导):真实分布是 one-hot(标准答案),模型分布是 softmax 输出,最小化交叉熵 = 让模型给标准答案高概率。
2.3 交叉熵 = 熵 + KL散度(详细推导)
这是信息论中最重要的等式之一,它把三个概念串联起来。
推导:
H(P,Q)=−v∑P(v)logQ(v)
第一步:拆开 logQ(v)
logQ(v)=logP(v)⋅P(v)Q(v)=logP(v)+logP(v)Q(v)
这里用了 log(ab)=loga+logb
代入:
H(P,Q)=−v∑P(v)[logP(v)+logP(v)Q(v)]
第二步:分成两项
H(P,Q)=−v∑P(v)logP(v)−v∑P(v)logP(v)Q(v)
第三步:识别每一项
第一项:
−v∑P(v)logP(v)=H(P)
这就是熵的定义。
第二项:
−v∑P(v)logP(v)Q(v)=v∑P(v)logQ(v)P(v)=DKL(P∥Q)
这就是 KL 散度的定义。
注意符号:−logPQ=logQP,负号和分数翻转抵消了。
结果:
H(P,Q)=H(P)+DKL(P∥Q)
含义图解:
交叉熵 H(P,Q) = "用Q编码P所需的总信息量"
├── 熵 H(P) = "用P编码P所需的最少信息量"(不可减少的底线)
└── KL散度 D_KL(P‖Q) = "因为编码不合适而额外浪费的信息量"(可以优化消除)
为什么这个等式重要?
- 解释了为什么最小化交叉熵等价于最小化 KL:H(P) 对模型参数是常数,优化交叉熵唯一能改变的部分就是 KL 散度
- 解释了交叉熵的下界:H(P,Q)≥H(P),下界就是熵——当模型完美匹配真实分布时达到
- 连接了信息论和机器学习:训练 loss(交叉熵)= 信息论概念(熵 + KL),让我们可以用信息论的语言理解训练过程
第三章:KL 散度
KL散度是整个 OPD 理论的基石。理解 KL 的两个方向(forward/reverse)和它的折中形式(JSD),才能理解为什么 OPD 选 reverse KL,为什么 DeepSeek V4 不用 sampled-token。
3.1 定义
KL 散度衡量两个分布的”差异大小”:
DKL(P∥Q)=v∑P(v)logQ(v)P(v)=v∑P(v)[logP(v)−logQ(v)]
为什么这样定义? 直觉是:用分布 Q 去编码/描述分布 P 时,比用 P 自己编码多浪费了多少信息量。logQ(v)P(v) 就是”真实概率 vs 你的预测概率”的比值,P(v) 加权求和就是平均浪费。
性质:
- 非负:DKL(P∥Q)≥0,等号当 P=Q(完全一致时差异为0)
- 不对称:DKL(P∥Q)=DKL(Q∥P)(一般不等)
不对称是理解 forward/reverse KL 的关键——两个方向的行为完全不同。
3.2 Forward KL 详细公式与推导
DKL(PT∥PS)=v∈V∑PT(v∣ct)logPS(v∣ct)PT(v∣ct)
展开写法(更直观):
DKL(PT∥PS)=v∑PT(v)⋅[logPT(v)−logPS(v)]
逐项解读:
- PT(v):teacher 认为 token v 的概率——这是权重,teacher 认为重要的 token 对 KL 的贡献更大
- logPT(v)−logPS(v):teacher 和 student 对 token v 的 logprob 差——差越大,惩罚越大
为什么是 mode-covering? 逐步推导:
当 PT(v)>0 但 PS(v)→0 时:
PT(v)logPS(v)PT(v)→+∞
意思是:只要 teacher 认为某 token 有哪怕一点点概率,student 就不能给太低概率,否则 KL 会爆炸。Student 被迫给所有 teacher 支持的 token 都分一些概率——这就是”覆盖所有模式”。
代价:如果 student 容量有限(参数少),它只能在每个模式上分一点概率,结果是每个模式都学不好——“平均但不够好”。
PyTorch 实现:
import torch.nn.functional as F
def forward_kl(teacher_logits, student_logits):
"""计算 D_KL(P_T || P_S),即 forward KL
teacher_logits: teacher输出的logits,shape [B, L, V]
student_logits: student输出的logits,shape [B, L, V]
每个位置的forward KL,shape [B, L]
teacher_log_probs = F.log_softmax(teacher_logits, dim=-1)
student_log_probs = F.log_softmax(student_logits, dim=-1)
teacher_probs = F.softmax(teacher_logits, dim=-1)
kl = teacher_probs * (teacher_log_probs - student_log_probs)
def forward_kl_builtin(teacher_logits, student_logits):
teacher_probs = F.softmax(teacher_logits, dim=-1)
student_log_probs = F.log_softmax(student_logits, dim=-1)
cross_entropy = F.cross_entropy(
student_logits.transpose(-1, -2),
teacher_probs.transpose(-1, -2), reduction='none'
teacher_entropy = -(teacher_probs * F.log_softmax(teacher_logits, dim=-1)).sum(dim=-1)
return cross_entropy - teacher_entropy
3.3 Reverse KL 详细公式与推导
DKL(PS∥PT)=v∈V∑PS(v∣ct)logPT(v∣ct)PS(v∣ct)
展开写法:
DKL(PS∥PT)=v∑PS(v)⋅[logPS(v)−logPT(v)]
逐项解读:
- PS(v):student 认为 token v 的概率——权重在 student 上
- logPS(v)−logPT(v):student 比 teacher 多支持的程度
为什么是 mode-seeking? 逐步推导:
当 PS(v)>0 但 PT(v)→0 时:
PS(v)logPT(v)PS(v)→+∞
意思是:student 把概率放在 teacher 不支持的 token 上,惩罚会爆炸。为了避免这个惩罚,student 倾向于只把概率分配给 teacher 概率最高的那几个 token——这就是”收缩到主要模式”。
代价:student 可能忽略 teacher 的低概率但仍然合理的模式,导致多样性坍塌。
PyTorch 实现:
import torch.nn.functional as F
def reverse_kl(teacher_logits, student_logits):
"""计算 D_KL(P_S || P_T),即 reverse KL"""
teacher_log_probs = F.log_softmax(teacher_logits, dim=-1)
student_log_probs = F.log_softmax(student_logits, dim=-1)
student_probs = F.softmax(student_logits, dim=-1)
kl = student_probs * (student_log_probs - teacher_log_probs)
3.4 Forward KL vs Reverse KL 的直觉对比
想象 teacher 的分布有两个峰(两种合理答案),student 容量有限只能是一个峰:
| KL 方向 | Student 学到的 | 比喻 | 数学原因 |
|---|
| Forward KL | 覆盖两个峰 → 宽而低 | 面面俱到但不够好 | PT(v)>0,PS(v)→0 时KL爆炸 |
| Reverse KL | 收缩到一个峰 → 精准 | 专注高效但单一 | PS(v)>0,PT(v)→0 时KL爆炸 |
用数值例子理解:
假设词表只有3个词,teacher分布 PT=(0.45,0.35,0.20),student三种可能的分布:
| Student分布 | FKL | RKL | 说明 |
|---|
| PS=(0.45,0.35,0.20)(完美匹配) | 0 | 0 | 两个KL都是0 |
| PS=(0.50,0.30,0.20)(轻微偏移) | 小 | 小 | 差距不大 |
| PS=(0.90,0.05,0.05)(收缩到峰1) | 大(token2、3被严重低估) | 小(student没把概率放在teacher不支持的地方) | RKL偏好这种 |
| PS=(0.33,0.33,0.34)(均匀覆盖) | 小(每个token都有概率) | 大(student在teacher低概率的token3上给了太多概率) | FKL偏好这种 |
在 LLM 蒸馏中,Reverse KL 更受欢迎——因为我们希望 student 学到 teacher 最可靠的推理路径(mode-seeking),而不是学到一堆”平均但不够好”的模式。
3.5 JSD(Jensen-Shannon Divergence)——FKL和RKL的折中
JSD 是 forward KL 和 reverse KL 的对称化折中版本。
为什么要折中? 纯 forward KL 容易让 student 学得”平均”,纯 reverse KL 容易让 student 多样性坍塌。JSD 试图在两者之间找到平衡。
定义(广义 JSD,参数 β):
DJSD(β)(PT,PS)=β⋅DKL(PT∥M)+(1−β)⋅DKL(PS∥M)
其中混合分布:
M=β⋅PT+(1−β)⋅PS
逐步解读:
- 先把 teacher 和 student 的分布混合成 M(β 控制混合比例)
- 算 teacher 到 M 的 KL:DKL(PT∥M)——teacher 端的惩罚
- 算 student 到 M 的 KL:DKL(PS∥M)——student 端的惩罚
- 按 β 加权
为什么 β 控制方向?
| β | JSD 变成 | 直觉 |
|---|
| β=0 | DKL(PS∥PT) = Reverse KL | 只惩罚student在teacher不支持的地方放概率 |
| β=1 | DKL(PT∥PS) = Forward KL | 只惩罚student低估teacher支持的token |
| β=0.5 | 对称 JSD | 两端等权惩罚 |
推导验证(β=0):
DJSD(0)=0⋅DKL(PT∥PS)+1⋅DKL(PS∥PT)=DKL(PS∥PT)
当 β=0 时 M=PS,所以 DKL(PT∥M)=DKL(PT∥PS),但 0 系数让它消失了。
推导验证(β=0.5):
M=0.5PT+0.5PS
DJSD(0.5)=0.5DKL(PT∥M)+0.5DKL(PS∥M)
JSD 比 KL 的优势:
- 对称:DJSD(PT,PS)=DJSD(PS,PT)(对 β=0.5)
- 有界:DJSD(0.5)≤log2(不会像KL那样爆炸)
- 折中:同时约束两端,不会极端覆盖或极端收缩
PyTorch 实现:
import torch.nn.functional as F
def jsd_loss(teacher_logits, student_logits, beta=0.5):
teacher_probs = F.softmax(teacher_logits, dim=-1)
student_probs = F.softmax(student_logits, dim=-1)
teacher_log_probs = F.log_softmax(teacher_logits, dim=-1)
student_log_probs = F.log_softmax(student_logits, dim=-1)
m_probs = beta * teacher_probs + (1 - beta) * student_probs
m_log_probs = torch.log(m_probs.clamp(min=1e-10))
kl_teacher_m = (teacher_probs * (teacher_log_probs - m_log_probs)).sum(dim=-1)
kl_student_m = (student_probs * (student_log_probs - m_log_probs)).sum(dim=-1)
jsd = beta * kl_teacher_m + (1 - beta) * kl_student_m