首页 / 模块 2 · 概率统计、信息论与最优化 / 第 5 课(共 10 课)

信息论——熵、交叉熵、KL

从零到前沿 ML 自学课程 · 阶段0:数学与工具基础 · 能力点:信息论——交叉熵/KL:深度学习最常用损失的来源(VAE/RLHF 的埋点)

上一课我们用最大似然(MLE)把"概率假设"翻译成了"损失函数":分类得到交叉熵、高斯回归得到 MSE。但你可能还有个疑问——为什么那个 −∑ p log q 长这样?它到底在度量什么?为什么我们优化它,却说自己在"逼近真实分布"?这一课用信息论把这层窗户纸彻底捅破:交叉熵、KL 散度、最大似然其实是同一件事的三种说法,它们在一个恒等式上"三线汇合"。

读完这一课,你将能够

  • 用"惊讶程度"解释自信息 \(-\log p\),并说清 bit 与 nat 的区别;
  • 手算一个离散分布的熵 \(H(p)\)、交叉熵 \(H(p,q)\) 与 KL 散度 \(D_{\mathrm{KL}}(p\|q)\),并验证恒等式 \(H(p,q)=H(p)+D_{\mathrm{KL}}(p\|q)\);
  • 说明为什么"最小化交叉熵 ⟺ 最小化 KL ⟺ 最大化似然",以及为何我们优化交叉熵而非直接优化 KL;
  • 区分前向 KL(覆盖众峰)与反向 KL(锁单峰)的行为差异,并指出 KL 为什么不是距离;
  • 用 numpy 实现 entropy/cross_entropy/kl,并理解 log-sum-exp 的数值稳定技巧。

一、自信息:一件事有多"令人惊讶"

先抛开公式,想一个现实问题:一条消息携带的"信息量"该怎么量化?

直觉上有三条要求。第一,越罕见的事,发生时信息量越大。"今天太阳照常升起"几乎是废话(信息量≈0),"楼下中了一注头奖"则信息量爆表。第二,必然发生的事(概率为 1)信息量应为 0。第三,两个独立事件一起发生,信息量应该相加:你告诉我"硬币正面"再告诉我"骰子是 6",总信息量等于分别告知之和。

什么函数同时满足"概率越小值越大""\(p=1\) 时为 0""乘法变加法"?答案是负对数:

\[ I(x) = -\log p(x). \]

把它叫作自信息(self-information)惊讶度(surprisal)。验证三条性质:\(p\to 0\) 时 \(-\log p\to+\infty\)(越罕见越惊讶);\(p=1\) 时 \(-\log 1=0\)(毫不惊讶);独立事件 \(p(x,y)=p(x)p(y)\),于是 \(-\log p(x,y)=-\log p(x)-\log p(y)\)(信息相加)。乘法变加法,正是对数的看家本领。

自信息 I(x)=−log p(x) 关于概率 p 的曲线自信息 I(x) = −log p(x)pI00.250.50.751.00123 natp=1/2:−log₂(0.5)=1 bitI=0 必然事件越罕见越惊讶 →∞(p→0)纵轴:I(x)=−ln p(nat);右侧虚线对照 bit 刻度
自信息 I(x)=−log p(x) 关于概率 p 的曲线:p→0 时信息量→∞(罕见事件极令人惊讶),p=1 时信息量=0(必然事件毫无信息)。叠加标出 p=1/2 处自信息恰为 1 bit。

bit 还是 nat?只是换了把尺子。用 \(\log_2\) 时单位是 bit(比特):一次公平抛硬币的结果 \(p=1/2\),自信息 \(-\log_2(1/2)=1\) bit——恰好是"一个二进制位"能携带的信息。用自然对数 \(\ln\) 时单位是 nat(奈特)。两者只差一个常数因子:从 nat 换到 bit 要除以 \(\ln 2\),即 \(1\text{ nat}=1/\ln 2\approx 1.4427\text{ bit}\),反过来 \(1\text{ bit}=\ln 2\approx 0.6931\text{ nat}\)。深度学习里一律用 \(\ln\)(求导干净),所以本课公式默认 \(\ln\),只在讲"编码长度"直觉时用 \(\log_2\)。

