☰
PyTorch CrossEntropyLoss 原理、参数陷阱与工程排查
2026/10/26 3:18:23 网站建设 项目流程

nn.CrossEntropyLoss()这个类,我估计绝大多数做分类模型的人第一天就会写上import torch.nn as nn,然后在训练循环里敲一行loss = criterion(out, target)。就这么一行,我在代码评审里见过它惹出来的麻烦比任何一层网络结构都多:有人把 softmax 之后的概率喂进去,loss 卡在 0.5 上下再也不降;有人加了一组自定义 weight 之后训练直接发散;有人在变长序列任务里忘了处理 ignore_index,模型老老实实学会了“预测 padding 这个毫无意义的类别”,验证集准确率虚高一大截。这篇文章我想把 CrossEntropyLoss 彻底拆一遍——它内部到底算了什么、几个参数各自埋着什么坑、报错的时候按什么顺序定位、以及那些官方文档里基本不会写的经验。刚接触分类任务的新手可以顺着读,写过几十次但没深究过参数的老手可以直接跳到第 2 节和第 4 节,那里是踩坑密度最高的地方。

1. 先从“它在算什么”讲起,别急着调参

1.1 模型输出的是打分,不是概率

很多人对 CrossEntropyLoss 的困惑,源头在于没分清模型最后一层吐出来的东西到底是什么。一个nn.Linear(hidden, num_classes)输出的是一排实数打分,业内叫 logits,它们的取值范围是负无穷到正无穷,加起来也不等于 1,甚至可能全是负数。logits 的含义是“模型对每个类别的偏好强度”,只有相对大小有意义,绝对值没有意义。把 logits 除以一个常数,所有相对关系不变,预测结果也不变,这一点在后面解释温度、解释学习率、解释数值稳定性时都会反复用到。

真实概率必须满足两个条件:非负、求和为 1。softmax 就是干这件事的:先对每个 logit 取指数把负数变成正数,再归一化。指数运算有个副作用,logit 稍微大一点,指数就爆掉。所以工程实现里从来不会老老实实算exp(x) / sum(exp(x)),而是先减去这一行的最大值再算,这就是所谓的“数值稳定实现”。F.log_softmax内部做的就是这件事,这也是后面我要强调“别自己先 softmax”的技术原因。

CrossEntropyLoss 接收的就是这批未经处理的 logits。它内部第一步就是做 log_softmax,第二步才是取目标类别那一项算负对数。你把已经 softmax 过的概率再喂进去,等于让模型在概率空间里又做了一次 softmax,分布被压得更平,梯度信号被严重削弱,最典型的表现就是 loss 很快掉到一个不低的位置然后躺平不动。我第一次遇到这个问题时排了半天数据管线,最后打印了一下输出张量的行和,发现全都等于 1,才反应过来。

1.2 一行公式拆成三段看

交叉熵的定义是-sum_j y_j * log(p_j),其中 y 是 one-hot 标签,p 是预测概率。因为 one-hot 只有一个位置是 1,求和号实际上只剩一项,公式退化成-log(p_target)。所以对单样本、单标签的分类任务来说,交叉熵本质上就是“目标类别的预测概率取负对数”。预测概率 1.0,loss 是 0;预测概率 0.5,loss 约 0.693;预测概率 0.01,loss 约 4.6。这个单调关系非常直观,也是它比均方误差更适合分类的根本原因。

为什么不用 MSE 直接回归概率?我用一个具体的数字说明。假设真实标签是类别 0,模型 A 给出p = [0.9, 0.1],模型 B 给出p = [0.51, 0.49]。从“预测对了没有”的角度看,两个模型都预测正确。但 MSE 给的惩罚分别是 0.01 和 0.24,差距是 24 倍;交叉熵给的惩罚是 0.105 和 0.673,差距约 6.4 倍。更关键的是梯度行为:softmax 加交叉熵的组合,对 logits 的梯度是softmax - onehot,形式极其干净,预测越离谱梯度越大,预测接近正确时梯度自动趋近 0。MSE 配 softmax 会得到一个带饱和项的梯度,预测错得越狠梯度反而越小,训练前期几乎推不动。这是分类任务几乎无条件选交叉熵的核心原因,不是习惯问题。

