qinghuan
← 机器学习与统计推断 / 全部笔记

单元 07 / 机器学习与统计推断

P0 · 必会主线L1 · 基础

对数、熵、交叉熵与 KL 散度

从“越少见的结果越意外”理解负对数信息量,再学习熵、交叉熵和 KL,检查零概率、比较方向与分类损失的关系。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能计算离散分布的熵、交叉熵和 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 的非对称性。

experiment.py · python
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

按顺序看,每一步做了什么

  1. 确认目标与预测分布的事件顺序。
  2. 固定对数底和平均单位。
  3. 先处理零目标与零预测的支持边界。
  4. 分别累计熵、交叉熵和 KL。
  5. 验证分解关系并解释任务含义。

03对照实现代码

PyTorch v2.9.1:Categorical KL 的零质量规则 (新标签页)↗

PyTorch v2.9.1;torch/distributions/kl.py · _kl_categorical_categorical

以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。

核心机制代码 · python
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 概率,以可解释的规则作为参考。

代码先调用谁,再调用谁

  1. 构造并验证两个 Categorical 分布
  2. KL 注册分派选择类别分布实现
  3. 计算 p 加权对数概率差
  4. 修正零预测和零目标项
  5. 沿事件维求和

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,能帮助发现符号和加权方向错误。非负性是精确数学性质,浮点结果可能出现极小负舍入误差,实际容差判断应明确;不能用大范围截零掩盖颠倒概率或漏归一的实现缺陷。

为什么这样写是对的处理前缀后,三个累加器分别包含该前缀按支持规则定义的信息量贡献。

时间 O(k),k 为事件数量。额外空间 O(1) 额外空间。

两次概率校验加一次逐项累计;每项固定次数对数和浮点运算。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
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方向与非对称性
  • 支持集与零概率

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    对三类分布 p=[1/2,1/4,1/4],分别以 bit 与 nat 计算各事件的信息量和熵,说明二者数值的转换依据。

    提交内容
    手算表与单位说明
    怎样算完成
    • 对数底明确
    • 信息量与平均信息量分开

    练到的知识点对数底与信息单位 · 负对数信息量 · 离散熵

  2. 作业 02原理问答

    用 p=[0.6,0.4,0] 与 q=[0.3,0.3,0.4] 比较两个方向的 KL;明确写出每项的定义域与零概率约定。

    提交内容
    逐项计算与边界说明
    怎样算完成
    • 支持集处理完整
    • 方向标记始终一致

    练到的知识点交叉熵的目标分布 · KL方向与非对称性 · 支持集与零概率

  3. 作业 03代码推演

    给 p=(0.5,0.25,0.25,0)、q=(0.25,0.25,0,0.5),把前两类与后两类分别合并。比较合并前后支持集和两个方向 KL 的定义域,审查原有区分信息是否保留。

    提交内容
    支持集对照与定义审查
    怎样算完成
    • 零项约定清晰
    • 有限值与无穷情况区分

    练到的知识点KL方向与非对称性 · 支持集与零概率

  4. 作业 04原理问答

    对 q 中的零概率统一加 epsilon 再归一化,会改变什么计算对象?取同一 p,在三种自定 epsilon 下记录目标定义及 KL 变化。

    提交内容
    目标变化说明与数值表
    怎样算完成
    • 平滑后分布完整记录
    • 新目标不被说成原目标精确值

    练到的知识点KL方向与非对称性 · 支持集与零概率

综合应用

  1. 作业 05编程实践

    编写交叉熵核对器,支持硬标签与概率目标,以及 sum、按样本 mean 两种归约;用同一组 logits 验证批量拆分前后的统计。

    提交内容
    CPU 脚本与归约测试
    怎样算完成
    • 目标分布校验明确
    • 分母可追踪

    练到的知识点交叉熵的目标分布 · 单标签负对数似然 · 损失归约与可比性

  2. 作业 06排错实验

    构造两个模型拥有相同 top-1 预测但损失不同的三分类样本集,记录预测、概率与逐样本损失;评审只用准确率挑模型会遗漏什么。

    提交内容
    最小数据集与评审记录
    怎样算完成
    • 样本不只覆盖易例
    • 结论基于实际记录

    练到的知识点支持集与零概率 · 单标签负对数似然

  3. 作业 07面试场景

    两份语言模型报告分别使用字符级和子词级 token,并用不同对数底。制定比较表,指出哪些字段可以换算、哪些需要重新评测。

    提交内容
    比较前提清单
    怎样算完成
    • 评价单位明确
    • 无法直接换算的因素可解释

    练到的知识点对数底与信息单位 · 损失归约与可比性

  4. 作业 08编程实践

    用 Python 标准库实现离散分布的支持集审计器,输出导致交叉熵或 KL 不有限的坐标;测试空分布、负项、未归一化及合法稀疏分布。

    提交内容
    审计代码及异常报告
    怎样算完成
    • 输入校验与数学边界分开
    • 错误坐标可追踪

    练到的知识点KL方向与非对称性 · 支持集与零概率

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    设计一个有限分布实验,逐步改变预测分布集中程度,同时观察熵、交叉熵与 KL;在看到结果之前写下待检验的问题。

    提交内容
    预注册问题、代码与结果图
    怎样算完成
    • 真实分布固定
    • 不同量的解释边界准确

    练到的知识点离散熵 · 交叉熵的目标分布 · KL方向与非对称性

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    把 KL 用于比较“只覆盖热门类别”和“覆盖所有类别但概率粗糙”的两个预测器:自行构造真实分布,研究交换 KL 方向是否改变模型选择。

    提交内容
    三组分布及选择分析
    怎样算完成
    • 比较方向明确
    • 结论限定给定支持集

    练到的知识点KL方向与非对称性 · 支持集与零概率

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