交叉熵:用错的信念,每个字要多付多少
前九章我们假设概率是已知的。现在换一个更诚实的设定:你手上只有一个估计,而它是错的。这个错误有一个精确的价格,单位是比特——而你大概每天都在优化这个价格。
真实分布是 90% / 10%,但你的编码表是按 50% / 50% 建的。你每个符号多付多少比特?
一个很具体的场景
你要给一条信道设计编码。真实的符号分布是 p = (0.9, 0.1),但你不知道——你以为是 q = (0.5, 0.5)。
于是你按 q 建了码表:两个符号各 1 比特。
现在算账。关键在于:码长是按你的信念 q 定的,但每个码字被用到的频率是按真实的 p 定的。
# 你给每个符号定的码长(按你的信念 q): 甲:−log₂ 0.5 = 1 比特 乙:−log₂ 0.5 = 1 比特 # 但它们实际出现的频率是 p: 甲出现 90% 的时间 乙出现 10% 的时间 # 你实际的平均码长: 0.9 × 1 + 0.1 × 1 = 【1.000 比特】 # 而如果你知道真相 p,本来只要: 0.9 × 0.152 + 0.1 × 3.322 = 【0.469 比特】 # 你多付了:1.000 − 0.469 = 【0.531 比特 / 每个符号】
这三个量各有名字,而且它们的关系是这一章唯一需要记住的东西:
交叉熵 H(p, q) = −Σ p(x) log₂ q(x) ← 你实际付的账单
熵 H(p) = −Σ p(x) log₂ p(x) ← 谁都躲不掉的地板
KL 散度 D(p‖q) = Σ p(x) log₂ (p/q) ← 你的错误要付的罚金
【 H(p, q) = H(p) + D(p‖q) 】
1.000 = 0.469 + 0.531
用长度定义的读法:码长按 q 定,频率按 p 数。
第一项是这件事本身的不确定性,你再聪明也省不掉。第二项是你错误的代价,它是唯一你能改的部分,而且它永远 ≥ 0,只有 q = p 时才等于 0。
两条区间条叠起来看,比公式直观得多:
这个式子就是 cross-entropy loss
如果你训练过任何分类模型,你写过这行代码:
loss = -sum(y_true[i] * log(y_pred[i]) for i in range(n)) # 或者 loss = F.cross_entropy(logits, labels)
把它和上面的定义并排放:
交叉熵定义: H(p, q) = −Σ p(x) · log q(x) 你的 loss : loss = −Σ y_true · log y_pred # p = y_true (真实分布,通常是 one-hot) # q = y_pred (你的模型输出的分布)
(用自然对数的话单位是奈特,乘 1.4427 换成比特。)
「模型的 loss 是 2.3」的完整意思是:这个模型平均每个样本浪费掉 2.3 个奈特 ≈ 3.3 个比特。
而「浪费」是字面意思——如果你拿这个模型去驱动第 8 章那台算术编码器,它压出来的文件真的会比理论最优大这么多。第 12 章会把这件事真的做一遍,并把两个数字放在一起给你看。
顺带解决一个常见困惑:为什么 loss 降不到 0?因为 loss = H(p) + D(p‖q),你能优化的只有 D。H(p) 是数据本身的噪声下限,它不属于你的模型,它属于这个世界。一个 loss 真的降到 0 的模型,多半是数据泄漏了。
KL 不是距离,而这件事很要紧
「散度」这个词是故意用的——因为 KL 不满足对称性:
真相 p = (0.9, 0.1),信念 q = (0.5, 0.5) D(p‖q) = 0.531 比特 反过来:真相 (0.5, 0.5),信念 (0.9, 0.1) D(q‖p) = 0.737 比特 # 两个方向差了 39%。
这不是数学上的小瑕疵,它有清楚的现实含义:
D(p‖q) 大:真实世界里常发生的事,你的模型认为不太可能。「意料之外」型错误。每次那件事发生,你都要付一大笔。
D(q‖p) 大:你的模型认为很可能的事,现实里其实不常发生。「杞人忧天」型错误。
信息论里我们几乎总是关心前者——因为账单是按真实频率结算的。
而在机器学习里,这个不对称有个具体后果:
- 最小化 D(p‖q)(真实在前)会让 q 覆盖 p 的所有峰——因为一旦 p 有质量而 q 是 0,代价是无穷。这叫 mean-seeking,最大似然估计就是这个。
- 最小化 D(q‖p)(模型在前)会让 q 缩到 p 的某一个峰里——因为 q 在 p 很小的地方放质量代价很大,而 q 干脆不去那些地方是安全的。这叫 mode-seeking,变分推断用的就是这个。
所以同一对分布,换个方向优化,得到的结果可以完全不同。看到 KL 时永远要先看清楚哪个在前面。
那个「无穷大」不是理论洁癖
在 demo 里把 q 拉到 0.01(你非常笃定甲不会发生),看罚金怎么涨。
如果你把某件事的概率写成恰好 0,而它发生了:
码长 = −log₂ 0 = ∞ # 你的编码表里根本没有这个符号的位置。 # 编码器会当场卡死,或者你的 loss 会变成 NaN。
任何一台要真的拿去压缩数据的语言模型,都必须保证每个符号的概率严格大于 0。做法有很多:
- 加一平滑(拉普拉斯平滑):给每个计数加 1。粗暴但有效。
- 回退 / 插值:高阶模型没见过的,就退到低阶去问。这本书那台 n-gram 引擎用的就是这个。
- softmax:神经网络天然就不会输出 0——
e^x永远是正的。softmax 不只是「把分数变成概率」,它顺手保证了你永远不会付无穷大的账。
而「概率为 0 的事发生了」在现实里也有名字,叫黑天鹅。信息论给了它一个精确的定价:无穷大。一个把某件事的概率设成 0 的模型,不是「大部分时候很准,偶尔出错」,是「一旦出错就全盘皆输」——因为它没有为那个可能性预留任何编码空间。
这也是为什么在风控、医疗、安全这些领域,「这个从来没发生过」和「这个不可能发生」之间的距离,是无穷远。
「你的信念错了,一定要多付钱,不可能少付」——这句话需要证明,而证明只有两行。
D(p‖q) = Σ p log₂(p/q)
= −Σ p log₂(q/p)
≥ −log₂( Σ p · (q/p) ) # 詹森不等式,log 是凹函数
= −log₂( Σ q )
= −log₂ 1 = 0 # 等号成立当且仅当 q = p
这个结论有一个很值得记住的读法:
「说真话的编码最省」。任何偏离真实分布的信念,都会让你付出严格更多的比特。而且这不是「通常」,是「一定」。
于是「找到最好的模型」这件事,被翻译成了一个纯粹的最小化问题:让 D(p‖q) 尽可能小,也就是让你的账单尽可能接近那个躲不掉的地板。
H(p, q) = H(p) + D(p‖q),而 H(p) 不含模型参数。对模型求梯度时,两者完全一样。
那为什么框架里用交叉熵而不是 KL?因为 H(p) 要知道真实分布才能算,而在监督学习里 p 是 one-hot,H(p) = 0——这时候交叉熵和 KL 数值上就是同一个数。直接算交叉熵省事。
但这个区别在知识蒸馏里会变得重要:那时 p 是老师模型的软标签,H(p) ≠ 0,两者数值就不一样了(虽然梯度还是一样)。如果你想看「学生离老师还有多远」,要看 KL,不是交叉熵——因为交叉熵里混进了老师自己的熵。
《打分》讲强化学习,主线是「你写的不是策略,是打分器,而它会精确最大化你写下的东西」。这一章是同一枚硬币的另一面:交叉熵是一个打分器,而模型会精确地最小化它。
两本书里都会撞上同一个问题:你写下的那个分数,真的是你想要的东西吗?《打分》里那只「站着不动拿满分」的生物,和第 23 章那个 Goodhart 台,是同一件事的两次现身。
而且这个数字你在第 2 章见过——1 − H(0.9) = 0.531。它还会在第 14 章再出现一次,那时它叫「二元对称信道在翻位率 0.1 时的容量」。
同一个数字在三个完全不同的场合出现,不是巧合。它们都是「一个比特的空间里,被不确定性占掉多少、剩下多少」这同一笔账。
换算成实感:一百万个符号,你多花 531000 比特 ≈ 65 KB。而如果你的模型只错这么一点点,那已经算是个不错的模型了。
这一章的一句话
交叉熵 = 熵 + KL。第一项是世界的噪声,你改不了;第二项是你的错误,它是你唯一能优化的东西——而这个式子字面上就是你每天在写的那个 loss。
下一章:交叉熵是个抽象的数字,2.3 和 1.8 差多少很难有实感。换一个单位就好懂多了:把它变成「等效多少面骰子」。这个换算叫困惑度,是所有语言模型的通用刻度——而它会让你第一次看清「模型好了一点点」在物理上意味着什么。