还有一个常被问到的数字:如果类别数是 C,模型完全随机瞎猜,期望 loss 是多少?答案是ln(C)。10 分类的基线是 2.303,100 分类是 4.605,1000 分类是 6.908。这个数字我用得特别多,训练一开始打印 loss,如果它离ln(C)很远,说明初始化或者数据管线八成有问题;训练结束时如果 loss 还在ln(C)附近晃,模型基本等于没学到东西。想看更符合直觉的指标就取个指数,exp(loss)就是困惑度,含义是“模型平均在多少个类别之间犹豫”。

1.3 它其实是一个组合算子

从 PyTorch 的实现角度看,nn.CrossEntropyLoss()等价于nn.LogSoftmax(dim=1)接nn.NLLLoss(),官方文档里写得很清楚。这个等价关系看着像冷知识,实际排查问题时非常有用:NLLLoss 的输入必须是 log 概率,接收的是(N, C)的 log 概率和(N,)的整数标签;CrossEntropyLoss 把这两步合并了,接收的是原始 logits。所以当你看到某段代码里写了F.log_softmax又套了CrossEntropyLoss,那就是实打实的重复计算,等价于对 log 概率再做一次 softmax,通常表现为 loss 偏小、梯度偏弱、收敛变慢。

合并成一个算子的好处不只是少写一行。数值上,log_softmax 用的是减最大值再取 log 的稳定实现,如果拆成 softmax 再取 log,中间那步 softmax 可能产生 0,取 log 直接得到负无穷,反向传播时梯度就是 NaN。工程上,合并后的算子可以被融合、被自动混合精度框架特殊照顾、在反向传播时省掉一个中间张量的存储。所以除非你在做对比实验或者调试,别拆开写。

顺带说一句nn.CrossEntropyLoss()和F.cross_entropy的区别,这不是纯风格问题。类实例会把weight注册成 buffer,跟着.to(device)一起搬到显卡上;函数式调用传进去的 weight 如果是 CPU 张量,运行时就会报设备不一致。另外类实例可以挂在 Module 里,参与序列化和参数管理。我在项目里统一用类实例,就是为了避开设备问题这个低级但高频的坑。

2. 四个参数,四种不同性质的坑

2.1 weight:类别不平衡的第一把扳手,也是最容易用错的一把

weight参数接收一个长度为 C 的张量,给每个类别的 loss 乘一个系数。长尾分布的数据集里这是最直接的补偿手段。但这里有个绝大多数人没注意到的细节:当reduction='mean'时,PyTorch 计算的是加权平均,分母是被加权后的权重之和,而不是样本数。也就是说,loss 等于sum(w_i * l_i) / sum(w_i)。

这个细节带来一个反直觉的结论:如果你把所有权重整体乘 2,用 mean 模式算出来的 loss 数值完全不变,梯度也不变,因为分子分母同时放大。只有权重的相对比例才影响训练。而如果你用的是reduction='sum',整体缩放就实实在在地改变了 loss 尺度和梯度尺度。我见过有人在训练中途把 weight 从[1, 1, 1]改成[10, 10, 10],然后困惑为什么 loss 一点变化都没有——答案就在这里。

权重怎么定?我的做法是先统计训练集各类样本数n_c,然后取w_c = (1 / n_c) ** beta再归一化到均值为 1,beta是个 0 到 1 之间的调节系数。beta = 1是完全逆频率,激进;beta = 0.5是平方根逆频率,温和,实践中往往更好用,因为它不会把极少数类的权重抬到离谱的量级,避免个别样本主导整个梯度。归一化到均值 1 的目的是让 loss 的绝对尺度和不加权时可比,这样你之前调好的学习率、梯度裁剪阈值还能继续用。