二、熵:平均惊讶 = 平均不确定性 = 最优编码长度

自信息说的是"某一个事件"。但一个随机变量 \(X\) 会按分布 \(p\) 吐出各种取值,我们关心的是平均下来有多惊讶——这就是熵(entropy),即自信息关于 \(p\) 的期望:

\[ H(p) \;=\; \mathbb{E}_{x\sim p}\big[-\log p(x)\big] \;=\; -\sum_x p(x)\log p(x). \]

它有三个等价的解读,建议你全都收下:

两个极端把直觉钉牢:

  • 完全确定 ⇒ 熵 = 0。若某个取值概率为 1,其余为 0,则 \(H=-1\cdot\log 1=0\)。没有任何不确定性,无需任何编码。
  • 均匀分布 ⇒ 熵最大。\(n\) 个等可能取值时 \(H=\log n\)(这是 \(n\) 个取值能达到的上限)。最摸不准,最难压缩。

用偏置硬币把这条曲线画出来最清楚。一枚正面概率为 \(p\) 的硬币,其熵(伯努利熵)是

\[ H(p) = -p\log p-(1-p)\log(1-p). \]

\(p=0\) 或 \(p=1\) 时硬币结果已注定,\(H=0\);\(p=0.5\) 时最公平、最难猜,熵取最大值(\(\ln 2\approx0.693\) nat,即 1 bit——回到第一节那把尺子,\(1\text{ bit}=\ln 2\text{ nat}\),两边对得上)。曲线是一个对称的拱形。

伯努利熵 H(p) 关于偏置 p 的对称拱形曲线最大不确定性ln2≈0.693 nat = 1 bit完全确定H=0完全确定H=000.510.6930偏置 p熵 H(p)(nat)
伯努利熵 H(p)=−p log p−(1−p) log(1−p) 关于偏置 p 的曲线:在 p=0.5 处取最大(最难猜),在 p=0 与 p=1 两端为 0(结果已注定)。呈对称拱形。

例题:三点分布的熵

设 \(p=(0.5,\,0.25,\,0.25)\)。逐项算自信息再加权平均(用 \(\ln\)):

\[ H(p) = -\big(0.5\ln 0.5 + 0.25\ln 0.25 + 0.25\ln 0.25\big). \]

由 \(-\ln 0.5=0.6931,\ -\ln 0.25=1.3863\):

\[ H(p)=0.5(0.6931)+0.25(1.3863)+0.25(1.3863)=1.0397\ \text{nat}. \]

换成 bit:\(1.0397/\ln 2 = 1.5\) bit。这个 \(1.5\) 很美——它说"用最优编码,平均每个符号要 1.5 个二进制位"。事实上把 0.5 那个符号编成 0(1 位)、另两个编成 1011(各 2 位),平均长度 \(0.5\cdot1+0.25\cdot2+0.25\cdot2=1.5\) bit,正好达到熵的下界。

三、交叉熵:用"错的码本"去编码会多花多少

现实里我们几乎从不知道真分布 \(p\)。模型给出的是一个估计 \(q\)。假设真实数据来自 \(p\),但我们手里只有为 \(q\) 设计的那套编码(把 \(q\) 认为常见的符号编短),用这套"可能不对的码本"去编码真实数据,平均要花多长?这就是交叉熵(cross-entropy)

\[ H(p,q) \;=\; \mathbb{E}_{x\sim p}\big[-\log q(x)\big] \;=\; -\sum_x p(x)\log q(x). \]

注意区别:抽样的频率用真分布 \(p\)(数据真的从这儿来),但每个符号的编码长度 \(-\log q(x)\) 用错的 \(q\)(我们以为它来自这儿)。如果 \(q=p\),码本完美匹配数据,交叉熵就退化成熵 \(H(p)\)。如果 \(q\neq p\),必然有浪费。这条"浪费不会是负的"由 Gibbs 不等式保证:

\[ \boxed{\,H(p,q)\ \ge\ H(p)\,}\qquad\text{等号当且仅当 } q=p. \]

ML 和 ML 的联系:分类交叉熵就是这个交叉熵

上一课那个分类损失,正是交叉熵 \(H(p,q)\) 的特例。把真标签写成 one-hot 分布 \(p\)(正确类那一项为 1,其余为 0),把模型 softmax 输出写成预测分布 \(q=\hat y\)。代入定义:

\[ H(p,\hat y) = -\sum_c p_c\log \hat y_c = -1\cdot\log \hat y_{\text{正确类}} = -\log \hat y_{\text{正确类}}. \]

求和里只有正确类那一项幸存(其余 \(p_c=0\)),剩下的恰好是上一课的 负对数似然(NLL)。"分类交叉熵 = NLL"在这里彻底对上了:one-hot 是一种特别"尖"的真分布。

四、KL 散度:交叉熵比熵多出来的那块"浪费"

Gibbs 不等式说 \(H(p,q)\ge H(p)\),那这个"多出来的差"本身就是个有用的量——它度量 \(q\) 偏离 \(p\) 有多远。给它起名 KL 散度(Kullback–Leibler divergence)

\[ D_{\mathrm{KL}}(p\|q) \;=\; H(p,q)-H(p) \;=\; \sum_x p(x)\log\frac{p(x)}{q(x)}. \]

两种写法是同一回事(把 \(H(p,q)-H(p)\) 展开,\(-\sum p\log q+\sum p\log p=\sum p\log(p/q)\))。它的含义非常具体:因为用了错的码本 \(q\),每个符号平均多花的 bit 数

KL 的三条性质,每条都要记住:

  • 非负:\(D_{\mathrm{KL}}(p\|q)\ge 0\),当且仅当 \(p=q\) 时为 0(这正是 Gibbs 不等式的另一种说法)。
  • 不对称:一般 \(D_{\mathrm{KL}}(p\|q)\neq D_{\mathrm{KL}}(q\|p)\)。"p 偏离 q"和"q 偏离 p"是两个不同的数。
  • 不满足三角不等式,所以它不是一个距离(metric)

易错:别把 KL 当"距离"。很多人脑子里把 \(D_{\mathrm{KL}}(p\|q)\) 想成 "\(p\) 和 \(q\) 之间的距离",这会害你。距离必须对称(\(d(a,b)=d(b,a)\))且满足三角不等式,KL 两条都不满足。正确的叫法是散度(divergence)——一个"从 \(p\) 看 \(q\) 有多别扭"的有向、不对称的度量。后面前向/反向 KL 行为迥异,根子就在这个不对称上。

五、★核心:三线汇合 —— 交叉熵、KL、最大似然是同一件事

把上面那个定义移项,得到本课的中心恒等式:

\[ \boxed{\,H(p,q) \;=\; H(p) \;+\; D_{\mathrm{KL}}(p\|q)\,} \]

用一根柱子看最直观:底部是 \(H(p)\)(数据本身的不可压缩信息),上面叠一段 \(D_{\mathrm{KL}}(p\|q)\)(因模型不完美造成的浪费),加起来才是交叉熵 \(H(p,q)\)。\(q\) 越接近 \(p\),上面那段越短;\(q=p\) 时浪费归零,柱子缩到只剩 \(H(p)\)。

交叉熵分解为熵 H(p) 加 KL 散度,训练只能压缩 KL 段交叉熵的分解:H(p,q) = H(p) + D_KL(p‖q)H(p)D_KL(p‖q)H(p,q)交叉熵H(p) 数据固有信息(常数,与 θ 无关)D_KL(p‖q) 模型偏差的浪费压缩训练 = 压缩这一段 ⟺ 最小化 KL ⟺ 最大似然(H(p) 是地板,训练动不了)H(p)浪费归零q=p 时
三线汇合的柱状分解:交叉熵 H(p,q) = 底部的熵 H(p)(数据固有、与参数无关的常数)+ 上方叠加的 KL 散度 D_KL(p‖q)(模型不完美造成的额外浪费)。训练只能压缩上面那段。

现在看关键一步。训练时,真分布 \(p\)(数据分布)是给定的、与模型参数 \(\theta\) 无关的——数据就摆在那,它的熵 \(H(p)\) 是个常数。我们只能调 \(q=q_\theta\)。于是对 \(\theta\) 求最小:

\[ \min_\theta\, H(p,q_\theta) \;=\; \min_\theta\Big[\underbrace{H(p)}_{\text{常数}} + D_{\mathrm{KL}}(p\|q_\theta)\Big] \;\Longleftrightarrow\; \min_\theta\, D_{\mathrm{KL}}(p\|q_\theta). \]

