为什么分类器不直接优化准确率?从 0-1 损失到二元交叉熵
从准确率不可导的问题出发,推导 Sigmoid、二元交叉熵与 logit 梯度,手算损失,并用 PyTorch 写出数值稳定的二分类训练。
上一篇建立了训练、验证、测试三者的边界。边界解决了“用什么数据评价”,但还没有回答“训练时到底优化什么”。
垃圾邮件分类最终只看预测对不对,最直观的目标似乎就是准确率(Accuracy)。可真实训练通常优化二元交叉熵(Binary Cross-Entropy,BCE),而不是准确率。为什么不直接优化真正关心的指标?
今天只讲透一个核心问题:怎样把离散的分类对错,变成可优化、能表达置信程度的损失?
01 准确率为什么不能直接指导更新?#
二分类器先输出一个实数分数 ,再用阈值产生类别:
- :模型尚未阈值化的原始分数。
- :条件成立时为 1,否则为 0。
- :最终预测类别。
把预测错误记为 1、正确记为 0,得到 0-1 损失(0-1 Loss):
它正好对应“错了多少个”,却有一个致命问题:分数在阈值同一侧移动时,损失完全不变。
真实标签 y = 1
损失 1 ◄───────────────○
z=0
损失 0 ○────────────────► z
-2 -1 0 1 2
z 从 -2 增加到 -0.01:仍然预测 0,损失一直是 1
z 从 0.01 增加到 5:仍然预测 1,损失一直是 0text除阈值处外,它对 的导数几乎处处为 0;阈值处又不可导。梯度无法告诉模型参数应该向哪个方向移动、移动多少。
02 从分数到概率:Sigmoid 做了什么?#
我们希望模型不仅说“0 或 1”,还表达相信正类的程度。对线性二分类器:
若一批数据包含 个样本、每个样本有 个特征:
X: [B, D]
w: [D, 1]
b: [1] 可广播到每个样本
z: [B, 1]
y: [B, 1]text叫 对数几率分数(Logit),取值没有上下界。用 S 形函数(Sigmoid Function) 将它压到 :
我们把 解释为模型估计的 。若 ,等价于 ,预测正类。
| logit | 正类概率 | 直觉 |
|---|---|---|
| 强烈倾向负类 | ||
| 无法区分 | ||
| 强烈倾向正类 |
概率保留了阈值丢掉的信息:0.51 和 0.99 都预测正类,但后者确信得多。
03 二元交叉熵怎样惩罚“自信地犯错”?#
若真实标签 ,希望预测概率 越大越好,可以使用 ;若 ,希望 越大越好,可以使用 。合并后就是 BCE:
- :真实标签。
- :正类预测概率。
- :单个样本的标量损失。
因为 只能是 0 或 1,公式每次实际只保留一项:
一个可手算的对比#
有两个正样本,两个模型都把它们判对:
| 模型 | 准确率 | 平均 BCE | ||
|---|---|---|---|---|
| A:勉强正确 | 0.51 | 0.55 | 100% | |
| B:更有把握 | 0.80 | 0.90 | 100% |
准确率看不出差别,BCE 能推动模型继续改进概率。
再看真实标签 时的错误预测:
| 预测 | BCE | 含义 |
|---|---|---|
| 0.4 | 0.916 | 判断错误,但仍保留不确定性 |
| 0.1 | 2.303 | 很自信地判断错误 |
| 0.001 | 6.908 | 极度自信地判断错误 |
对数让“自信地犯错”付出巨大代价,同时仍保持平滑可导。
交互手算:负样本被预测成 0.8,损失是多少?
此时 ,所以 。若概率降到 0.2,损失变成 。
04 为什么它的梯度特别干净?#
把 Sigmoid 和 BCE 连起来,对 logit 求导:
这个结果值得记住,因为它直接说明更新信号是什么:预测概率减真实标签。
假设 ,则 :
梯度下降会减去负梯度,使 增大,正类概率随之上升。若 ,梯度是 ,更新会压低 。
线性模型 进一步得到:
于是数据流完整闭环:
输入 X [B,D]
│
▼
线性层 Xw+b ──► logits z [B,1]
│
▼
BCEWithLogitsLoss ◄── 标签 y [B,1]
│
▼
标量 mean loss
│ backward
▼
w.grad [D,1],b.grad [1]text05 为什么代码里不要先手动 Sigmoid?#
直接计算 sigmoid(z) 后再取对数,在 很大时可能发生数值问题。例如 的 Sigmoid 在有限精度中接近 0,随后 log(0) 会得到负无穷。
PyTorch 当前官方接口 torch.nn.BCEWithLogitsLoss 把 Sigmoid 与 BCE 合并,并使用 对数和指数技巧(Log-Sum-Exp Trick) 稳定计算。训练时应把原始 logits 直接传给它:
import torch
X = torch.tensor([
[0.0, 1.0],
[1.0, 0.0],
[1.0, 1.0],
[2.0, 1.0],
]) # [B=4, D=2]
y = torch.tensor([[0.0], [0.0], [1.0], [1.0]]) # [4, 1]
model = torch.nn.Linear(in_features=2, out_features=1)
loss_fn = torch.nn.BCEWithLogitsLoss(reduction='mean')
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
for step in range(200):
logits = model(X) # [4, 1],不要先 sigmoid
loss = loss_fn(logits, y) # [],批内样本损失的均值
optimizer.zero_grad()
loss.backward()
optimizer.step()
with torch.no_grad():
logits = model(X) # [4, 1]
probabilities = torch.sigmoid(logits)
predictions = (probabilities >= 0.5).to(torch.int64)
print(probabilities.shape) # torch.Size([4, 1])
print(predictions.shape) # torch.Size([4, 1])python重要 API 的输入输出:
torch.nn.Linear(2, 1):最后一维从 2 个特征映射为 1 个 logit。BCEWithLogitsLoss:input与target形状必须一致;标签是浮点数且通常位于 。reduction='mean':先得到每个元素的损失,再取均值,最终输出零维标量。torch.sigmoid(logits):只在需要概率做评价或推理时显式调用。
06 从最大似然看交叉熵#
二分类标签可以看作 伯努利分布(Bernoulli Distribution) 的一次观测:
对 个独立样本,最大化全部标签的似然,等价于最小化负对数似然:
这正是平均 BCE。它不是随意选出的光滑函数,而是在模型输出被解释为概率时,对观测数据做 最大似然估计(Maximum Likelihood Estimation) 的结果。
不过,“概率输出”不自动等于“概率可信”。模型可能预测 0.9 的样本只有 70% 真为正类,这涉及后续会讲的 概率校准(Calibration)。
07 BCE、均方误差和多分类交叉熵不要混用#
| 任务 | 模型输出 | 常见训练损失 | 标签形状 |
|---|---|---|---|
| 二分类 | 1 个 logit | BCEWithLogitsLoss | [B, 1] 浮点 0/1 |
| 多标签分类 | 每个标签 1 个独立 logit | BCEWithLogitsLoss | [B, C] 浮点 0/1 |
| 单标签多分类 | 个互斥类别 logits | CrossEntropyLoss | [B] 整数类别索引 |
| 回归 | 连续实数 | MSELoss、L1Loss 等 | 与预测形状一致 |
均方误差也能拟合 0/1 标签,但配合 Sigmoid 时,极端错误区域的梯度可能变弱;BCE 的概率解释和梯度结构更适合标准二分类。
多标签任务中的每个类别可以同时为 1,例如一张图片既有“室内”又有“人物”,因此用独立 Sigmoid;单标签多分类只能选一个类别,类别之间竞争,因此用 Softmax 形式的多分类交叉熵。二者不能仅凭输出列数判断。
08 工程中最常见的错误与调试方法#
- logits 与概率混淆。 打印最小值和最大值;概率必须在 ,logits 可以是任意实数。
- 标签形状触发广播。 预测为
[B,1]而标签为[B]时,先显式y = y.float().view(-1, 1),再断言形状相同。 - 标签类型错误。 BCE 需要浮点标签;整数类别索引是
CrossEntropyLoss的接口习惯。 - 类别极不平衡。 只看平均损失可能忽视少数类;结合每类召回率,并在训练集上估计合适的
pos_weight。 - 阈值固定等于 0.5。 0.5 是概率到类别的默认决策,不是普适业务最优点;应在验证集上按误报、漏报成本选择阈值。
- 损失突然为
nan。 检查输入是否含非有限值、学习率是否过大,并确认没有手动对 0 取对数。
最小调试断言:
assert logits.shape == y.shape
assert y.dtype.is_floating_point
assert torch.isfinite(logits).all()
assert torch.isfinite(loss)python09 它什么时候仍会失败?#
BCE 假设训练标签值得信任,并把每个样本对目标的贡献通过平均值汇总。以下情况需要额外设计:
- 标签含大量错误时,对数损失会重罚模型与错误标签不一致的高置信预测。
- 正负类成本不对称或比例极端失衡时,需要加权、重采样或专门损失。
- 数据分布改变时,训练出的概率可能失去校准。
- 只优化样本平均误差,不能自动满足公平性、延迟或业务约束。
- 阈值后的准确率仍然不可导,训练损失下降也不保证目标业务指标单调上升。
因此必须同时记录训练损失与验证指标:前者解释优化是否工作,后者判断任务是否真的变好。
10 今天真正需要记住什么?#
- 0-1 损失对应分类对错,却几乎处处梯度为 0,无法有效指导参数更新。
- Sigmoid 把 logit 映射为正类概率,BCE 同时利用标签和置信程度。
- Sigmoid 与 BCE 组合后,对 logit 的梯度是 。
- 训练时把原始 logits 传给
BCEWithLogitsLoss;推理时再显式计算概率和阈值。 - 损失负责可优化性,指标负责任务评价,两者要分别监控。
11 思考题与小练习#
练习 1:同样判断正确,谁的损失更小?
对正样本,模型 A 预测 0.6,模型 B 预测 0.9。两者准确率都为 1;BCE 分别约为 0.511 和 0.105,所以 B 的损失更小。
练习 2:写出一个负样本的更新方向
若 ,则 。梯度下降会减小 ,从而减小正类概率。
练习 3:故意制造重复 Sigmoid
把示例中的 loss_fn(logits, y) 改成 loss_fn(torch.sigmoid(logits), y),比较收敛速度和最终概率。解释为什么接口仍接受输入,但语义已经错误。
相关工作#
- Shannon: A Mathematical Theory of Communication ↗:信息熵与对数度量的奠基工作。
- Cox: The Regression Analysis of Binary Sequences ↗:二元响应逻辑模型的经典论文。
- Bridle: Probabilistic Interpretation of Feedforward Classification Network Outputs ↗:神经分类输出与交叉熵训练的早期系统讨论。
- Lin et al.: Focal Loss for Dense Object Detection ↗:针对极端类别不平衡改造交叉熵的重要工作。
- PyTorch: BCEWithLogitsLoss ↗:当前官方接口、形状、加权与数值稳定性说明。
12 下一篇预告#
损失函数已经把“错得多严重”变成了可导标量,但真实数据不可能每次全部塞进模型。下一篇将追踪一批样本如何经过前向、求平均、反向和更新,解释全批量、随机与小批量梯度下降究竟差在哪里。