权重设置方式公式适用场景我的评价
不加权w=1各类样本量差距在 3 倍以内首选,能不加就不加
逆频率w ∝ 1/n_c极度长尾,少数类极重要容易过拟合少数类,慎用
平方根逆频率w ∝ 1/sqrt(n_c)中等长尾最稳,我的默认选择
有效样本数w ∝ (1-β)/(1-β^n_c)样本量跨度极大效果好但多一个超参要调

还有一点要提醒:weight 调大不等于少数类一定变好。它放大的是少数类样本产生的梯度,但这些样本本身数量少、多样性差,权重一高,模型很容易把这几十个样本背下来。我一般的顺序是先用重采样或者数据增强把少数类的“数量”补上来,实在不行才动 weight,两个手段同时上通常收益递减。

2.2 ignore_index:变长序列和 padding 的必需品

ignore_index的默认值是 -100,含义是“标签等于这个值的样本不参与 loss 计算”。序列标注、机器翻译、语音识别这类变长任务里,一个 batch 内的样本要补齐到同一长度,补出来的位置标签不可能有真实值,必须被排除掉。PyTorch 里pad_sequence、pad_packed_sequence相关的一整套 API 默认用 -100 当填充值,和这个默认值是配套的。

这里最容易出问题的是忽略样本在 mean 模式下的分母处理。reduction='mean'时,分母是有效样本(或被忽略掉之后的加权和),这一点 PyTorch 处理得对,所以短句不会被长句的 padding 冲淡,每句话对 loss 的贡献大致是均衡的。但如果你的一个 batch 里所有标签都是 -100,分母就是 0,会得到 0/0。我在一个短句密集的数据集上真遇到过,某个 batch 因为采样策略问题全部由极短样本组成,长度只有 1,padding 全被忽略后没剩几个有效位,偶尔整批归零,loss 直接变成 nan,反向传播一传,模型参数全变 nan,第二天早上打开日志看到一片 nan。

规避手段很直接:在 DataLoader 的 collate 阶段保证每个 batch 至少有一个有效标签,或者训练循环里加一行判断,if not torch.isfinite(loss): continue。这行判断我觉得是每个训练脚本都该有的保险丝,成本几乎为零,救回来的可能是训练了一整晚的模型。

填充值到底用多少,也需要和标签空间对齐。如果你的标签从 0 编码到 C-1,那 -100 很安全。但如果有人把标签映射成了别的形式,比如从 -1 开始编码,或者用了 255 当 ignore 值(这在某些图像分割数据集里很常见),就必须显式传ignore_index=255,否则 -100 匹配不上,忽略逻辑完全不生效,而且不会报错,只会让 loss 悄悄变差。这类问题最难查,因为没有异常,只有效果不对。

2.3 reduction:评估阶段我几乎只用 sum

reduction有三个取值,'none'返回逐样本 loss,'sum'返回总和,'mean'返回平均。训练时用 mean 是常规做法,因为 loss 尺度和 batch size 解耦,学习率好调。但评估阶段我强烈建议用 sum 自己算平均,原因是分批评估时每个 batch 的大小可能不同,尤其是最后一个 batch 往往不满。如果你对每个 batch 的 mean loss 再取算术平均,大小不同的 batch 权重就一样了,最终指标会失真。

正确的做法是这样:评估循环里累加loss_sum += criterion(logits, target) * batch_size,同时累加样本总数,最后用总 loss 除以总样本数。多分类任务里这个误差通常只有零点几个百分点,看起来无关紧要,但如果你在做多个模型的横向对比,排名可能就因为这个微小偏差颠倒。序列任务里这个坑更明显,因为不同 batch 的有效 token 数差异可能好几倍。

criterion = nn.CrossEntropyLoss(reduction='sum') total_loss, total_count = 0.0, 0 model.eval() with torch.no_grad(): for x, y in val_loader: logits = model(x) total_loss += criterion(logits, y).item() total_count += y.numel() print('val loss =', total_loss / total_count)