常数项不影响极小值的位置。所以最小化交叉熵,等价于最小化 KL 散度。而上一课已证最小化交叉熵就是最大化似然。三者汇合:

\[ \min_\theta H(p,q_\theta)\;\Longleftrightarrow\;\min_\theta D_{\mathrm{KL}}(p\|q_\theta)\;\Longleftrightarrow\;\max_\theta\ \text{似然}. \]

这就是"我们优化交叉熵而不直接优化 KL"的全部原因:两者只差常数 \(H(p)\),梯度完全相同,而交叉熵更好算(不需要知道 \(H(p)\),one-hot 标签直接给出 \(-\log\hat y_{\text{正确}}\))。我们嘴上说"让模型逼近真分布"(最小化 KL),手上写的却是交叉熵——它们是同一个优化问题。

例题:验证三线汇合

真分布 \(p=(0.5,0.25,0.25)\),已算出 \(H(p)=1.0397\) nat。两个预测:

  • \(q_1=(0.4,0.4,0.2)\):\(H(p,q_1)=-[0.5\ln0.4+0.25\ln0.4+0.25\ln0.2]=1.0896\)。则 \(D_{\mathrm{KL}}(p\|q_1)=1.0896-1.0397=0.0499\)。
  • \(q_2=(0.1,0.45,0.45)\):\(H(p,q_2)=1.5505\),\(D_{\mathrm{KL}}(p\|q_2)=1.5505-1.0397=0.5108\)。

验证恒等式:\(H(p)+D_{\mathrm{KL}}(p\|q_1)=1.0397+0.0499=1.0896=H(p,q_1)\) ✓。两个都对得上。并且 \(q_1\) 比 \(q_2\) 离 \(p\) 近(KL 更小),所以 \(q_1\) 是更好的预测——交叉熵也确实更小,方向一致。

例题:one-hot 时交叉熵 = −log ŷ_正确

真标签是第 2 类,\(p=(0,1,0)\);模型预测 \(\hat y=(0.2,0.7,0.1)\)。交叉熵 \(H(p,\hat y)=-[0\cdot\ln0.2+1\cdot\ln0.7+0\cdot\ln0.1]=-\ln0.7=0.3567\)。直接算 \(-\ln\hat y_{\text{正确}}=-\ln0.7=0.3567\) ✓。注意此时 \(H(p)=0\)(one-hot 完全确定),所以 \(D_{\mathrm{KL}}(p\|\hat y)=H(p,\hat y)-0=0.3567\):交叉熵和 KL 数值相等。这解释了分类任务里"交叉熵就是 KL"——因为真标签的熵是 0。

六、前向 KL vs 反向 KL:覆盖众峰还是锁住一峰

KL 不对称,意味着"拿哪个分布当参照"会得到完全不同的拟合行为。这在生成模型里是生死攸关的设计选择。设真分布 \(p\) 是一个双峰分布,而我们的模型 \(q\) 只能是单峰(比如一个高斯)。

前向 KL 覆盖众峰 vs 反向 KL 锁单峰前向 KL vs 反向 KL:用单峰 q 拟合双峰 p前向 KL D_KL(p‖q):覆盖众峰x密度q又宽又矮,跨坐山谷上方不敢在 p>0 处留空 → 摊平反向 KL D_KL(q‖p):锁单峰x密度q被忽略又窄又高,精准锁住一个峰躲进一个峰,忽略另一个真分布 p(双峰)前向 q反向 q
用单峰 q 拟合双峰真分布 p:前向 KL D_KL(p‖q) 让 q 摊开横跨两峰(覆盖众峰/矩匹配),反向 KL D_KL(q‖p) 让 q 锁进其中一个峰(寻峰/锁单峰)。

一句话记忆:前向 KL "宁可摊平也不留空"(覆盖),反向 KL "宁可漏掉也不摊错"(锁峰)。最大似然/交叉熵训练用的是前向 \(D_{\mathrm{KL}}(p\|q)\)(参照真分布 \(p\)),所以最大似然天然倾向"覆盖"。变分推断(VAE 的某些视角)和很多 RL 目标则用反向 KL,倾向"锁峰"。

