最大似然估计,香农熵,交叉熵与KL散度的详细解读与实现
Ming | 2025.12
这篇文章将会依次讲解最大似然估计、香农熵、交叉熵与 KL 散度。它们之间逻辑紧密、层层递进,共同构成了现代机器学习与信息论中关于概率建模、信息度量与分布比较的核心基础。
1. 最大似然估计
什么是最/极大似然估计?它是用来解决什么问题的?
我们暂时抛开严谨的数学定义,从一个简单的例子入手。
假设我们面对这样一个问题: 有一个神秘袋子,里面装有红、绿、蓝三种颜色的球,但我们不知道每种颜色的球所占的比例。我们仅知道颜色的概率分布具有以下结构(其中 是未知参数):
| :红 | :绿 | :蓝 | |
|---|---|---|---|
| 概率 |
也就是说,抽到红球的概率是 ,抽到蓝球的概率是 ,抽到绿球的概率则是 。
现在我们唯一能做的,就是有放回地从袋中随机抽取若干次,观察颜色。除此之外,我们无法打开袋子查看内部情况。那么,如何根据有限的观测结果来估计参数 的值呢?
你可能会想到:只要抽取次数足够多,统计各颜色出现的频率,就能近似真实概率,从而推算出 。这确实是不错的做法。但在实际问题中,我们往往只能获得有限的样本,甚至样本数很少;或者变量维度很高(比如有 1000 种颜色,未知数也不在是一个,而是非常多的未知变量),直接统计频率可能不稳定或不可行。这时,就需要借助最大似然估计这一强大的思想。
还是以上面的问题为例:
假设我们抽取了 6 次,得到颜色序列为:,记该观测事件为 。根据上述分布,事件 发生的概率(在给定 下)为:
这里用到了独立事件的概率乘法公式:每次抽取相互独立,因此整个序列的概率等于各次抽取概率的乘积。
现在,关键的思想来了:这个观测结果 是已经发生的既定事实。既然它发生了,我们就有理由认为,这个事件发生的概率应当是比较大的。既然如此,我们现在的任务就是找到一个,使得的值最大。这就是极大似然估计的核心直觉——寻找最能让观测数据“看起来合理”的参数。
在数学中,直接求的最小值可能不是很方便,因为它是多个概率的乘积,可能非常小,且容易导致数值下溢。为了便于计算与分析,我们通常对其取自然对数,得到对数似然函数:
由于 是单调递增函数,最大化 等价于最大化 。对数转换能将连乘转为求和,既简化求导运算,也提升数值稳定性。
现在我们把最大似然估计的概念放到机器学习语境中去,那么上面那个分布表,就是我们定义的模型,我们有放回的抽样获得的数据,就是训练集;就是损失函数,要求其极小值。用更加专业一点的说法就是:在机器学习中,我们经常将训练数据视为从某个真实分布中采样得到的观测。我们定义一个参数化模型 ,希望用它来逼近真实分布。此时,似然函数 就度量了在该模型下观测到训练数据的“合理程度”。最大化似然函数等价于最小化负对数似然。而这正是许多监督学习模型(如逻辑回归、神经网络分类器)中使用的损失函数——交叉熵损失的理论来源。
2. 香农熵
在探讨香农熵之前,我们首先需要理解什么是“信息量”。信息量直观来说,是一个事件发生时所携带的信息的多少。我们可以通过一个简单的例子来体会:小明是一名优等生,每次考试都能进入年级前几名。因此,下一次考试他再次进入年级前几名的概率非常高。假设他真的做到了,这个事件所包含的信息量其实很少,因为大家早已预料到这一结果——小明成绩优秀是常态。相反,如果小明下一次考试成绩大幅下滑,排名跌至年级下游,那么这个事件所包含的信息量就会非常大,会引发各种猜测与关注:小明是不是生病了?是不是最近遇到了什么困难?或是家庭出现了什么变故?这种“意外”所携带的信息,远远超过符合预期的结果。
从这一点我们可以总结:一个事件发生的概率越大,它所包含的信息量就越小;反之,概率越小,信息量就越大。换句话说,信息量与事件发生的概率成反比。
对于事件,其发生的概率为,则它的信息量计算方式如下
为了更具体地理解这个公式,我们来看一个离散概率分布的示例:
| 0.05 | 0.3 | 0.65 |
根据公式计算每个事件的信息量就能得到
| 4.3219 | 1.7369 | 0.6214 |
可以看出,概率极小的事件 信息量很大,而概率很大的事件 信息量则很小。这正是我们直觉的数学体现。
接下来,我们引入香农熵的概念。如果我们不仅关心单个事件的信息量,而是希望衡量整个概率分布 的“平均不确定性”或“平均信息量”,就需要计算信息量的期望值,即香农熵:
香农熵描述的是:在概率分布 下,随机变量所携带的平均信息量。熵越大,表示分布的不确定性越高,平均信息量也越大;熵越小,则表示分布越集中,不确定性越低。
举个例子,如果某个分布中一个事件几乎必然发生(概率接近 1),那么熵会接近 0,因为几乎不需要额外信息来描述结果。相反,如果所有事件概率均等(如掷一枚均匀的骰子),熵就会达到最大值,因为每个结果都同样“意外”,描述结果所需的信息最多。
相信看了下面的代码你就会更加理解香农熵这个概念
# 三个不同的概率分布
p_1 = np.array([0.01, 0.0, 0.98,0.01])
p_2 = np.array([0.25, 0.25, 0.25,0.25])
p_3 = np.array([0.3, 0.2, 0.1,0.4])
def shannonEntropy(p: np.ndarray): # 计算香农熵的函数
return -np.sum(p * np.log2(p + 1e-12)) # 加一个1e-12防止出现log(0)的情况
print(shannonEntropy(p_1))
print(shannonEntropy(p_2))
print(shannonEntropy(p_3))
# 输出
0.16144054253749263
1.9999999999942293
1.8464393446652445
3. 交叉熵
交叉熵在机器学习,尤其是分类任务中无处不在。简单来说,交叉熵直接刻画了两个概率分布和 之间的差异程度:两个分布越相似,交叉熵越小;两个分布差异越大,交叉熵越大。
交叉熵计算公式如下:
为了直观理解,来看两组例子。
假设在一个三分类任务中,某样本的真实标签分布和模型的预测分布如下:
| 事件 | (猫) | (狗) | (鸟) |
|---|---|---|---|
| 真实分布 | 0.00 | 1.00 | 0.00 |
| 预测分布 | 0.05 | 0.90 | 0.05 |
这里,真实分布表示样本是“狗”(概率为1)。模型的预测也高度集中在“狗”上,只是给其他类别分配了很小的概率。计算其交叉熵:
这个值非常小,说明两者高度相似。
现在考虑另一种情况,模型做出了完全错误的预测:
| 事件 | (猫) | (狗) | (鸟) |
|---|---|---|---|
| 真实分布 | 0.00 | 1.00 | 0.00 |
| 预测分布 | 0.70 | 0.15 | 0.15 |
交叉熵值显著增大,清晰地反映了两个分布之间的巨大差异。
因此,交叉熵的本质是:以真实分布 为权重,对“用分布 定义的信息量”求期望。它衡量的是,基于错误的估计 ,描述真实事件 所需要的平均比特数。
或者换个更直白的比喻:
假设你要用英语(Q)向外国人(P)传达信息。
- 如果你的英语很好(Q接近P),你不需要说很多话就能让对方理解
- 如果你的英语水平不高(Q远离P),你需要说很多废话、打很多手势才能沟通
交叉熵衡量的是:用英语向外国人传达信息时,平均每句要多说多少废话
可以通过简单的代码来验证和感受交叉熵的计算:
import numpy as np
def cross_entropy(p: np.ndarray, q: np.ndarray) -> float:
"""
计算离散概率分布 P 和 Q 之间的交叉熵 H(P, Q)。
"""
return -np.sum(p * np.log2(q + 1e-12))
# 示例计算
p = np.array([0.3, 0.3, 0.4])
q = np.array([0.25, 0.25, 0.5])
print(cross_entropy(p, q)) # 输出: 1.599...
4. KL散度
KL散度和交叉熵的功能相同,都是来衡量两个不同的概率分布之间的相似性的。同样的,两个分布越相似,KL散度值越小;两个分布差异越大,KL散度值越大。
你可能会问:既然交叉熵也可以衡量分布之间的差异,为什么还需要KL散度?要理解这一点,我们可以直接观察KL散度的计算公式:
你会惊奇的发现概率分布和的KL散度竟然只是其交叉熵减去的香农熵这么简单!对,KL散度就是交叉熵与真实分布熵的差值。这意味着,KL散度在交叉熵的基础上,减去了真实分布自身的不确定性,从而更纯粹地反映了“近似分布 与真实分布 之间的差异”。换句话说,交叉熵同时包含了“真实分布的熵”与“两个分布之间的差异”,而KL散度则只提取了后者。
注意,从公式中还可以看出,KL散度具有以下重要数学性质:
任何两个概率分布的KL散度一定是大于等于0的,如果未来某一天你在计算KL散度的时候发现计算结果小于0,那你就要注意了,很可能意味着代码实现有误,或者概率值未正确归一化。另外,KL散度是不对称的,这一点仅从公式上就能看出来了,表示目标分布是,它的值为预测分布与目标的差异,而则相反。
我们可以通过下面的这份代码来更加深入的理解交叉熵,KL散度之间的关系:
def kl_divergence(p, q):
"""
计算离散分布P和Q之间的KL散度 D_KL(P || Q)
"""
# 添加一个小值避免log(0)
p = np.clip(p, 1e-12, 1)
q = np.clip(q, 1e-12, 1)
# 归一化确保是概率分布
p = p / np.sum(p)
q = q / np.sum(q)
return np.sum(p * np.log(p / q))
p = np.array([0.02, 0.08, 0.9])
q = np.array([0.1, 0.1, 0.8])
print("H(p) = " + str(shannonEntropy(p))) #计算p的香农熵
print("H(q) = " + str(shannonEntropy(q))) #计算q的香农熵
print("H(p,q) = " + str(cross_entropy(p, q))) #计算p与q之间的交叉熵
print("H(q,p) = " + str(cross_entropy(q, p))) #计算q与p之间的交叉熵
print("D_kl(p|q) = " + str(kl_divergence(p, q)))#计算p与q之间的KL散度
print("D_kl(q|p) = " + str(kl_divergence(q, p)))#计算q与p之间的KL散度
# 输出
H(p) = 0.5411884030736893
H(q) = 0.9219280948830342
H(p,q) = 0.6219280948842965
H(q,p) = 1.0503737127006858
D_kl(p|q) = 0.05596448973692631
D_kl(q|p) = 0.0890317178497243
上面说到:交叉熵同时包含了“真实分布的熵”与“两个分布之间的差异”,而KL散度则只提取了后者。因此KL散度更能很好的体现出两个概率分布之间的差异性,KL散度要比交叉熵更加的“干净,纯粹”。
p = np.array([0.02, 0.08, 0.9])
q = np.array([0.1, 0.1, 0.8])
k = np.array([0.5, 0.4, 0.1])
print("H(p,q) = " + str(cross_entropy(p, q)))
print("H(p,k) = " + str(cross_entropy(p, k)))
print("D_kl(p|q) = " + str(kl_divergence(p, q)))
print("D_kl(p|k) = " + str(kl_divergence(p, k)))
# 输出
H(p,q) = 0.6219280948842965
H(p,k) = 3.1154895329762846
D_kl(p|q) = 0.05596448973692631
D_kl(p|k) = 1.7843695701105056 # 可以看到KL散度明显比交叉熵要小很多,因为它只包含差异信息,不包含额外的目标概率分布的熵的信息
既然这样,为什么在机器学习中经常使用交叉熵做损失函数,而很少听到KL散度呢?
其实在机器学习中,一直用的都是KL散度来做损失函数,只不过机器学习的训练数据的标签是固定的,因此是一个常数,根据公式
要求的最小值,其实就是求的最小值。