注意这里用的是y.numel()而不是len(y),二维标签的场景下两者差一个维度。如果用了 ignore_index,分母还得换成有效标签数,我一般会额外算一下(y != -100).sum()。

reduction='none'的价值在于做样本级分析,这个后面第 5 节会展开。它在训练时也有用武之地,比如你想实现带难例挖掘的损失,或者想把 loss 按样本权重再处理一次,就得先拿到逐样本的值。

2.4 label_smoothing:便宜到几乎免费的正则

label_smoothing是后来版本才加的参数,接受一个 0 到 1 之间的小数,常见取值 0.1。它的做法是把 one-hot 标签软化:真实类别保留1 - eps,剩下的eps均匀分给其他 C-1 个类别。数学上等价于在原来的负对数似然上叠加一个对均匀分布的惩罚项,权重是 eps。

它解决的是一个真实存在的问题。one-hot 标签鼓励模型把目标类别的概率推到无限接近 1,这意味着对应的 logit 要比其他 logit 大出非常多,模型会变得越来越自信,训练后期 loss 还能继续下降,但泛化能力反而在退化。标签平滑给了一个上限,模型被要求“别太自信”,泛化往往更稳。图像分类上的经典结果,加上 0.1 的平滑通常能换来零点几个点的准确率提升,成本几乎为零。

不过它有副作用需要知道。一是模型输出的概率不再校准,softmax之后的最大值大概只能到 0.9 左右,因为训练目标就是这样。如果你下游要用概率做阈值筛选或者融合多个模型,这个偏移得考虑进去。二是它和蒸馏、和软标签目标不兼容,因为两者都在改标签分布,语义会打架。三是对已经严重过拟合的场景,0.1 可能不够,对欠拟合的场景,0.1 也可能拖慢收敛。我一般从 0.05 试起,效果不明显再加。

3. 手写一遍,把黑盒拆成白盒

3.1 三十行复现 CrossEntropyLoss

理解一个算子的最好办法是自己写一遍再对齐数值。下面的实现覆盖了 logits 输入、类别权重、忽略索引、标签平滑和三种 reduction,跑一遍和官方结果对比,误差应该在 1e-6 量级。这不是为了替代官方实现,而是为了在出问题时你能立刻定位是哪一步算错了。

import torch def my_cross_entropy(logits, target, weight=None, ignore_index=-100, label_smoothing=0.0, reduction='mean'): # logits: (N, C) 实数打分;target: (N,) 的 int64 类别索引 n, c = logits.shape # 第一步:减去行最大值,保证 exp 不溢出 shifted = logits - logits.max(dim=1, keepdim=True).values # 第二步:log-sum-exp,得到 log 归一化常数 log_z = shifted.exp().sum(dim=1, keepdim=True).log() log_prob = shifted - log_z # 这就是 F.log_softmax 的结果 valid = target != ignore_index safe_target = target.clone() safe_target[~valid] = 0 # 占位,避免 gather 越界 nll = -log_prob.gather(1, safe_target.view(-1, 1)).squeeze(1) if label_smoothing > 0: smooth = -log_prob.mean(dim=1) nll = (1 - label_smoothing) * nll + label_smoothing * smooth if weight is not None: w = weight[safe_target] nll = nll * w denom = w else: denom = torch.ones_like(nll) nll, denom = nll[valid], denom[valid] if reduction == 'none': out = torch.zeros(n, device=logits.device, dtype=logits.dtype) out[valid] = nll return out if reduction == 'sum': return nll.sum() return nll.sum() / denom.sum().clamp_min(1e-12)

对齐验证的写法很朴素,随机生成 logits 和标签,分别调官方和自写版本,打印绝对误差。我建议你顺手把weight、ignore_index、label_smoothing都打开各测一次,很多人第一次跑就发现自己在某个参数上理解错了。我自己写的时候就在分母上栽过一次,忘了 clamp,全忽略的 batch 直接除零。