七、互信息:两个变量共享了多少信息

熵度量单个变量的不确定性。如果观测了 \(Y\),对 \(X\) 的不确定性能减少多少?减少的那部分就是 \(X\) 与 \(Y\) 共享的信息——互信息(mutual information)

\[ I(X;Y) \;=\; H(X) - H(X\mid Y), \]

其中 \(H(X\mid Y)\) 是条件熵(知道 \(Y\) 之后 \(X\) 还剩多少不确定性)。\(I(X;Y)\ge 0\),且 \(X,Y\) 独立时为 0(知道 \(Y\) 对猜 \(X\) 毫无帮助)。它还能写成一个 KL:\(I(X;Y)=D_{\mathrm{KL}}\big(p(x,y)\,\|\,p(x)p(y)\big)\)——即"联合分布"离"假装独立"有多远。

ML 和 ML 的联系:互信息是表示学习的理论底座

对比学习(contrastive learning,如 SimCLR/CLIP)的核心思想是:让同一张图的两个增强视图(或图文配对)的表示互信息尽量大。著名的 InfoNCE 损失与互信息密切相关——更准确地说,它是互信息的一个有偏下界,且这个界被 \(\log N\)(一个 batch 里的负样本数)封顶,所以"InfoNCE = 最大化互信息"只是一个近似口径,严格的对应留到 M8 表示学习那一课再展开。直觉层面记住"学到好表示" ≈ "保留尽量多与目标相关的互信息、丢掉无关的"就够用了。这一课的熵/KL 给了你读懂那些目标函数的语言。

八、数值稳定:log-sum-exp 与 softmax 减最大值

交叉熵的实现绕不开 \(\log\) 和 \(\exp\),而这两个函数在边界处会爆炸或下溢。两个必须掌握的工程技巧:

(1) softmax 减最大值。softmax \(\hat y_i=\dfrac{e^{z_i}}{\sum_j e^{z_j}}\) 里,若某个 logit \(z_i\) 很大,\(e^{z_i}\) 会溢出成 inf。利用一个事实:给所有 logit 同时减去常数 \(c\),softmax 值不变(分子分母同乘 \(e^{-c}\) 抵消)。取 \(c=\max_j z_j\),则最大的指数变成 \(e^0=1\),绝不溢出:

\[ \hat y_i=\frac{e^{z_i-c}}{\sum_j e^{z_j-c}},\qquad c=\max_j z_j. \]

(2) log-sum-exp(LSE)。计算 \(\log\sum_j e^{z_j}\) 时同样先提最大值:

\[ \log\sum_j e^{z_j} \;=\; c + \log\sum_j e^{z_j-c},\qquad c=\max_j z_j. \]

这样括号内最大项是 \(e^0=1\),既不溢出也不会全部下溢成 0。实践中千万别先 softmax 再取 log(两步各自损失精度),而是用一体化的 log_softmax = z - log_sum_exp(z)。框架里的 CrossEntropyLoss 内部正是这么做的。

易错:手写交叉熵时 log(0)如果预测概率某项恰为 0 而真标签在该项非 0,\(\log 0=-\infty\),loss 变 nan。务必从 logits 出发用 log-softmax,或给概率加一个极小的 eps 裁剪。下面代码里我们用 np.where(p>0, ...) 来安全地跳过 \(p=0\) 的项(约定 \(0\log 0=0\))。

调一调,观察现象

下面三个小实验都只用 numpy + print,几秒跑完。动手改一改,亲眼看现象比记结论牢十倍。

任务 1:把分布从"尖"调到"平",看熵怎么变。p 从接近 one-hot 到均匀。预期现象:越均匀熵越大,均匀分布 \((1/3,1/3,1/3)\) 达到最大 \(\ln 3\approx1.0986\);接近 one-hot 时熵趋近 0。为什么:熵是平均不确定性,摊得越平越难猜。

import numpy as np
def entropy(p):
    p = np.asarray(p, float)
    return -np.sum(np.where(p>0, p*np.log(p), 0.0))
for p in [[0.98,0.01,0.01],[0.6,0.3,0.1],[1/3,1/3,1/3]]:
    print(p, '-> H =', round(entropy(p),4), 'nat')
