跳转到内容
新建笔记

损失函数:从 Sigmoid 到稳定交叉熵与回归误差

原笔记只介绍了 Sigmoid。它是把实数映射到概率范围的函数;损失函数还要使用目标值,衡量预测与目标的差异。把二者分开,才能正确连接模型输出、标签和反向传播。

本文使用小型 CPU 数值例子说明二分类、多分类与回归。API 依据 PyTorch 2.8 文档;示例不依赖数据集,也不代表某个模型的训练效果。

对于实数分数 zz,Sigmoid 为

p=σ(z)=11+e−z.p=\sigma(z)=\frac{1}{1+e^{-z}}.

数学上 0<p<10<p<1,σ(0)=0.5\sigma(0)=0.5;zz 趋向正无穷时输出趋向 1,趋向负无穷时趋向 0。浮点计算存在有限精度,足够大的正数或负数可能得到恰好 1 或 0,不能据数学开区间假定数值计算永远不会触及端点。

它的导数是

dσ(z)dz=σ(z)(1−σ(z)).\frac{\mathrm d\sigma(z)}{\mathrm dz} =\sigma(z)\bigl(1-\sigma(z)\bigr).

这里还没有标签,也没有损失。网络最后一层输出的未归一化分数通常称为 logit;推理时可将二分类 logit 通过 Sigmoid 转成分数,但它是否具有可靠的概率校准还需要评价。Sigmoid 文档

2. 二分类:把 logit 直接交给损失

跳转到“2. 二分类:把 logit 直接交给损失”

对目标 y∈[0,1]y\in[0,1],无加权的二元交叉熵为

ℓ(p,y)=−ylog⁡p−(1−y)log⁡(1−p).\ell(p,y)=-y\log p-(1-y)\log(1-p).

硬标签取 0 或 1;软标签可以处于二者之间。将 p=σ(z)p=\sigma(z) 代入,可以得到数值稳定的等价表达式:

ℓ(z,y)=max⁡(z,0)−zy+log⁡(1+e−∣z∣).\ell(z,y)=\max(z,0)-zy+\log\left(1+e^{-|z|}\right).

因此,训练时优先使用接收原始 logit 的 BCEWithLogitsLoss。不要先调用 Sigmoid 再交给它,否则损失会把已经限制到 [0,1][0,1] 的数值再次当作 logit。已经明确持有概率时才考虑 BCELoss。BCEWithLogitsLoss 文档

单个、未加权项对 logit 的导数为

∂ℓ∂z=σ(z)−y.\frac{\partial\ell}{\partial z}=\sigma(z)-y.

它来自损失与 Sigmoid 的复合求导,不是在该式之外再乘一次 σ(z)(1−σ(z))\sigma(z)(1-\sigma(z))。如果对 MM 个元素取平均,梯度还要除以 MM;使用权重或其他归约方式时应重新按对应公式推导。

3. 多分类与多标签是两种目标

跳转到“3. 多分类与多标签是两种目标”
任务模型输出目标常用损失
二分类每个样本一个 logit同形状浮点 0/1 或软标签BCEWithLogitsLoss
多标签分类每个样本每个标签一个 logit同形状浮点标签,可同时有多个正类BCEWithLogitsLoss
互斥多分类每个样本 CC 个 logits一个类别编号,通常为 torch.longCrossEntropyLoss
连续值回归与目标含义一致的数值通常与预测形状相同的浮点目标MSELoss、L1Loss 等

多标签的各个 Sigmoid 分数没有“总和必须为 1”的要求。互斥多分类则通常使用 Softmax 分布。对类别编号 yy,无权重的交叉熵为

ℓ(z,y)=log⁡∑c=0C−1ezc−zy.\ell(\mathbf z,y) =\log\sum_{c=0}^{C-1}e^{z_c}-z_y.

CrossEntropyLoss 接收原始 logits,内部采用稳定计算。类别索引形式等价于 log_softmax 后接 NLLLoss,不要再给它预先 Softmax 的结果。它也支持与 logits 同形状的浮点概率目标,此时应由调用者保证每项合法、各类别和为 1;“函数返回了一个数”并不证明软标签有效。CrossEntropyLoss 文档

常见形状是:分类输入 [N, C]、编号目标 [N];二维分割输入 [N, C, H, W]、编号目标 [N, H, W]。类别维和批次维不能互换。

4. 回归损失与误差尺度

跳转到“4. 回归损失与误差尺度”

记误差 ei=y^i−yie_i=\widehat y_i-y_i。均方误差为

LMSE=1M∑i=1Mei2,L_{\mathrm{MSE}}=\frac1M\sum_{i=1}^{M}e_i^2,

平均绝对误差为

LL1=1M∑i=1M∣ei∣.L_{\mathrm{L1}}=\frac1M\sum_{i=1}^{M}|e_i|.

平方会放大大误差,所以在相同误差尺度下,L1 对远离主体的数据点通常比 MSE 更不敏感。不能据此断言 L1 适合所有数据,也不能忽略测量单位:米改成毫米会使 MSE 数值放大 10610^6 倍,使 L1 放大 10310^3 倍。MSELoss、L1Loss

Huber 在阈值以内使用二次项,外侧使用线性项;SmoothL1Loss 与 Huber 的阈值和整体缩放有关,不能只因为曲线形状类似就认为任意参数下两者数值相同。HuberLoss、SmoothL1Loss

5. 完整例子:损失、梯度与标签形状

跳转到“5. 完整例子:损失、梯度与标签形状”
import torch
import 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.shape
assert 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.shape
error = prediction - target
torch.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 约定。较小的训练损失不自动代表更好的泛化,还要检查独立验证集、数据泄漏、类别覆盖和任务指标。

先检查标签表达的任务,再检查输出和目标的形状、数据类型、取值范围与设备。随后确认损失需要 logits、概率还是对数概率,并明确有没有已经做过相同变换。

最后核对归约:none 保留各项,sum 求和,mean 按该损失的规则平均。加权交叉熵、忽略标签和自己实现的掩码平均可能使用不同分母;不要把所有 mean 都理解成“除以批次大小”。若掩码没有任何有效元素,应先定义如何跳过或处理该批次,再做除法。

张量形状与复制行为见张量基础;模块与损失分类见torch.nn。