分类损失:交叉熵与 BCE
statistic
本文字数:2.2k 字 | 阅读时长 ≈ 8 min

分类损失:交叉熵与 BCE

statistic
本文字数:2.2k 字 | 阅读时长 ≈ 8 min

分类任务中经常会遇到交叉熵、BCE、NLLLoss 这些名字。它们之间有什么关系,模型输出要不要先经过 Sigmoid 或 Softmax?这里把分类损失的原理和 PyTorch 用法放在一起,通过几个小例子说明。

1. 先区分分类任务

选择损失函数之前,先看一个样本可以对应几个标签:

任务 例子 标签含义
二分类 判断图片里有没有猫 只有“有”和“没有”两种结果
多分类(单标签) 在猫、狗、猪中选一个类别 多个类别互斥,只能选一个
多标签分类 判断图片里是否有猫、狗、猪 一张图片可以同时有猫和狗

模型最后一层通常输出原始分数,称为 logits。它可以是任意实数,还不是概率。将分数转换为概率时,常用两个函数:

所以,输出三个分数既可能是三分类,也可能是三个标签各自的二分类判断,不能只看输出维度来区分任务。逐项使用 Sigmoid,也不意味着现实中的这些标签在统计上互不相关。

2. 多分类交叉熵

2.1 从分数到损失

假设类别顺序是猫、狗、猪,模型输出三个分数。Softmax 将它们转换为概率:

\[ p_c=\frac{e^{z_c}}{\sum_{j=1}^{C}e^{z_j}} \]

其中 \(C\) 是类别数,\(z_c\) 是第 \(c\) 类的分数,\(p_c\) 是对应的预测概率。给定真实标签的 one-hot 表示 \(y_c\),单个样本的交叉熵为:

\[ L=-\sum_{c=1}^{C}y_c\log p_c \]

one-hot 就是在正确类别的位置填 1,其余位置填 0。比如真实类别是猫,标签为 \((1,0,0)\),预测概率为 \((0.6,0.2,0.2)\),则:

\[ L=-[1\times\log 0.6+0\times\log 0.2+0\times\log 0.2] =-\log 0.6\approx 0.5108 \]

只有正确类别对应的项留下来,所以对于类别索引 \(t\),可以直接写成 \(L=-\log p_t\)。这里的 \(\log\) 是自然对数,正确类别的概率越接近 1,Loss 越接近 0。

这也叫负对数似然(Negative Log-Likelihood,NLL)。对于独立采样的数据,最大似然希望真实标签的预测概率乘积尽可能大;取对数将乘积变成求和,再加负号,就得到最小化损失的形式。对一个 batch,通常取各个样本损失的平均值。

2.2 CrossEntropyLoss、LogSoftmax 与 NLLLoss

我们沿用两个样本、三个类别的例子,直接给定模型分数和真实标签:

import torch
import torch.nn as nn

logits = torch.tensor([[1.0, 2.0, 3.0],
                       [2.0, 4.0, 6.0]])
targets = torch.tensor([2, 1], dtype=torch.long)

loss = nn.CrossEntropyLoss()(logits, targets)

# 拆开计算,便于理解
log_probs = nn.LogSoftmax(dim=1)(logits)
loss_split = nn.NLLLoss()(log_probs, targets)

print(log_probs)
print(f"CrossEntropyLoss: {loss.item():.4f}")
print(f"LogSoftmax + NLLLoss: {loss_split.item():.4f}")

这里的 logits 形状为 [B, C]B=2 表示当前 batch 有两个样本,C=3 表示三个类别。targets 的形状为 [B][2, 1] 表示第一个样本是猪,第二个样本是狗,索引从 0 开始。

LogSoftmax 在数学上就是对 Softmax 的结果取自然对数。按公式计算,log_probs 约为:

[[-2.4076, -1.4076, -0.4076],
 [-4.1429, -2.1429, -0.1429]]

NLLLoss 取出第一行索引 2、第二行索引 1 对应的值,取负后求平均:

\[ L=-\frac{-0.4076+(-2.1429)}{2}\approx 1.2753 \]

因此两种写法都应得到约 1.2753。在这里的默认设置下,CrossEntropyLoss 等价于 LogSoftmax + NLLLossNLLLoss 接收的是对数概率,不能直接把原始分数当作对数概率传进去。

计算 CrossEntropyLoss 时直接输入 logits,不要提前做 Softmax。需要查看预测概率时,再单独计算 torch.softmax(logits, dim=1)。上面使用的是类别索引标签;该函数也支持合法的概率分布标签,不能把多标签的 [1, 1, 0] 直接当成这样的分布。输入约定见 CrossEntropyLoss 文档

3. 二分类交叉熵

3.1 只需要一个概率

判断图片里有没有猫时,用 \(y=1\) 表示有猫,\(y=0\) 表示没有猫。模型只需要输出一个分数 \(z\),经过 Sigmoid 得到有猫的概率 \(p\):

