信息论 信息论对信息、意外程度以及概率分布之间的差异进行量化。本文件介绍熵、交叉熵、KL 散度、互信息和自信息——这些概念是机器学习中每一个分类损失函数、VAE 目标函数和数据压缩方案背后的基础。 信息论由克劳德·香农(Claude Shannon)于 1948 年创立,为我们提供了一套量化信息的数学框架。它能回答这样的问题:你对一个事件应该有多意外?一条消息携带了多少信息?两个概率分布有多大差别? 这些问题听起来抽象,但它们是 ML 损失函数、数据压缩和通信系统的基础。交叉熵损失——分类中最常见的损失函数——就直接来自信息论。 从最简单的问题开始:单个事件携带了多少信息? 自信息(surprisal,也叫 self-information)度量一个事件有多令人意外。
信息论对信息、意外程度以及概率分布之间的差异进行量化。本文件介绍熵、交叉熵、KL 散度、互信息和自信息——这些概念是机器学习中每一个分类损失函数、VAE 目标函数和数据压缩方案背后的基础。
信息论由克劳德·香农(Claude Shannon)于 1948 年创立,为我们提供了一套量化信息的数学框架。它能回答这样的问题:你对一个事件应该有多意外?一条消息携带了多少信息?两个概率分布有多大差别?
这些问题听起来抽象,但它们是 ML 损失函数、数据压缩和通信系统的基础。交叉熵损失——分类中最常见的损失函数——就直接来自信息论。
从最简单的问题开始:单个事件携带了多少信息?
**自信息(surprisal,也叫 self-information)**度量一个事件有多令人意外。如果一件很可能的事发生了,你几乎学不到什么;如果一件罕见的事发生了,你学到的就很多。
如果你住在沙漠,有人告诉你今天晴天,这没多大信息量。但如果他告诉你今天下雪了,那就极有信息量。自信息把这种直觉形式化:
当我们用 \log_2 时,单位是比特(bits)。一次均匀硬币投掷的自信息是 -\log_2(0.5) = 1 比特。一个概率为 1/8 的事件自信息是 \log_2(8) = 3 比特。
为什么用对数而不用 1/p?有三个理由:
**熵(entropy)**是自信息的期望,也就是你从某个分布中每采样一个事件平均获得的信息量。它衡量分布的不确定性或"不可预测性":
一枚均匀硬币的熵是 H = -0.5\log_2(0.5) - 0.5\log_2(0.5) = 1 比特。不确定性最大。
一枚偏硬币(p = 0.9)的熵是 H = -0.9\log_2(0.9) - 0.1\log_2(0.1) \approx 0.469 比特。不确定性更小,所以熵也更小。
一个确定性事件(p = 1)的熵是 H = 0。毫无不确定性。
当所有结果等可能时,熵达到最大。对 n 个等可能结果,H = \log_2 n。一颗均匀骰子的熵是 \log_2 6 \approx 2.585 比特。
熵的实际含义是压缩。香农的信源编码定理说,你不能在不丢失信息的前提下,把数据压缩到低于它的熵率。一张每个像素都等可能的图像(最大熵)无法压缩。一张大部分是白色的图像(低熵)则压缩得很好。
对量级有个直观感受:一个灰度像素(256 个取值)的最大熵是 8 比特。一张 1080p 灰度图像最多有 1920 \times 1080 \times 8 \approx 1660 万比特。真实图像的熵要低得多,因为相邻像素是相关的,这也是 JPEG 压缩有效的原因。
对连续随机变量,离散求和就变成了积分。**微分熵(differential entropy)**是:
方差为 \sigma^2 的高斯分布的微分熵是 h = \frac{1}{2}\log_2(2\pi e \sigma^2)。在所有方差相同的分布中,高斯分布的熵最大。这正是高斯分布在建模中如此常见的一个原因:在给定均值和方差之外,它做的额外假设最少。
**互信息(mutual information)**度量知道一个变量能告诉你多少关于另一个变量的信息。它是观察到 Y 后关于 X 的不确定性的减少量:
如果 X 和 Y 独立,p(x,y) = p(x)p(y),互信息为零。它们越相关,互信息越大。
在 ML 中,互信息用于特征选择(挑选与目标互信息高的特征)、信息瓶颈方法,以及评估聚类质量。
**交叉熵(cross-entropy)**衡量的是:用为分布 q 优化的编码去编码来自分布 p 的事件,平均需要多少比特:
如果 q 与 p 完美匹配,交叉熵就等于熵:H(p, p) = H(p)。如果 q 是个糟糕的近似,交叉熵就更高。多出来的比特来自这种不匹配。
这正是交叉熵成为 ML 分类标准损失函数的原因。真实标签定义了 p(一个 one-hot 分布),模型预测的概率定义了 q。最小化交叉熵就是把 q 推向 p:
对真实类别为 c 的单个样本,这简化为 \mathcal{L} = -\log \hat{y}_c。损失就是真实类别在模型预测下的自信息。如果模型给正确类别很高的概率,损失就很低。
**KL 散度(KL divergence,Kullback-Leibler divergence,也叫相对熵 relative entropy)**衡量一个分布与另一个分布相差多大:
KL 散度不对称:D_{\text{KL}}(p \| q) \ne D_{\text{KL}}(q \| p)。这种不对称性很重要。D_{\text{KL}}(p \| q) 会惩罚 q 在 p 概率高的地方给了低概率(因为 \log(p/q) 会爆炸)。D_{\text{KL}}(q \| p) 则惩罚相反的情形。
这种不对称性带来了两种近似风格:
由于 H(p) 相对于模型是常数,最小化交叉熵 H(p, q) 等价于最小化 D_{\text{KL}}(p \| q)。这就是为什么我们可以使用交叉熵损失,同时确信我们也在最小化真实分布与预测分布之间的 KL 散度。
KL 散度在贝叶斯更新中扮演核心角色。后验 P(\theta | D) 是在所有与观测数据一致的分布中,最接近先验 P(\theta)(以 KL 散度衡量)的那个分布。每一次新观测都更新后验,降低关于 \theta 的不确定性。
在变分自编码器(VAE)中,损失函数有两项:一个重构损失(交叉熵)和一个 KL 散度项,后者正则化隐空间使其接近标准正态分布。
把一切串起来:熵告诉你一个分布内在的不确定性,交叉熵告诉你模型对现实的近似有多好,而 KL 散度告诉你两者之间的差距。这三个量构成了现代 ML 优化的骨干。
import jax.numpy as jnp def entropy(p): """以比特为单位计算熵。过滤掉零概率事件。""" p = p[p > 0] return -jnp.sum(p * jnp.log2(p)) # 均匀骰子 fair = jnp.ones(6) / 6 print(f"Fair die entropy: {entropy(fair):.4f} bits (max = log2(6) = {jnp.log2(6.):.4f})") # 灌铅骰子 loaded = jnp.array([0.1, 0.1, 0.1, 0.1, 0.1, 0.5]) print(f"Loaded die entropy: {entropy(loaded):.4f} bits") # 确定性 det = jnp.array([0.0, 0.0, 0.0, 0.0, 0.0, 1.0]) print(f"Deterministic: {entropy(det):.4f} bits") # 均匀硬币 coin = jnp.array([0.5, 0.5]) print(f"Fair coin entropy: {entropy(coin):.4f} bits")
import jax.numpy as jnp def cross_entropy(p, q): return -jnp.sum(p * jnp.log2(jnp.clip(q, 1e-10, 1.0))) def kl_divergence(p, q): mask = p > 0 return jnp.sum(jnp.where(mask, p * jnp.log2(p / jnp.clip(q, 1e-10, 1.0)), 0.0)) def entropy(p): p = p[p > 0] return -jnp.sum(p * jnp.log2(p)) p = jnp.array([0.4, 0.3, 0.2, 0.1]) # 真实分布 for name, q in [("perfect match", p), ("slight mismatch", jnp.array([0.35, 0.30, 0.25, 0.10])), ("big mismatch", jnp.array([0.1, 0.1, 0.1, 0.7]))]: h_p = entropy(p) h_pq = cross_entropy(p, q) kl = kl_divergence(p, q) print(f"{name:20s}: H(p)={h_p:.4f}, H(p,q)={h_pq:.4f}, " f"KL={kl:.4f}, H(p,q)-H(p)={h_pq-h_p:.4f}")
import jax.numpy as jnp def kl_div(p, q): mask = p > 0 return float(jnp.sum(jnp.where(mask, p * jnp.log2(p / jnp.clip(q, 1e-10, 1.0)), 0.0))) p = jnp.array([0.9, 0.1]) q = jnp.array([0.5, 0.5]) print(f"D_KL(p || q) = {kl_div(p, q):.4f}") print(f"D_KL(q || p) = {kl_div(q, p):.4f}") print(f"Not the same! KL divergence is asymmetric.")
import jax.numpy as jnp import matplotlib.pyplot as plt # 真实标签:4 个类别中的第 2 类 true_label = jnp.array([0, 0, 1, 0]) # 模拟越来越好的预测 steps = [] losses = [] for confidence in jnp.linspace(0.25, 0.99, 50): # 模型对第 2 类越来越有信心 remaining = (1 - confidence) / 3 pred = jnp.array([remaining, remaining, confidence, remaining]) loss = -jnp.sum(true_label * jnp.log(jnp.clip(pred, 1e-10, 1.0))) steps.append(float(confidence)) losses.append(float(loss)) plt.figure(figsize=(8, 4)) plt.plot(steps, losses, color="#e74c3c", linewidth=2) plt.xlabel("Model confidence in true class") plt.ylabel("Cross-entropy loss") plt.title("Cross-entropy loss decreases as predictions improve") plt.grid(alpha=0.3) plt.show()