print('ln 3 =', round(np.log(3),4))

任务 2:让 q 越来越接近 p,看交叉熵塌向熵、KL 塌向 0。预期现象:随着 q 趋近 p,\(H(p,q)\) 单调下降逼近 \(H(p)\),\(D_{\mathrm{KL}}\) 逼近 0;当 q=p 时 KL 恰为 0。为什么:交叉熵的"浪费部分"就是 KL,码本对了浪费归零。

import numpy as np
def entropy(p):
    p=np.asarray(p,float); return -np.sum(np.where(p>0,p*np.log(p),0.0))
def cross(p,q):
    p=np.asarray(p,float); q=np.asarray(q,float)
    return -np.sum(np.where(p>0,p*np.log(q),0.0))
p=np.array([0.5,0.25,0.25])
for t in [0.0,0.5,0.9,1.0]:          # t=插值系数,t=1 时 q=p
    q=(1-t)*np.array([1/3,1/3,1/3])+t*p
    print('t=',t,'H(p,q)=',round(cross(p,q),4),'KL=',round(cross(p,q)-entropy(p),4))
print('H(p)=',round(entropy(p),4))

任务 3:亲手验证 KL 不对称。交换 pq 的位置算两次 KL。预期现象:\(D_{\mathrm{KL}}(p\|q_1)=0.0499\) 而 \(D_{\mathrm{KL}}(q_1\|p)=0.0541\),两数不等。为什么:KL 是有向散度,求和加权用的分布不同(一次按 \(p\)、一次按 \(q_1\)),所以不对称——这也是它"不是距离"的铁证。

import numpy as np
def kl(p,q):
    p=np.asarray(p,float); q=np.asarray(q,float)
    return np.sum(np.where(p>0, p*np.log(p/q), 0.0))
p =np.array([0.5,0.25,0.25])
q1=np.array([0.4,0.4,0.2])
print('KL(p||q1) =', round(kl(p,q1),4))
print('KL(q1||p) =', round(kl(q1,p),4))
print('相等吗?', np.isclose(kl(p,q1), kl(q1,p)))

