原笔记只介绍了 Sigmoid。它是把实数映射到概率范围的函数;损失函数还要使用目标值,衡量预测与目标的差异。把二者分开,才能正确连接模型输出、标签和反向传播。
本文使用小型 CPU 数值例子说明二分类、多分类与回归。API 依据 PyTorch 2.8 文档;示例不依赖数据集,也不代表某个模型的训练效果。
1. Sigmoid 是输出变换
跳转到“1. Sigmoid 是输出变换”对于实数分数 ,Sigmoid 为
数学上 ,; 趋向正无穷时输出趋向 1,趋向负无穷时趋向 0。浮点计算存在有限精度,足够大的正数或负数可能得到恰好 1 或 0,不能据数学开区间假定数值计算永远不会触及端点。
它的导数是
这里还没有标签,也没有损失。网络最后一层输出的未归一化分数通常称为 logit;推理时可将二分类 logit 通过 Sigmoid 转成分数,但它是否具有可靠的概率校准还需要评价。Sigmoid 文档
2. 二分类:把 logit 直接交给损失
跳转到“2. 二分类:把 logit 直接交给损失”对目标 ,无加权的二元交叉熵为
硬标签取 0 或 1;软标签可以处于二者之间。将 代入,可以得到数值稳定的等价表达式:
因此,训练时优先使用接收原始 logit 的 BCEWithLogitsLoss。不要先调用 Sigmoid 再交给它,否则损失会把已经限制到 的数值再次当作 logit。已经明确持有概率时才考虑 BCELoss。BCEWithLogitsLoss 文档
单个、未加权项对 logit 的导数为
它来自损失与 Sigmoid 的复合求导,不是在该式之外再乘一次 。如果对 个元素取平均,梯度还要除以 ;使用权重或其他归约方式时应重新按对应公式推导。
3. 多分类与多标签是两种目标
跳转到“3. 多分类与多标签是两种目标”| 任务 | 模型输出 | 目标 | 常用损失 |
|---|---|---|---|
| 二分类 | 每个样本一个 logit | 同形状浮点 0/1 或软标签 | BCEWithLogitsLoss |
| 多标签分类 | 每个样本每个标签一个 logit | 同形状浮点标签,可同时有多个正类 | BCEWithLogitsLoss |
| 互斥多分类 | 每个样本 个 logits | 一个类别编号,通常为 torch.long | CrossEntropyLoss |
| 连续值回归 | 与目标含义一致的数值 | 通常与预测形状相同的浮点目标 | MSELoss、L1Loss 等 |
多标签的各个 Sigmoid 分数没有“总和必须为 1”的要求。互斥多分类则通常使用 Softmax 分布。对类别编号 ,无权重的交叉熵为
CrossEntropyLoss 接收原始 logits,内部采用稳定计算。类别索引形式等价于 log_softmax 后接 NLLLoss,不要再给它预先 Softmax 的结果。它也支持与 logits 同形状的浮点概率目标,此时应由调用者保证每项合法、各类别和为 1;“函数返回了一个数”并不证明软标签有效。CrossEntropyLoss 文档
常见形状是:分类输入 [N, C]、编号目标 [N];二维分割输入 [N, C, H, W]、编号目标 [N, H, W]。类别维和批次维不能互换。
4. 回归损失与误差尺度
跳转到“4. 回归损失与误差尺度”记误差 。均方误差为
平均绝对误差为
平方会放大大误差,所以在相同误差尺度下,L1 对远离主体的数据点通常比 MSE 更不敏感。不能据此断言 L1 适合所有数据,也不能忽略测量单位:米改成毫米会使 MSE 数值放大 倍,使 L1 放大 倍。MSELoss、L1Loss
Huber 在阈值以内使用二次项,外侧使用线性项;SmoothL1Loss 与 Huber 的阈值和整体缩放有关,不能只因为曲线形状类似就认为任意参数下两者数值相同。HuberLoss、SmoothL1Loss
5. 完整例子:损失、梯度与标签形状
跳转到“5. 完整例子:损失、梯度与标签形状”import torchimport torch.nn.functional as F
torch.manual_seed(7)
# 大幅度 logit 下仍能得到有限的二元交叉熵与梯度。z = torch.tensor([-1000.0, -2.0, 0.0, 2.0, 1000.0], dtype=torch.float64, requires_grad=True)y = torch.tensor([1.0, 0.0, 0.5, 1.0, 0.0], dtype=z.dtype)elementwise = F.binary_cross_entropy_with_logits(z, y, reduction="none")stable_formula = z.clamp_min(0) - z * y + torch.log1p(torch.exp(-z.abs()))torch.testing.assert_close(elementwise, stable_formula)assert torch.isfinite(elementwise).all()loss = elementwise.mean()loss.backward()torch.testing.assert_close(z.grad, (z.detach().sigmoid() - y) / z.numel())assert torch.isfinite(z.grad).all()
# 互斥三分类:每行三个原始 logits,标签为一个类别编号。logits = torch.tensor([[2.0, -1.0, 0.5], [-0.2, 0.3, 1.2]], dtype=torch.float64, requires_grad=True)labels = torch.tensor([0, 2], dtype=torch.long)ce = F.cross_entropy(logits, labels)manual = (torch.logsumexp(logits, dim=1) - logits.gather(1, labels[:, None]).squeeze(1)).mean()torch.testing.assert_close(ce, manual)one_hot = F.one_hot(labels, num_classes=3).to(dtype=logits.dtype)torch.testing.assert_close(ce, F.cross_entropy(logits, one_hot))ce.backward()torch.testing.assert_close( logits.grad, (logits.detach().softmax(dim=1) - one_hot) / labels.numel())
# 多标签:一个样本允许标签0和标签2同时为正。multi_logits = torch.tensor([[0.2, -0.5, 1.0]])multi_target = torch.tensor([[1.0, 0.0, 1.0]])assert multi_logits.shape == multi_target.shapeassert torch.isfinite(F.binary_cross_entropy_with_logits(multi_logits, multi_target))
# 回归:明确相同形状,防止意外广播形成另一组误差。prediction = torch.tensor([1.0, 3.0, 8.0])target = torch.tensor([0.0, 3.0, 4.0])assert prediction.shape == target.shapeerror = prediction - targettorch.testing.assert_close(F.mse_loss(prediction, target), error.square().mean())torch.testing.assert_close(F.l1_loss(prediction, target), error.abs().mean())print("损失公式、标签形状和解析梯度检查通过")这个例子验证数学关系和 API 约定。较小的训练损失不自动代表更好的泛化,还要检查独立验证集、数据泄漏、类别覆盖和任务指标。
6. 训练前的排查顺序
跳转到“6. 训练前的排查顺序”先检查标签表达的任务,再检查输出和目标的形状、数据类型、取值范围与设备。随后确认损失需要 logits、概率还是对数概率,并明确有没有已经做过相同变换。
最后核对归约:none 保留各项,sum 求和,mean 按该损失的规则平均。加权交叉熵、忽略标签和自己实现的掩码平均可能使用不同分母;不要把所有 mean 都理解成“除以批次大小”。若掩码没有任何有效元素,应先定义如何跳过或处理该批次,再做除法。