logits、概率与损失函数:多做一次 softmax,准确率几乎看不出来
一个分类模型的最后一层加了 softmax,因为「模型应该输出概率」,损失用的是
CrossEntropyLoss。训练收敛了,准确率也说得过去。直到有人用模型输出的置信度做阈值过滤,才发现它对错误的预测也很自信。
PyTorch 的分类损失函数对输入有明确的约定:BCEWithLogitsLoss 和 CrossEntropyLoss 接收的是 logits,也就是还没有经过 sigmoid 或 softmax 的原始分数,它们在内部做归一化,并用数值稳定的方式计算对数。把概率传进去,程序不会报错,只是算出来的东西变了。
本文在 PyTorch 2.14.1(CPU)上逐个检查这类约定被违反时的损失和梯度,以及类别编号错一位的后果。实验见文末。
一、二分类:先 sigmoid 再传 BCEWithLogitsLoss
logit=-10、标签 1:传 logits 损失 10.0000、梯度 -1.0000;先 sigmoid 再传 损失 0.6931、梯度 -0.000023
logit=0、标签 1:传 logits 损失 0.6931、梯度 -0.5000;先 sigmoid 再传 损失 0.4741、梯度 -0.094385
logit=10、标签 1:传 logits 损失 0.0000、梯度 -0.0000;先 sigmoid 再传 损失 0.3133、梯度 -0.000012最需要纠正的是第一行:模型非常确信地错了(logit=-10,标签是 1)。正确用法的梯度是 -1,模型会被用力拉回来;多做一次 sigmoid 后,sigmoid(-10) 约等于 0,再过一次 sigmoid 得到 0.5,损失看起来是一个「普通」的 0.69,梯度却几乎为零。模型越错,越学不动。第三行反过来:完全正确的预测,损失也降不到 0(再过一次 sigmoid 的输出不会超过 sigmoid(1) ≈ 0.73),损失值失去了「离目标还有多远」的含义。
二、BCELoss 的截断
如果确实要自己做 sigmoid,就该用 BCELoss。但它有一个数值问题:
logit=-20、标签 1:sigmoid 后 2.06e-09,BCELoss 20.00;BCEWithLogitsLoss 20.00
logit=-200、标签 1:sigmoid 后 0,BCELoss 100.00;BCEWithLogitsLoss 200.00sigmoid(-200) 在 float32 里下溢成 0,log(0) 是负无穷,BCELoss 为了避免无穷大把 log 截断在 -100。BCEWithLogitsLoss 直接在 logit 上计算 log-sigmoid,不经过这个会下溢的中间值。混合精度下,float16 的下溢来得更早。所以除非有特殊理由,二分类就用 BCEWithLogitsLoss 加上原始 logits;需要概率时,在推理阶段单独做 sigmoid。
三、多分类:先 softmax 再传 CrossEntropyLoss
10 类、模型完全确信且正确:传 logits 损失 0.0000;先 softmax 再传 损失 1.4612(概率在 [0,1],再做一次 softmax 后最多 e/(e+9))
模型确信但错了:传 logits 时正确类的梯度 -1.0000;先 softmax 再传 -4.87e-18CrossEntropyLoss 内部会做 log-softmax。传进去的如果已经是概率,就相当于做了两次 softmax:输入被压在 [0, 1] 之间,第二次 softmax 最多只能给正确类 e/(e+9) ≈ 0.23 的概率,所以完全正确的预测损失也有 1.46。确信但错误的预测,梯度小到 1e-18,相当于没有。
用重叠类别的合成数据训练一个线性分类器 1000 步,在独立测试集上比较(两个模型的输出都按 logits 解释):
训练 1000 步,在独立测试集上(模型输出按 logits 解释):传 logits 准确率 0.403、交叉熵 1.749、平均最高概率 0.410
训练 1000 步,在独立测试集上(模型输出按 logits 解释):先 softmax 再传 准确率 0.386、交叉熵 2.382、平均最高概率 0.690准确率只差了不到两个百分点,交叉熵却明显更高,平均最高概率 0.69 而准确率只有 0.39:模型严重过度自信。开头那个「对错误预测也很自信」的现象就是这样来的。原因在于:
softmax 不改变 argmax:同一组 logits 的预测类别相同:True(只看准确率不容易发现这个错误)只看准确率的评估,很难发现这个问题。评估分类模型时,至少同时看一个依赖概率值的指标(交叉熵、校准误差),尤其当下游会用置信度做决策时。
四、类别编号错一位
标签取 1..10、类别只有 0..9:遇到 10 时 IndexError: Target 10 is out of bounds.
训练标签整体错一位但都在 0..9 内:不报错;测试集上预测与错位标签一致的比例 0.403,与真实标签 0.062类别编号从 1 开始(数据库里的 ID、人工标注表)是很常见的。越界时会报错,算是幸运;如果整体错位但都在范围内(例如做了一次 (label + 1) % C 这样的转换,或者两张映射表顺序不一致),训练会正常进行,模型在错位标签上表现得和正确训练的模型一样好,在真实标签上却接近随机。
防御的办法是让映射成为数据的一部分:类别名到编号的映射随模型一起保存,训练和推理用同一份;评估时抽几条样本,把预测编号反查回类别名,由人看一眼是否合理。语言模型的「位置错一位」是同一类问题,见微调样本的标签对齐。
五、偏好分数:只约束差值,也会学走混杂
奖励模型常用成对偏好训练:同一个提示下,标注者选了回答 A 而不是 B,损失是 -log sigmoid(r_A - r_B)。这个目标只看两个分数的差:
分数 (2, 1) 的成对损失 0.313262;都加 100 后 0.313262;都乘 2 后 0.126928
单个分数 2 与 102 的 sigmoid:0.880797 与 1.000000,训练目标却认为这两组分数一样好
同一个分数 2:与 [1] 比较时 softmax 0.731059,与 [1, 2] 比较时 0.422319分数整体平移,损失不变,所以单个分数的绝对值没有被训练约束过,对它做 sigmoid 得到的不是「这个回答好的概率」。softmax 之后的值取决于和谁比较,加入一个同样好的候选,它就从 0.73 掉到 0.42。偏好分数适合在同一个提示下给候选排序,不能直接当作正确率或置信度使用,需要正确率时,要用独立标注的正确性数据另行训练或校准。
偏好标签本身也会带进混杂。用一个只有两个特征(是否满足约定的检查、长度)的线性评分器,在两组合成偏好数据上训练,再看它对一个保留样本的判断:
训练数据里被选中的回答总是更长:学到的权重(约定检查, 长度)= (1.195, 1.854);保留样本「短而满足约定」得分 3.049,「长而不满足」得分 11.126,偏好 后者
训练数据里长短与选择无关:学到的权重(约定检查, 长度)= (2.897, 0.033);保留样本「短而满足约定」得分 2.930,「长而不满足」得分 0.196,偏好 前者第一组数据里,被选中的回答恰好都更长,评分器分不清标注者偏好的是「满足约定」还是「更长」,长度权重学得比约定检查还大,于是在保留样本上选了又长又不满足约定的回答。第二组数据里长短与选择无关,长度权重接近 0。真实的奖励模型面对的是文本,长度、格式、礼貌程度都可能这样混进偏好里,评估时要专门构造「风格占优但内容错误」的对照样本。
几个常被混用的训练对象:
| 对象 | 训练信号 | 输出 | 不能直接推出 |
|---|---|---|---|
| 标量奖励模型 | 同一提示下的偏好对 | 每个回答一个分数 | 分数的 sigmoid 是正确率 |
| 分类器 | 带类别标签的样本 | 类别 logits | 偏好分数可以当类别概率用 |
| DPO | 偏好对、参考策略 | 直接优化生成策略,不单独产出奖励模型 | 训练后得到了一个可单独部署的评分器 |
| KTO | 单条回答被标为可取或不可取,不需要成对 | 直接优化生成策略 | 得到了一个独立的评分器(名称取自 Kahneman 与 Tversky 的前景理论) |
表中 DPO 与 KTO 的描述依据原论文,本文的实验只涉及标量分数的数学与线性评分器,没有训练任何语言模型。
六、约定速查
| 任务 | 模型最后一层输出 | 损失函数 | 推理时 |
|---|---|---|---|
| 二分类 / 多标签 | logits | BCEWithLogitsLoss | sigmoid 得到概率 |
| 多分类 | logits | CrossEntropyLoss(标签为 0..C-1 的类别编号,或同形状的概率分布) | softmax 得到概率 |
| 已经输出 log 概率 | log_softmax | NLLLoss | exp 得到概率 |
CrossEntropyLoss 默认 ignore_index=-100:标签 -100 的位置不计入损失七、检查清单
- 模型输出 logits,归一化交给损失函数;需要概率时只在推理阶段做。
- 二分类用
BCEWithLogitsLoss,不要在模型里加 sigmoid 后再用它。 - 评估时同时看准确率和依赖概率值的指标。
- 类别映射随模型保存,训练与推理共用;抽样反查类别名做人工检查。
- 偏好分数只用于同一提示下的排序;评估时构造风格占优但内容错误的对照样本。
配套实验
- codesphere-labs/ai/logits-and-loss-contracts:Python 3.14.8、PyTorch 2.14.1 CPU:sigmoid 与 softmax 多做一次时的损失与梯度、BCELoss 截断、训练后的测试集指标、类别编号越界与错位(验证记录)
- codesphere-labs/ai/preference-score-contract:Python 3.14.8 标准库:成对偏好损失的平移与缩放、sigmoid 与 softmax、线性评分器在两组合成偏好数据上学到的长度权重(验证记录)
参考资料
- PyTorch 文档:BCEWithLogitsLoss
- PyTorch 文档:BCELoss(log 截断在 -100)
- PyTorch 文档:CrossEntropyLoss
- Long Ouyang 等,Training language models to follow instructions with human feedback(成对偏好训练奖励模型)
- Rafael Rafailov 等,Direct Preference Optimization
- Kawin Ethayarajh 等,KTO: Model Alignment as Prospect Theoretic Optimization
- Chuan Guo 等,On Calibration of Modern Neural Networks