动手练习

  1. 三函数与恒等式。实现 entropy(p)cross_entropy(p,q)kl(p,q) 三个函数(用 np.where(p>0, ...) 处理 \(0\log0\)),对 \(p=(0.5,0.25,0.25)\)、\(q=(0.4,0.4,0.2)\) 数值验证 \(H(p,q)=H(p)+D_{\mathrm{KL}}(p\|q)\)(断言 np.isclose)。骨架:
    import numpy as np
    def entropy(p):
        p=np.asarray(p,float); return -np.sum(np.where(p>0,p*np.log(p),0.0))
    def cross_entropy(p,q):
        p=np.asarray(p,float); q=np.asarray(q,float)
        return -np.sum(np.where(p>0,p*np.log(q),0.0))
    def kl(p,q):
        p=np.asarray(p,float); q=np.asarray(q,float)
        return np.sum(np.where(p>0, p*np.log(p/q), 0.0))
    p=np.array([0.5,0.25,0.25]); q=np.array([0.4,0.4,0.2])
    print('H(p,q)        =', cross_entropy(p,q))
    print('H(p)+KL(p||q) =', entropy(p)+kl(p,q))
    print('恒等式成立?', np.isclose(cross_entropy(p,q), entropy(p)+kl(p,q)))
    print('KL 不对称: ', kl(p,q), 'vs', kl(q,p))
    
  2. one-hot 验证。取真标签 p=[0,1,0] 与预测 yhat=[0.2,0.7,0.1],用上面的 cross_entropy 算交叉熵,并与 -np.log(yhat[1]) 比较,确认相等;再算 \(H(p)\) 确认它是 0,从而 \(D_{\mathrm{KL}}=H(p,\hat y)\)。
  3. 稳定的 log-softmax。写一个 log_softmax(z)=z - (c + np.log(np.sum(np.exp(z-c))))(\(c=\)max),对 z=[1000., 1001., 1002.] 这种大 logit 测试不出现 inf/nan;再用它实现 nll = -log_softmax(z)[label] 作为单样本交叉熵。
  4. 伯努利熵曲线(打印版)。p in np.linspace(0,1,11) 打印 \(H(p)=-p\ln p-(1-p)\ln(1-p)\),确认最大值在 \(p=0.5\) 处约 0.6931 nat、两端为 0。注意:写 np.where(p>0, p*np.log(p), 0) 时,np.log(p) 仍会在 \(p=0\) 处被求值并打印 RuntimeWarning(虽然 np.where 最终选了 0、结果数值正确)。别被警告吓到——它不代表算错。要么用 with np.errstate(divide='ignore', invalid='ignore'): 包住计算抑制警告,要么先掩码取出 p>0 的项再 log。骨架:
    import numpy as np
    def bern_H(p):
        with np.errstate(divide='ignore', invalid='ignore'):
            terms = np.where((p>0)&(p<1), -p*np.log(p)-(1-p)*np.log(1-p), 0.0)
        return terms
    ps = np.linspace(0,1,11)
    for p in ps:
        print('p=', round(float(p),1), 'H=', round(float(bern_H(p)),4))
    
  5. (选做)前向 vs 反向 KL 的偏好。真分布是离散双峰 p=[0.45,0.10,0.45](注意它已归一,三项和为 1——先 assert np.isclose(p.sum(),1),不归一会让 KL 算出负值,违反 KL≥0)。在候选单峰 qA=[0.9,0.05,0.05](锁左峰、几乎不给右峰质量)和 qB=[0.4,0.2,0.4](覆盖双峰)中,分别算前向 \(D_{\mathrm{KL}}(p\|q)\) 与反向 \(D_{\mathrm{KL}}(q\|p)\)。预期现象:前向 KL \(D_{\mathrm{KL}}(p\|q_A)=0.7461\) 远大于 \(D_{\mathrm{KL}}(p\|q_B)=0.0367\)——前向明显偏爱覆盖型 qB,因为锁左峰的 qA 在右峰处 \(q\approx0\) 而 \(p\) 有质量,被狠狠惩罚。反向 KL \(D_{\mathrm{KL}}(q_A\|p)=0.4793\)、\(D_{\mathrm{KL}}(q_B\|p)=0.0444\):反向对锁峰型 qA 的惩罚(0.4793)远小于前向对它的惩罚(0.7461)——这就是"反向相对宽容锁峰"的数值证据。
    import numpy as np
    def kl(p,q):
        p=np.asarray(p,float); q=np.asarray(q,float)
        return np.sum(np.where(p>0, p*np.log(p/q), 0.0))
    p =np.array([0.45,0.10,0.45]); assert np.isclose(p.sum(),1)
    qA=np.array([0.9,0.05,0.05]);  assert np.isclose(qA.sum(),1)   # 锁左峰
    qB=np.array([0.4,0.2,0.4]);    assert np.isclose(qB.sum(),1)   # 覆盖双峰
    print('前向 KL(p||qA) =', round(kl(p,qA),4), ' 前向 KL(p||qB) =', round(kl(p,qB),4))
    print('反向 KL(qA||p) =', round(kl(qA,p),4), ' 反向 KL(qB||p) =', round(kl(qB,p),4))
    

掌握自检

埋点(下面这两处你会再见到 KL):VAE 的 ELBO = 重构项 \(-\)(KL 正则项):那个 KL 把编码器输出的隐分布拉向先验 \(\mathcal{N}(0,I)\),防止它乱长。② RLHF 的目标 = 奖励 \(-\ \beta\cdot D_{\mathrm{KL}}(\pi\|\pi_{\text{ref}})\):那个 KL 像一根橡皮筋,把微调后的策略 \(\pi\) 拴在参考模型 \(\pi_{\text{ref}}\) 附近,防止它为了刷奖励而跑偏、说胡话。今天你建立的"KL = 偏离参照分布的代价"这个直觉,到那两课会直接复用。

下一课预告:我们已经会"度量两个分布有多远",但还有个更基础的能力没解决——怎么从一个分布里真的抽出样本来?下一课讲采样:逆变换采样、重参数化技巧(reparameterization)与蒙特卡洛估计。它们是 VAE、扩散模型、强化学习共同依赖的底层操作,也会让今天的 KL 正则项真正"动起来"。