开始前,会这些就够了
- 知道正确类别的概率越低,负对数损失越大
- 知道类别索引与one-hot可表示同一个类别
- 先读 Softmax 分类与数值稳定的损失函数:先理解正确类负对数损失、类别索引与one-hot标签。
两个样本的真实类别依次是0和1,正确类概率分别为0.8和0.4。类别权重设为[1,3]。按PyTorch 2.14文档,类别索引目标的mean约为0.743004;表示同样类别的浮点one-hot目标,mean却约为1.486008。
差异不在标签内容,也不在每个样本的加权损失,而在最后的分母。下面用标准库复现文档公式,不把公式计算冒充一次真实torch运行。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。程序分别按类别索引公式与one-hot逐类公式计算未聚合项,再比较两个分母;16组权重核对比值与共同缩放性质。未导入PyTorch,未调用CrossEntropyLoss,也未训练模型。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
同样的逐样本损失,mean 的分母却变了吗?
正在准备实验控件…
两个样本、两类,真实类别依次0和1;正确类概率固定为0.8和0.4。按PyTorch2.14文档比较类别索引与浮点one-hot目标;权重为正,label_smoothing=0,没有ignore_index或旧版reduce/size_average覆盖。标准库/浏览器公式推演,不是本机运行torch。
先把两种标签写法对应起来
类别索引写成[0,1]。对应的one-hot是[[1,0],[0,1]]:第一行只认类别0,第二行只认类别1。预测概率行分别为[0.8,0.2]、[0.6,0.4]。
没有标签平滑,没有忽略标签,所有类别权重均为正。用索引选出正确类,或把同一行的每类损失乘one-hot再相加,都只留下同一项。
因此两项未聚合损失都是ℓ₁=−1×ln0.8≈0.223144、ℓ₂=−3×ln0.4≈2.748872,总和约为2.972016。若只检查reduction=none或sum,看不出本篇的区别。
mean并不总是“加起来除以样本数”
类别索引路径的加权mean,分母是这些目标实际命中的类别权重和。本例为1+3=4,所以结果是2.972016/4≈0.743004。它与LogSoftmax后接NLLLoss的加权平均规则一致。
概率目标路径把各样本的加权交叉熵相加后,除以样本数N。本例是one-hot这种特殊概率目标,N=2,所以结果为2.972016/2≈1.486008。
分子相同,分母一个是4、一个是2,后者正好是前者的两倍。一般地,在本篇的one-hot、无忽略项条件下,两种mean的比值为(Σw_y)/N。这个关系不是对任意软标签都逐项照搬的承诺。
把所有类别权重一起乘2,哪条路径会变?
把[1,3]改成[2,6],两项损失和总和都翻倍。索引目标的分母也从4变为8,所以mean仍为0.743004。one-hot目标分母仍为2,mean变成2.972016。
所以“类别权重只关心相对比例”必须结合归约规则说。对这里的索引加权mean,共同倍数会抵消;对这里的概率目标mean,共同倍数保留在最终损失中。
在固定这批目标与权重时,损失相差一个正常数,对logit的梯度也相差这个常数。它可能影响与其他损失项的相对权重;但不能直接断言所有优化器的实际参数步长也同比例变化。
运行完整小例子,再与独立公式核对
程序分别按类别索引公式与one-hot逐类公式计算未聚合项,再比较两个分母;16组权重核对比值与共同缩放性质。未导入PyTorch,未调用CrossEntropyLoss,也未训练模型。
本轮实际使用 Windows / Python 3.14.0 运行。保存为 demo.py,执行 python demo.py;只用标准库。检查使用显式 AssertionError,python -O 不会删除这些检查。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Documented reduction formulas, not an execution of torch.nn.CrossEntropyLoss."""
from math import isclose, isfinite, log
def require(condition, message):
if not condition:
raise AssertionError(message)
def compare(weights):
probabilities = ((0.8, 0.2), (0.6, 0.4))
labels = (0, 1)
one_hot = ((1.0, 0.0), (0.0, 1.0))
if not isinstance(weights, (list, tuple)) or len(weights) != 2:
raise ValueError("need two class weights")
if any(type(w) not in (int, float) or not isfinite(w) or not 0 < w <= 100
for w in weights):
raise ValueError("finite positive weights at most 100 required")
indexed_terms = [-weights[y] * log(row[y]) for row, y in zip(probabilities, labels)]
probability_terms = [-sum(weights[c] * target[c] * log(row[c]) for c in range(2))
for row, target in zip(probabilities, one_hot)]
require(all(isclose(a, b, abs_tol=1e-12) for a, b in zip(indexed_terms, probability_terms)),
"one-hot and index unreduced terms agree")
numerator = sum(indexed_terms)
weight_sum = sum(weights[y] for y in labels)
return numerator, numerator / weight_sum, sum(probability_terms) / len(labels)
def main():
print("fixed correct-class probabilities=[0.8, 0.4]; N=2; no ignored labels; label_smoothing=0")
for weights in ([1, 3], [2, 6], [1, 1]):
total, index_mean, onehot_mean = compare(weights)
print(f"weights={weights}; sum={total:.6f}; index_mean={index_mean:.6f}; onehot_mean={onehot_mean:.6f}")
print(f"denominators: index={sum(weights)}; onehot=2; ratio={onehot_mean / index_mean:.6f}")
count = 0
for relative in (0.1, 1, 3, 10):
for scale in (0.1, 1, 2, 10):
weights = [scale, relative * scale]
_, indexed, probability = compare(weights)
require(isclose(probability / indexed, sum(weights) / 2, abs_tol=1e-12), "ratio oracle")
require(isclose(indexed, compare([1, relative])[1], abs_tol=1e-12), "common scale cancellation")
count += 1
for invalid in ([0, 1], [-1, 1], [True, 1], [float('inf'), 1], [1]):
try:
compare(invalid)
except ValueError:
pass
else:
raise AssertionError("invalid weights accepted")
print(f"PASS: {count} reduction-ratio cases; matching unreduced terms; weight-scale invariance")
if __name__ == '__main__':
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-26 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。程序分别按类别索引公式与one-hot逐类公式计算未聚合项,再比较两个分母;16组权重核对比值与共同缩放性质。未导入PyTorch,未调用CrossEntropyLoss,也未训练模型。
运行后对照结果
fixed correct-class probabilities=[0.8, 0.4]; N=2; no ignored labels; label_smoothing=0
weights=[1, 3]; sum=2.972016; index_mean=0.743004; onehot_mean=1.486008
denominators: index=4; onehot=2; ratio=2.000000
weights=[2, 6]; sum=5.944031; index_mean=0.743004; onehot_mean=2.972016
denominators: index=8; onehot=2; ratio=4.000000
weights=[1, 1]; sum=1.139434; index_mean=0.569717; onehot_mean=0.569717
denominators: index=2; onehot=2; ratio=1.000000
PASS: 16 reduction-ratio cases; matching unreduced terms; weight-scale invariance真正调用接口时,输入仍是logit
为了手算,我们直接给出预测概率p并计算lnp。真实CrossEntropyLoss的input按文档应是未归一化logits;不要把这些概率当成原始logit直接传入。若想构造相同概率,可选择logits=lnp,因为这些行的概率和已经是1。
类别索引目标与概率目标的形状和数据类型也不同:前者是整型[N],后者是浮点[N,C]。本例只取合法的one-hot分布;概率目标是否非负、每行是否和为1,需要调用方自己检查。
本文固定label_smoothing=0,不使用ignore_index,不传已弃用的size_average或reduce覆盖项;每一类权重都为正,不讨论分母为零的退化情况。空间维度展开、一般软标签和混合目标,应重新核对对应文档公式,不能机械套用本页两行的分母。
记住这三点
- 相同标签语义与相同逐样本项,并不保证不同目标表示使用相同mean分母。
- 类别索引的加权mean与概率目标的mean,需要分别写出归一化方式。
- 改标签类型、权重或损失归约时,先检查none、sum和分母,再比较最终标量。
别把例子用过头
- 把
CrossEntropyLoss输入概率当成logits,意外又做了一次softmax。 - 带权one-hot目标与索引目标只比较逐项损失,就断言最终mean相同。
- 把本例结论推广到标签平滑、忽略标签、零权重或任意软标签。
自己试一小步
什么权重会让这两个mean相同?仅看两种最终标量足够排查吗?
- 保持目标为0和1,试一组正权重,使命中权重和恰好等于样本数。
- 把两个目标都改为类别1,重新计算命中权重和。
- 分别打印未聚合项、总和、两种分母,说明哪一步发生变化。
做到这些,就算完成
- 权重按类别索引取,不是按样本位置随意取。
- 全程保留
label_smoothing=0与无忽略标签的条件。 - 实验说明是公式复现,还是实际调用指定版本的框架。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- CrossEntropyLoss — PyTorch 2.14 (新标签页)↗
类别索引与概率目标的两套未聚合
/mean公式;input、target、ignore_index和label_smoothing约束。 - NLLLoss — PyTorch 2.14 (新标签页)↗
类别索引负对数似然的mean使用目标命中权重和归一化,帮助核对索引路径。