3.2 数值稳定性的对照实验

为了看清“为什么不能先 softmax 再取 log”,可以做个放大实验:把 logits 乘 100 再算。这模拟的是训练后期模型非常自信、logit 差距被拉得很大的情形。

torch.manual_seed(0) logits = torch.randn(4, 10) * 100 naive = logits.softmax(dim=1).log() # 先 softmax 再 log stable = torch.log_softmax(logits, dim=1) # 稳定实现 print('非法值个数:', torch.isinf(naive).sum().item()) print('最大绝对误差:', (naive - stable).abs().max().item())

跑出来 naive 那一行里会出现负无穷,因为大部分概率在浮点精度下被压成了 0,取 log 就是 -inf。反向传播时,-inf 参与求导会产生 nan,一个 nan 污染整个参数张量只需要一步。而 CrossEntropyLoss 走的是稳定实现,同样的输入只损失一点精度,不会崩。

这个实验的价值在于,当你看到训练日志里某一步突然全是 nan,能立刻想到去检查:是不是自己手写了 softmax 加 log,是不是 loss 在外面又包了一层自定义变换,是不是学习率太大导致某些层的输出被推到极端值。反向排查比盲猜快得多。

3.3 混合精度下半精度与浮点的边界

半精度训练能省显存、提速,但exp和log在半精度下的动态范围很窄,容易溢出或精度不足。好在自动混合精度的算子白名单里,交叉熵这类涉及归约和指数的算子通常会被提升到单精度执行,所以用torch.autocast包裹的情况下你一般不需要手动处理。

真正需要小心的是自己手写损失或者做自定义变换的场合。这时候记得把 logits 提升到单精度再算:

with torch.autocast(device_type='cuda', dtype=torch.float16): logits = model(x) loss = criterion(logits.float(), y) # 在 autocast 作用域外,显式提升

有个细节:logits.float()会新建张量,反向传播的链路仍然连得回原图,梯度会正常回传并自动转换类型,不会断梯度。但如果你在这一步做了.detach(),那梯度就真的断了,这个错误我见新手犯过。

4. 报错和异常,按这个顺序查

4.1 类型与取值:最常见的两类硬报错

“expected scalar type Long but found Float” 这条报错,九成是因为 target 是浮点张量。常见来源有三个:从 dataframe 或 numpy 读出来的标签没转整型;做数据增强时不小心参与了一次浮点运算;或者你把 one-hot 编码误当成类别索引传了进去。处理很简单,target.long(),但如果标签本身是 one-hot 形状(N, C),光转类型没用,得用argmax(1)先压成索引,或者改用支持概率目标的写法。

“Target x is out of bounds” 是取值范围越界,合法范围是[0, C-1]加上 ignore_index。这个报错不会误报,看到就直接查标签映射表。我在做图像分割时踩过一次,数据集里背景类标的是 255,模型输出只有 21 类,训练第一步就炸。解决办法是在构建数据集时把 255 重映射成一个合法类别,或者在标签加载阶段统一转成 -100 并传给 ignore_index。

还有一类不报错但结果全错的陷阱:标签编码从 1 开始时,类别 0 永远不会出现,模型学不到它,而且没有任何提示。我养成了一个习惯,数据加载完先跑一段自检,打印target.min()、target.max()、torch.unique(target).numel(),和num_classes对一下。三行代码,能省掉很多次“训练正常但指标很怪”的排查。

4.2 形状对不上:一步步定位

报错信息 “Expected input batch_size (32) to match target batch_size (16)” 说明输入的某个维度和标签不一致。分类任务里最常见的成因是维度没压平:卷积网络输出(N, C, 1, 1)直接送进 loss,PyTorch 会把它当成 C 维带空间信息的情形处理,然后抱怨 N 对不上。处理方式是在模型forward的末尾加一个flatten(1)或者adaptive_avg_pool后reshape(N, -1)。

