读完这篇,你能做什么
能计算离散分布的熵、交叉熵和 KL,验证交叉熵分解,并识别目标有正概率、预测却为零时的无穷损失。
学习理由、准备要求与知识点
为什么学它
分类器和语言模型常用对数损失。先看它给哪些结果分配概率、怎样求和或取平均,才能解释损失为何改变。
先读这些内容
知识点清单
- 对数底与信息单位
- 负对数信息量
- 离散熵
- 交叉熵的目标分布
- KL方向与非对称性
- 支持集与零概率
- 单标签负对数似然
- 损失归约与可比性
运行前先确认Java 17;Python 3.14 CPU 教学实验,无外部数据、预训练权重、GPU 或多机训练。源代码基线与局限见源码模块。
01先弄懂它是什么
预测时,“很常见的事发生了”和“极少见的事发生了”带来的意外程度不同。事件概率 p 越小,发生时越意外,可以用 -log(p) 表示这种信息量。对数还能把概率的乘法变成加法,方便合并计算。本篇 log 使用自然对数,单位叫 nat;换成以二为底的对数,单位就叫 bit,两者可以按固定比例换算。
要描述一整组可能结果有多不确定,可以把每种结果的信息量按出现概率取平均,这就是熵:H(p)=-sum(p_i*log(p_i))。如果只有一种结果可能发生,熵为零;如果 k 种结果一样可能,熵为 log(k)。它描述的是这个分布有多分散。比较熵的大小前,要先看它算的是哪些结果;熵高低本身不能直接说明模型预测准不准。
交叉熵问的是:按目标分布 p 出现的结果,预测分布 q 给了它们多少概率?例如目标中四分之三是 Java 问题、四分之一是数据库问题,就按这两个比例加权计算 -log(q_i),得到 H(p,q)=-sum(p_i*log(q_i))。
再用 KL(p||q)=sum(p_i*(log(p_i)-log(q_i))) 表示这一方向的分布差别,就有 H(p,q)=H(p)+KL(p||q)。因此,目标 p 不变时,降低交叉熵就是降低这个方向的 KL;目标本身有多不确定,也就是 H(p),不会因调整预测 q 而消失。
文中这几个词,先认一认
- 支持集
- 概率大于零的那些结果。例如普通六面骰点数分布的支持集是一到六。
- 交叉熵
- 按目标分布取平均,计算模型给正确目标的概率有多低;模型越不愿给目标分配概率,这个值通常越大。
- KL 散度
- 先按目标分布 p 给各种结果加权,再看用预测 q 计算的平均对数损失比用 p 本身多多少。交换 p、q 通常会改变结果;如果 p 认为可能的结果被 q 分到零概率,值会是正无穷。
02看看它怎么工作
以二分类为例,目标 p=(1,0),预测 q=(0.8,0.2),交叉熵为 -log(0.8),因为第二类目标权重为零。若预测改成 (0.1,0.9),损失明显增大;模型对错误类别越自信,真实类别概率越小,对数损失越重。这解释了为什么分类损失不只关心最终类别是否选对,还关心分配给目标的概率。
KL 通常不对称。p=(0.9,0.1)、q=(0.5,0.5) 时,KL(p||q) 与 KL(q||p) 使用不同分布加权,得到不同结果。虽然它非负且相同分布时为零,但一般不满足距离的对称性和三角不等式。优化哪一个方向可能产生不同偏好,不能只交换参数后认为目标完全相同。
零概率需要按极限约定处理。p_i=0 时,该项对熵和 KL 的贡献取零,即使 q_i 也为零;但 p_i>0、q_i=0 时,预测给真实可能事件零概率,交叉熵和 KL 变为正无穷。不能直接让机器计算 0*log(0) 得到 NaN 后再忽略,也不能无说明地加一个 epsilon 改变问题。
分类标签只有一个正确类别时,可以用 one-hot 表示:正确位置为一,其余为零,交叉熵就只剩该类别预测概率的负对数。对一批样本取平均,是在近似数据分布上的平均损失;标签噪声、采样方式和样本权重都会改变实际优化的目标。如果用软标签或标签平滑,目标 p 已经把概率分给多个类别,就必须按完整分布计算,不能再只取一个类别索引。
语言模型常按 token 平均负对数似然,并将其指数称为相应设定下的困惑度。比较时必须固定分词器、掩码、有效 token 数和对数底;同一个文本被切成不同 token,平均单位也变了。损失下降说明在该评估分布和计算契约下概率预测改善,不能直接推出任意任务能力都提升。
先跑通一个最小例子
Python stdlib 实验显式处理零概率,核对交叉熵分解与 KL 的非对称性。
import math
def measures(p,q):
if len(p)!=len(q) or not p:
raise ValueError("shape")
for values in (p,q):
if any(not math.isfinite(x) or x<0 or x>1 for x in values) or not math.isclose(sum(values),1.,abs_tol=1e-12):
raise ValueError("distribution")
h=ce=kl=0.
for a,b in zip(p,q):
if a==0:
continue # 目标零质量的项按极限约定为零。
h-=a*math.log(a)
if b==0:
ce=kl=math.inf
else:
ce-=a*math.log(b); kl+=a*(math.log(a)-math.log(b))
return h,ce,kl
h,ce,kl=measures([.5,.5],[.5,.5])
assert math.isclose(h,math.log(2))
assert kl==0.
assert math.isclose(measures([1.,0.],[.8,.2])[1],-math.log(.8))
a=measures([.9,.1],[.5,.5])
assert math.isclose(a[1],a[0]+a[2])
assert not math.isclose(a[2],measures([.5,.5],[.9,.1])[2])
assert math.isinf(measures([1.,0.],[0.,1.])[2])
assert measures([0.,1.],[0.,1.])[2]==0.
print("PASS 7: entropy, identity, NLL, decomposition, direction, support")
核对运行结果打印 PASS 7;支持集不匹配返回无穷而非 NaN,两个 KL 方向一般不相等。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
A["目标分布 p"] --> C["对预测负对数概率加权"]
B["预测分布 q"] --> C
C --> D["交叉熵"]
A --> E["目标自身熵"]
D --> F["与目标熵之差为 KL"]
E --> F按顺序看,每一步做了什么
- 确认目标与预测分布的事件顺序。
- 固定对数底和平均单位。
- 先处理零目标与零预测的支持边界。
- 分别累计熵、交叉熵和 KL。
- 验证分解关系并解释任务含义。
03对照实现代码
PyTorch v2.9.1;torch/distributions/kl.py · _kl_categorical_categorical
以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。
import math
def categorical_kl(p,q):
result=0.
for a,b in zip(p,q):
if a==0:
continue # 两边都为零时,也由目标零质量优先决定贡献为零。
if b==0:
return math.inf
result+=a*(math.log(a)-math.log(b)) # 避免先求 a/b 产生不必要的范围问题。
return result # 调用者负责相同形状和分布合法性。
PyTorch 的 _kl_categorical_categorical 先用 p.probs 乘两侧归一化 logits 之差,随后对 q.probs 为零的项设置无穷,再对 p.probs 为零的项设置零,最后沿事件维求和。这个覆盖顺序确保两边同时为零时贡献为零,而真实支持被预测排除时产生无穷。
这里的 Categorical 对象已经负责把输入整理成概率和相应的对数值。原始 logits 只是各类别的打分,例如 [2,1],还不是 log(p)。若直接把两组原始分数相减,再乘 p_i,得到的不能直接叫 KL。
要先分别减去各自的 logsumexp,也就是“所有分数先取指数、相加,再取对数”的归一化项,才能得到对应的对数概率;这一项不能省略。实际计算还要采用数值稳定的 logsumexp 写法。
原创列表模型优先跳过零目标,再检查零预测,表达与数学支持规则一致的结果,但省略批量 Tensor、注册分派、广播和参数约束。真实实现依赖分布对象的契约;精简模型则明确要求调用方先验证形状与概率总和。
框架的概率到 logits 转换和数值截断可能影响极接近零或一时的有限精度细节。支持规则与精确数学表达式应分开讨论,不能只看到普通样本输出接近就推断任意极值逐位一致。本文完整算法直接操作已验证的 double 概率,以可解释的规则作为参考。
代码先调用谁,再调用谁
- 构造并验证两个 Categorical 分布
- KL 注册分派选择类别分布实现
- 计算 p 加权对数概率差
- 修正零预测和零目标项
- 沿事件维求和
04动手写一个例子
离散熵、交叉熵与 KL 的一致计算
输入相同事件顺序的两个非空概率数组,均有限、非负且和约为一。返回三种以自然对数计量的信息量,显式支持零概率。
- 输入 1
p=q=[.5,.5]- 输出 1
H=ln2,CE=ln2,KL=0
- 输入 2
p=[1,0],q=[0,1]- 输出 2
CE=KL=正无穷
先对两侧独立验证分布合法性,避免把非法负数或未归一权重放进对数。遍历时先判断 p_i 是否为零,因为该项不应参与 log(p_i) 或 log(q_i);只有目标正质量的项才需要检查预测支持。
若出现正目标配零预测,交叉熵和 KL 标记为无穷,熵仍可正常累计。否则用 log(p_i)-log(q_i) 计算对数比,不先形成 p_i/q_i,避免比值在对数之前溢出。算法不加未声明的 epsilon,也不把无穷偷偷替换成某个大常数。
三种结果分别计算,测试再核对 CE≈H+KL,能帮助发现符号和加权方向错误。非负性是精确数学性质,浮点结果可能出现极小负舍入误差,实际容差判断应明确;不能用大范围截零掩盖颠倒概率或漏归一的实现缺陷。
为什么这样写是对的处理前缀后,三个累加器分别包含该前缀按支持规则定义的信息量贡献。
两次概率校验加一次逐项累计;每项固定次数对数和浮点运算。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
public class Main {
record Info(double entropy,double crossEntropy,double kl){}
static void check(double[] p){
if(p==null||p.length==0)throw new IllegalArgumentException("empty");
double sum=0;for(double x:p){if(!Double.isFinite(x)||x<0||x>1)throw new IllegalArgumentException("probability");sum+=x;}
if(Math.abs(sum-1)>1e-12)throw new IllegalArgumentException("sum");
}
static Info info(double[] p,double[] q){
check(p);check(q);if(p.length!=q.length)throw new IllegalArgumentException("shape");
double h=0,ce=0,kl=0;
for(int i=0;i<p.length;i++){
if(p[i]==0)continue; // 零目标项贡献为零,不计算零的对数。
double lp=Math.log(p[i]);h-=p[i]*lp;
if(q[i]==0){ce=Double.POSITIVE_INFINITY;kl=Double.POSITIVE_INFINITY;continue;} // 支持缺失必须显式呈现。
double lq=Math.log(q[i]);ce-=p[i]*lq;kl+=p[i]*(lp-lq); // 对数之差避免先算巨大比值。
}
return new Info(h,ce,kl);
}
static void near(double a,double b){if(!Double.isFinite(a)||Math.abs(a-b)>1e-12)throw new AssertionError();}
static void bad(Runnable r){try{r.run();}catch(IllegalArgumentException e){return;}throw new AssertionError();}
public static void main(String[] args){
Info uniform=info(new double[]{.5,.5},new double[]{.5,.5});near(uniform.entropy,Math.log(2));near(uniform.kl,0);
Info single=info(new double[]{1,0},new double[]{.8,.2});near(single.entropy,0);near(single.crossEntropy,-Math.log(.8));
Info a=info(new double[]{.9,.1},new double[]{.5,.5});near(a.crossEntropy,a.entropy+a.kl);
if(Math.abs(a.kl-info(new double[]{.5,.5},new double[]{.9,.1}).kl)<.01)throw new AssertionError("direction");
if(!Double.isInfinite(info(new double[]{1,0},new double[]{0,1}).kl))throw new AssertionError("support");
near(info(new double[]{0,1},new double[]{0,1}).kl,0);
bad(()->info(new double[]{.1,.1},new double[]{.5,.5}));
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 目标零质量项为零。
- 正目标与零预测形成无穷。
- 概率和不合法时拒绝。
- 浮点近零 KL 应按明确容差解释。
进阶选读 · 海量类别的稳定对数损失
场景类别或词表很大,概率可能极小。
做法从 logits 直接计算稳定 logsumexp 和目标负对数概率,避免先显式 softmax 再取 log;明确采样近似是否改变目标。
复杂度完整精确归一化通常随类别数线性,近似方法另有偏差与成本。
好处与代价可以改善数值范围,但不能无代价忽略未采样类别或改变评估单位。
05项目里怎么用
比较模型损失时,先固定数据、目标分布和归约规则。类别不平衡重采样、样本权重、标签平滑都会改变优化目标,不能把不同设置的平均损失直接排成优劣。应同时报告实际任务指标和概率校准,了解降低对数损失是否转化为需要的行为。
处理零概率时要区分模型定义和数值事故。理论上模型明确禁止某事件,与浮点 softmax 下溢产生零,是不同原因;前者涉及支持集设计,后者可以用 log-softmax 计算改善。无说明地把概率裁剪到 epsilon 会改变损失和梯度,应记录阈值并分析其影响。
记录 loss 时,要同时写明“总和除以了什么”。可以每个样本算一票,也可以每个 token 算一票,或只数没有被掩码排除的元素,这些平均数不是同一单位。例如一批只有两个有效 token,另一批有八个,把两个批次的平均 loss 再各按一半合并,就没有让十个 token 等权。在分布式训练和梯度累积中也要核对这个分母,所以应一起记录有效数量,而不只保存一个 loss 数字。
容易踩的坑
- 把 KL 当成对称距离。
- 直接计算 0*
log(0)得到NaN。 - 将原始 logits 当作对数概率。
- 跨分词器或不同归约单位比较平均损失。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 概率域公式 | 直观便于手算 | 极小概率易下溢 | 教学与小分布 |
| 对数域计算 | 适合极端分数和连乘 | 需要稳定归一化 | 训练损失 |
| 概率裁剪或平滑 | 可控制极端损失 | 改变原目标与支持规则 | 有明确建模依据的容错 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01熵为零意味着什么?
展开参考答案与推导
对于有限离散分布,全部概率集中在一个事件上时熵为零。它说明该分布没有取值不确定性,不说明预测的那个事件一定符合真实世界。
02为什么交叉熵可分解为熵加 KL?
展开参考答案与推导
将 KL 中的 p 加权 log(p)-log(q) 展开,第一部分等于负熵,第二部分等于交叉熵,移项即可得到关系;两者必须使用同一对数底和支持约定。
03KL 参数交换后为什么变?
展开参考答案与推导
权重分布和对数比方向同时改变,两者强调的事件区域不同。它不是满足对称性的几何距离,优化方向需要单独说明。
换个情况,还会处理吗
01分类正确但损失变大可能吗?
展开参考答案与推导
可能。只要真实类别仍排第一,预测类别不变,但分给它的概率下降会增加负对数损失。准确率和概率质量衡量不同性质。
02训练出现无穷损失如何判断?
展开参考答案与推导
检查目标正质量是否对应预测零概率,再区分真实支持限制与数值下溢。若是计算问题,优先使用稳定对数域算子,而非直接吞掉非有限值。
03模型 A 困惑度更低就一定更好吗?
展开参考答案与推导
只有在相同分词、数据、掩码和计量约定下才有可比基础。即便可比,它主要衡量语言建模概率,不自动覆盖工具使用或其他任务表现。
写代码试一试
01计算单标签交叉熵。
- 输入 / 条件
目标类别概率0.25- 输出 / 预期
ln4≈1.3863 nat
提示 只取目标负对数。
展开参考答案与推导
one-hot 目标使其他类别权重为零,损失是 -log(0.25)。若改成以二为底,则为二 bit,单位必须说明。
02处理两边同时为零。
- 输入 / 条件
p_i=0,q_i=0- 输出 / 预期
该项贡献0
提示 先看目标质量。
展开参考答案与推导
按极限约定跳过目标零质量项,不能执行零乘负无穷得到 NaN;这与正目标、零预测产生无穷是不同分支。
03验证分解关系。
- 输入 / 条件
p=(.9,.1),q=(.5,.5)- 输出 / 预期
CE≈H+KL
提示 分别实现再核对。
展开参考答案与推导
用同一自然对数分别累计三项,检查误差在容差内。这个测试能发现漏掉 p 权重或把对数比符号写反的错误。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能计算离散分布的熵、交叉熵和 KL,验证交叉熵分解,并识别目标有正概率、预测却为零时的无穷损失。
本篇重点加练
KL 的方向与支持集决定损失是否有限,也决定两个模型分布能否比较。
- KL方向与非对称性
- 支持集与零概率
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
对三类分布 p=[1
/2,1/4,1/4],分别以 bit 与 nat 计算各事件的信息量和熵,说明二者数值的转换依据。- 提交内容
- 手算表与单位说明
- 怎样算完成
- 对数底明确
- 信息量与平均信息量分开
练到的知识点对数底与信息单位 · 负对数信息量 · 离散熵
- 作业 02原理问答
用 p=[0.6,0.4,0] 与 q=[0.3,0.3,0.4] 比较两个方向的 KL;明确写出每项的定义域与零概率约定。
- 提交内容
- 逐项计算与边界说明
- 怎样算完成
- 支持集处理完整
- 方向标记始终一致
练到的知识点交叉熵的目标分布 · KL方向与非对称性 · 支持集与零概率
- 作业 03代码推演
给 p=(0.5,0.25,0.25,0)、q=(0.25,0.25,0,0.5),把前两类与后两类分别合并。比较合并前后支持集和两个方向 KL 的定义域,审查原有区分信息是否保留。
- 提交内容
- 支持集对照与定义审查
- 怎样算完成
- 零项约定清晰
- 有限值与无穷情况区分
练到的知识点KL方向与非对称性 · 支持集与零概率
- 作业 04原理问答
对 q 中的零概率统一加 epsilon 再归一化,会改变什么计算对象?取同一 p,在三种自定 epsilon 下记录目标定义及 KL 变化。
- 提交内容
- 目标变化说明与数值表
- 怎样算完成
- 平滑后分布完整记录
- 新目标不被说成原目标精确值
练到的知识点KL方向与非对称性 · 支持集与零概率
综合应用
- 作业 05编程实践
编写交叉熵核对器,支持硬标签与概率目标,以及 sum、按样本 mean 两种归约;用同一组 logits 验证批量拆分前后的统计。
- 提交内容
- CPU 脚本与归约测试
- 怎样算完成
- 目标分布校验明确
- 分母可追踪
练到的知识点交叉熵的目标分布 · 单标签负对数似然 · 损失归约与可比性
- 作业 06排错实验
构造两个模型拥有相同 top-1 预测但损失不同的三分类样本集,记录预测、概率与逐样本损失;评审只用准确率挑模型会遗漏什么。
- 提交内容
- 最小数据集与评审记录
- 怎样算完成
- 样本不只覆盖易例
- 结论基于实际记录
练到的知识点支持集与零概率 · 单标签负对数似然
- 作业 07面试场景
两份语言模型报告分别使用字符级和子词级 token,并用不同对数底。制定比较表,指出哪些字段可以换算、哪些需要重新评测。
- 提交内容
- 比较前提清单
- 怎样算完成
- 评价单位明确
- 无法直接换算的因素可解释
练到的知识点对数底与信息单位 · 损失归约与可比性
- 作业 08编程实践
用 Python 标准库实现离散分布的支持集审计器,输出导致交叉熵或 KL 不有限的坐标;测试空分布、负项、未归一化及合法稀疏分布。
- 提交内容
- 审计代码及异常报告
- 怎样算完成
- 输入校验与数学边界分开
- 错误坐标可追踪
练到的知识点KL方向与非对称性 · 支持集与零概率
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
设计一个有限分布实验,逐步改变预测分布集中程度,同时观察熵、交叉熵与 KL;在看到结果之前写下待检验的问题。
- 提交内容
- 预注册问题、代码与结果图
- 怎样算完成
- 真实分布固定
- 不同量的解释边界准确
练到的知识点离散熵 · 交叉熵的目标分布 · KL方向与非对称性
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
把 KL 用于比较“只覆盖热门类别”和“覆盖所有类别但概率粗糙”的两个预测器:自行构造真实分布,研究交换 KL 方向是否改变模型选择。
- 提交内容
- 三组分布及选择分析
- 怎样算完成
- 比较方向明确
- 结论限定给定支持集
练到的知识点KL方向与非对称性 · 支持集与零概率
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PyTorch v2.9.1:Categorical KL 的零质量规则 (新标签页)↗
核对类别分布 KL 的加权、事件维求和以及零质量覆盖顺序。
- 深入参考文档英文
MacKay:Information Theory, Inference, and Learning Algorithms (新标签页)↗
阅读信息量、熵、相对熵与概率推断章节。
- 深入参考文档英文
PyTorch 2.9 distributions (新标签页)↗
阅读 Categorical 和
kl_divergence的分布接口。 - 深入参考文档英文
MIT 18.05:概率统计 (新标签页)↗
复习期望与概率加权,为交叉熵的平均含义建立基础。