\[ p=\sigma(z)=\frac{1}{1+e^{-z}} \]

没有猫的概率就是 \(1-p\)。将这两个概率代入交叉熵,就得到二分类交叉熵(Binary Cross Entropy,BCE):

\[ L=-[y\log p+(1-y)\log(1-p)] \]

当 \(y=1\) 时,损失为 \(-\log p\);当 \(y=0\) 时,损失为 \(-\log(1-p)\)。两种情况都是看模型给正确答案分配了多少概率。

比如两个样本的预测概率为 [0.9, 0.4],真实标签为 [1, 0]

\[ L=-\frac{\log 0.9+\log(1-0.4)}{2}\approx 0.3081 \]

3.2 BCELoss 与 BCEWithLogitsLoss

如果已经拿到了概率,可以用 BCELoss 复现上面的计算:

import torch
import torch.nn as nn

probs = torch.tensor([0.9, 0.4])
targets = torch.tensor([1.0, 0.0])

loss = nn.BCELoss()(probs, targets)
print(f"loss: {loss.item():.4f}")

结果应约为 0.3081BCELoss 的输入必须是 [0, 1] 范围内的概率,标签使用相同形状的浮点张量;默认对所有元素的损失求平均。BCELoss 文档

实际训练时通常直接拿模型输出的分数计算损失,这时使用 BCEWithLogitsLoss

import torch
import torch.nn as nn

logits = torch.tensor([2.0, -1.0])
targets = torch.tensor([1.0, 0.0])

loss = nn.BCEWithLogitsLoss()(logits, targets)

# 拆开写只是为了对照计算
probs = torch.sigmoid(logits)
loss_split = nn.BCELoss()(probs, targets)

print(f"BCEWithLogitsLoss: {loss.item():.4f}")
print(f"Sigmoid + BCELoss: {loss_split.item():.4f}")

两个分数对应的概率约为 [0.8808, 0.2689],按公式计算,两种写法都应得到约 0.2201

BCEWithLogitsLoss 在数学上结合了 Sigmoid 和 BCE,并使用更稳定的计算方式,适合直接处理模型输出的 logits。不要在它之前再加一次 Sigmoid。BCEWithLogitsLoss 文档

二分类也可以输出两个分数,再使用 CrossEntropyLoss。区别在于:一个分数配合 Sigmoid 表示正类概率;两个分数配合 Softmax 表示两个互斥类别的概率。选择哪一种,模型输出和标签格式就要跟着保持一致。

4. 多标签分类怎么计算

如果一张图里可以同时出现猫和狗,标签就可以是 [1, 1, 0],分别表示有猫、有狗、没有猪。这时每一列都是一个二分类判断,可以逐项计算 BCE。

下面保留一个两张图片、三个标签的例子:

import torch
import torch.nn as nn

probs = torch.tensor([[0.9, 0.1, 0.3],
                      [0.4, 0.8, 0.2]])
targets = torch.tensor([[1.0, 0.0, 0.0],
                        [0.0, 1.0, 0.0]])

loss = nn.BCELoss()(probs, targets)
print(f"loss: {loss.item():.4f}")

这里 [B, C] 的两行代表两个样本,三列代表三个标签。虽然这个小例子里每张图只有一个标签为 1,但计算方式允许一行有多个 1,每行概率也不要求加起来等于 1。

默认的平均损失,是把六个位置的二分类损失相加,再除以 6:

\[ L=-\frac{\log 0.9+\log 0.9+\log 0.7+\log 0.6+\log 0.8+\log 0.8}{6} \approx 0.2541 \]

如果输入换成模型输出的分数,就用 BCEWithLogitsLoss,标签仍然保持 [B, C] 的浮点张量。多标签任务用逐项 Sigmoid,是为了允许多个标签同时成立。

5. 实际使用时怎么选

下面按普通 batch 分类任务整理输入格式,B 是样本数,C 是类别或标签数:

场景 损失函数 输入 标签
二分类,模型输出一个分数 BCEWithLogitsLoss [B][B, 1] 的 logits 与输入同形状,浮点 0/1
二分类,已经有概率 BCELoss [B][B, 1] 的概率 与输入同形状,浮点 0/1
互斥多分类,也包括输出两个分数的二分类 CrossEntropyLoss [B, C] 的 logits [B]long 类别索引
多标签分类,模型输出多个分数 BCEWithLogitsLoss [B, C] 的 logits [B, C] 的浮点 0/1
已经计算了 LogSoftmax NLLLoss [B, C] 的对数概率 [B]long 类别索引

计算 Loss 时,先确认传入的是分数、概率还是对数概率,再检查标签的含义和形状,通常就能避免重复做激活函数、把多分类和多标签混用这类问题。

后面的 InfoNCE Loss 也会用到交叉熵,只是它的每一列代表候选样本,标签表示指定的正样本位置。

Sep 06, 2026
Aug 01, 2026