这篇重点学什么
本篇重点:先推导交叉熵与 p − one-hot 梯度,再比较把多条损失求和或取平均(loss reduction)会怎样改变训练。
开始读正文 ↓学过相近内容?查看哪些可以跳过
两条路线有些基础相同。已经学会的部分可以跳过,另一条路线只用来补你当前需要的内容。
概率与 softmax
共同基础概率归一化与数值稳定性是共同基础;采样和求梯度是不同任务。
另一条路线补充什么?
先累计每项概率,再用二分查找选中一次采样结果;检查边界输入和调用预算,观察生成结果的随机性。
读完这篇,你能做什么
能手算二分类特例,编写稳定单样本分类损失,并将推导与 PyTorch 批量交叉熵及实际梯度对齐。
学习理由、准备要求与知识点
为什么学它
类别维度、求和与平均方式,或误传概率,都会改变分类训练目标。稳定计算还能避免极大分数让梯度失效。
先读这些内容
知识点清单
- Logits 与互斥类别
- Softmax 类别归一化
- 平移不变性
- 减最大值数值稳定
LogSoftmax计算次序- 硬标签交叉熵
- 概率减独热梯度
- 批量平均归约
- 概率目标与校准边界
运行前先确认Java 17;Python 3.14、PyTorch 2.9.1+cpu。源码定位 PyTorch v2.9.1 的 CPU LogSoftmax 辅助内核;Java 为原创单样本数学实现。只验证 CPU 小张量,不涉及 GPU 或概率校准结论。
01先弄懂它是什么
模型要从多个互斥类别中选一个时,通常先给每个类别打一个分数。这 C 个实数分数 z 叫 logits,可以为负,也不要求相加为一。Softmax 用 p_j=exp(z_j)/sum(exp(z_k)) 把它们转成非负、总和为一的数。本篇一次处理一批样本,输入形状为 [B,C]:B 是样本数,C 是类别数,每个样本都在自己的类别维度上单独归一化。
训练时需要明确哪个类别是正确答案。硬标签 t 就是该类别的整数编号,范围从零到 C-1。单个样本的交叉熵是 L=-log(p_t):模型给正确类别的概率越高,损失越小。它也可以理解为对正确类别的条件似然取负对数。但模型报出零点九,不代表它在这类预测中一定有百分之九十正确率;这种对应关系叫概率校准,需要用独立数据检查。
最常用的做法是把原始分数 logits 和正确类别的整数编号直接交给 PyTorch CrossEntropyLoss,前面不用再做一次 softmax。本篇只推导这种硬标签情况:不给类别加权,不忽略标签,也不做标签平滑。这个接口还支持为各类别分别提供概率的浮点目标,形状要与 logits 相同。换用其他模式时,要重新核对损失如何求和或取平均、分母是什么,以及梯度怎样计算。
文中这几个词,先认一认
- 类别轴
- 同一个样本里,各个类别分数排列的那个方向。Softmax 要沿这个方向计算。
- LogSoftmax
- 直接算出归一化后的对数概率,避免先算出非常小的概率,再取对数时出现数值问题。
- 独热向量
- 用一串零和一标记正确类别:正确位置是一,其余位置全是零。
- 概率校准
- 检查模型说的概率与实际发生比例是否相符。例如大量报出零点九的预测,是否约有九成正确。
02看看它怎么工作
所有 logits 加同一个常数不会改变 softmax,因为分子分母同时乘同一个指数因子。取 m=max(z),改用 exp(z_j-m) 后,指数自变量都不大于零,避免 exp(1000) 这类上溢。至少一个指数等于一,因此非空有限输入的归一化分母不会因所有项下溢而变成零。
稳定对数概率可写为 log(p_j)=(z_j-m)-log(sum(exp(z_k-m)))。运算顺序有意义:先减 m 再减对数和,可以避免先把很大的 m 与很小的校正项相加时丢失信息。数学上等价的表达式在浮点中不一定精度相同,不能只凭代数变形随意重排。
对正确类别 t,损失可写为 log(sum(exp(z_k-m)))+(m-z_t)。若用独热目标 y 表示答案,每个 logit 的导数就是 p_j-y_j。正确类通常得到负梯度,所以沿梯度反方向更新,会提高它相对其他类的分数。全部类别的梯度加起来为零,也符合前面的结论:把所有分数同时加上相同数,不会改变损失。
把 B 个样本的损失加起来再除以 B,就是按样本取平均;每个样本对 logits 的梯度也要除以 B。如果选择 sum,只把损失相加,就没有这一步除法。给类别加权、忽略部分标签或改用软目标后,要按具体模式检查分母,不能一律照搬 B。对比手写梯度与库结果前,先把这些设置逐项对齐。
直接先 softmax 再取 log 可能把很小概率舍入为零,导致本可有限表示的对数概率成为负无穷。例如 logits=[1000,-1000]、正确类为后一类,稳定损失约为二千;即使后一类概率已下溢到零,稳定对数表达仍能保留有限损失。本篇对超出 double 可表示范围的最终损失显式报错。
先跑通一个最小例子
使用第 13 篇准备的 Python 3.14、PyTorch 2.9.1+cpu 隔离环境执行 experiment.py,例如 .\.venv\Scripts\python.exe experiment.py。这里 logits 为 [2,3] 的双精度 Tensor,target 为 [2] 的 long 类别索引。
import math,torch
import torch.nn.functional as F
torch.set_num_threads(1)
z=torch.tensor([[1000.,1001.,999.],[-2.,0.,2.]],dtype=torch.float64,requires_grad=True)
target=torch.tensor([1,2],dtype=torch.long)
loss=F.cross_entropy(z,target)
centered=z-z.max(dim=1,keepdim=True).values
logp=centered-centered.exp().sum(dim=1,keepdim=True).log()
manual=-logp[torch.arange(2),target].mean()
assert torch.allclose(loss,manual,atol=1e-12)
loss.backward()
expected=z.detach().softmax(dim=1)
expected[torch.arange(2),target]-=1
expected/=2 # 平均损失对应每个样本梯度除以批次大小。
assert torch.allclose(z.grad,expected,atol=1e-12)
assert torch.allclose(z.grad.sum(dim=1),torch.zeros(2,dtype=torch.float64),atol=1e-12)
assert torch.allclose(F.cross_entropy(z.detach()+10000,target),loss.detach(),atol=1e-10)
extreme=torch.tensor([[1000.,-1000.]],dtype=torch.float64)
assert F.cross_entropy(extreme,torch.tensor([1])).item()==2000.
assert math.isclose(F.cross_entropy(torch.zeros(1,2,dtype=torch.float64),torch.tensor([0])).item(),math.log(2))
assert torch.allclose(z.detach().softmax(dim=1).sum(dim=1),torch.ones(2,dtype=torch.float64))
assert torch.isfinite(z.grad).all()
print("PASS 8; loss =",round(loss.item(),6),"extreme_loss = 2000")
核对运行结果打印 PASS 8;手写稳定损失与库一致,梯度为批量平均的 p-onehot,极端错误类损失为 2000。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
A["每行 C 个 logits"] --> B["求本行最大值 m"]
B --> C["减 m 后求指数和"]
C --> D["直接计算对数概率"]
D --> E["取正确类别负对数"]
E --> F["按批次定义归约"]
F --> G["梯度 p 减 onehot"]按顺序看,每一步做了什么
- 检查非空批次、类别轴与标签范围。
- 按每个样本独立减最大 logit。
- 直接形成稳定对数概率和损失。
- 按目标归约计算梯度尺度。
- 用解析值、平移不变性和自动微分交叉核验。
03对照实现代码
PyTorch v2.9.1;aten/src/ATen/native/cpu/LogSoftmaxKernelImpl.h · serial_vec_log_softmax_lastdim_range
以下 Python 代码为原创单行数学模型,不是原样 C++ 内核或逐行翻译。已核对固定版本 C++ 最大值归约、指数和及减法顺序,未实现其向量化与并行调度。
import math
def log_softmax_row(logits):
if not logits or not all(math.isfinite(v) for v in logits):
raise ValueError("finite nonempty logits")
maximum=max(logits)
shifted=[v-maximum for v in logits]
logsum=math.log(math.fsum(math.exp(v) for v in shifted))
return [v-logsum for v in shifted] # 先减最大值,再减对数和。
固定版本的串行范围辅助函数先按每一行归约最大值,再计算减去该最大值后的指数和。它处理调用者分配的行范围,并使用 chunk 暂存行级结果;源码中的批处理布局服务于内核效率,不改变每一行独立归一化的数学目标。
随后函数对每行指数和取对数,最后按 input-max_input-tmp_sum 写出对数概率。源码专门提示保持这个运算顺序,因为先把最大值与对数校正相加,在大幅输入、细小差异时可能损失精度;这正是本篇手写算法采用中心化表达的原因。
SoftMaxKernel.cpp 中的 _vec_log_softmax_lastdim 计算块大小并通过 parallel_for 调用该辅助函数。本篇选择定位实际数学循环所在头文件,避免把仅负责并行分派的外层函数描述成直接实现所有稳定归约。
这个内核计算 LogSoftmax,并不自行选择正确类、处理标签平滑或完成交叉熵全部归约。原创数学模型只展示单行有限输入,完整分类 API 的标签约束和损失模式需要再读 CrossEntropyLoss 文档;不能把内核功能扩张成整个分类训练过程。
代码先调用谁,再调用谁
- 公开
log_softmax进入本机实现分派 - CPU 最后一维路径分配行范围
serial_vec_log_softmax_lastdim_range计算最大值与指数和- 按中心化顺序写出对数概率
- 分类损失另处理标签与归约
04动手写一个例子
稳定单样本交叉熵及其梯度
给定非空有限 logits 和有效硬标签,返回 softmax 概率、单样本交叉熵及对 logits 的梯度;最终损失超出 double 范围时拒绝返回成功结果。
- 输入 1
logits=[0,0],target=0- 输出 1
loss=ln2,gradient=[-.5,.5]
- 输入 2
logits=[1000,-1000],target=1- 输出 2
loss=2000,gradient=[1,-1]
第一次扫描验证输入并寻找最大值,第二次计算中心化指数及其和。最大值对应指数一,避免全部指数下溢;特别小的其他指数允许下溢到零,这在 double 中是合理近似,不应借此再用 log(probability) 求损失。
损失单独使用中心化对数和加正确类差值计算,梯度则由概率复制后在正确类位置减一。概率和梯度使用独立数组,避免减一操作污染返回概率;单样本无批量平均,所以本函数不额外除以类别数。
测试相等分数、共同平移、极端正确与错误类、梯度和及非法标签。对于正负接近 double 极限的值,差值或最终损失可能超出表示范围,程序抛出范围错误,而不是声称减最大值能解决所有浮点问题。
为什么这样写是对的中心化后所有指数位于 [0,1] 且至少一项为一;返回梯度为同一组概率减去目标独热向量。
固定次数扫描 C 个类别,没有构造 C×C 雅可比矩阵;利用交叉熵组合后的简化导数直接得到梯度。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
public class Main {
record Result(double[] probabilities,double loss,double[] gradient){}
static Result evaluate(double[] z,int target){
if(z==null||z.length==0||target<0||target>=z.length)throw new IllegalArgumentException("shape or target");
double max=Double.NEGATIVE_INFINITY;
for(double v:z){if(!Double.isFinite(v))throw new IllegalArgumentException("finite logits");max=Math.max(max,v);}
double[] p=new double[z.length];double sum=0;
for(int i=0;i<z.length;i++){p[i]=Math.exp(z[i]-max);sum+=p[i];} // 减最大值后指数不会正向溢出。
double loss=Math.log(sum)+(max-z[target]); // 不从可能下溢为零的概率再取对数。
if(!Double.isFinite(loss))throw new ArithmeticException("loss range");
double[] gradient=new double[z.length];
for(int i=0;i<z.length;i++){p[i]/=sum;gradient[i]=p[i];}
gradient[target]-=1; // 独立梯度数组,保留原概率。
return new Result(p,loss,gradient);
}
static void near(double a,double b){if(!Double.isFinite(a)||Math.abs(a-b)>1e-10)throw new AssertionError(a+" != "+b);}
static void bad(Runnable r){try{r.run();}catch(IllegalArgumentException e){return;}throw new AssertionError();}
public static void main(String[] args){
Result equal=evaluate(new double[]{0,0},0);near(equal.loss,Math.log(2));near(equal.probabilities[0],.5);near(equal.gradient[0],-.5);
near(evaluate(new double[]{1000,1000},0).loss,equal.loss);
near(evaluate(new double[]{1000,-1000},0).loss,0);
Result wrong=evaluate(new double[]{1000,-1000},1);near(wrong.loss,2000);near(wrong.gradient[0],1);near(wrong.gradient[1],-1);
Result ordinary=evaluate(new double[]{1,2,3},2);double gsum=0,psum=0;
for(int i=0;i<3;i++){gsum+=ordinary.gradient[i];psum+=ordinary.probabilities[i];}near(gsum,0);near(psum,1);
bad(()->evaluate(new double[]{1,2},2));
bad(()->evaluate(new double[]{Double.NaN},0));
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 空类别与越界标签明确拒绝。
- 输入不接受
NaN或无穷大。 - 概率下溢不等于稳定对数损失必须无穷大。
- 最终损失仍可能超出
double表达范围。
进阶选读 · 软标签、类别权重与标签平滑
场景类别不平衡或希望使用概率目标训练。
做法从目标定义重新推导加权交叉熵及归约,验证目标非负且归一化;用小例比较库的各模式,避免沿用硬标签分母。
复杂度通常仍 O(B*C),但样本权重、掩码和统计归约更复杂。
好处与代价改变目标可能改善某些指标,也可能影响概率解释与少数类表现,需要独立验证和校准评估。
05项目里怎么用
数据管道应在进入损失前检查标签编码。类别编号从零开始还是从一开始、训练与部署类别表是否相同,都会影响意义;即使编号未越界,也可能把标签语义整体错位。保存类别映射与模型版本,不能只保存一个权重文件。
稳定算子解决特定数值路径,不会修复所有异常。极大 logits 可能来自学习率、输入尺度或上游非有限运算,仍应定位产生原因;将 NaN 替换成零通常改变优化目标并掩盖错误。记录首次非有限层、梯度和参数范围,比只观察最后 loss 更容易诊断。
得到类别概率后,业务还要规定怎样据此行动。Argmax 只是选出分数最高的类别;实际系统可能还要设置接受门槛、允许拒答,或考虑不同错分的代价。本篇用 Softmax 处理互斥类别,也就是一个样本只归入一类。多标签任务允许多个类别同时成立,通常会对各类分别使用 sigmoid 等方式,因此需要另行选择输出形式和损失函数。
容易踩的坑
- 对 logits 先 softmax 后再传
CrossEntropyLoss。 - 把批次轴作为类别轴归一化。
- 用
log(softmax)替代稳定log_softmax而忽略下溢。 - 把归一化分数直接当作已经校准的可信概率。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 稳定组合交叉熵 | 损失与梯度路径一致,避免中间概率下溢 | 需要正确标签模式与归约 | 常见互斥多分类训练 |
显式 log_softmax 加 NLL | 便于观察对数概率 | 必须保持接口和维度一致 | 调试与组合损失 |
| 逐类 sigmoid | 适合非互斥标签 | 概率不要求跨类和为一 | 多标签建模 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么减最大值不改变概率?
展开参考答案与推导
所有指数分子分母共同乘以 exp(-max),这个因子相消;同时指数自变量非正,降低上溢风险。
02交叉熵梯度为什么不除以类别数?
展开参考答案与推导
本篇单样本损失是正确类负对数,其导数直接为 p-onehot。只有明确对批次或其他单位取平均时才加入对应分母。
03softmax 输出是否已经校准?
展开参考答案与推导
不一定。非负且和为一只是归一化性质,预测数值与真实事件频率是否匹配需要独立评估。
换个情况,还会处理吗
01极小类别概率已经为零,损失一定无穷吗?
展开参考答案与推导
数学对数概率可能仍在浮点可表达范围。应直接使用稳定 log_softmax 或组合交叉熵,而不是对已舍入为零的概率再取对数。
02两种实现损失差一倍先查什么?
展开参考答案与推导
检查批次平均与求和、有效样本数、权重和忽略标签设置。归约尺度不同会同时改变梯度,不能只比较输出数值。
03多标签数据能直接套本篇 softmax 吗?
展开参考答案与推导
通常不合适。Softmax 将类别视为互斥选择,多标签允许多个同时成立,需要对应的逐类目标与损失。
写代码试一试
01手算相等二类分数。
- 输入 / 条件
z=[0,0],t=0- 输出 / 预期
p=[.5,.5],L=ln2,g=[-.5,.5]
提示 正确类梯度减一。
展开参考答案与推导
两类指数相同,概率各一半;负对数为 ln2,梯度向量概率减独热目标得到负一半与正一半。
02计算极端错误类损失。
- 输入 / 条件
z=[1000,-1000],t=1- 输出 / 预期
约2000
提示 使用中心化对数表达。
展开参考答案与推导
指数和约为一,对数项约为零;最大值减目标 logit 为二千,因此稳定损失仍有限。
03核对批量梯度。
- 输入 / 条件
B=2,平均损失- 输出 / 预期
每行(p-onehot)/2
提示 分母来自样本平均。
展开参考答案与推导
先逐行按类别计算概率减目标,再除以二。不能除以类别数,也不能忘记加权或忽略标签可能改变分母。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能手算二分类特例,编写稳定单样本分类损失,并将推导与 PyTorch 批量交叉熵及实际梯度对齐。
本篇重点加练
概率梯度与目标分布需要共同理解,重点训练软标签、权重和校准解释的边界。
- 概率减独热梯度
- 概率目标与校准边界
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
对 logits=[-2,1,3] 及整体加 100 后的向量,分别计算类别概率并核对类别轴;解释 logits 的绝对值是否有唯一含义。
- 提交内容
- 计算记录与轴说明
- 怎样算完成
- 类别互斥前提明确
- 平移前后比较可核验
练到的知识点Logits 与互斥类别 · Softmax 类别归一化 · 平移不变性
- 作业 02原理问答
比较先算概率再取对数与直接计算
LogSoftmax两条路径,列出各自中间值的表示范围及可能丢失的信息。- 提交内容
- 数值流程对照图
- 怎样算完成
- 中间量标注完整
- 数学等价与浮点行为分开
练到的知识点减最大值数值稳定 · LogSoftmax 计算次序 · 硬标签交叉熵
- 作业 03代码推演
三分类目标为 [0.7,0.2,0.1],自选非相等 logits,推导交叉熵对 logits 的梯度,并与同一 logits 的硬标签目标比较。
- 提交内容
- 两种目标与梯度记录
- 怎样算完成
- 目标归一化可核验
- 目标类型变化明确
练到的知识点概率减独热梯度 · 概率目标与校准边界
综合应用
- 作业 04编程实践
实现 [B=3,C=4] 硬标签交叉熵及解析梯度,使用双精度有限差分核对;同时支持 sum 和 mean 归约。
- 提交内容
- CPU 实现与梯度检查
- 怎样算完成
- 类别和批次轴正确
- 两种归约分母可追踪
练到的知识点硬标签交叉熵 · 概率减独热梯度 · 批量平均归约
- 作业 05排错实验
以 logits=[1000,-1000,0] 和两个不同标签检验三种损失实现,记录有限性和梯度;为不能接受的输入另建校验测试。
- 提交内容
- 极值实验与错误分类
- 怎样算完成
- 数值异常与输入异常分开
- 梯度和损失一起记录
练到的知识点减最大值数值稳定 · LogSoftmax 计算次序 · 硬标签交叉熵
- 作业 06面试场景
数据中一条样本可同时属于两个类别,但现有接口强制所有类别概率和为一。评审任务定义与损失选择,设计一个暴露问题的标注样例。
- 提交内容
- 任务契约修订与样例
- 怎样算完成
- 互斥假设明确
- 训练目标与评价目标一致
练到的知识点Logits 与互斥类别 · 概率目标与校准边界
- 作业 07编程实践
实现类别加权的硬标签交叉熵梯度检查,明确 mean 按样本数还是权重和归约;对含零权重类别的三样本批次核对自动求导。
- 提交内容
- CPU 检查器及归约契约
- 怎样算完成
- 两种分母不混用
- 零权重边界经验证
练到的知识点概率减独热梯度
- 作业 08设计评审
一个模型经温度变换后置信度改变。设计评价区分排序、分类正确率与概率质量,预先定义各指标及用于拟合温度的数据。
- 提交内容
- 评价矩阵与数据协议
- 怎样算完成
- 不同性质分别度量
- 拟合与最终评估分开
练到的知识点概率目标与校准边界
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
制作一个固定预测器的校准评估实验,按置信度分组报告实际正确率;若拟合温度参数,明确划分拟合数据与最终评价数据。
- 提交内容
- 校准图、脚本与划分说明
- 怎样算完成
- 分组计数完整
- 校准不由训练准确率代替
练到的知识点概率目标与校准边界
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
如果目标向量各项非负但和不是一,沿用概率目标交叉熵会优化什么?从一般目标和的情形推导梯度,并讨论能否仍解释为概率校准。
- 提交内容
- 一般推导与解释边界
- 怎样算完成
- 目标质量总量显式
- 数学目标和概率语义区分
练到的知识点概率减独热梯度 · 概率目标与校准边界
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PyTorch v2.9.1:CPU LogSoftmax 的稳定减法顺序 (新标签页)↗
核对固定版本 CPU
LogSoftmax的行级最大值、指数归约及保持减法顺序的数值理由。 - 深入参考文档英文
PyTorch 2.9 CrossEntropyLoss (新标签页)↗
对照硬标签、概率目标、权重、忽略标签和归约定义。
- 深入参考文档英文
PyTorch 2.9 log_softmax (新标签页)↗
阅读为何直接组合实现比
log(softmax)更稳定。 - 深入参考文档英文
PyTorch v2.9.1 CPU SoftMaxKernel (新标签页)↗
核对最后一维路径怎样分配行范围并调用辅助内核。