几乎所有 ML 损失函数、生成模型目标、对齐数学,底层都是同一套信息论语言。今天讲清四个原子概念:熵(量化不确定性)、KL 散度(量化两个分布的差距)、互信息(量化两变量共享多少信息)、ELBO(让 intractable 的贝叶斯推断变可算)。理解它们,你再看 cross-entropy loss、VAE、对比学习就不再是黑箱。
熵就是一份数据「理论上的最优压缩字节数下限」——你熟悉的 gzip、Huffman 编码,压到极限能压多小,由熵决定。一个全是 AAAA... 的文件熵接近 0(gzip 能压到几乎没有),一个真随机字节流熵最大(压不动)。熵 = 这份数据的「真实信息含量」,与你用什么编码无关。
Shannon 1948 年要回答一个工程问题:通信信道里,一条消息最少需要多少 bit 才能无损传输?这就要先量化「信息量」。核心直觉是 惊讶度(surprise / self-information):
越罕见的事件(p 越小)信息量越大——「太阳明天升起」p≈1,信息量≈0,没人会转发;「明天下陨石」p 极小,信息量爆炸,上头条。用 log 是因为信息要可加:两个独立事件一起发生,信息量应当相加,而概率是相乘的,log 正好把乘法变加法。底数取 2 时单位是 bit。
熵就是惊讶度的期望(按概率加权平均)——「平均每个事件带来多少信息」:
分布越均匀(每个结果都可能),熵越大;分布越尖锐(某个结果几乎必然),熵越小。公平硬币 H=1 bit,灌铅到 99% 正面的硬币 H≈0.08 bit。
import numpy as np def entropy(p, base=2): p = np.asarray(p, dtype=float) p = p[p > 0] # log(0) 无定义,丢掉零概率项 return -np.sum(p * np.log(p) / np.log(base)) print(entropy([0.5, 0.5])) # 1.0 公平硬币 = 1 bit print(entropy([0.99, 0.01])) # 0.08 几乎确定,信息量极低 print(entropy([0.25, 0.25, 0.25, 0.25])) # 2.0 4 个等概率事件 = 2 bit # 直觉验证:4 个等概率结果需要 2 bit 编码(00/01/10/11),熵正好 = 2
KL 散度 = 用错统计直方图的代价。数据库查询优化器靠列的统计分布(histogram)估基数、选执行计划。如果真实数据分布是 P,优化器却拿着一份过期的、偏差的分布 Q 去做决策——白白多扫的行数、多花的 I/O,就是 KL(P‖Q)。分布估得越偏,代价越大;估得完全准,代价为 0。
需求:量化「真实分布 P」和「我的近似分布 Q」差多远。信息论的答案极优雅——用为 Q 设计的最优编码去压缩实际来自 P 的数据,平均每个符号多花的 bit 数:
拆开看更清楚,它正好等于交叉熵减去真实熵:
这就是整个深度学习训练的底层逻辑:分类任务最小化 cross-entropy loss,而 H(P) 是数据固有常数,所以最小化交叉熵 = 最小化 KL(P‖Q) = 让模型预测分布 Q 逼近真实标签分布 P。你天天用的 cross-entropy loss,本质是在压缩 KL。
三个关键性质:(1) KL ≥ 0,当且仅当 P=Q 时为 0;(2) 非对称:KL(P‖Q) ≠ KL(Q‖P)——它不是距离!(3) 方向有语义:forward KL(最小化 KL(P‖Q))倾向「覆盖 P 的所有模式」(mean-seeking,会糊在一起),reverse KL(最小化 KL(Q‖P))倾向「锁定一个模式」(mode-seeking,会漏掉其他峰)。VAE、变分推断用 reverse KL,所以 VAE 生成图像偏「保守、糊」。
import numpy as np def kl(p, q): p, q = np.asarray(p,float), np.asarray(q,float) return np.sum(np.where(p > 0, p * np.log(p / q), 0)) # p=0 处贡献为 0 p = [0.5, 0.5] q = [0.9, 0.1] print(kl(p, q)) # 0.51 用偏的 Q 编码 P,多付 0.51 nat print(kl(q, p)) # 0.37 反方向 ≠ 正方向,证明非对称 # 工程含义:训练里「真实标签 P 在前、模型预测 Q 在后」的方向不能写反, # 写反了优化的目标就变了(mean-seeking vs mode-seeking)
互信息 = 两张表之间的函数依赖强度(functional dependency)。数据库里如果知道 zip_code 几乎就能推出 city,这两列高度冗余——互信息高。如果两列完全独立(知道一个对另一个毫无帮助),互信息为 0。互信息量化的是「知道 X 能帮你消除多少关于 Y 的不确定性」。
痛点:皮尔逊相关系数(Pearson correlation)只能抓线性关系。Y = X² 这种强依赖,相关系数可能是 0,会骗你说「无关」。互信息抓任意形式的统计依赖。定义有两个等价视角:
第一视角:X 原本的不确定性,减去「已知 Y 之后」X 剩下的不确定性——差值就是 Y 替你消除掉的那部分不确定性。完全独立时知道 Y 没用,差值为 0;完全决定时知道 Y 就锁死 X,差值 = H(X)。
第二视角更深刻:互信息就是「真实联合分布」偏离「假装两者独立」的 KL 散度。如果真独立,p(x,y)=p(x)p(y),KL=0。偏离越远,依赖越强。一句话:互信息 = 独立性被违反的程度。
import numpy as np from sklearn.feature_selection import mutual_info_regression from scipy.stats import pearsonr np.random.seed(0) x = np.random.uniform(-3, 3, 2000) y = x**2 + np.random.normal(0, 0.1, 2000) # 强非线性依赖 print(pearsonr(x, y)[0]) # ≈ 0.01 线性相关骗你说「无关」 print(mutual_info_regression(x.reshape(-1,1), y)[0]) # ≈ 1.5 互信息抓到强依赖 # 结论:相关系数=0 不代表独立;互信息才是「真独立」的可靠检测器
ELBO = 用近似查询代替全表精确聚合。你想算一个精确值,但它需要对海量隐藏状态求和/积分——计算上不可行(像在 PB 级表上跑 COUNT(DISTINCT) 精确去重,扫全表会爆)。工程上你怎么办?用 HyperLogLog、采样估算这类可计算的近似,并保证它不会高估真值。ELBO 就是贝叶斯推断里的这个「保证不高估的下界估计」,而且能不断把下界往上顶,逼近真值。
贝叶斯推断的核心障碍:要算证据(evidence) p(x),即数据 x 的边际似然,需要对所有隐变量 z 积分:
算不动,怎么办?引入一个可计算的近似后验 q(z),做一个漂亮的恒等分解:
ELBO 本身可以拆成两个有直觉的项:
第一项 = 重构项:从隐变量 z 解码回 x 能还原得多好(越像原数据越高)。第二项 = 正则项:近似后验 q(z) 别偏离先验 p(z) 太远(通常先验是标准正态)。这正是 VAE(变分自编码器)的损失函数——Kingma & Welling 2013 的 Auto-Encoding Variational Bayes:encoder 产出 q(z|x),decoder 算 p(x|z),训练就是最大化 ELBO。Diffusion 模型、变分推断全建在这套框架上。
import torch import torch.nn.functional as F def vae_loss(x, x_recon, mu, logvar): # 第一项:重构项(负号因为要最大化 ELBO = 最小化 -ELBO) recon = F.binary_cross_entropy(x_recon, x, reduction='sum') # 第二项:KL( q(z|x)=N(mu,σ²) ‖ p(z)=N(0,1) ),高斯有闭式解 kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) return recon + kl # = -ELBO,最小化它 = 最大化 ELBO # 直觉:recon 逼模型「学会还原数据」,kl 逼隐空间「贴近标准正态、别乱长」。 # 两者拉锯 —— 这就是 VAE 生成又连续又稍糊的根本原因。