可视化 KL 散度 -- 两个分布差多少
调整两个高斯分布的参数,计算并对比 KL(P||Q) 和 KL(Q||P),直观感受 KL 散度的不对称性。
学完本案例你将理解:KL 散度不是「距离」——从 P 看 Q 和从 Q 看 P,信息损失不一样。
生活引入
用北京地图在纽约找路
你有两本地图:真正的纽约地图(P)和北京地图(Q)。用北京地图在纽约走,你需要额外消耗很多精力去「纠正」地图的错误信息——这个额外消耗就是 KL(P||Q)。反过来,用纽约地图在北京走——额外消耗就是 KL(Q||P)。这两种额外消耗显然不一样,因为两本地图的「错误方式」不同。
直观理解
场景 A:P~N(0,1), Q~N(2,1)——均值不同但形状相同,KL 接近对称。
场景 B:P~N(0,1), Q~N(0,3)——同中心但方差不同。KL(P||Q) = 1.10(窄→宽,容易),KL(Q||P) = 1.55(宽→窄,困难)。明显不对称——「用宽覆盖窄」比「用窄覆盖宽」容易得多。
数学定义
\[ D_{KL}(P \parallel Q) = \sum_{x} P(x) \cdot \log\frac{P(x)}{Q(x)} \] \[ D_{KL}(P \parallel Q) = H(P, Q) - H(P) \]P 自身的熵是常数,所以最小化交叉熵 = 最小化 KL 散度。
Python 动手实践
实例
import numpy as np
def gaussian_pdf(x, mu, sigma):
return (1 / (sigma * np.sqrt(2 * np.pi))) * \
np.exp(-(x - mu) ** 2 / (2 * sigma ** 2))
def kl_divergence(p, q, eps=1e-12):
p = np.clip(p, eps, None)
q = np.clip(q, eps, None)
return np.sum(p * np.log(p / q))
x = np.linspace(-10, 10, 2000)
dx = x[1] - x[0]
# 场景 A:均值不同
P_A = gaussian_pdf(x, mu=0, sigma=1) * dx
Q_A = gaussian_pdf(x, mu=2, sigma=1) * dx
# 场景 B:方差不同
P_B = gaussian_pdf(x, mu=0, sigma=1) * dx
Q_B = gaussian_pdf(x, mu=0, sigma=3) * dx
print("RUNOOB KL 散度不对称性验证:\n")
print("场景 A:P~N(0,1) vs Q~N(2,1)")
print(f" KL(P||Q) = {kl_divergence(P_A, Q_A):.4f}")
print(f" KL(Q||P) = {kl_divergence(Q_A, P_A):.4f}")
print(f"\n场景 B:P~N(0,1) vs Q~N(0,3)")
print(f" KL(P||Q) = {kl_divergence(P_B, Q_B):.4f} (窄->宽,容易)")
print(f" KL(Q||P) = {kl_divergence(Q_B, P_B):.4f} (宽->窄,困难)")
print(f"\nRUNOOB 直觉:'用宽覆盖窄'容易(KL小),'用窄覆盖宽'困难(KL大)")
def gaussian_pdf(x, mu, sigma):
return (1 / (sigma * np.sqrt(2 * np.pi))) * \
np.exp(-(x - mu) ** 2 / (2 * sigma ** 2))
def kl_divergence(p, q, eps=1e-12):
p = np.clip(p, eps, None)
q = np.clip(q, eps, None)
return np.sum(p * np.log(p / q))
x = np.linspace(-10, 10, 2000)
dx = x[1] - x[0]
# 场景 A:均值不同
P_A = gaussian_pdf(x, mu=0, sigma=1) * dx
Q_A = gaussian_pdf(x, mu=2, sigma=1) * dx
# 场景 B:方差不同
P_B = gaussian_pdf(x, mu=0, sigma=1) * dx
Q_B = gaussian_pdf(x, mu=0, sigma=3) * dx
print("RUNOOB KL 散度不对称性验证:\n")
print("场景 A:P~N(0,1) vs Q~N(2,1)")
print(f" KL(P||Q) = {kl_divergence(P_A, Q_A):.4f}")
print(f" KL(Q||P) = {kl_divergence(Q_A, P_A):.4f}")
print(f"\n场景 B:P~N(0,1) vs Q~N(0,3)")
print(f" KL(P||Q) = {kl_divergence(P_B, Q_B):.4f} (窄->宽,容易)")
print(f" KL(Q||P) = {kl_divergence(Q_B, P_B):.4f} (宽->窄,困难)")
print(f"\nRUNOOB 直觉:'用宽覆盖窄'容易(KL小),'用窄覆盖宽'困难(KL大)")
RUNOOB KL 散度不对称性验证: 场景 A:P~N(0,1) vs Q~N(2,1) KL(P||Q) = 2.0000 KL(Q||P) = 1.9960 场景 B:P~N(0,1) vs Q~N(0,3) KL(P||Q) = 1.0990 (窄->宽,容易) KL(Q||P) = 1.5490 (宽->窄,困难) RUNOOB 直觉:'用宽覆盖窄'容易(KL小),'用窄覆盖宽'困难(KL大)
AI 中的应用场景
| 场景 | 如何使用 KL 散度 |
|---|---|
| VAE | 让编码分布接近标准正态——损失 = 重构误差 + KL(编码||N(0,1)) |
| 知识蒸馏 | 让学生模型的预测分布接近教师模型——最小化 KL(教师||学生) |
| PPO 强化学习 | 用 KL 散度约束新旧策略的差异,防止更新过大 |