另一种常见错误是标签多了一维,形状是(N, 1)而不是(N,)。PyTorch 对索引形式和空间形式都有支持,但如果两边维度不匹配,报错信息可能很晦涩。排查顺序我固定按这个来:

检查项打印什么期望
模型输出形状logits.shape(N, C)或(N, C, d1, d2, ...)
标签形状target.shape(N,)或(N, d1, d2, ...)
标签类型target.dtypetorch.int64
标签范围target.min(), target.max()[0, C-1]或含 ignore 值
输出是否已归一化logits.sum(1)[:5]不等于 1,等于 1 就是被 softmax 过了
设备一致性logits.device, target.device完全相同

最后一项设备一致性既是报错来源,也是性能陷阱。两者在不同设备上会直接报错,这个好查;但如果 DataLoader 的pin_memory配错,数据搬运会拖慢整体吞吐,这个不报错,只能靠 profile 发现。

4.3 loss 不降、忽然变成 nan、训练和评估对不上

这三个现象看着不相干,实际上排查路径有很多重叠。我按自己踩过的顺序整理成一张速查表。

现象高概率原因快速验证处理
loss 长期停在ln(C)附近输出被提前 softmax,梯度消失打印输出行和,看是否等于 1去掉多余的 softmax
loss 前几步就变成 nan学习率过大,logits 溢出学习率降 10 倍再跑加 warmup、加梯度裁剪
loss 变成 nan 且伴随空 batch全 batch 被 ignore,0/0统计每个 batch 的有效标签数过滤空 batch,加有限性判断
训练 loss 降、验证 loss 涨过拟合,或标签泄漏打乱训练集标签再跑一遍看验证是否也降加正则、查数据划分
训练 loss 正常但验证 loss 偏高训练和验证的 reduction 方式不同对齐两者的计算方式统一用 sum 除以总数
前几个 epoch 正常,之后突降为 0数据里存在重复样本或标签错误检查最大 loss 的样本清洗数据
部分类别从不被预测权重设置失衡或标签编码问题打印各类别的逐样本 loss调权重,查类别分布

“用 8 个样本过拟合”这个土办法我推荐所有人在怀疑模型坏了的时候先跑一遍:取一个极小 batch,把学习率调到正常水平,训练几百步,如果 loss 不降到接近 0,那问题一定在模型结构或数据管线上,跟正则化、跟超参数都没关系。这个方法能在十分钟内明确方向,比漫无目的地改配置高效太多。

5. 进阶用法和几个工程经验

5.1 逐样本 loss 是排查问题的显微镜

用reduction='none'拿到逐样本 loss 后,可以做很多聚合分析。最有价值的是按类别分组统计平均 loss,直接看出模型在哪些类别上吃力。下面这段代码我在每个验证 epoch 结束都会跑一次。

criterion_none = nn.CrossEntropyLoss(reduction='none') per_sample = criterion_none(logits, target) for cls in range(num_classes): mask = target == cls if mask.sum() > 0: print(f'类别 {cls}: n={mask.sum().item()}, ' f'mean_loss={per_sample[mask].mean().item():.4f}')

要注意一点,如果这个 criterion 带了 weight,逐样本 loss 已经被乘过权重了,此时组内均值反映的是加权后的量,不能直接和另一个类比。做纯诊断时我会另建一个不带权重的 criterion,避免混淆。

比这更细的一层是看同一个类别内部 loss 的分布。如果某类的均值不高但有几个样本 loss 极大,基本可以确定是标签错了或者样本质量差。我在一个项目里靠这招找出过一批标注反了的样本,占总量 2%,改完之后验证准确率涨了将近 4 个点。这种收益不是靠调模型结构能拿到的。

5.2 软标签、知识蒸馏、多标签:三个容易走错门的邻居

软标签(标签是概率分布而不是类别索引)在较新版本的 PyTorch 里可以直接传给F.cross_entropy,只要 target 和 logits 形状一致且是浮点类型即可,实现上会按-sum_j y_j * log_softmax_j计算。老版本不支持,需要自己展开或者改用 KL 散度。

