信息论里最容易被"学过就忘"的四个概念,恰好也是实际工作里出现频率最高的四个:熵、相对熵、互信息、链式法则。我第一次啃《Elements of Information Theory》的时候,每个定义都能看懂,合上书就不知道能拿来干什么。真正把它们用起来是在做模型调参、特征筛选和日志压缩的那段时间——损失函数为什么长得那样、数据该按哪个维度切、两个指标到底有没有冗余,这些问题的答案都在这四个概念里。
这篇内容面向三类人:正在准备课程或者面试、需要把公式推导捋顺的学生;天天调交叉熵损失却说不清它从哪来的工程同学;以及想用信息论做特征筛选、做分布对比的算法从业者。下面我会把四个概念的推导链条、参数计算、Python 和 MATLAB 的落地写法、以及我自己踩过的数值坑,按能直接抄作业的方式展开。
1. 先把这四个概念的位置摆清楚
1.1 从一次模型不收敛的排查说起
前两年帮一个朋友看他训练的二分类模型,现象很怪:训练集 loss 稳定下降,验证集 loss 从第三个 epoch 开始往上飘,模型输出的概率几乎全挤在 0.9 以上。他换了好几版网络结构都没用。我让他把标签分布打出来一看,正样本占 92%。问题瞬间清楚了——他用的普通交叉熵,模型只要学会"全猜正类",就能拿到很低的 loss,梯度也就没什么动力再去区分难样本。
这里的每一步判断都用到信息论的语言:标签分布的熵只有约 0.4 bit,说明这个任务本身携带的判别信息量就少;模型的预测分布和真实标签分布之间的相对熵没人去约束,只在均值上对齐;正负样本与特征的互信息没有被显式关注,模型自然会走捷径;而链式法则告诉我们,联合分布 $P(X,Y)$ 的熵可以拆成 $H(Y) + H(X|Y)$,其中 $H(Y)$ 这一项小得可怜,剩下那一项才是真正能榨出性能的部分。
把问题翻译成信息论语言之后,解决方案就变得很直接:加类别权重、或者换成 focal loss 这类对易分类样本降权的形式。改完之后验证 loss 的曲线立刻正常了。我没有动一行网络结构代码,动的是对目标函数的理解。
这段经历给我的教训是,信息论不是一门"学完就能放着"的理论课,它更像是诊断工具箱。你不需要每天都用,但一旦遇到分布层面的问题,它能把"感觉哪里不对"变成"具体哪一项不对"。
1.2 四者之间的等式关系网
很多人记不住这几个概念,是因为把它们当成四个孤立定义背。实际上它们是一张等式网,记住其中两三条主线,其余都能推出来。
主线一,熵与条件熵的关系:
$$H(X,Y) = H(X) + H(Y|X) = H(Y) + H(X|Y)$$
主线二,相对熵把熵和交叉熵串起来:
$$H(p,q) = H(p) + D_{KL}(p | q)$$
这条式子非常关键,它说明交叉熵损失里"真实分布的熵"这一项对参数求导是常数,真正被优化的只有 KL 散度。
主线三,互信息本身就是一种特殊的相对熵:
$$I(X;Y) = D_{KL}\big(p(x,y) ,|, p(x)p(y)\big)$$
这条式子把互信息解释得特别直白:互信息衡量的是"联合分布离独立有多远"。两个变量完全独立时,联合分布等于边缘分布之积,KL 散度为 0,互信息也就是 0。
主线四,互信息和熵的三种等价写法:
$$I(X;Y) = H(X) - H(X|Y) = H(Y) - H(Y|X) = H(X) + H(Y) - H(X,Y)$$
这几条式子在做特征筛选时极其有用,因为三种写法的计算成本完全不同,实际工程里要根据数据规模选最省的那条。
提示:不要试图把这十几个公式全背下来。先记住"互信息等于不确定性的减少量"这一句,其余推导都能从这句出发。
1.3 符号约定与读法
为了让后面的推导不产生歧义,这里统一下约定,你后面看到不合常规的地方可以直接套用这套规则。
随机变量用大写字母 $X, Y$ 表示,具体取值用小写 $x, y$,概率质量函数写作 $p(x)$,联合分布写作 $p(x,y)$。对数默认以 2 为底,单位是比特(bit);如果在代码里用自然对数,单位是奈特(nat),两者相差一个常数因子 $\ln 2 \approx 0.693$。这一条看起来是小事,但在跨团队对齐时经常出岔子——A 同学报的熵是 3.2 bit,B 同学用 Python 算出来是 2.2 nat,两个人对着同一份数据吵了半天,最后发现只是底数不同。
求和号 $\sum_x$ 表示对所有可能取值遍历,连续情形下换成积分 $\int$。当出现 $0 \log 0$ 这种形式时,按极限约定取值为 0,因为在代码里直接算会得到 NaN,这个坑后面单独讲。
2. 熵:不确定性到底该怎么量
2.1 自信息:为什么偏偏用负对数
要理解熵,得先理解它的积木块——自信息。一个事件发生的概率越小,它带给我们的"信息量"就越大,这是常识。你说"太阳明天从东边升起",没人觉得你提供了信息;你说"明天要下雪"(在南方城市),大家都会抬头看你一眼。
把这个直觉形式化,需要满足三个条件:概率为 1 的事件信息量为 0;概率越小信息量越大,即单调递减;两个独立事件同时发生的信息量等于各自信息量之和。满足这三条的连续函数,在归一化之后只有一种形式:
$$I(x) = -\log p(x)$$
我当初最不理解的是第三条为什么必须是"相加"。举个具体例子:抛两枚独立硬币,同时猜中两枚的概率是 1/4。你希望"猜中两枚"的信息量是"猜中一枚"的两倍,而概率从 1/2 变成 1/4 是乘了个 1/2。要把乘法变成加法,对数就是唯一的选择。所以不是"偏偏"用对数,而是加法性和单调性这两个要求把对数逼出来了。
至于那个负号,纯粹是因为概率在 0 到 1 之间,对数结果是负的,加个负号让信息量变成正数,方便叙述。
2.2 熵的三种等价理解
自信息是单个事件的,熵是它的期望:
$$H(X) = -\sum_x p(x) \log p(x) = \mathbb{E}_{x \sim p}\big[-\log p(x)\big]$$
同一个式子,可以从三个不同角度去理解,我建议你三个都过一遍。
角度一,平均意外程度。熵高说明每次抽样你都很难预测结果,平均下来"惊讶"的次数多。均匀分布的熵最大,因为每种结果一样难以预测。
角度二,最优编码的平均码长。香农信源编码定理告诉我们,对服从分布 $p$ 的符号做无损编码,平均码长的下界就是熵,而且用霍夫曼编码可以做到熵加 1 以内的码长。这是熵最有工程味道的解释,后面讲链式法则和压缩时还会用到。
角度三,不确定性的体积。如果 $X$ 有 $n$ 种等可能取值,熵就是 $\log n$。所以熵的对数形式可以看成是"用多少比特的存储空间来描述这个随机变量"。
三种理解指向同一个数,但适用场景不同。做压缩的时候我想角度二,做实验设计的时候我想角度一,做量化指标比较的时候我想角度三。同一个概念有不同的心智模型,用起来才不别扭。
2.3 手算一遍,再用 Python 和 MATLAB 验证
光看公式容易飘,手算一个最简单的例子能立刻落地。假设 $X$ 服从二元分布,$p(1)=0.9$,$p(0)=0.1$:
$$H(X) = -0.9\log_2 0.9 - 0.1\log_2 0.1 \approx 0.9 \times 0.152 + 0.1 \times 3.322 \approx 0.469 \text{ bit}$$
对比均匀分布 $p=0.5$ 时的 $H = 1$ bit。可以看到高度倾斜的分布熵确实低得多。这个 0.469 就是前面那个 92% 正样本例子里 $H(Y)$ 量级相当的数字。
Python 版本,从频率估计熵是日常最高频的操作:
import numpy as np def entropy_from_counts(counts, base=2): counts = np.asarray(counts, dtype=np.float64) counts = counts[counts > 0] # 关键:先剔除零计数 p = counts / counts.sum() return -np.sum(p * np.log(p)) / np.log(base) # 一维离散数据的熵 data = np.array([0, 1, 1, 2, 2, 2, 3, 3, 3, 3]) labels, counts = np.unique(data, return_counts=True) print(entropy_from_counts(counts)) # 约 1.846 bit关于"matlab 中怎么计算一维数据信息熵"这个问题,是搜索里出现得很频繁的一条,我把完整写法放在这里:
function H = entropy1d(x, base) if nargin < 2, base = 2; end x = x(:); [~, ~, idx] = unique(x); n = numel(idx); counts = accumarray(idx, 1, [max(idx), 1]); p = counts / n; p = p(p > 0); % 零概率必须剔除 H = -sum(p .* log(p)) / log(base); end调用就是H = entropy1d(randi([0,3], 1, 1000));。这里有几个细节值得说清楚。
第一,accumarray比histcounts更适合处理已经是整数标签的数据,速度更快;如果是连续值,先做分箱再统计。
第二,p > 0这一步不能省。MATLAB 里0 * log(0)得到的是NaN,会把整个求和污染掉。Python 的numpy同样会给出RuntimeWarning和nan。
第三,如果你统计的是连续变量的"微分熵",用直方图估计会引入偏差,箱数选得太多会让熵被高估,选得太少会低估。实践上我一般先用 Freedman-Diaconis 规则定箱宽,再乘一个 0.8 的收缩系数,经验上比较稳。
2.4 最大熵、单位与常见误区
均匀分布的熵最大,这件事有个漂亮的证明,用 Jensen 不等式三行就能推完:因为 $-\log$ 是凸函数,所以 $\mathbb{E}[-\log p(X)] \geq -\log \mathbb{E}[p(X)] = -\log(1/n) = \log n$。这个结论在实际建模里叫最大熵原理——在只掌握部分约束的前提下,选熵最大的那个分布作为假设,因为它是"最少添加额外信息"的那个。
单位问题在上一小节提过,这里再强调一次:Python 的scipy.stats.entropy默认用自然对数,返回 nat;sklearn.metrics.mutual_info_score用自然对数;而信息论教材默认 bit。跨工具对比数字之前,先确认底数,这个习惯能省掉很多无谓的争论。
最后一个高频误区:把熵当成"数据质量指标"。熵高不等于数据差,熵低也不等于数据好。一个恒定的常数序列熵为 0,但它没有任何分类价值;一个完全随机的标签序列熵为 1 bit(二分类),但它同样没有价值。熵描述的是分布本身的性质,是否"有用"要看它和别的变量的关系,这就引出了互信息。
3. 相对熵:KL散度为什么不是"距离"
3.1 定义与三步推导
相对熵,也叫 KL 散度,衡量的是"用分布 $q$ 去近似真实分布 $p$ 时,多付出的代价":
$$D_{KL}(p | q) = \sum_x p(x) \log \frac{p(x)}{q(x)}$$
很多教程直接甩出这个式子,然后说"它非负"。我更喜欢自己推一遍,因为推导过程本身就解释了它为什么不是距离。
第一步,把式子改写:
$$D_{KL}(p|q) = \sum_x p(x) \log\frac{p(x)}{q(x)} = -\sum_x p(x)\log\frac{q(x)}{p(x)} = -\mathbb{E}_p\left[\log\frac{q(X)}{p(X)}\right]$$
第二步,用 Jensen 不等式。$-\log$ 是凸函数,所以 $\mathbb{E}[f(Z)] \geq f(\mathbb{E}[Z])$,取 $f = -\log$:
$$-\mathbb{E}_p\left[\log\frac{q(X)}{p(X)}\right] \geq -\log \mathbb{E}_p\left[\frac{q(X)}{p(X)}\right]$$
第三步,展开期望里的那项:
$$\mathbb{E}_p\left[\frac{q(X)}{p(X)}\right] = \sum_x p(x)\frac{q(x)}{p(x)} = \sum_x q(x) = 1$$
代入得 $D_{KL}(p|q) \geq -\log 1 = 0$。等号成立当且仅当 $p = q$ 处处成立。
三步里最容易被忽略的是第三步那个求和等于 1。这一步隐含了一个前提:$q$ 必须在 $p$ 的支撑集上是正的。如果某个 $x$ 满足 $p(x) > 0$ 但 $q(x) = 0$,KL 散度直接变成无穷大。这个"无穷大"在实际训练里会变成梯度爆炸或者 NaN,后面数值稳定性那节会专门讲怎么处理。
至于为什么不是距离,看一眼定义就明白了:$D_{KL}(p|q)$ 和 $D_{KL}(q|p)$ 通常不相等,而且它不满足三角不等式。所以严格来说它是"散度"而不是"距离"。这个区别不是抠字眼,前向和反向的优化行为差得非常远。
3.2 交叉熵从哪冒出来的
把 KL 散度的定义式拆开,就得到交叉熵:
$$D_{KL}(p|q) = \sum_x p(x)\log p(x) - \sum_x p(x)\log q(x) = -H(p) + H(p,q)$$
移项之后:
$$H(p,q) = H(p) + D_{KL}(p|q)$$
这就是前面提到的主线二。交叉熵 $H(p,q)$ 由两部分组成:真实分布自身的熵 $H(p)$,加上两个分布的差异 $D_{KL}(p|q)$。在做监督学习时,$p$ 是固定的标签分布,$H(p)$ 对模型参数求导为 0,所以最小化交叉熵等价于最小化 KL 散度。这是几乎所有分类损失函数的理论根据。
追问一句:既然二者等价,为什么不直接用 KL 散度?答案是计算量。KL 散度要算 $H(p)$,而 $H(p)$ 需要遍历所有类别求和,交叉熵则可以写成 $-y\log \hat{y}$ 这样简洁的形式,少算一项,工程上更省。这就是"理论上更本质,工程上更便宜"的典型案例。
再说说 BCE 的设计原理。二分类时,真实分布是伯努利分布,模型输出 $\hat{y} = \sigma(z)$,交叉熵展开就是:
$$\text{BCE} = -y\log\sigma(z) - (1-y)\log(1-\sigma(z))$$
对它求导要过一层 sigmoid,用 $\sigma'(z) = \sigma(z)(1-\sigma(z))$ 化简,会发现:当 $y=1$ 时梯度是 $\sigma(z) - 1$,当 $y=0$ 时梯度是 $\sigma(z)$,统一写成 $\hat{y} - y$。这个形式极其干净——预测与真实值的差,就是梯度。这也是 BCE 配合 sigmoid 比配合其他激活函数更好用的深层原因:误差结构被 sigmoid 的导数刚好抵消掉了。
3.3 从GAN公式追问那个负号
"原始 GAN 公式的交叉熵为什么没有负号",这个问题在网上重复率很高,我在几个社区都见过。答案其实很朴素:因为GAN 的目标是最大化,而不是最小化。
标准交叉熵损失取的是负对数似然,形式是 $-\log \hat{y}$,因为我们要最小化它。GAN 判别器的目标写成:
$$\max_D ; \mathbb{E}{x\sim p{data}}[\log D(x)] + \mathbb{E}_{z}[\log(1 - D(G(z)))]$$
把它改写成最小化问题,加上负号,就变成:
$$\min_D ; -\mathbb{E}[\log D(x)] - \mathbb{E}[\log(1-D(G(z)))]$$
这正好是两个交叉熵之和:前一项是"把真实样本判为真"的交叉熵,后一项是"把生成样本判为假"的交叉熵。所以不是 GAN 不用负号,而是负号已经在max这个记号里被吸收掉了。理解这一点之后,再去看各种 GAN 变体的目标函数,就不会被符号绕晕。
注意:写代码的时候,
torch.nn.BCEWithLogitsLoss内部已经把 sigmoid 和 BCE 融合在一起,并且做了 log-sum-exp 稳定化处理。不要自己先sigmoid再BCELoss,那样既慢又容易在极端值上溢出。
3.4 前向KL与反向KL:一个均值,一个众数
这是 KL 散度最反直觉、也最有意思的地方。假设你要用一个简单的分布 $q$ 去拟合一个双峰的真实分布 $p$:
最小化 $D_{KL}(p|q)$(前向 KL),会得到 mode-covering 行为。原因是 $p(x) > 0$ 而 $q(x) \to 0$ 的位置会被处以极大的惩罚,所以 $q$ 不敢漏掉任何 $p$ 有质量的地方,结果就是 $q$ 铺得很宽,把两个峰都覆盖住,中间会填上不少概率密度。
最小化 $D_{KL}(q|p)$(反向 KL),会得到 mode-seeking 行为。这时惩罚来自 $q(x) > 0$ 而 $p(x) \to 0$ 的位置,所以 $q$ 宁愿只贴住其中一个峰,也不敢往 $p$ 没有质量的地方伸。结果通常是 $q$ 塌缩到某一个峰上。
用代码验证这件事只需要几行:
import numpy as np x = np.linspace(-6, 6, 2000) dx = x[1] - x[0] p = 0.5*np.exp(-(x-2)**2/2) + 0.5*np.exp(-(x+2)**2/2) p = p/ (p.sum()*dx) def kl(p, q): q = np.clip(q, 1e-12, None) return np.sum(p * np.log(p/q)) * dx # 用一个单高斯族做拟合,网格搜索均值和方差 best = None for mu in np.linspace(-3, 3, 121): for sd in np.linspace(0.3, 4.0, 80): q = np.exp(-(x-mu)**2/(2*sd**2)) / (sd*np.sqrt(2*np.pi)) fwd = kl(p, q) # D(p||q) rev = kl(q, p) # D(q||p) if best is None: best = {'f': (fwd, mu, sd), 'r': (rev, mu, sd)} else: if fwd < best['f'][0]: best['f'] = (fwd, mu, sd) if rev < best['r'][0]: best['r'] = (rev, mu, sd) print('前向KL最优解:', best['f']) print('反向KL最优解:', best['r'])跑出来的典型结果是:前向 KL 的最优均值接近 0(落在两峰之间),标准差约 2.3 左右;反向 KL 的最优均值会偏向 ±2 中的某一个,标准差小得多。这个实验我做给好几个同事看过,比任何文字解释都直观。
理解了这一点,你就能明白变分推断为什么用反向 KL,以及为什么它倾向于给出过于自信的后验。
4. 互信息:两个变量共享了多少信息
4.1 三种计算路径与选择标准
互信息的定义有好几种写法,实际上对应三种不同的计算成本,工程上要按数据规模选。
第一种,从熵的角度:$I(X;Y) = H(X) + H(Y) - H(X,Y)$。需要估计三个熵,适合变量取值空间小、样本量大的场景,比如用户标签、商品类目这类离散特征。
第二种,从条件熵的角度:$I(X;Y) = H(X) - H(X|Y)$。语义上最直观——"知道 $Y$ 之后,$X$ 的不确定性减少了多少"。做特征筛选时我一般用这个写法来解释结果。
第三种,从 KL 的角度:$I(X;Y) = D_{KL}(p(x,y) | p(x)p(y))$。这个写法在推导理论性质时最方便,比如证明数据处理不等式——对 $X$ 或 $Y$ 做任意确定性变换,互信息只会减少不会增加。这条性质有一个很实际的推论:特征工程做错了只会丢信息,不会凭空造出信息。所以当你发现加了某个"巧妙"的特征反而让模型变差,问题大概率出在优化环节,而不是信息量不足。
4.2 和相关系数比,互信息强在哪
相关系数只能捕捉线性关系,这是它的硬伤。构造一个最简单也最经典的反例:$X \sim U(-1,1)$,$Y = X^2$。这两个变量存在完全确定的函数关系,但相关系数算出来是 0。
import numpy as np from sklearn.feature_selection import mutual_info_regression rng = np.random.default_rng(0) x = rng.uniform(-1, 1, 20000) y = x**2 + rng.normal(0, 0.01, 20000) print('相关系数:', np.corrcoef(x, y)[0, 1]) print('互信息:', mutual_info_regression(x.reshape(-1,1), y, random_state=0)[0])相关系数会给出接近 0 的值,而互信息会给出一个明显大于 0 的值(具体数值取决于估计器参数,一般在 0.5 到 1.5 nat 之间)。这个例子我在做特征分析脚本时写成了单元测试,用来防止有人误把相关系数当成通用依赖度量。
互信息的另一个优势是对单调变换不变,严格说是对可逆变换不变。你把某个特征乘以 1000,或者取对数(在定义域为正的前提下),互信息基本不变,但相关系数可能会因为非线性而改变。这一点在数据尺度不统一的场景下非常实用。
不过互信息也有代价:它的估计比相关系数难得多,样本量需求大,而且不同估计器给出的数值差异可能很大。相关系数是个"便宜但有偏"的指标,互信息是"贵但通用"的指标,选哪个取决于你在分析阶段还是建模阶段。
4.3 用互信息做特征筛选的完整流程
离散标签、离散特征的场景最简单,直接用sklearn的mutual_info_classif:
import numpy as np from sklearn.feature_selection import mutual_info_classif from sklearn.datasets import make_classification X, y = make_classification(n_samples=5000, n_features=60, n_informative=6, n_redundant=10, random_state=42) mi = mutual_info_classif(X, y, discrete_features=False, n_neighbors=5, random_state=0) idx = np.argsort(mi)[::-1] print('Top10 特征:', idx[:10]) print('Top10 互信息:', np.round(mi[idx[:10]], 4))几个参数值得说明。n_neighbors默认是 3,我一般调到 5 到 8,因为较大的邻居数能降低方差,但会引入一些偏差;在样本量上千之后,5 是个比较稳的平衡点。discrete_features一定要设对,如果连续特征被当成离散处理,估计器会按唯一值个数去切分,结果完全失真。
筛选策略上,我踩过一个坑:早期我按互信息排序后直接取 Top-K,结果被选中的特征是几个高度相关的"兄弟特征",它们各自的互信息都很高,但提供的是同一份信息。后来改成两步走——先按互信息排序,再对入选特征做一次贪心去冗余:逐个加入候选特征,只有当它与已选特征的平均互信息低于阈值时才保留。这个改动让模型在相同特征数下的 AUC 提升了约 0.02,比调网络结构划算得多。
提示:互信息筛选和 PCA 这类线性降维不冲突。我的常规顺序是先用互信息剔掉明显无关的特征,再做标准化和降维。反过来做的话,降维后的主成分含义模糊,互信息就没法解释了。
4.4 高维下的估计难题与KSG
连续变量的互信息估计是个真正的难点。直方图法在二维还可以,到了五六维就彻底失效——样本被稀释到每个箱子里只有零头几个点,估计出的熵偏差极大。
目前的常用方案是 Kraskov 等人提出的 KSG 估计器,核心思路是不去直接估计熵,而是利用互信息等于 $H(X) + H(Y) - H(X,Y)$ 的关系,让三个熵的偏差项相互抵消。sklearn的mutual_info_regression内部用的就是这套方法。
它的核心参数是n_neighbors,含义是在联合空间中寻找第 k 个最近邻的半径,然后用这个半径去数边缘空间中的邻居个数。实现上会用 KD 树或者球树加速,所以样本量上万之后速度会明显变慢,这时候我会先做随机下采样再估计,代价是方差变大但趋势仍然可靠。
这里有个很容易掉的坑:KSG 估计的结果有可能是负数。理论证明互信息非负,但估计器有方差,在小样本或者变量确实独立的情况下,输出 -0.02 这种值是正常的。处理方式很简单,把负值截断到 0 再排序,不要在报告里出现负的互信息,否则会被质疑。
5. 链式法则:把复杂的联合分布拆成积木
5.1 从两个变量推广到 n 个
链式法则的基本形式是:
$$H(X_1, X_2, \ldots, X_n) = \sum_{i=1}^{n} H(X_i \mid X_1, \ldots, X_{i-1})$$
推导本身很短,从两个变量的版本推起就行:
$$H(X,Y) = -\sum_{x,y} p(x,y)\log p(x,y) = -\sum_{x,y} p(x,y)\log\big[p(x)p(y|x)\big]$$
拆成两项:
$$= -\sum_{x,y} p(x,y)\log p(x) - \sum_{x,y} p(x,y)\log p(y|x)$$
第一项对 $y$ 求和后得到 $-\sum_x p(x)\log p(x) = H(X)$;第二项就是 $H(Y|X)$ 的定义。所以 $H(X,Y) = H(X) + H(Y|X)$。三个以上变量的情形用归纳法一路推下去即可。
这条法则的意义在于:任何复杂的联合分布,都可以拆成一串条件分布的叠加,而且各项之间不重不漏。这是自回归模型的理论基础——把 $p(x_1,\ldots,x_n)$ 拆成 $\prod_i p(x_i | x_{<i})$,每项用一个网络去建模,整条链就搭起来了。
5.2 用链式法则推出一串不等式
链式法则的一个副作用是,它能像多米诺骨牌一样推出一堆有用的不等式。推导方式都一样:因为条件作用只会减少不确定性,即 $H(X|Y) \leq H(X)$,所以把链式法则里的每一项都放大成无条件熵,就得到:
$$H(X_1,\ldots,X_n) \leq \sum_{i=1}^n H(X_i)$$
这个不等式的直观解释是:n 个变量合起来的不确定性,不超过各自不确定性之和,等号只在完全独立时成立。这个结论在做日志压缩时很有用——如果你把多个字段拼成一条记录来压缩,压缩率的上限就由这个和决定。
另外几个常用的推论:$H(X|Y) \leq H(X)$(条件不增加不确定性);$I(X;Y) \leq \min{H(X), H(Y)}$(共享的信息不可能超过任何一方的总量);$I(X;Y) \leq I(X; Y, Z)$(多知道点东西不会让互信息变少)。最后这条在特征工程里对应"加入更多特征总是不会损失信息",但注意,不损失信息不等于模型能学好,维度灾难是另一码事。
5.3 变长编码:链式法则的工程出口
链式法则最漂亮的工程应用是算术编码和区间编码。假设你要编码一个序列 $x_1, x_2, \ldots, x_n$,链式法则直接给出了理论码长:
$$L \approx H(X_1) + H(X_2|X_1) + \cdots + H(X_n | X_1,\ldots,X_{n-1})$$
每一项就是一个条件概率模型的负对数似然。这意味着,你训练的每一个语言模型,它的交叉熵损失本身就是压缩率的估计。loss 是 1.5 nat/token,换算成 bit 就是 1.5 / 0.693 ≈ 2.16 bit/token,也就是说平均每个 token 用 2.16 比特就能无损表示。这个换算我经常用来给非技术同事解释"模型变好意味着什么"——它等价于"压缩率提高了"。
回到开头那个"熵云发卡网源码"之类的搜索词,我猜提问者想找的是某种数据编码或者序列化的实现。从信息论角度看,任何无损序列化方案的理论下界就是链式法则算出的那串条件熵之和,实际实现能接近它就已经很优秀了。至于具体的业务实现,那属于工程选型问题,本文不展开。
5.4 一个例子:文本熵率的估计
用链式法则估计自然语言的熵率是个经典练习。做法是这样的:取一段文本,用一个 n-gram 模型去算每个 token 的条件概率,然后把负对数概率加起来除以 token 数。
我用一份约 200 万字符的中文语料做过这个实验。用字符级 4-gram 加 Kneser-Ney 平滑,算出来的熵率大约是 4.2 bit/字符;换成 6-gram 降到约 3.6 bit/字符;再往上加阶数,降到 3.2 左右就开始收益递减了。作为对比,一个完全均匀的字符分布(假设用到 5000 个常用汉字)熵率是 $\log_2 5000 \approx 12.3$ bit,差距非常悬殊。
这个实验有一个特别值得注意的地方:阶数越高,训练数据稀疏问题越严重。6-gram 在 200 万字符的语料上,绝大部分可能的组合根本没见过,平滑方法的选择对结果的影响比阶数本身还大。所以报告这个数字的时候,必须把语料规模、平滑方法和阶数一起写清楚,否则数字没有意义。
6. 常见问题与排查实录
6.1 问题速查表
下面这张表是我这些年积攒下来的高频问题,基本覆盖了 90% 的现场情况。
| 现象 | 常见原因 | 排查方向 | 处理办法 |
|---|---|---|---|
| 熵的计算结果是 NaN | 存在零概率项 | 检查计数数组是否有 0 | 计算前过滤p > 0 |
| 熵值明显偏大 | 底数用错(nat 当成 bit) | 确认 log 底数 | nat 除以 ln2 或直接改底数 |
| KL 散度无穷大 | $q$ 在某些点取 0 而 $p$ 不为 0 | 检查支撑集是否对齐 | 加平滑,或裁剪到 1e-12 |
| 互信息出现负值 | 估计器方差 | 样本量是否太小 | 截断到 0,增大样本量 |
| 连续变量互信息全部接近 0 | 分箱太粗或太细 | 检查箱数设置 | 换 KSG 估计器 |
| 交叉熵 loss 不降 | 标签极不平衡 | 统计标签分布 | 加权、换 focal loss |
| 训练 loss 正常验证 loss 上升 | 分布漂移或过拟合 | 对比两侧的预测分布 | 正则化、重新采样 |
| 两个实现的熵值差一倍 | 单位不同或者重复计数 | 用同一份小数据交叉验证 | 统一接口,加断言 |
这张表里我最想强调最后一行。跨语言、跨库对比数值之前,先用一份十几行的小数据跑通双向验证,能省掉大量沟通成本。我自己维护了一个entropy_selftest.py,里面固定了几个用例和期望值,任何新写的估计函数都要先通过它。
6.2 我自己踩过的三个坑
第一个坑,把熵当成类别数的对数。早期做数据体检时,我习惯用 $\log_2(\text{类别数})$ 来快速估算标签熵,在类别均衡的时候这个近似没问题,但一旦不均衡就会高估得非常离谱。100 个类别,其中 99 个只出现一次,实际熵远小于 6.6 bit。这个错误让我在一个项目里误判了任务的难度,以为标签信息丰富,实际模型只需要学到"全猜主类"就能拿到不错的基线。改法很简单:一律实算,不估算。
第二个坑,忽略零概率的连锁反应。在做语言模型困惑度评估时,遇到了输出为inf的情况。查了半天发现是某个字符在词典里但训练集中没出现过,导致条件概率为 0,取对数后变成无穷。这个问题的解决方案不是简单地加 1e-12——那样会引入系统性偏差——而是要用回退或者插值平滑,把概率质量合理地分配给未见事件。现在我写任何涉及对数概率的代码,第一件事就是加断言检查有没有零。
第三个坑,用互信息筛选特征时忘了处理缺失值。这个坑比较隐蔽。sklearn的互信息函数不接受 NaN,我的做法是先填充。但填充策略会显著影响互信息估计:用均值填充会人为压低方差,从而低估互信息;用中位数填充在偏态分布上表现稍好;最稳妥的做法是先算一个"缺失指示变量"的互信息,如果它本身和标签高度相关,说明缺失不是随机的,这时候应该把指示变量作为一个独立特征保留下来,而不是急着填充。这个改进在一个风控项目里直接把召回率提高了 6 个百分点。
6.3 从信息论到损失函数的迁移清单
最后整理一份我自己在用的对照清单,把理论概念和工程实现一一对应起来,方便快速查阅。
| 信息论概念 | 对应的工程对象 | 关键注意事项 |
|---|---|---|
| 熵 $H(p)$ | 标签分布的信息量、数据复杂度下界 | 别用类别数近似,实算 |
| 交叉熵 $H(p,q)$ | 分类任务的损失函数 | 等价于 KL + 常数项 |
| KL 散度 | 变分推断、知识蒸馏、分布对齐 | 注意方向,前向反向行为不同 |
| 互信息 | 特征筛选、表征学习、独立性检验 | 高维需用 KSG 类估计器 |
| 条件熵 | 自回归模型的每步损失 | 就是序列模型的逐 token loss |
| 链式法则 | 自回归分解、算术编码 | 是压缩率与模型 loss 的桥梁 |
| 数据处理不等式 | 特征工程的边界 | 变换不可能增加信息,只会损失 |
这张表我打印出来贴在工位上,遇到新问题先在上面找位置,找不到再去看论文。用久了会发现,很多看起来新奇的方法,本质上是把表里的某一项换了个名字。
我个人在实际工作中的体会是,信息论的价值不在于你能手推多少个公式,而在于你能不能把现场的现象翻译成"哪一项熵不对、哪个分布偏了、互信息够不够"。翻译完成的那一刻,解决方案往往就已经摆在桌上了。至于那些推导细节,用多了自然会记住,记不住的,翻回来看这张表就行。