香农熵、交叉熵和相对熵这三个概念,基本是所有搞机器学习、数据分析、信号处理甚至通信的人绕不过去的坎。不管你是刚接触损失函数的新手,还是已经调过几个月模型的实践者,弄懂这三者的来龙去脉,绝对能让你看问题的视角提升一个层次。很多文章要么堆公式、要么只给结论,看完还是一头雾水。我这篇打算换个思路,从直觉讲起,把公式拆开揉碎,再落到实际项目里的应用场景和训练细节上,保证你能读完就上手。
这套理论说到底是在回答一个问题:信息到底怎么度量?它的答案我们今天仍然在用——把信息量定义为“不确定性减少的程度”。越不可能发生的事,一旦发生,带来的信息量就越大。这也是整个信息论的地基。理解了这一点,交叉熵、相对熵的知识就不再是孤立公式,而是一条清晰的逻辑链。
适合看这篇文章的人很广泛:正在学习机器学习基础、想搞懂损失函数原理的学生,需要选型损失函数的算法工程师,或者单纯对信息论感兴趣、想建立数学直觉的爱好者。只要你有高中数学基础,能看懂求和符号和log,这篇就能轻松读完。
1. 从直觉出发:信息量到底怎么算
1.1 越“意外”的消息,信息量越大
先说一个最朴素的感受。你每天早上出门前判断“今天太阳从东边升起”,这句话几乎不携带任何信息,因为你早就知道它一定发生。但如果天气预报说明天有台风登陆,这个消息就非常“值钱”,因为它的概率很低,它的发生让你对世界状态有了新的认知。
信息量跟概率成反比。一个事件发生的概率越小,它带来的信息量就越大。概率为百分之百的事件,信息量是零;概率接近零的事件,信息量趋向无穷大。
为了把这个直觉公式化,信息论选择了对数函数来定义单个事件的信息量:
I(x) = -log(P(x))
取对数有几个很自然的理由。第一,它满足“概率小、信息大”的单调性。第二,它把乘法变成加法——两个独立事件同时发生的信息量应该等于各自信息量之和,而对数恰好能做到这一点。第三,底数的选取只是影响单位,常用底数为2、自然常数e和10,单位分别对应比特(bit)、纳特(nat)和哈特利(hartley)。大多数机器学习框架里用的自然对数,所以损失值以nat为单位,这对后续计算没有任何影响,因为在求梯度的时候常数因子会被优化器吸收掉。
1.2 从单个事件到整个概率分布
上面定义的是某个特定事件的信息量。但我们研究一个随机系统时,关心的是它的整体特性——对所有可能事件的信息量取期望,这就是香农熵的定义。
设随机变量X的取值集合为{x1, x2, ..., xn},对应概率为P(xi),香农熵写作:
H(P) = -Σ P(xi) * log(P(xi))
这个公式读起来就是:把所有可能事件的信息量按各自概率加权平均。熵度量的是一个概率分布本身的平均不确定性。这个值越大,说明系统越混乱、越难以预测;越小,说明系统越有序、越容易判断。
我还记得第一次接触熵的时候,总觉得它只是一个抽象的数学定义。直到后来做序列建模,看到模型在预测下一帧数据时输出概率分布特别平坦——所有候选词的概率都差不多——这时候熵就会很高,模型实际上处于“迷茫”状态。反之,如果一个模型的输出概率高度集中在某个词上,熵就会很低,代表模型很有把握。熵这个概念,在实操中就是衡量模型置信度的天然指标。
1.3 二元分布的熵长什么样
我们拿最常见的二元分布来画个图感受一下。设事件A发生的概率为p,那么它的熵为:
H = -p*log(p) - (1-p)*log(1-p)
当p=0或p=1时,系统完全确定,熵为0。当p=0.5时,熵达到最大值log(2),约等于0.693(自然对数单位)或1比特。这就是“抛硬币”——最简单的不确定系统。
这个规律可以推广:在给定取值范围内,均匀分布的熵最大。因为它把概率均匀摊给每个可能结果,不确定性最高。对机器学习稍微有点经验的朋友,看到这儿已经能联想到softmax输出的温度系数了——调高温度就是让输出分布更接近均匀分布、熵更大,调低温度就是让分布更尖锐、熵更小。
2. 交叉熵与相对熵:度量两个分布的差异
2.1 交叉熵的编码视角
单看一个分布的熵还不够,现实中我们经常要比较两个概率分布。比如真实数据分布是P,模型输出的分布是Q,我们想知道这两个分布有多“像”。
交叉熵的定义是:
H(P, Q) = -Σ P(xi) * log(Q(xi))
这个公式的形式跟熵很像,唯一的区别是log里面换成了Q。它的实际含义是:用分布Q的编码方式来编码服从分布P的事件,平均需要的比特数。如果两个分布完全一致,交叉熵就等于熵;如果它们不一致,交叉熵一定比熵大。这个“大出来的部分”是有意义的——它代表因为用了错误的编码方式而额外付出的代价。
我常用一个叫“编码长度”的类比来解释它。假设你有一套最优编码方案,专门针对分布P设计,平均每条消息需要H(P)个比特。现在你手里另一套编码方案是为分布Q设计的,拿它去编码P的消息,平均长度就成了交叉熵H(P,Q)。由于编码方案不匹配,这个长度一定不会优于最优值。理论上可以证明H(P,Q)≥H(P),这个差值正是相对熵要度量的东西。
2.2 KL散度:信息论里的“距离”
相对熵就是KL散度,定义是:
KL(P || Q) = Σ P(xi) * log(P(xi) / Q(xi))
拆开来看,就是交叉熵减去熵:
KL(P || Q) = H(P, Q) - H(P)
所以KL散度的直觉理解特别简单:它是用Q去近似P时,额外多付出的编码代价。也可以说,它是“P相对于Q的信息增量”。
KL散度有两个性质值得我们记住。第一,它恒大于等于0,当且仅当P等于Q时取等。但它不是严格意义上的距离,因为不满足对称性——KL(P||Q)通常不等于KL(Q||P),也不满足三角不等式。所以准确的说法是:它是一个“不听话的距离”,在优化问题里需要选定方向。
我在项目里遇到过对称性踩坑的情况。有一次用KL散度来做知识蒸馏的损失函数,直接把teacher和student的输出分布做了对称化处理,结果发现收敛路径完全不同。后来才意识到,KL(P||Q)和KL(Q||P)的优化难度不一样,这直接让我下定决心单独测试两种方向。结果不出所料,蒸馏任务里KL(student || teacher)比KL(teacher || student)稳定得多,因为前者的梯度聚焦在teacher高置信度的区域——这正是我们想让student学习的关键区域。
2.3 一条公式把三者的关系串起来
很多资料把香农熵、交叉熵和相对熵当作三个孤立概念来讲,其实它们之间的关系非常紧密:
H(P, Q) = H(P) + KL(P || Q)
也就是说,从交叉熵里减去真实分布的熵,剩下的就是模型分布和真实分布的“偏离程度”。在一个监督学习任务里,真实分布P通常固定不变,所以H(P)是常数。这就解释了为什么我们直接优化交叉熵和优化KL散度本质上是等价的——它们相差一个无关紧要的常数。
这个关系在数学上极其简单,但它统一了三个概念。我建议你把这个公式抄在笔记本上,做题和设计网络的时候多回头想想它,能帮你省掉很多从零推公式的麻烦。
3. 机器学习中最常用的落地场景
3.1 分类问题的损失函数为什么是交叉熵
现在聊点实战的。在深度学习里,分类任务用CrossEntropyLoss已经是标准操作。我来解释它为什么成立。
假设有一个三分类任务,某个样本的真实类别是第二类。我们用one-hot向量表示真实分布P,即P = [0, 1, 0]。模型输出经过softmax得到Q = [0.2, 0.7, 0.1]。那么交叉熵就是:
H(P, Q) = -(0log(0.2) + 1log(0.7) + 0*log(0.1)) = -log(0.7)
因为one-hot向量在真实类别处为1、其余为0,公式里所有非真实类别的项全部为0,交叉熵直接退化成了“对真实类别所对应预测概率取负对数”。这正是我们在分类项目中最熟悉的形式。
如果真实类别的预测概率接近1,交叉熵就接近0;如果预测概率很小,比如0.001,交叉熵就是6.9左右,损失值巨大,梯度也会非常明显。这种性质让模型在错得离谱的样本上获得大梯度、迅速修正,在接近正确的样本上获得小梯度、放缓脚步。整个训练过程,本质上就是在不断地降低交叉熵,等价于让预测分布逐步贴向目标分布。
3.2 为什么不直接用均方误差(MSE)
我刚开始做深度学习的时候也好奇过这个问题:都是损失函数,分类任务为什么清一色选择交叉熵,而不像回归问题那样用MSE?
答案隐藏在梯度的形状里。拿带softmax的分类输出来说,如果用MSE,误差反向传播到softmax之前那一层时,梯度里会乘上softmax函数本身的导数项。softmax的导数曲线是类似钟形的,在概率接近0或1的时候导数趋近于0,导致梯度消失。也就是说,模型在最需要大幅更新的时候,反而更新不动了。
而交叉熵加上softmax的组合,梯度公式异常简洁。设模型输出的logits经过softmax后的概率为q_i,真实one-hot标签为p_i,那么损失对logits的梯度正好是:
∂L/∂z_i = q_i - p_i
也就是“预测概率减真实标签”。这是一个极其干净的线性差值形式。预测对了差值接近0,预测错了差值就大,更新方向一目了然。所以仅仅因为梯度质量这一点,交叉熵就是分类任务当之无愧的首选。
3.3 从KL散度视角理解训练过程
有了KL散度的概念,我们还能换个角度理解模型训练。模型训练就是在最小化:
KL(P_data || P_model)
在这个式子中,P_data表示真实数据分布,P_model是模型学到的条件分布。因为真实分布的熵固定,最小化KL散度等价于最小化交叉熵。
不过要注意这里的方向。我们是在固定P_data、优化P_model,用P_model去近似P_data,所以用的是KL(P_data || P_model)。这种方向的KL散度倾向于“覆盖”真实分布中出现的所有模式——哪怕有些模式概率比较低,模型也会努力给它们分配非零概率。如果你换成了反向的KL(P_model || P_data),模型就会倾向于只抓住概率最高的那些模式,而忽略尾部事件。这在生成模型里就是两种完全不同行为的来源,某些场景下甚至会演化为所谓的“模式坍塌”。如果你写过生成对抗网络或变分自编码器,对这两个方向带来的差异会感受更深。
4. 实际项目中的细节问题与排查技巧
4.1 到底该用log2还是loge
我见过很多初学者在计算熵或损失的时候纠结底数问题,其实完全不必。log的底数只影响数值刻度,不改变相对关系和梯度方向。以2为底得到熵的单位是比特,以e为底得到单位是纳特。在深度学习框架里,无论是PyTorch的CrossEntropyLoss还是TensorFlow的SoftmaxCrossEntropyWithLogits,内部用的都是自然对数。你用log2算出来的数值大约是用loge算出来的1.4427倍,但对模型训练毫无影响。
如果你要跟别人交流信息量数值,先确认对方的底数约定。我习惯在文章或代码注释里标明底数,避免读者对不上号。
4.2 处理零概率问题
交叉熵和KL散度公式里都有log,如果分布Q的某一项概率严格为0,log(Q)就成了负无穷,损失函数直接炸掉。这个坑几乎每个做过NLP或语音任务的人都踩过。
一个典型场景:语言模型输出词表很大,有些词概率被模型压到极低,甚至因为浮点数下溢变成0。如果真实标签恰好落在这些词上,梯度就完全无法计算。实际工程里有几种常用处理办法。
第一种是数值裁剪,把概率限制在一个很小的下界上,比如1e-7。这个做法简单,但会破坏概率分布的归一化性质,所以只在调试时用。
第二种是把计算放在log空间里做,使用log_softmax而不是先softmax再log。这就是框架里LogSoftmax存在的理由。它从数学上保证了你拿到的log概率是一个有效数值,而不会出现在softmax阶段下溢成0、再log变无穷的尴尬。
第三种是在交叉熵计算函数里显式忽略那些“不可能出现的词”。很多框架的损失函数提供一个ignore_index参数,把padding位置或某些不需要计算loss的token排除掉。我们做序列标注任务的时候,padding位置的标签设为-100,就能直接规避它参与反向传播。
4.3 softmax与交叉熵的组合陷阱
我发现不少人在手动实现分类模型时,习惯先调torch.softmax得到概率,再取log,然后跟标签算交叉熵。这种做法其实效率很低,而且容易踩数值稳定性的坑。正确做法是直接用LogSoftmax,再配合NLLLoss;或者干脆用封装好的CrossEntropyLoss,它内部会把softmax和log合并计算,避免中间变量的精度损失。
数值稳定性是一个被低估的细节。softmax公式分母是若干指数的和,如果logits里出现一个很大的数,比如100,指数运算在浮点数下会溢出成inf。标准解决方案是所有logits统一减掉最大值,这样最大项变成e^0=1,整个计算安全了。大多数框架的LogSoftmax内部已经做了这一步,不需要自己操心。
我在一个跨平台推理项目里遇到过一次诡异现象:同一套权重,在Python训练环境里loss正常,部署到嵌入式端(浮点数精度较低)后,某些样本loss变成了NaN。排查到最后一层才发现,是对方的手写实现里漏了数值平移。把那个“减去max”的步骤补上以后,问题立刻消失。这让我后来对自己写的任何涉及指数的代码都格外谨慎。
5. 手动计算与代码验证
5.1 拿一个完整小例子算一遍
光看公式不长记性,我建议你亲手算一遍。假设有两个分布:
P = [0.4, 0.6] Q = [0.3, 0.7]
用自然对数计算。
香农熵:
H(P) = -(0.4ln0.4 + 0.6ln0.6) H(P) ≈ -(0.4*(-0.9163) + 0.6*(-0.5108)) H(P) ≈ 0.6730
交叉熵:
H(P,Q) = -(0.4ln0.3 + 0.6ln0.7) H(P,Q) ≈ -(0.4*(-1.2040) + 0.6*(-0.3567)) H(P,Q) ≈ 0.6958
KL散度:
KL(P||Q) = H(P,Q) - H(P) ≈ 0.6958 - 0.6730 = 0.0228
换一个Q,比如Q' = [0.1, 0.9],再算交叉熵:
H(P,Q') ≈ -(0.4ln0.1 + 0.6ln0.9) H(P,Q') ≈ -(0.4*(-2.3026) + 0.6*(-0.1054)) H(P,Q') ≈ 0.9841
KL(P||Q') ≈ 0.9841 - 0.6730 = 0.3111。这个值比之前大得多,说明Q'偏离P更多。你观察一下就能发现,KL散度越大,两个分布差异越大,这跟直觉完全吻合。
5.2 用Python快速验证
写一小段代码验证上面的计算,顺便帮你建立手感:
import numpy as np def entropy(p, eps=1e-12): p = np.asarray(p, dtype=np.float64) return -np.sum(p * np.log(p + eps)) def cross_entropy(p, q, eps=1e-12): p = np.asarray(p, dtype=np.float64) q = np.asarray(q, dtype=np.float64) return -np.sum(p * np.log(q + eps)) def kl_divergence(p, q, eps=1e-12): p = np.asarray(p, dtype=np.float64) q = np.asarray(q, dtype=np.float64) return np.sum(p * np.log((p + eps) / (q + eps))) P = np.array([0.4, 0.6]) Q1 = np.array([0.3, 0.7]) Q2 = np.array([0.1, 0.9]) print("H(P) =", entropy(P)) print("H(P,Q1) =", cross_entropy(P, Q1)) print("KL(P||Q1) =", kl_divergence(P, Q1)) print("H(P,Q2) =", cross_entropy(P, Q2)) print("KL(P||Q2) =", kl_divergence(P, Q2))注意到代码里我加了一个eps,这是为了数值保险,防止P或Q中出现0导致log(0)。实际调试的时候,你可以把eps调大一点看看损失值的变化——你会直观体会到那种“硬截断”对梯度的破坏性。
5.3 用PyTorch的交叉熵损失对比
再补充一段PyTorch示例,让你到训练代码里能顺利对上号:
import torch import torch.nn as nn criterion = nn.CrossEntropyLoss() # 模拟batch_size=2、分类数=3的logits logits = torch.tensor([[1.2, 0.3, -0.5], [0.8, 2.0, 1.1]]) # 真实标签 targets = torch.tensor([0, 2]) loss = criterion(logits, targets) print(loss.item())这段代码里标签不是one-hot,而是整数索引。框架内部会自动做三件事:把logits过softmax、取log、按索引收集真实类别的负对数概率并求平均。等价的手写方式是:
log_probs = torch.nn.functional.log_softmax(logits, dim=1) print(-log_probs.gather(1, targets.view(-1, 1)).mean().item())两种写法结果一致。我建议你在本地跑一下这个对比,它会让你彻底明白框架里的CrossEntropyLoss到底做了什么。
6. 分享几条实战心得
6.1 正则化与损失函数的关系
在训练模型时,总损失里除了交叉熵,还可能加上L1或L2正则项。你应该把正则项看作对分布的一个“先验约束”,它鼓励模型学到一个更平滑、更简单的分布,而交叉熵本身则负责拟合数据。两者在梯度上的作用方向时常是相反的,需要手动平衡。我常用做法是把正则系数初始调得很小(比如1e-5),等交叉熵开始收敛后再慢慢加大,这样能避免模型一开始就被正则项带偏。
6.2 熵当作反作弊指标
我个人非常喜欢用熵指标来监控模型运行状态。如果在线推理时,某个模型的输出概率熵值突然升高,往往表示输入分布漂移了或者模型权重被破坏。我之前维护过一段时间推荐系统模型,线上布置了按小时记录的熵监控。有一次系统报异常,说是某些用户群体点击率掉得厉害,我查了模型输出熵,发现它在那几个小时明显上升——说明模型对所有候选都开始给出近乎均匀的概率,基本等于在随机推荐。后来定位到原因是特征拼接时有一个字段的下线导致大量输入变成默认值。如果没有熵这个指标,我可能要花好久才能反应过来。
6.3 训练时的“损失值不下降”别急着调学习率
如果你发现交叉熵损失长时间降不下去,先不要着急动学习率。第一步应该检查是否数值溢出,第二步检查标签和输出的维度是否匹配,第三步检查数据增强是否引入噪声。我处理过不少分类任务,最终原因经常是标签错位,而不是模型容量或优化器参数问题。损失函数的选择虽然重要,但数据质量始终是第一位的。这一点套用信息论也很好理解:如果数据生成过程本身随机性很大,真实条件熵就很高,你再怎么优化也压不到0。你能降低的,只是模型带来的那部分额外不确定性。
说到底,香农熵、交叉熵和相对熵不只是考试题里的公式,它们是理解模型在“学什么”“学得怎么样”的两把钥匙。你理解得越深,调参就越有方向,改模型结构的时候就越有底气。拿个简单数据集,把手写交叉熵的脚本跑通,再换成各种框架内置损失函数对比一下,这套知识很快就长在你身上了。