知识蒸馏是软标签的典型应用。教师模型输出的分布经过温度 T 软化后,学生模型去拟合它。这里有个常被忽略的点:温度会影响梯度的量级,标准做法是在训练损失里乘T^2做补偿,否则调温度等于隐式调学习率。另外学生模型和教师模型的 logits 都要除以 T 再算损失,只在一边除是错的。

多标签分类是完全不同的场景,每个样本可以同时属于多个类别,标签之间不互斥。这时候用 CrossEntropyLoss 就是错的,因为它强制各类概率求和为 1,会把多标签问题强行压成单选。正确的选择是BCEWithLogitsLoss,每个类别单独做二分类,输出维度是 C 而不是 C+1,也不需要 softmax。区分方法很朴素:问自己“一个样本能不能同时属于两个类别”,能就用 BCE,不能就用 CE。二分类任务里这两种写法在数学上可以互相转换,但实践中 BCE 只有一个输出通道,参数更少,我一般二分类直接用 BCE 配一个输出单元。

5.3 梯度累积、分布式训练下的尺度一致性

显存不够的时候用梯度累积模拟大 batch,这是常规操作。但 loss 的尺度要注意:每个 micro-batch 算出来的 mean loss,如果没有除以累积步数就直接反向传播,累积了 k 步之后的等效梯度是正常值的 k 倍,学习率相当于被放大。正确写法是loss = criterion(...) / accum_steps再反向传播。

分布式数据并行下情况更复杂。每张卡各自算 mean loss,梯度在卡间取平均,如果各卡的 batch 大小不一致(比如最后几个 batch),加权就不对了。稳妥的写法是每张卡算 sum loss,再除以全局样本总数。这个细节在数据量能被卡数整除时看不出问题,一旦不能整除,最后几个 batch 就会带来微小偏差,跑长周期的对比实验时足够影响结论。

还有一个容易忽略的:当 weight 和梯度累积、分布式同时出现时,加权 mean 的分母会变成各卡权重之和的再平均,语义上仍然说得通,但和单卡跑出来的数值会有细微差别。如果你在做需要严格复现的实验,建议把 batch 大小、卡数、累积步数全都记进实验配置,别只记 learning rate。

6. 一些我在实战里攒下来的判断

关于 weight 和重采样哪个先上,我的顺序始终是:先看数据能不能补,补不了再动 weight,两个一起上的时候只调其中一个。同时调两个补偿手段,效果往往是负的,因为模型会过度偏向少数类,把多数类的准确率拉垮,最终指标反而降。

关于类别数很多的情况,比如十万级的商品分类、推荐召回,直接上 CrossEntropyLoss 在计算上未必扛得住,softmax 的分母要对全类别求和,单步开销随类别数线性增长。这类场景我会先上采样或者分层 softmax,把候选集缩到几百上千,再用交叉熵精排。这不是算法层面的问题,是工程层面的取舍,早想清楚能少走很多弯路。

关于 loss 数值本身的可比性,我有一条硬规矩:不同实验之间的 loss 只有在数据、标签、reduction、weight 全部一致时才能比较大小,任何一项变了都不能直接比。我见过太多次“新方案 loss 更低所以更好”的结论,一查发现一个用了 sum 一个用了 mean,或者一个加了标签平滑另一个没加。损失函数的数值从来不是绝对指标,它只在你自己的实验体系内部有意义。

最后说一个我觉得最实用的习惯:训练脚本第一行日志,我永远会打印类别数、ln(C)的期望值、以及第一个 batch 的 loss。三个数字放在一起,几乎能立刻判断出配置有没有问题——loss 接近ln(C)说明初始化正常;明显低于ln(C),多半是标签泄漏或者输出被处理过;明显高于ln(C)好几倍,回去看 logits 的量级和学习率。这三个数字加起来一行代码,抵得上事后几个小时的排查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询