系列回顾:上一篇我们讲透了信息熵和交叉熵——熵衡量不确定性,交叉熵衡量"用错分布的代价",最小化交叉熵就是让模型分布靠近真实分布。这一篇深入KL 散度:它是衡量两个概率分布"距离"的工具,交叉熵和它只差一个常数。更重要的是,KL 散度直接出现在 DeepSeek R1 的 GRPO 训练目标里——那个约束项到底在做什么,为什么要用 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,这篇文章全部讲清楚。
上一篇的结尾留了一个悬念:
$$H(P, Q) = H(P) + D_{KL}(P \| Q)$$
交叉熵 = 真实熵 + KL 散度。最小化交叉熵时,$H(P)$ 是常数,真正被最小化的是 $D_{KL}(P \| Q)$。
那 KL 散度到底是什么?为什么它能衡量两个分布的"差距"?
更具体的问题:DeepSeek R1 的 GRPO 损失函数里有一项:
$$\beta \cdot D_{KL}(\pi_\theta \| \pi_{ref})$$
这一项从哪里来?为什么是 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是 $D_{KL}(\pi_{ref} \| \pi_\theta)$?$\beta$ 控制什么?去掉这一项会怎样?
这些问题,是这篇文章要回答的。
KL 散度(Kullback-Leibler Divergence),也叫相对熵(Relative Entropy),衡量分布 $Q$ 相对于分布 $P$ 的"额外信息代价":
离散情形:
$$D_{KL}(P \| Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)} = \mathbb{E}_{x \sim P}\left[\log \frac{P(x)}{Q(x)}\right]$$
连续情形:
$$D_{KL}(P \| Q) = \int p(x) \log \frac{p(x)}{q(x)} \, dx$$
展开 KL 散度:
$$D_{KL}(P \| Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)}$$
$$= \sum_x P(x)[\log P(x) - \log Q(x)]$$
$$= \underbrace{-\sum_x P(x) \log Q(x)}_{H(P,Q)\text{,交叉熵}} - \underbrace{\left(-\sum_x P(x) \log P(x)\right)}_{H(P)\text{,熵}}$$
$$= H(P, Q) - H(P)$$
因此:$H(P, Q) = H(P) + D_{KL}(P \| Q)$
KL 散度 = 交叉熵 - 真实熵 = 用 $Q$ 替代 $P$ 编码数据,比用 $P$ 本身多付出的"额外代价"。
$D_{KL}(P \| Q) \geq 0$,等号当且仅当 $P = Q$(几乎处处相等)。
证明(用 Jensen 不等式):
$\log$ 是凹函数,Jensen 不等式给出 $\mathbb{E}[f(X)] \leq f(\mathbb{E}[X])$(对凹函数):
$$-D_{KL}(P \| Q) = \mathbb{E}_{x \sim P}\left[\log \frac{Q(x)}{P(x)}\right] \leq \log \mathbb{E}_{x \sim P}\left[\frac{Q(x)}{P(x)}\right] = \log \sum_x P(x) \frac{Q(x)}{P(x)} = \log 1 = 0$$
所以 $D_{KL}(P \| Q) \geq 0$。
等号成立的条件:Jensen 不等式取等号,要求 $\frac{Q(x)}{P(x)}$ 是常数,即 $Q = cP$。结合归一化条件 $\sum Q = \sum P = 1$,得 $c=1$,即 $P = Q$。
直觉:当 $P = Q$ 时,用正确的分布编码,没有任何额外代价,KL 散度为 0。一旦 $Q \neq P$,代价必然是正的。
数学上的"距离"(度量)需要满足三个条件: 1. $d(P, P) = 0$(自距离为零)✓ 2. $d(P, Q) = d(Q, P)$(对称性)✗ 3. $d(P, R) \leq d(P, Q) + d(Q, R)$(三角不等式)✗
KL 散度不满足对称性:$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。
所以 KL 散度不是严格意义上的距离,而是一种"散度"——衡量方向性的差异。
设两个简单分布:
$$P = [0.9, 0.1], \quad Q = [0.5, 0.5]$$
$$D_{KL}(P \| Q) = 0.9 \log\frac{0.9}{0.5} + 0.1 \log\frac{0.1}{0.5}$$ $$= 0.9 \times 0.588 + 0.1 \times (-1.609) = 0.529 - 0.161 = 0.368$$
$$D_{KL}(Q \| P) = 0.5 \log\frac{0.5}{0.9} + 0.5 \log\frac{0.5}{0.1}$$ $$= 0.5 \times (-0.588) + 0.5 \times 1.609 = -0.294 + 0.805 = 0.511$$
$D_{KL}(P \| Q) = 0.368 \neq 0.511 = D_{KL}(Q \| P)$,方向不同,结果不同。
$D_{KL}(P \| Q)$:期望用 $P$ 算(在 $P$ 有概率的地方都要考虑),对数比较 $P$ 和 $Q$。
"前向 KL" $D_{KL}(P \| Q)$($P$ 是真实,$Q$ 是近似):
在 $P(x) > 0$ 的地方,要求 $Q(x) > 0$,否则 $\log \frac{P(x)}{Q(x)} = +\infty$,KL 散度无穷大。
行为:$Q$ 必须覆盖 $P$ 的所有支撑(support)。如果 $P$ 在某处有概率而 $Q$ 没有,损失无穷大。这迫使 $Q$ 是"质量覆盖"(mass-covering)的——宁愿在不该有概率的地方也留一点概率,也不能漏掉 $P$ 有概率的地方。
"反向 KL" $D_{KL}(Q \| P)$($Q$ 是近似,$P$ 是真实):
在 $Q(x) > 0$ 的地方,要求 $P(x) > 0$,否则 $\log \frac{Q(x)}{P(x)} = +\infty$。
行为:$Q$ 只在 $P$ 有概率的地方才能有概率。这迫使 $Q$ 是"质量寻求"(mode-seeking)的——集中在 $P$ 的某个高概率区域,形成尖峰,而不是扩散覆盖所有区域。
假设 $P$ 是一个双峰分布(有两个高概率区域):
最小化前向 KL $D_{KL}(P \| Q)$($Q$ 近似 $P$):$Q$ 被迫覆盖两个峰,通常会在两峰之间形成一个"平均"的宽分布。
最小化反向 KL $D_{KL}(Q \| P)$($Q$ 近似 $P$):$Q$ 只需要集中在一个峰(任意一个高概率区域即可),形成尖峰模式。
这两种行为对应截然不同的近似策略,在不同的机器学习任务中各有用武之地。
DeepSeek R1 用 GRPO(Group Relative Policy Optimization)进行强化学习训练。完整目标函数:
$$\mathcal{J}_{GRPO}(\theta) = \mathbb{E}_{q \sim \mathcal{D}, \{o_i\}_{i=1}^G \sim \pi_{\theta_{old}}(\cdot|q)}\left[\frac{1}{G}\sum_{i=1}^G \min\left(r_i(\theta) A_i,\ \text{clip}(r_i(\theta), 1-\varepsilon, 1+\varepsilon) A_i\right)\right] - \beta \cdot D_{KL}(\pi_\theta \| \pi_{ref})$$
其中 $r_i(\theta) = \frac{\pi_\theta(o_i|q)}{\pi_{\theta_{old}}(o_i|q)}$ 是重要性采样比,$A_i$ 是归一化优势,$\pi_{ref}$ 是参考策略(SFT 后的模型),$\beta$ 是 KL 惩罚系数。
聚焦 KL 项:
$$-\beta \cdot D_{KL}(\pi_\theta \| \pi_{ref}) = -\beta \cdot \mathbb{E}_{o \sim \pi_\theta}\left[\log \frac{\pi_\theta(o|q)}{\pi_{ref}(o|q)}\right]$$
没有 KL 约束会怎样?
强化学习只优化奖励,模型会"走捷径"——找到奖励高但质量差的策略:
KL 约束的作用:惩罚策略偏离参考策略过远,把强化学习"锁定"在参考策略(SFT 模型)的附近,保留预训练和 SFT 阶段的能力,同时用奖励信号做针对性提升。
关键在于不对称性:
$D_{KL}(\pi_\theta \| \pi_{ref})$(反向 KL,$\pi_\theta$ 是近似,$\pi_{ref}$ 是参考):
期望在 $\pi_\theta$ 下算:$\mathbb{E}_{o \sim \pi_\theta}\left[\log \frac{\pi_\theta(o)}{\pi_{ref}(o)}\right]$
实践含义: 1. 计算可行:只需从当前策略 $\pi_\theta$ 采样就能估计,不需要从 $\pi_{ref}$ 采样 2. Mode-seeking:惩罚 $\pi_\theta$ 在 $\pi_{ref}$ 概率为零的地方分配概率(避免生成完全不像参考模型的输出) 3. 梯度信号局部:只对 $\pi_\theta$ 有概率的地方施加梯度,不强求覆盖 $\pi_{ref}$ 的所有模式
$D_{KL}(\pi_{ref} \| \pi_\theta)$(前向 KL,$\pi_{ref}$ 是真实,$\pi_\theta$ 是近似):
期望在 $\pi_{ref}$ 下算,计算上不可行:需要从 $\pi_{ref}$ 采样来估计梯度,但强化学习的核心是从当前策略 $\pi_\theta$ 采样。
结论:$D_{KL}(\pi_\theta \| \pi_{ref})$ 是强化学习场景下唯一计算可行的 KL 方向,同时其 mode-seeking 性质恰好满足需求——让 $\pi_\theta$ 的输出落在 $\pi_{ref}$ 的"合理区域"内。
$$\mathcal{J} = \underbrace{\text{奖励项}}_{\text{追求高奖励}} - \beta \underbrace{D_{KL}(\pi_\theta \| \pi_{ref})}_{\text{靠近参考策略}}$$
$\beta$ 是两个目标之间的权衡系数:
DeepSeek R1 的 $\beta$:论文中 $\beta = 0.04$(DAPO 变体中不同阶段有所调整)。
这个值不是随意选的,是通过消融实验确定的平衡点——足够小让模型学到推理能力,足够大防止奖励黑客和灾难性遗忘。
在实际实现中,$D_{KL}(\pi_\theta \| \pi_{ref})$ 对完整序列的计算可以用链式法则分解为逐 token 的 KL 之和:
$$D_{KL}(\pi_\theta(\cdot|q) \| \pi_{ref}(\cdot|q)) = \sum_{t=1}^T D_{KL}\left(\pi_\theta(\cdot|q, o_{ 每个位置 $t$,在给定前文的条件下,当前策略和参考策略的输出分布之差: $$D_{KL}^{(t)} = \sum_{k=1}^{|\mathcal{V}|} \pi_\theta(k|q, o_{ 在所有时间步上求和,就是序列级别的 KL 散度。 第四部分:KL 散度的两种等价形式 形式一:期望对数比 $$D_{KL}(P \| Q) = \mathbb{E}_{x \sim P}\left[\log \frac{P(x)}{Q(x)}\right] = \mathbb{E}_{x \sim P}[\log P(x) - \log Q(x)]$$ 梯度计算友好:对 $Q$ 求梯度时,$\mathbb{E}_{x \sim P}[-\nabla_Q \log Q(x)]$ 直接可用。 形式二:交叉熵减熵 $$D_{KL}(P \| Q) = H(P, Q) - H(P)$$ 理解友好:KL 散度 = 用近似分布替代真实分布多付出的编码代价。 在训练语言模型时,$P$ 是数据分布(固定),$H(P)$ 是常数,最小化交叉熵等价于最小化 KL 散度。 第五部分:高斯分布之间的 KL 散度(VAE 中的应用) 推导 两个高斯分布 $P = \mathcal{N}(\mu_1, \sigma_1^2)$ 和 $Q = \mathcal{N}(\mu_2, \sigma_2^2)$ 之间的 KL 散度有解析解: $$D_{KL}(P \| Q) = \log\frac{\sigma_2}{\sigma_1} + \frac{\sigma_1^2 + (\mu_1-\mu_2)^2}{2\sigma_2^2} - \frac{1}{2}$$ 特殊情形:$Q = \mathcal{N}(0, 1)$(标准正态),$P = \mathcal{N}(\mu, \sigma^2)$: $$D_{KL}(\mathcal{N}(\mu, \sigma^2) \| \mathcal{N}(0, 1)) = \frac{\mu^2 + \sigma^2 - 1 - \log\sigma^2}{2}$$ 这是VAE 训练目标中的正则化项的解析形式,可以直接对 $\mu$ 和 $\sigma$ 求梯度,无需采样估计。 手算验证 设 $P = \mathcal{N}(1, 2)$(均值 1,方差 2),$Q = \mathcal{N}(0, 1)$(标准正态): $$D_{KL}(P \| Q) = \frac{1^2 + 2 - 1 - \log 2}{2} = \frac{1 + 2 - 1 - 0.693}{2} = \frac{1.307}{2} \approx 0.654$$ 含义:用标准正态 $Q$ 来近似均值为 1、方差为 2 的分布 $P$,每个样本平均多付出 0.654 nats 的编码代价。 VAE 的训练目标 VAE 的证据下界(ELBO): $$\mathcal{L}_{ELBO} = \underbrace{\mathbb{E}_{z \sim q_\phi(z|x)}[\log p_\theta(x|z)]}_{\text{重建损失}} - \underbrace{D_{KL}(q_\phi(z|x) \| p(z))}_{\text{KL正则化项}}$$ 重建损失:编码后解码,尽量还原输入 KL 项:让后验 $q_\phi(z|x)$(编码器输出)靠近先验 $p(z) = \mathcal{N}(0,I)$ KL 项的方向是 $D_{KL}(q \| p)$(反向 KL): 计算可行:从 $q_\phi$ 采样(重参数化技巧),有解析解 Mode-seeking:$q_\phi$ 不能在 $p$ 没有概率的地方放概率(先验约束) 防止后验坍塌:如果 KL 项过强($\beta$ 太大),后验退化为先验,失去编码信息 这和 GRPO 里的 KL 约束是同一个数学结构:都是用反向 KL 把近似分布约束在参考/先验分布附近。 第六部分:对称化的 KL——Jensen-Shannon 散度 JS 散度 由于 KL 散度不对称,实践中有时用Jensen-Shannon 散度(JS 散度),它是对称的: $$D_{JS}(P \| Q) = \frac{1}{2}D_{KL}\left(P \| M\right) + \frac{1}{2}D_{KL}\left(Q \| M\right)$$ 其中 $M = \frac{P+Q}{2}$ 是混合分布。 JS 散度满足 $0 \leq D_{JS}(P \| Q) \leq \log 2$,且是对称的($D_{JS}(P \| Q) = D_{JS}(Q \| P)$)。 GAN 与 JS 散度 生成对抗网络(GAN) 的原始训练目标,等价于最小化生成分布 $P_G$ 和真实数据分布 $P_{data}$ 之间的 JS 散度: $$\mathcal{L}_{GAN} = -\left[\mathbb{E}_{x \sim P_{data}}\log D(x) + \mathbb{E}_{z \sim P_z}\log(1-D(G(z)))\right]$$ 当判别器 $D$ 达到最优时,这个损失等于 $2 D_{JS}(P_{data} \| P_G) - \log 4$。 GAN 训练不稳定的一个根本原因就是 JS 散度的性质:当 $P_{data}$ 和 $P_G$ 支撑不重叠时,JS 散度恒等于 $\log 2$,梯度为 0,生成器无法更新。这推动了 Wasserstein GAN(用 Wasserstein 距离替代 JS 散度)的发展。 第七部分:KL 散度与信息瓶颈 信息瓶颈理论 信息瓶颈(Information Bottleneck) 框架用 KL 散度描述神经网络的表示学习目标: $$\min_{p(T|X)} \left[I(X; T) - \beta I(T; Y)\right]$$ $I(X; T)$:输入 $X$ 和表示 $T$ 之间的互信息(压缩),最小化让表示尽量简洁 $I(T; Y)$:表示 $T$ 和标签 $Y$ 之间的互信息(预测),最大化让表示保留任务相关信息 $\beta$ 控制压缩和预测的权衡 这和 VAE 的 ELBO 在结构上完全类似:VAE 的 KL 正则项正是最小化 $I(Z; X)$(在先验约束下),重建项是最大化 $I(Z; X)$ 的下界。 深度学习的表示学习,从信息瓶颈角度看,就是在"压缩输入"和"保留标签信息"之间找平衡,KL 散度是衡量这个压缩程度的工具。 第八部分:完整代码 import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. KL散度的基本计算与非负性验证 ===== print("===== KL散度:基本性质验证 =====\n") def kl_divergence(P, Q, eps=1e-10): P, Q = np.array(P, dtype=float), np.array(Q, dtype=float) P, Q = P / P.sum(), Q / Q.sum() mask = P > 0 return np.sum(P[mask] * np.log((P[mask] + eps) / (Q[mask] + eps))) # 验证非负性 distributions = [ ([0.5, 0.5], [0.5, 0.5], "P=Q(均等)"), ([0.9, 0.1], [0.5, 0.5], "P尖峰,Q均匀"), ([0.5, 0.5], [0.9, 0.1], "P均匀,Q尖峰"), ([0.7, 0.2, 0.1], [0.3, 0.5, 0.2], "三类分布"), ([1.0, 0.0], [0.5, 0.5], "P确定(δ分布)"), ] print(f"{'描述':20s} | {'KL(P‖Q)':>10} | {'KL(Q‖P)':>10} | {'对称?':>6}") print("-" * 55) for P, Q, desc in distributions: kl_pq = kl_divergence(P, Q) kl_qp = kl_divergence(Q, P) symmetric = "是" if abs(kl_pq - kl_qp) < 1e-6 else "否" print(f"{desc:20s} | {kl_pq:>10.4f} | {kl_qp:>10.4f} | {symmetric:>6}") print("\n→ KL散度总是 ≥ 0,且通常不对称(KL(P‖Q) ≠ KL(Q‖P))") # ===== 2. 不对称性的直觉:前向KL vs 反向KL 的拟合行为 ===== print("\n===== 前向KL vs 反向KL 的拟合行为 =====\n") # 真实分布P:双峰(两个高概率区域) x = np.arange(10) P_bimodal = np.array([0.01, 0.01, 0.01, 0.17, 0.30, 0.01, 0.01, 0.17, 0.30, 0.01]) P_bimodal /= P_bimodal.sum() # 用单峰高斯近似P # 前向KL最优:Q覆盖P的两个峰,取"平均" Q_forward_optimal = np.array([0.01, 0.02, 0.05, 0.15, 0.22, 0.10, 0.15, 0.15, 0.12, 0.03]) Q_forward_optimal /= Q_forward_optimal.sum() # 反向KL最优:Q集中在P的某一个峰 Q_reverse_optimal = np.array([0.01, 0.01, 0.02, 0.20, 0.50, 0.15, 0.07, 0.02, 0.01, 0.01]) Q_reverse_optimal /= Q_reverse_optimal.sum() kl_forward_pq = kl_divergence(P_bimodal, Q_forward_optimal) # 前向:P‖Q kl_forward_qp = kl_divergence(Q_forward_optimal, P_bimodal) # Q‖P的 kl_reverse_qp = kl_divergence(Q_reverse_optimal, P_bimodal) # 反向:Q‖P kl_reverse_pq = kl_divergence(P_bimodal, Q_reverse_optimal) # P‖Q的 print("真实分布(双峰)近似策略对比:") print(f"\n前向KL近似(覆盖两峰)Q_forward:") print(f" 最小化 KL(P‖Q) = {kl_forward_pq:.4f}") print(f" 对应 KL(Q‖P) = {kl_forward_qp:.4f}") print(f"\n反向KL近似(集中单峰)Q_reverse:") print(f" 最小化 KL(Q‖P) = {kl_reverse_qp:.4f}") print(f" 对应 KL(P‖Q) = {kl_reverse_pq:.4f}") print(f"\n→ 前向KL(P‖Q):Q被迫覆盖P的所有模式(质量覆盖)") print(f"→ 反向KL(Q‖P):Q集中在P的某一个模式(模式寻求)") print(f"→ GRPO用反向KL,让π_θ集中在π_ref的高概率区域(合理输出)") # ===== 3. GRPO中的KL约束模拟 ===== print("\n===== GRPO KL约束:β对训练的影响 =====\n") torch.manual_seed(0) vocab_size = 20 # 参考策略(SFT模型):在几个词上有较高概率 logits_ref = torch.zeros(vocab_size) logits_ref[3] = 3.0; logits_ref[7] = 2.5; logits_ref[12] = 2.0 pi_ref = F.softmax(logits_ref, dim=0) # 模拟强化学习更新后的策略(奖励信号让token 15的概率升高) def simulate_rl_update(pi_ref, reward_token, update_strength, beta): """模拟一次RL更新后的策略""" logits_new = torch.log(pi_ref + 1e-10).clone() logits_new[reward_token] += update_strength pi_new = F.softmax(logits_new, dim=0) return pi_new reward_token = 15 # 奖励token(不在参考策略高概率区域) print(f"参考策略 π_ref 的高概率token: " f"token3={pi_ref[3]:.3f}, token7={pi_ref[7]:.3f}, token12={pi_ref[12]:.3f}") print(f"奖励token: token{reward_token} (π_ref中概率={pi_ref[reward_token]:.4f})\n") print(f"{'更新强度':>8} | {'KL(π_θ‖π_ref)':>14} | {'token{} 概率'.format(reward_token):>12} | " f"{'高概率token总概率':>16} | {'风险'}") print("-" * 75) for strength in [0.0, 0.5, 1.0, 2.0, 3.0, 5.0]: pi_new = simulate_rl_update(pi_ref, reward_token, strength, beta=0.04) kl = (pi_new * (torch.log(pi_new + 1e-10) - torch.log(pi_ref + 1e-10))).sum().item() prob_reward = pi_new[reward_token].item() prob_orig = (pi_new[3] + pi_new[7] + pi_new[12]).item() risk = "危险!" if kl > 1.0 else ("注意" if kl > 0.3 else "安全") print(f"{strength:>8.1f} | {kl:>14.4f} | {prob_reward:>12.4f} | {prob_orig:>16.4f} | {risk}") print(f"\nβ=0.04时,KL惩罚 = 0.04 × KL,平衡奖励和约束") # ===== 4. 高斯KL散度:VAE中的解析计算 ===== print("\n===== 高斯KL散度(VAE正则化项)=====\n") def gaussian_kl(mu, log_var): """KL(N(mu, sigma²) || N(0,1)) 的解析公式""" return 0.5 * (mu**2 + log_var.exp() - 1 - log_var) # 不同参数下的KL值 print(f"{'μ':>6} {'log σ²':>8} | {'σ':>6} | {'KL散度':>10} | {'含义'}") print("-" * 60) params = [ (0.0, 0.0, "与标准正态完全相同"), (0.0, 1.0, "方差更大(σ=e^0.5≈1.65)"), (0.0, -1.0, "方差更小(σ=e^-0.5≈0.61)"), (1.0, 0.0, "均值偏移"), (2.0, 0.0, "均值大偏移"), (1.0, 1.0, "均值偏移+方差更大"), ] for mu, log_var, desc in params: mu_t = torch.tensor(float(mu)) lv_t = torch.tensor(float(log_var)) kl = gaussian_kl(mu_t, lv_t).item() sigma = np.exp(log_var / 2) print(f"{mu:>6.1f} {log_var:>8.1f} | {sigma:>6.3f} | {kl:>10.4f} | {desc}") print("\n→ μ=0, log_σ²=0(即σ=1)时KL=0,后验=先验") print("→ 偏离标准正态越远,KL越大,VAE正则化惩罚越强") # ===== 5. JS散度:对称版本 ===== print("\n===== JS散度(对称化的KL)=====\n") def js_divergence(P, Q): P, Q = np.array(P, dtype=float), np.array(Q, dtype=float) P, Q = P/P.sum(), Q/Q.sum() M = 0.5 * (P + Q) return 0.5 * kl_divergence(P, M) + 0.5 * kl_divergence(Q, M) pairs = [ ([0.9, 0.1], [0.1, 0.9], "完全相反"), ([0.9, 0.1], [0.5, 0.5], "一个尖峰一个均匀"), ([0.7, 0.3], [0.6, 0.4], "相近分布"), ([1.0, 0.0], [0.0, 1.0], "支撑不重叠"), ] print(f"{'描述':18s} | {'KL(P‖Q)':>10} | {'KL(Q‖P)':>10} | {'JS散度':>10} | {'JS上界log2':>10}") print("-" * 70) for P, Q, desc in pairs: kl_pq = kl_divergence(P, Q) kl_qp = kl_divergence(Q, P) js = js_divergence(P, Q) print(f"{desc:18s} | {kl_pq:>10.4f} | {kl_qp:>10.4f} | {js:>10.4f} | {np.log(2):>10.4f}") print(f"\n→ JS散度:对称,有界(≤ log2≈0.693),支撑不重叠时=log2(不是∞)") print(f"→ KL散度:不对称,无界,支撑不重叠时=∞(这是GAN训练不稳定的原因之一)") # ===== 6. 逐token KL计算(GRPO实现细节)===== print("\n===== 逐token KL计算(GRPO实现) =====\n") seq_len = 5 vocab_size_small = 10 # 模拟一条生成序列的每个位置的logits torch.manual_seed(1) logits_theta = torch.randn(seq_len, vocab_size_small) logits_ref2 = torch.randn(seq_len, vocab_size_small) pi_theta = F.softmax(logits_theta, dim=-1) pi_ref2 = F.softmax(logits_ref2, dim=-1) # 逐token KL kl_per_token = (pi_theta * (torch.log(pi_theta + 1e-10) - torch.log(pi_ref2 + 1e-10))).sum(dim=-1) # 序列级KL = 逐token KL之和 kl_sequence = kl_per_token.sum() print(f"序列长度: {seq_len}, 词表大小: {vocab_size_small}") print(f"\n逐token KL散度:") for t, kl_t in enumerate(kl_per_token): print(f" 位置{t+1}: KL = {kl_t.item():.4f}") print(f"\n序列级KL(求和): {kl_sequence.item():.4f}") print(f"GRPO惩罚项(β=0.04): {0.04 * kl_sequence.item():.4f}") 总结 这篇文章从 KL 散度的定义出发,把它的数学性质、不对称性含义,以及在 GRPO、VAE、GAN 中的具体应用全部打通了。 第一,KL 散度 $D_{KL}(P \| Q) = \mathbb{E}_{x \sim P}[\log \frac{P(x)}{Q(x)}]$ 衡量"用分布 $Q$ 替代 $P$ 的额外代价",等于交叉熵减去真实熵。非负性由 Jensen 不等式保证,等号当且仅当 $P = Q$。 第二,KL 散度是不对称的,$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。前向 KL($P \| Q$)是"质量覆盖"型:$Q$ 被迫覆盖 $P$ 的所有支撑。反向 KL($Q \| P$)是"模式寻求"型:$Q$ 集中在 $P$ 的某个高概率区域。 第三,GRPO 用反向 KL $D_{KL}(\pi_\theta \| \pi_{ref})$ 约束策略更新,原因有二:计算可行(只需从当前策略采样);mode-seeking 性质确保 $\pi_\theta$ 不在参考策略概率为零的地方分配概率(防止奖励黑客)。$\beta = 0.04$ 是奖励追求和约束保守性的平衡点。 第四,高斯分布之间的 KL 散度有解析解,直接支撑了 VAE 的训练(ELBO 中的 KL 正则化项),无需采样估计梯度。同样是反向 KL,后验近似先验,防止潜变量空间坍塌。 第五,JS 散度是对称化的 KL,有界($\leq \log 2$),GAN 的原始目标等价于最小化 JS 散度。但支撑不重叠时 JS 散度梯度为零(KL 散度为无穷),这是 GAN 训练不稳定的信息论根源,推动了 Wasserstein GAN 的发展。 下一篇预告: 第17篇,我们进入第四阶段:优化算法,从最朴素的梯度下降讲起——为什么沿负梯度方向走能让损失减小?学习率怎么选?为什么全量梯度下降在深度学习里不实用? KL 散度不只是一个公式,它是一种看待"差异"的方式——不是对称的几何距离,而是有方向的信息代价。DeepSeek R1 选择 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,不是随意为之,是基于计算可行性和模式寻求性质的精确数学选择。理解了方向,就理解了为什么强化学习能在保留语言能力的同时提升推理能力。
每个位置 $t$,在给定前文的条件下,当前策略和参考策略的输出分布之差:
$$D_{KL}^{(t)} = \sum_{k=1}^{|\mathcal{V}|} \pi_\theta(k|q, o_{ 在所有时间步上求和,就是序列级别的 KL 散度。 第四部分:KL 散度的两种等价形式 形式一:期望对数比 $$D_{KL}(P \| Q) = \mathbb{E}_{x \sim P}\left[\log \frac{P(x)}{Q(x)}\right] = \mathbb{E}_{x \sim P}[\log P(x) - \log Q(x)]$$ 梯度计算友好:对 $Q$ 求梯度时,$\mathbb{E}_{x \sim P}[-\nabla_Q \log Q(x)]$ 直接可用。 形式二:交叉熵减熵 $$D_{KL}(P \| Q) = H(P, Q) - H(P)$$ 理解友好:KL 散度 = 用近似分布替代真实分布多付出的编码代价。 在训练语言模型时,$P$ 是数据分布(固定),$H(P)$ 是常数,最小化交叉熵等价于最小化 KL 散度。 第五部分:高斯分布之间的 KL 散度(VAE 中的应用) 推导 两个高斯分布 $P = \mathcal{N}(\mu_1, \sigma_1^2)$ 和 $Q = \mathcal{N}(\mu_2, \sigma_2^2)$ 之间的 KL 散度有解析解: $$D_{KL}(P \| Q) = \log\frac{\sigma_2}{\sigma_1} + \frac{\sigma_1^2 + (\mu_1-\mu_2)^2}{2\sigma_2^2} - \frac{1}{2}$$ 特殊情形:$Q = \mathcal{N}(0, 1)$(标准正态),$P = \mathcal{N}(\mu, \sigma^2)$: $$D_{KL}(\mathcal{N}(\mu, \sigma^2) \| \mathcal{N}(0, 1)) = \frac{\mu^2 + \sigma^2 - 1 - \log\sigma^2}{2}$$ 这是VAE 训练目标中的正则化项的解析形式,可以直接对 $\mu$ 和 $\sigma$ 求梯度,无需采样估计。 手算验证 设 $P = \mathcal{N}(1, 2)$(均值 1,方差 2),$Q = \mathcal{N}(0, 1)$(标准正态): $$D_{KL}(P \| Q) = \frac{1^2 + 2 - 1 - \log 2}{2} = \frac{1 + 2 - 1 - 0.693}{2} = \frac{1.307}{2} \approx 0.654$$ 含义:用标准正态 $Q$ 来近似均值为 1、方差为 2 的分布 $P$,每个样本平均多付出 0.654 nats 的编码代价。 VAE 的训练目标 VAE 的证据下界(ELBO): $$\mathcal{L}_{ELBO} = \underbrace{\mathbb{E}_{z \sim q_\phi(z|x)}[\log p_\theta(x|z)]}_{\text{重建损失}} - \underbrace{D_{KL}(q_\phi(z|x) \| p(z))}_{\text{KL正则化项}}$$ 重建损失:编码后解码,尽量还原输入 KL 项:让后验 $q_\phi(z|x)$(编码器输出)靠近先验 $p(z) = \mathcal{N}(0,I)$ KL 项的方向是 $D_{KL}(q \| p)$(反向 KL): 计算可行:从 $q_\phi$ 采样(重参数化技巧),有解析解 Mode-seeking:$q_\phi$ 不能在 $p$ 没有概率的地方放概率(先验约束) 防止后验坍塌:如果 KL 项过强($\beta$ 太大),后验退化为先验,失去编码信息 这和 GRPO 里的 KL 约束是同一个数学结构:都是用反向 KL 把近似分布约束在参考/先验分布附近。 第六部分:对称化的 KL——Jensen-Shannon 散度 JS 散度 由于 KL 散度不对称,实践中有时用Jensen-Shannon 散度(JS 散度),它是对称的: $$D_{JS}(P \| Q) = \frac{1}{2}D_{KL}\left(P \| M\right) + \frac{1}{2}D_{KL}\left(Q \| M\right)$$ 其中 $M = \frac{P+Q}{2}$ 是混合分布。 JS 散度满足 $0 \leq D_{JS}(P \| Q) \leq \log 2$,且是对称的($D_{JS}(P \| Q) = D_{JS}(Q \| P)$)。 GAN 与 JS 散度 生成对抗网络(GAN) 的原始训练目标,等价于最小化生成分布 $P_G$ 和真实数据分布 $P_{data}$ 之间的 JS 散度: $$\mathcal{L}_{GAN} = -\left[\mathbb{E}_{x \sim P_{data}}\log D(x) + \mathbb{E}_{z \sim P_z}\log(1-D(G(z)))\right]$$ 当判别器 $D$ 达到最优时,这个损失等于 $2 D_{JS}(P_{data} \| P_G) - \log 4$。 GAN 训练不稳定的一个根本原因就是 JS 散度的性质:当 $P_{data}$ 和 $P_G$ 支撑不重叠时,JS 散度恒等于 $\log 2$,梯度为 0,生成器无法更新。这推动了 Wasserstein GAN(用 Wasserstein 距离替代 JS 散度)的发展。 第七部分:KL 散度与信息瓶颈 信息瓶颈理论 信息瓶颈(Information Bottleneck) 框架用 KL 散度描述神经网络的表示学习目标: $$\min_{p(T|X)} \left[I(X; T) - \beta I(T; Y)\right]$$ $I(X; T)$:输入 $X$ 和表示 $T$ 之间的互信息(压缩),最小化让表示尽量简洁 $I(T; Y)$:表示 $T$ 和标签 $Y$ 之间的互信息(预测),最大化让表示保留任务相关信息 $\beta$ 控制压缩和预测的权衡 这和 VAE 的 ELBO 在结构上完全类似:VAE 的 KL 正则项正是最小化 $I(Z; X)$(在先验约束下),重建项是最大化 $I(Z; X)$ 的下界。 深度学习的表示学习,从信息瓶颈角度看,就是在"压缩输入"和"保留标签信息"之间找平衡,KL 散度是衡量这个压缩程度的工具。 第八部分:完整代码 import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. KL散度的基本计算与非负性验证 ===== print("===== KL散度:基本性质验证 =====\n") def kl_divergence(P, Q, eps=1e-10): P, Q = np.array(P, dtype=float), np.array(Q, dtype=float) P, Q = P / P.sum(), Q / Q.sum() mask = P > 0 return np.sum(P[mask] * np.log((P[mask] + eps) / (Q[mask] + eps))) # 验证非负性 distributions = [ ([0.5, 0.5], [0.5, 0.5], "P=Q(均等)"), ([0.9, 0.1], [0.5, 0.5], "P尖峰,Q均匀"), ([0.5, 0.5], [0.9, 0.1], "P均匀,Q尖峰"), ([0.7, 0.2, 0.1], [0.3, 0.5, 0.2], "三类分布"), ([1.0, 0.0], [0.5, 0.5], "P确定(δ分布)"), ] print(f"{'描述':20s} | {'KL(P‖Q)':>10} | {'KL(Q‖P)':>10} | {'对称?':>6}") print("-" * 55) for P, Q, desc in distributions: kl_pq = kl_divergence(P, Q) kl_qp = kl_divergence(Q, P) symmetric = "是" if abs(kl_pq - kl_qp) < 1e-6 else "否" print(f"{desc:20s} | {kl_pq:>10.4f} | {kl_qp:>10.4f} | {symmetric:>6}") print("\n→ KL散度总是 ≥ 0,且通常不对称(KL(P‖Q) ≠ KL(Q‖P))") # ===== 2. 不对称性的直觉:前向KL vs 反向KL 的拟合行为 ===== print("\n===== 前向KL vs 反向KL 的拟合行为 =====\n") # 真实分布P:双峰(两个高概率区域) x = np.arange(10) P_bimodal = np.array([0.01, 0.01, 0.01, 0.17, 0.30, 0.01, 0.01, 0.17, 0.30, 0.01]) P_bimodal /= P_bimodal.sum() # 用单峰高斯近似P # 前向KL最优:Q覆盖P的两个峰,取"平均" Q_forward_optimal = np.array([0.01, 0.02, 0.05, 0.15, 0.22, 0.10, 0.15, 0.15, 0.12, 0.03]) Q_forward_optimal /= Q_forward_optimal.sum() # 反向KL最优:Q集中在P的某一个峰 Q_reverse_optimal = np.array([0.01, 0.01, 0.02, 0.20, 0.50, 0.15, 0.07, 0.02, 0.01, 0.01]) Q_reverse_optimal /= Q_reverse_optimal.sum() kl_forward_pq = kl_divergence(P_bimodal, Q_forward_optimal) # 前向:P‖Q kl_forward_qp = kl_divergence(Q_forward_optimal, P_bimodal) # Q‖P的 kl_reverse_qp = kl_divergence(Q_reverse_optimal, P_bimodal) # 反向:Q‖P kl_reverse_pq = kl_divergence(P_bimodal, Q_reverse_optimal) # P‖Q的 print("真实分布(双峰)近似策略对比:") print(f"\n前向KL近似(覆盖两峰)Q_forward:") print(f" 最小化 KL(P‖Q) = {kl_forward_pq:.4f}") print(f" 对应 KL(Q‖P) = {kl_forward_qp:.4f}") print(f"\n反向KL近似(集中单峰)Q_reverse:") print(f" 最小化 KL(Q‖P) = {kl_reverse_qp:.4f}") print(f" 对应 KL(P‖Q) = {kl_reverse_pq:.4f}") print(f"\n→ 前向KL(P‖Q):Q被迫覆盖P的所有模式(质量覆盖)") print(f"→ 反向KL(Q‖P):Q集中在P的某一个模式(模式寻求)") print(f"→ GRPO用反向KL,让π_θ集中在π_ref的高概率区域(合理输出)") # ===== 3. GRPO中的KL约束模拟 ===== print("\n===== GRPO KL约束:β对训练的影响 =====\n") torch.manual_seed(0) vocab_size = 20 # 参考策略(SFT模型):在几个词上有较高概率 logits_ref = torch.zeros(vocab_size) logits_ref[3] = 3.0; logits_ref[7] = 2.5; logits_ref[12] = 2.0 pi_ref = F.softmax(logits_ref, dim=0) # 模拟强化学习更新后的策略(奖励信号让token 15的概率升高) def simulate_rl_update(pi_ref, reward_token, update_strength, beta): """模拟一次RL更新后的策略""" logits_new = torch.log(pi_ref + 1e-10).clone() logits_new[reward_token] += update_strength pi_new = F.softmax(logits_new, dim=0) return pi_new reward_token = 15 # 奖励token(不在参考策略高概率区域) print(f"参考策略 π_ref 的高概率token: " f"token3={pi_ref[3]:.3f}, token7={pi_ref[7]:.3f}, token12={pi_ref[12]:.3f}") print(f"奖励token: token{reward_token} (π_ref中概率={pi_ref[reward_token]:.4f})\n") print(f"{'更新强度':>8} | {'KL(π_θ‖π_ref)':>14} | {'token{} 概率'.format(reward_token):>12} | " f"{'高概率token总概率':>16} | {'风险'}") print("-" * 75) for strength in [0.0, 0.5, 1.0, 2.0, 3.0, 5.0]: pi_new = simulate_rl_update(pi_ref, reward_token, strength, beta=0.04) kl = (pi_new * (torch.log(pi_new + 1e-10) - torch.log(pi_ref + 1e-10))).sum().item() prob_reward = pi_new[reward_token].item() prob_orig = (pi_new[3] + pi_new[7] + pi_new[12]).item() risk = "危险!" if kl > 1.0 else ("注意" if kl > 0.3 else "安全") print(f"{strength:>8.1f} | {kl:>14.4f} | {prob_reward:>12.4f} | {prob_orig:>16.4f} | {risk}") print(f"\nβ=0.04时,KL惩罚 = 0.04 × KL,平衡奖励和约束") # ===== 4. 高斯KL散度:VAE中的解析计算 ===== print("\n===== 高斯KL散度(VAE正则化项)=====\n") def gaussian_kl(mu, log_var): """KL(N(mu, sigma²) || N(0,1)) 的解析公式""" return 0.5 * (mu**2 + log_var.exp() - 1 - log_var) # 不同参数下的KL值 print(f"{'μ':>6} {'log σ²':>8} | {'σ':>6} | {'KL散度':>10} | {'含义'}") print("-" * 60) params = [ (0.0, 0.0, "与标准正态完全相同"), (0.0, 1.0, "方差更大(σ=e^0.5≈1.65)"), (0.0, -1.0, "方差更小(σ=e^-0.5≈0.61)"), (1.0, 0.0, "均值偏移"), (2.0, 0.0, "均值大偏移"), (1.0, 1.0, "均值偏移+方差更大"), ] for mu, log_var, desc in params: mu_t = torch.tensor(float(mu)) lv_t = torch.tensor(float(log_var)) kl = gaussian_kl(mu_t, lv_t).item() sigma = np.exp(log_var / 2) print(f"{mu:>6.1f} {log_var:>8.1f} | {sigma:>6.3f} | {kl:>10.4f} | {desc}") print("\n→ μ=0, log_σ²=0(即σ=1)时KL=0,后验=先验") print("→ 偏离标准正态越远,KL越大,VAE正则化惩罚越强") # ===== 5. JS散度:对称版本 ===== print("\n===== JS散度(对称化的KL)=====\n") def js_divergence(P, Q): P, Q = np.array(P, dtype=float), np.array(Q, dtype=float) P, Q = P/P.sum(), Q/Q.sum() M = 0.5 * (P + Q) return 0.5 * kl_divergence(P, M) + 0.5 * kl_divergence(Q, M) pairs = [ ([0.9, 0.1], [0.1, 0.9], "完全相反"), ([0.9, 0.1], [0.5, 0.5], "一个尖峰一个均匀"), ([0.7, 0.3], [0.6, 0.4], "相近分布"), ([1.0, 0.0], [0.0, 1.0], "支撑不重叠"), ] print(f"{'描述':18s} | {'KL(P‖Q)':>10} | {'KL(Q‖P)':>10} | {'JS散度':>10} | {'JS上界log2':>10}") print("-" * 70) for P, Q, desc in pairs: kl_pq = kl_divergence(P, Q) kl_qp = kl_divergence(Q, P) js = js_divergence(P, Q) print(f"{desc:18s} | {kl_pq:>10.4f} | {kl_qp:>10.4f} | {js:>10.4f} | {np.log(2):>10.4f}") print(f"\n→ JS散度:对称,有界(≤ log2≈0.693),支撑不重叠时=log2(不是∞)") print(f"→ KL散度:不对称,无界,支撑不重叠时=∞(这是GAN训练不稳定的原因之一)") # ===== 6. 逐token KL计算(GRPO实现细节)===== print("\n===== 逐token KL计算(GRPO实现) =====\n") seq_len = 5 vocab_size_small = 10 # 模拟一条生成序列的每个位置的logits torch.manual_seed(1) logits_theta = torch.randn(seq_len, vocab_size_small) logits_ref2 = torch.randn(seq_len, vocab_size_small) pi_theta = F.softmax(logits_theta, dim=-1) pi_ref2 = F.softmax(logits_ref2, dim=-1) # 逐token KL kl_per_token = (pi_theta * (torch.log(pi_theta + 1e-10) - torch.log(pi_ref2 + 1e-10))).sum(dim=-1) # 序列级KL = 逐token KL之和 kl_sequence = kl_per_token.sum() print(f"序列长度: {seq_len}, 词表大小: {vocab_size_small}") print(f"\n逐token KL散度:") for t, kl_t in enumerate(kl_per_token): print(f" 位置{t+1}: KL = {kl_t.item():.4f}") print(f"\n序列级KL(求和): {kl_sequence.item():.4f}") print(f"GRPO惩罚项(β=0.04): {0.04 * kl_sequence.item():.4f}") 总结 这篇文章从 KL 散度的定义出发,把它的数学性质、不对称性含义,以及在 GRPO、VAE、GAN 中的具体应用全部打通了。 第一,KL 散度 $D_{KL}(P \| Q) = \mathbb{E}_{x \sim P}[\log \frac{P(x)}{Q(x)}]$ 衡量"用分布 $Q$ 替代 $P$ 的额外代价",等于交叉熵减去真实熵。非负性由 Jensen 不等式保证,等号当且仅当 $P = Q$。 第二,KL 散度是不对称的,$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。前向 KL($P \| Q$)是"质量覆盖"型:$Q$ 被迫覆盖 $P$ 的所有支撑。反向 KL($Q \| P$)是"模式寻求"型:$Q$ 集中在 $P$ 的某个高概率区域。 第三,GRPO 用反向 KL $D_{KL}(\pi_\theta \| \pi_{ref})$ 约束策略更新,原因有二:计算可行(只需从当前策略采样);mode-seeking 性质确保 $\pi_\theta$ 不在参考策略概率为零的地方分配概率(防止奖励黑客)。$\beta = 0.04$ 是奖励追求和约束保守性的平衡点。 第四,高斯分布之间的 KL 散度有解析解,直接支撑了 VAE 的训练(ELBO 中的 KL 正则化项),无需采样估计梯度。同样是反向 KL,后验近似先验,防止潜变量空间坍塌。 第五,JS 散度是对称化的 KL,有界($\leq \log 2$),GAN 的原始目标等价于最小化 JS 散度。但支撑不重叠时 JS 散度梯度为零(KL 散度为无穷),这是 GAN 训练不稳定的信息论根源,推动了 Wasserstein GAN 的发展。 下一篇预告: 第17篇,我们进入第四阶段:优化算法,从最朴素的梯度下降讲起——为什么沿负梯度方向走能让损失减小?学习率怎么选?为什么全量梯度下降在深度学习里不实用? KL 散度不只是一个公式,它是一种看待"差异"的方式——不是对称的几何距离,而是有方向的信息代价。DeepSeek R1 选择 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,不是随意为之,是基于计算可行性和模式寻求性质的精确数学选择。理解了方向,就理解了为什么强化学习能在保留语言能力的同时提升推理能力。
在所有时间步上求和,就是序列级别的 KL 散度。
$$D_{KL}(P \| Q) = \mathbb{E}_{x \sim P}\left[\log \frac{P(x)}{Q(x)}\right] = \mathbb{E}_{x \sim P}[\log P(x) - \log Q(x)]$$
梯度计算友好:对 $Q$ 求梯度时,$\mathbb{E}_{x \sim P}[-\nabla_Q \log Q(x)]$ 直接可用。
$$D_{KL}(P \| Q) = H(P, Q) - H(P)$$
理解友好:KL 散度 = 用近似分布替代真实分布多付出的编码代价。
在训练语言模型时,$P$ 是数据分布(固定),$H(P)$ 是常数,最小化交叉熵等价于最小化 KL 散度。
两个高斯分布 $P = \mathcal{N}(\mu_1, \sigma_1^2)$ 和 $Q = \mathcal{N}(\mu_2, \sigma_2^2)$ 之间的 KL 散度有解析解:
$$D_{KL}(P \| Q) = \log\frac{\sigma_2}{\sigma_1} + \frac{\sigma_1^2 + (\mu_1-\mu_2)^2}{2\sigma_2^2} - \frac{1}{2}$$
特殊情形:$Q = \mathcal{N}(0, 1)$(标准正态),$P = \mathcal{N}(\mu, \sigma^2)$:
$$D_{KL}(\mathcal{N}(\mu, \sigma^2) \| \mathcal{N}(0, 1)) = \frac{\mu^2 + \sigma^2 - 1 - \log\sigma^2}{2}$$
这是VAE 训练目标中的正则化项的解析形式,可以直接对 $\mu$ 和 $\sigma$ 求梯度,无需采样估计。
设 $P = \mathcal{N}(1, 2)$(均值 1,方差 2),$Q = \mathcal{N}(0, 1)$(标准正态):
$$D_{KL}(P \| Q) = \frac{1^2 + 2 - 1 - \log 2}{2} = \frac{1 + 2 - 1 - 0.693}{2} = \frac{1.307}{2} \approx 0.654$$
含义:用标准正态 $Q$ 来近似均值为 1、方差为 2 的分布 $P$,每个样本平均多付出 0.654 nats 的编码代价。
VAE 的证据下界(ELBO):
$$\mathcal{L}_{ELBO} = \underbrace{\mathbb{E}_{z \sim q_\phi(z|x)}[\log p_\theta(x|z)]}_{\text{重建损失}} - \underbrace{D_{KL}(q_\phi(z|x) \| p(z))}_{\text{KL正则化项}}$$
KL 项的方向是 $D_{KL}(q \| p)$(反向 KL):
这和 GRPO 里的 KL 约束是同一个数学结构:都是用反向 KL 把近似分布约束在参考/先验分布附近。
由于 KL 散度不对称,实践中有时用Jensen-Shannon 散度(JS 散度),它是对称的:
$$D_{JS}(P \| Q) = \frac{1}{2}D_{KL}\left(P \| M\right) + \frac{1}{2}D_{KL}\left(Q \| M\right)$$
其中 $M = \frac{P+Q}{2}$ 是混合分布。
JS 散度满足 $0 \leq D_{JS}(P \| Q) \leq \log 2$,且是对称的($D_{JS}(P \| Q) = D_{JS}(Q \| P)$)。
生成对抗网络(GAN) 的原始训练目标,等价于最小化生成分布 $P_G$ 和真实数据分布 $P_{data}$ 之间的 JS 散度:
$$\mathcal{L}_{GAN} = -\left[\mathbb{E}_{x \sim P_{data}}\log D(x) + \mathbb{E}_{z \sim P_z}\log(1-D(G(z)))\right]$$
当判别器 $D$ 达到最优时,这个损失等于 $2 D_{JS}(P_{data} \| P_G) - \log 4$。
GAN 训练不稳定的一个根本原因就是 JS 散度的性质:当 $P_{data}$ 和 $P_G$ 支撑不重叠时,JS 散度恒等于 $\log 2$,梯度为 0,生成器无法更新。这推动了 Wasserstein GAN(用 Wasserstein 距离替代 JS 散度)的发展。
信息瓶颈(Information Bottleneck) 框架用 KL 散度描述神经网络的表示学习目标:
$$\min_{p(T|X)} \left[I(X; T) - \beta I(T; Y)\right]$$
这和 VAE 的 ELBO 在结构上完全类似:VAE 的 KL 正则项正是最小化 $I(Z; X)$(在先验约束下),重建项是最大化 $I(Z; X)$ 的下界。
深度学习的表示学习,从信息瓶颈角度看,就是在"压缩输入"和"保留标签信息"之间找平衡,KL 散度是衡量这个压缩程度的工具。
import numpy as np import torch import torch.nn.functional as F np.random.seed(42) torch.manual_seed(42) # ===== 1. KL散度的基本计算与非负性验证 ===== print("===== KL散度:基本性质验证 =====\n") def kl_divergence(P, Q, eps=1e-10): P, Q = np.array(P, dtype=float), np.array(Q, dtype=float) P, Q = P / P.sum(), Q / Q.sum() mask = P > 0 return np.sum(P[mask] * np.log((P[mask] + eps) / (Q[mask] + eps))) # 验证非负性 distributions = [ ([0.5, 0.5], [0.5, 0.5], "P=Q(均等)"), ([0.9, 0.1], [0.5, 0.5], "P尖峰,Q均匀"), ([0.5, 0.5], [0.9, 0.1], "P均匀,Q尖峰"), ([0.7, 0.2, 0.1], [0.3, 0.5, 0.2], "三类分布"), ([1.0, 0.0], [0.5, 0.5], "P确定(δ分布)"), ] print(f"{'描述':20s} | {'KL(P‖Q)':>10} | {'KL(Q‖P)':>10} | {'对称?':>6}") print("-" * 55) for P, Q, desc in distributions: kl_pq = kl_divergence(P, Q) kl_qp = kl_divergence(Q, P) symmetric = "是" if abs(kl_pq - kl_qp) < 1e-6 else "否" print(f"{desc:20s} | {kl_pq:>10.4f} | {kl_qp:>10.4f} | {symmetric:>6}") print("\n→ KL散度总是 ≥ 0,且通常不对称(KL(P‖Q) ≠ KL(Q‖P))") # ===== 2. 不对称性的直觉:前向KL vs 反向KL 的拟合行为 ===== print("\n===== 前向KL vs 反向KL 的拟合行为 =====\n") # 真实分布P:双峰(两个高概率区域) x = np.arange(10) P_bimodal = np.array([0.01, 0.01, 0.01, 0.17, 0.30, 0.01, 0.01, 0.17, 0.30, 0.01]) P_bimodal /= P_bimodal.sum() # 用单峰高斯近似P # 前向KL最优:Q覆盖P的两个峰,取"平均" Q_forward_optimal = np.array([0.01, 0.02, 0.05, 0.15, 0.22, 0.10, 0.15, 0.15, 0.12, 0.03]) Q_forward_optimal /= Q_forward_optimal.sum() # 反向KL最优:Q集中在P的某一个峰 Q_reverse_optimal = np.array([0.01, 0.01, 0.02, 0.20, 0.50, 0.15, 0.07, 0.02, 0.01, 0.01]) Q_reverse_optimal /= Q_reverse_optimal.sum() kl_forward_pq = kl_divergence(P_bimodal, Q_forward_optimal) # 前向:P‖Q kl_forward_qp = kl_divergence(Q_forward_optimal, P_bimodal) # Q‖P的 kl_reverse_qp = kl_divergence(Q_reverse_optimal, P_bimodal) # 反向:Q‖P kl_reverse_pq = kl_divergence(P_bimodal, Q_reverse_optimal) # P‖Q的 print("真实分布(双峰)近似策略对比:") print(f"\n前向KL近似(覆盖两峰)Q_forward:") print(f" 最小化 KL(P‖Q) = {kl_forward_pq:.4f}") print(f" 对应 KL(Q‖P) = {kl_forward_qp:.4f}") print(f"\n反向KL近似(集中单峰)Q_reverse:") print(f" 最小化 KL(Q‖P) = {kl_reverse_qp:.4f}") print(f" 对应 KL(P‖Q) = {kl_reverse_pq:.4f}") print(f"\n→ 前向KL(P‖Q):Q被迫覆盖P的所有模式(质量覆盖)") print(f"→ 反向KL(Q‖P):Q集中在P的某一个模式(模式寻求)") print(f"→ GRPO用反向KL,让π_θ集中在π_ref的高概率区域(合理输出)") # ===== 3. GRPO中的KL约束模拟 ===== print("\n===== GRPO KL约束:β对训练的影响 =====\n") torch.manual_seed(0) vocab_size = 20 # 参考策略(SFT模型):在几个词上有较高概率 logits_ref = torch.zeros(vocab_size) logits_ref[3] = 3.0; logits_ref[7] = 2.5; logits_ref[12] = 2.0 pi_ref = F.softmax(logits_ref, dim=0) # 模拟强化学习更新后的策略(奖励信号让token 15的概率升高) def simulate_rl_update(pi_ref, reward_token, update_strength, beta): """模拟一次RL更新后的策略""" logits_new = torch.log(pi_ref + 1e-10).clone() logits_new[reward_token] += update_strength pi_new = F.softmax(logits_new, dim=0) return pi_new reward_token = 15 # 奖励token(不在参考策略高概率区域) print(f"参考策略 π_ref 的高概率token: " f"token3={pi_ref[3]:.3f}, token7={pi_ref[7]:.3f}, token12={pi_ref[12]:.3f}") print(f"奖励token: token{reward_token} (π_ref中概率={pi_ref[reward_token]:.4f})\n") print(f"{'更新强度':>8} | {'KL(π_θ‖π_ref)':>14} | {'token{} 概率'.format(reward_token):>12} | " f"{'高概率token总概率':>16} | {'风险'}") print("-" * 75) for strength in [0.0, 0.5, 1.0, 2.0, 3.0, 5.0]: pi_new = simulate_rl_update(pi_ref, reward_token, strength, beta=0.04) kl = (pi_new * (torch.log(pi_new + 1e-10) - torch.log(pi_ref + 1e-10))).sum().item() prob_reward = pi_new[reward_token].item() prob_orig = (pi_new[3] + pi_new[7] + pi_new[12]).item() risk = "危险!" if kl > 1.0 else ("注意" if kl > 0.3 else "安全") print(f"{strength:>8.1f} | {kl:>14.4f} | {prob_reward:>12.4f} | {prob_orig:>16.4f} | {risk}") print(f"\nβ=0.04时,KL惩罚 = 0.04 × KL,平衡奖励和约束") # ===== 4. 高斯KL散度:VAE中的解析计算 ===== print("\n===== 高斯KL散度(VAE正则化项)=====\n") def gaussian_kl(mu, log_var): """KL(N(mu, sigma²) || N(0,1)) 的解析公式""" return 0.5 * (mu**2 + log_var.exp() - 1 - log_var) # 不同参数下的KL值 print(f"{'μ':>6} {'log σ²':>8} | {'σ':>6} | {'KL散度':>10} | {'含义'}") print("-" * 60) params = [ (0.0, 0.0, "与标准正态完全相同"), (0.0, 1.0, "方差更大(σ=e^0.5≈1.65)"), (0.0, -1.0, "方差更小(σ=e^-0.5≈0.61)"), (1.0, 0.0, "均值偏移"), (2.0, 0.0, "均值大偏移"), (1.0, 1.0, "均值偏移+方差更大"), ] for mu, log_var, desc in params: mu_t = torch.tensor(float(mu)) lv_t = torch.tensor(float(log_var)) kl = gaussian_kl(mu_t, lv_t).item() sigma = np.exp(log_var / 2) print(f"{mu:>6.1f} {log_var:>8.1f} | {sigma:>6.3f} | {kl:>10.4f} | {desc}") print("\n→ μ=0, log_σ²=0(即σ=1)时KL=0,后验=先验") print("→ 偏离标准正态越远,KL越大,VAE正则化惩罚越强") # ===== 5. JS散度:对称版本 ===== print("\n===== JS散度(对称化的KL)=====\n") def js_divergence(P, Q): P, Q = np.array(P, dtype=float), np.array(Q, dtype=float) P, Q = P/P.sum(), Q/Q.sum() M = 0.5 * (P + Q) return 0.5 * kl_divergence(P, M) + 0.5 * kl_divergence(Q, M) pairs = [ ([0.9, 0.1], [0.1, 0.9], "完全相反"), ([0.9, 0.1], [0.5, 0.5], "一个尖峰一个均匀"), ([0.7, 0.3], [0.6, 0.4], "相近分布"), ([1.0, 0.0], [0.0, 1.0], "支撑不重叠"), ] print(f"{'描述':18s} | {'KL(P‖Q)':>10} | {'KL(Q‖P)':>10} | {'JS散度':>10} | {'JS上界log2':>10}") print("-" * 70) for P, Q, desc in pairs: kl_pq = kl_divergence(P, Q) kl_qp = kl_divergence(Q, P) js = js_divergence(P, Q) print(f"{desc:18s} | {kl_pq:>10.4f} | {kl_qp:>10.4f} | {js:>10.4f} | {np.log(2):>10.4f}") print(f"\n→ JS散度:对称,有界(≤ log2≈0.693),支撑不重叠时=log2(不是∞)") print(f"→ KL散度:不对称,无界,支撑不重叠时=∞(这是GAN训练不稳定的原因之一)") # ===== 6. 逐token KL计算(GRPO实现细节)===== print("\n===== 逐token KL计算(GRPO实现) =====\n") seq_len = 5 vocab_size_small = 10 # 模拟一条生成序列的每个位置的logits torch.manual_seed(1) logits_theta = torch.randn(seq_len, vocab_size_small) logits_ref2 = torch.randn(seq_len, vocab_size_small) pi_theta = F.softmax(logits_theta, dim=-1) pi_ref2 = F.softmax(logits_ref2, dim=-1) # 逐token KL kl_per_token = (pi_theta * (torch.log(pi_theta + 1e-10) - torch.log(pi_ref2 + 1e-10))).sum(dim=-1) # 序列级KL = 逐token KL之和 kl_sequence = kl_per_token.sum() print(f"序列长度: {seq_len}, 词表大小: {vocab_size_small}") print(f"\n逐token KL散度:") for t, kl_t in enumerate(kl_per_token): print(f" 位置{t+1}: KL = {kl_t.item():.4f}") print(f"\n序列级KL(求和): {kl_sequence.item():.4f}") print(f"GRPO惩罚项(β=0.04): {0.04 * kl_sequence.item():.4f}")
这篇文章从 KL 散度的定义出发,把它的数学性质、不对称性含义,以及在 GRPO、VAE、GAN 中的具体应用全部打通了。
第一,KL 散度 $D_{KL}(P \| Q) = \mathbb{E}_{x \sim P}[\log \frac{P(x)}{Q(x)}]$ 衡量"用分布 $Q$ 替代 $P$ 的额外代价",等于交叉熵减去真实熵。非负性由 Jensen 不等式保证,等号当且仅当 $P = Q$。
第二,KL 散度是不对称的,$D_{KL}(P \| Q) \neq D_{KL}(Q \| P)$。前向 KL($P \| Q$)是"质量覆盖"型:$Q$ 被迫覆盖 $P$ 的所有支撑。反向 KL($Q \| P$)是"模式寻求"型:$Q$ 集中在 $P$ 的某个高概率区域。
第三,GRPO 用反向 KL $D_{KL}(\pi_\theta \| \pi_{ref})$ 约束策略更新,原因有二:计算可行(只需从当前策略采样);mode-seeking 性质确保 $\pi_\theta$ 不在参考策略概率为零的地方分配概率(防止奖励黑客)。$\beta = 0.04$ 是奖励追求和约束保守性的平衡点。
第四,高斯分布之间的 KL 散度有解析解,直接支撑了 VAE 的训练(ELBO 中的 KL 正则化项),无需采样估计梯度。同样是反向 KL,后验近似先验,防止潜变量空间坍塌。
第五,JS 散度是对称化的 KL,有界($\leq \log 2$),GAN 的原始目标等价于最小化 JS 散度。但支撑不重叠时 JS 散度梯度为零(KL 散度为无穷),这是 GAN 训练不稳定的信息论根源,推动了 Wasserstein GAN 的发展。
下一篇预告:
第17篇,我们进入第四阶段:优化算法,从最朴素的梯度下降讲起——为什么沿负梯度方向走能让损失减小?学习率怎么选?为什么全量梯度下降在深度学习里不实用?
KL 散度不只是一个公式,它是一种看待"差异"的方式——不是对称的几何距离,而是有方向的信息代价。DeepSeek R1 选择 $D_{KL}(\pi_\theta \| \pi_{ref})$ 而不是反过来,不是随意为之,是基于计算可行性和模式寻求性质的精确数学选择。理解了方向,就理解了为什么强化学习能在保留语言能力的同时提升推理能力。
还没有评论,来抢沙发吧!
博客管理员
40 篇文章
还没有评论,来抢沙发吧!