PyTorch nn.CrossEntropyLoss() 交叉熵损失函数详解和要点提醒

文章目录

前置知识

深度学习:关于损失函数的一些前置知识(PyTorch Loss)

nn.CrossEntropyLoss() 交叉熵损失

torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean', label_smoothing=0.0)

This criterion computes the cross entropy loss between input logits and target.

该函数计算输入 logits 和目标之间的交叉熵损失。

参数

  • weight (Tensor, 可选): 一个形状为 ( C ) (C) (C) 的张量,表示每个类别的权重。如果提供了这个参数,损失函数会根据类别的权重来调整各类别的损失,适用于类别不平衡的问题。默认值是 None。
  • size_average (bool, 可选): 已弃用。如果 reduction 不是 'none',则默认情况下损失是取平均(True);否则,是求和(False)。默认值是 None。
  • ignore_index (int, 可选): 如果指定了这个参数,则该类别的索引会被忽略,不会对损失和梯度产生影响。默认值是 -100。
  • reduce (bool, 可选): 已弃用。请使用 reduction 参数。默认值是 None。
  • reduction (str, 可选): 指定应用于输出的归约方式。可选值为 'none'、'mean'、'sum'。'none' 表示不进行归约,'mean' 表示对所有样本的损失求平均,'sum' 表示对所有样本的损失求和。默认值是 'mean'。
  • label_smoothing (float, 可选): 标签平滑值,范围在 0.0, 1.0 之间。默认值是 0.0。标签平滑是一种正则化技术,通过在真实标签上添加一定程度的平滑来避免过拟合。

数学公式

附录部分会验证下述公式和代码的一致性。

假设有 N N N 个样本,每个样本属于 C C C 个类别之一。对于第 i i i 个样本,它的真实类别标签为 y i y_i yi,模型的输出 logits 为 x i = ( x i 1 , x i 2 , ... , x i C ) \mathbf{x}i = (x{i1}, x_{i2}, \ldots, x_{iC}) xi=(xi1,xi2,...,xiC),其中 x i c x_{ic} xic 表示第 i i i 个样本在第 c c c 类别上的原始输出分数(logits)。

交叉熵损失的计算步骤如下:

  1. Softmax 函数 :
    对 logits 进行 softmax 操作,将其转换为概率分布:
    p i c = exp ⁡ ( x i c ) ∑ j = 1 C exp ⁡ ( x i j ) p_{ic} = \frac{\exp(x_{ic})}{\sum_{j=1}^{C} \exp(x_{ij})} pic=∑j=1Cexp(xij)exp(xic)
    其中 p_{ic} 表示第 i 个样本属于第 c 类别的预测概率。
  2. 负对数似然(Negative Log-Likelihood) :
    计算负对数似然:
    ℓ i = − log ⁡ ( p i y i ) \ell_i = -\log(p_{iy_i}) ℓi=−log(piyi)
    其中 ℓ i \ell_i ℓi 是第 i i i 个样本的损失, p i y i p_{iy_i} piyi 表示第 i i i 个样本在真实类别 y i y_i yi 上的预测概率。
  3. 总损失 :
    计算所有样本的平均损失( reduction 参数默认为 'mean'):
    L = 1 N ∑ i = 1 N ℓ i = 1 N ∑ i = 1 N − log ⁡ ( p i y i ) \mathcal{L} = \frac{1}{N} \sum_{i=1}^{N} \ell_i = \frac{1}{N} \sum_{i=1}^{N} -\log(p_{iy_i}) L=N1i=1∑Nℓi=N1i=1∑N−log(piyi)
    如果 reduction 参数为 'sum',总损失为所有样本损失的和:
    L = ∑ i = 1 N ℓ i = ∑ i = 1 N − log ⁡ ( p i y i ) \mathcal{L} = \sum_{i=1}^{N} \ell_i = \sum_{i=1}^{N} -\log(p_{iy_i}) L=i=1∑Nℓi=i=1∑N−log(piyi)
    如果 reduction 参数为 'none',则返回每个样本的损失 ℓ i \ell_i ℓi 组成的张量。
    L = ℓ 1 , ℓ 2 , ... , ℓ N = − log ⁡ ( p i y 1 ) , − log ⁡ ( p i y 2 ) , ... , − log ⁡ ( p i y N ) \mathcal{L} = \\ell_1, \\ell_2, \\ldots, \\ell_N = -\\log(p_{iy_1}), -\\log(p_{iy_2}), \\ldots, -\\log(p_{iy_N}) L=ℓ1,ℓ2,...,ℓN=−log(piy1),−log(piy2),...,−log(piyN)

带权重的公式(weight)

如果指定了类别权重 w = ( w 1 , w 2 , ... , w C ) \mathbf{w} = (w_1, w_2, \ldots, w_C) w=(w1,w2,...,wC),则总损失公式为:

L = 1 N ∑ i = 1 N w y i ⋅ ℓ i = ∑ i = 1 N w y i ⋅ ( − log ⁡ ( p i y i ) ) ∑ i = 1 N w y i \mathcal{L} = \frac{1}{N} \sum_{i=1}^{N} w_{y_i} \cdot \ell_i = \frac{\sum_{i=1}^{N} w_{y_i} \cdot (-\log(p_{iy_i}))}{\sum_{i=1}^{N} w_{y_i}} L=N1i=1∑Nwyi⋅ℓi=∑i=1Nwyi∑i=1Nwyi⋅(−log(piyi))

其中 w y i w_{y_i} wyi 是第 i i i 个样本真实类别的权重。

标签平滑(label_smoothing)

如果标签平滑(label smoothing)参数 α \alpha α 被启用,目标标签 y i \mathbf{y}_i yi 会被平滑处理:

y i ′ = ( 1 − α ) ⋅ y i + α C \mathbf{y}_i' = (1 - \alpha) \cdot \mathbf{y}_i + \frac{\alpha}{C} yi′=(1−α)⋅yi+Cα

其中, y i \mathbf{y}_i yi 是原始的 one-hot 编码目标标签, y i ′ \mathbf{y}_i' yi′ 是平滑后的标签。

总的损失公式会相应调整:

ℓ i = − ∑ c = 1 C y i c ′ ⋅ log ⁡ ( p i c ) \ell_i = - \sum_{c=1}^{C} y_{ic}' \cdot \log(p_{ic}) ℓi=−c=1∑Cyic′⋅log(pic)

其中, y i c y_{ic} yic 是第 i i i 个样本在第 c c c 类别上的标签,为原标签 y i y_i yi 经过 one-hot 编码后 y i \mathbf{y}i yi 中的值。对于一个 one-hot 编码标签向量, y i c y{ic} yic 在样本属于类别 c c c 时为 1,否则为 0。

要点

  1. nn.CrossEntropyLoss() 接受的输入是 logits ,这说明分类的输出不需要提前经过 softmax。如果提前经过 softmax,则需要使用 nn.NLLLoss()(负对数似然损失)。

    python 复制代码
    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    
    # 定义输入和目标标签
    logits = torch.tensor([[2.0, 0.5], [0.5, 2.0]])  # 未经过 softmax 的 logits
    target = torch.tensor([0, 1])  # 目标标签
    
    # 使用 nn.CrossEntropyLoss 计算损失(接受 logits)
    criterion_ce = nn.CrossEntropyLoss()
    loss_ce = criterion_ce(logits, target)
    
    # 使用 softmax 后再使用 nn.NLLLoss 计算损失
    log_probs = F.log_softmax(logits, dim=1)
    criterion_nll = nn.NLLLoss()
    loss_nll = criterion_nll(log_probs, target)
    
    print(f"Loss using nn.CrossEntropyLoss: {loss_ce.item()}")
    print(f"Loss using softmax + nn.NLLLoss: {loss_nll.item()}")
    
    # 验证两者是否相等
    assert torch.allclose(loss_ce, loss_nll), "The losses are not equal, which indicates a mistake in the assumption."
    print("The losses are equal, indicating that nn.CrossEntropyLoss internally applies softmax.")
    python 复制代码
    >>> Loss using nn.CrossEntropyLoss: 0.2014133334159851
    >>> Loss using softmax + nn.NLLLoss: 0.2014133334159851
    >>> The losses are equal, indicating that nn.CrossEntropyLoss internally applies softmax.

    拓展: F.log_softmax()
    F.log_softmax 等价于先应用 softmax 激活函数,然后对结果取对数 log()。它是将 softmax 和 log 这两个操作结合在一起,以提高数值稳定性和计算效率。具体的数学定义如下:
    log_softmax ( x i ) = log ⁡ ( softmax ( x i ) ) = log ⁡ ( exp ⁡ ( x i ) ∑ j exp ⁡ ( x j ) ) = x i − log ⁡ ( ∑ j exp ⁡ ( x j ) ) \text{log\_softmax}(x_i) = \log\left(\text{softmax}(x_i)\right) = \log\left(\frac{\exp(x_i)}{\sum_j \exp(x_j)}\right) = x_i - \log\left(\sum_j \exp(x_j)\right) log_softmax(xi)=log(softmax(xi))=log(∑jexp(xj)exp(xi))=xi−log(j∑exp(xj))
    在代码中,F.log_softmax 的等价操作可以用以下步骤实现:

    1. 计算 softmax。
    2. 计算 softmax 的结果的对数。
    python 复制代码
    import torch
    import torch.nn.functional as F
    
    # 定义输入 logits
    logits = torch.tensor([[2.0, 1.0, 0.1], [1.0, 3.0, 0.2]])
    
    # 计算 log_softmax
    log_softmax_result = F.log_softmax(logits, dim=1)
    
    # 分开计算 softmax 和 log
    softmax_result = F.softmax(logits, dim=1)
    log_result = torch.log(softmax_result)
    
    print("Logits:")
    print(logits)
    
    print("\nLog softmax (using F.log_softmax):")
    print(log_softmax_result)
    
    print("\nSoftmax result:")
    print(softmax_result)
    
    print("\nLog of softmax result:")
    print(log_result)
    
    # 验证两者是否相等
    assert torch.allclose(log_softmax_result, log_result), "The results are not equal."
    print("\nThe results are equal, indicating that F.log_softmax is equivalent to softmax followed by log.")
    python 复制代码
    >>> Logits:
    >>> tensor([[2.0000, 1.0000, 0.1000],
    >>>         [1.0000, 3.0000, 0.2000]])
    
    >>> Log softmax (using F.log_softmax):
    >>> tensor([[-0.4170, -1.4170, -2.3170],
    >>>         [-2.1791, -0.1791, -2.9791]])
    
    >>> Softmax result:
    >>> tensor([[0.6590, 0.2424, 0.0986],
    >>>         [0.1131, 0.8360, 0.0508]])
    
    >>> Log of softmax result:
    >>> tensor([[-0.4170, -1.4170, -2.3170],
    >>>         [-2.1791, -0.1791, -2.9791]])
    
    >>> The results are equal, indicating that F.log_softmax is equivalent to softmax followed by log.

    从结果中可以看到 F.log_softmax 的结果等价于先计算 softmax 再取对数。

  2. nn.CrossEntropyLoss() 实际上默认(reduction='mean')计算的是每个样本的平均损失 ,已经做了归一化处理,所以不需要对得到的结果进一步除以 batch_size 或其他某个数,除非是用作 loss_weight。下面是一个简单的例子:

    python 复制代码
    import torch
    import torch.nn as nn
    
    # 定义损失函数
    criterion = nn.CrossEntropyLoss()
    
    # 定义输入和目标标签
    input1 = torch.tensor([[2.0, 0.5], [0.5, 2.0]], requires_grad=True)  # 批量大小为 2
    target1 = torch.tensor([0, 1])  # 对应的目标标签
    
    input2 = torch.tensor([[2.0, 0.5], [0.5, 2.0], [2.0, 0.5], [0.5, 2.0]], requires_grad=True)  # 批量大小为 4
    target2 = torch.tensor([0, 1, 0, 1])  # 对应的目标标签
    
    # 计算损失
    loss1 = criterion(input1, target1)
    loss2 = criterion(input2, target2)
    
    print(f"Loss with batch size 2: {loss1.item()}")
    print(f"Loss with batch size 4: {loss2.item()}")
    python 复制代码
    >>> Loss with batch size 2: 0.2014133334159851
    >>> Loss with batch size 4: 0.2014133334159851

    可以看到这里的 input2 实际上等价于 torch.cat([input1, input1], dim=0),target2 等价于 torch.cat([target1, target1], dim=0),简单拓展了 batch_size 大小但最终的 Loss 没变,这也就验证了之前的说法。

  3. 目标标签 target 期望两种格式:

    • 类别索引 : 类别的整数索引,而不是 one-hot 编码。范围在 [ 0 , C ) [0, C) [0,C) 之间,其中 C C C​ 是类别数。如果指定了 ignore_index,则该类别索引也会被接受(即便可能不在类别范围内)

      使用示例:

      python 复制代码
      # Example of target with class indices
      import torch
      import torch.nn as nn
      
      loss = nn.CrossEntropyLoss()
      input = torch.randn(3, 5, requires_grad=True)
      target = torch.empty(3, dtype=torch.long).random_(5)
      output = loss(input, target)
      output.backward()
    • 类别概率 : 类别的概率分布,适用于需要每个批次项有多个类别标签的情况,如标签平滑等。

      使用示例:

      python 复制代码
      # Example of target with class probabilities
      import torch
      import torch.nn as nn
      
      loss = nn.CrossEntropyLoss()
      input = torch.randn(3, 5, requires_grad=True)
      target = torch.randn(3, 5).softmax(dim=1)
      output = loss(input, target)
      output.backward()

      The performance of this criterion is generally better when target contains class indices , as this allows for optimized computation. Consider providing target as class probabilities only when a single class label per minibatch item is too restrictive.

      通常情况下,当目标为类别索引 时,该函数的性能更好,因为这样可以进行优化计算。只有在每个批次项的单一类别标签过于限制 时,才考虑使用类别概率。

附录

用于验证数学公式和函数实际运行的一致性

python 复制代码
import torch
import torch.nn.functional as F

# 假设有两个样本,每个样本有三个类别
logits = torch.tensor([[1.5, 2.0, 0.5], [1.0, 0.5, 2.5]], requires_grad=True)
targets = torch.tensor([1, 2])

# 根据公式实现 softmax
def softmax(x):
    return torch.exp(x) / torch.exp(x).sum(dim=1, keepdim=True)

# 根据公式实现 log-softmax
def log_softmax(x):
    return x - torch.log(torch.exp(x).sum(dim=1, keepdim=True))

# 根据公式实现负对数似然损失(NLLLoss)
def nll_loss(log_probs, targets):
    N = log_probs.size(0)
    return -log_probs[range(N), targets].mean()

# 根据公式实现交叉熵损失
def custom_cross_entropy(logits, targets):
    log_probs = log_softmax(logits)
    return nll_loss(log_probs, targets)

# 使用 PyTorch 计算交叉熵损失
criterion = torch.nn.CrossEntropyLoss(reduction='mean')
loss_torch = criterion(logits, targets)

# 使用根据公式实现的交叉熵损失
loss_custom = custom_cross_entropy(logits, targets)

# 打印结果
print("PyTorch 计算的交叉熵损失:", loss_torch.item())
print("根据公式实现的交叉熵损失:", loss_custom.item())

# 验证结果是否相等
assert torch.isclose(loss_torch, loss_custom), "数学公式验证失败"

# 带权重的交叉熵损失
weights = torch.tensor([0.7, 0.2, 0.1])
criterion_weighted = torch.nn.CrossEntropyLoss(weight=weights, reduction='mean')
loss_weighted_torch = criterion_weighted(logits, targets)

# 根据公式实现带权重的交叉熵损失
def custom_weighted_cross_entropy(logits, targets, weights):
    log_probs = log_softmax(logits)
    N = logits.size(0)
    weighted_loss = -log_probs[range(N), targets] * weights[targets]
    return weighted_loss.sum() / weights[targets].sum()

loss_weighted_custom = custom_weighted_cross_entropy(logits, targets, weights)

# 打印结果
print("PyTorch 计算的带权重的交叉熵损失:", loss_weighted_torch.item())
print("根据公式实现的带权重的交叉熵损失:", loss_weighted_custom.item())

# 验证结果是否相等
assert torch.isclose(loss_weighted_torch, loss_weighted_custom, atol=1e-6), "带权重的数学公式验证失败"

# 标签平滑的交叉熵损失
alpha = 0.1
criterion_label_smoothing = torch.nn.CrossEntropyLoss(label_smoothing=alpha, reduction='mean')
loss_label_smoothing_torch = criterion_label_smoothing(logits, targets)

# 根据公式实现标签平滑的交叉熵损失
def custom_label_smoothing_cross_entropy(logits, targets, alpha):
    N, C = logits.size()
    log_probs = log_softmax(logits)
    one_hot = torch.zeros_like(log_probs).scatter(1, targets.view(-1, 1), 1)
    smooth_targets = (1 - alpha) * one_hot + alpha / C
    loss = - (smooth_targets * log_probs).sum(dim=1).mean()
    return loss

loss_label_smoothing_custom = custom_label_smoothing_cross_entropy(logits, targets, alpha)

# 打印结果
print("PyTorch 计算的标签平滑的交叉熵损失:", loss_label_smoothing_torch.item())
print("根据公式实现的标签平滑的交叉熵损失:", loss_label_smoothing_custom.item())

# 验证结果是否相等
assert torch.isclose(loss_label_smoothing_torch, loss_label_smoothing_custom, atol=1e-6), "标签平滑的数学公式验证失败"
python 复制代码
>>> PyTorch 计算的交叉熵损失: 0.45524317026138306
>>> 根据公式实现的交叉熵损失: 0.4552431106567383
>>> PyTorch 计算的带权重的交叉熵损失: 0.5048722624778748
>>> 根据公式实现的带权重的交叉熵损失: 0.50487220287323
>>> PyTorch 计算的标签平滑的交叉熵损失: 0.5469098091125488
>>> 根据公式实现的标签平滑的交叉熵损失: 0.5469098091125488

输出没有抛出 AssertionError,验证通过。

参考链接

CrossEntropyLoss - Docs

相关推荐
Zguigo14 小时前
【CUDA6】CUDA Stream 是什么,为什么 CUDA 是异步执行,如何正确测量 GPU 时间以及多个任务如何重叠执行
人工智能·pytorch·深度学习
论文复现现场1 天前
RTX 3090 24GB 适合科研吗?单卡论文复现、PyTorch 显存检查与 OOM 排错
人工智能·pytorch·python·深度学习·cuda·rtx3090
论文复现现场1 天前
AutoDL、算家云与公有云 GPU 怎么选?环境复现、计费与断点恢复对比
pytorch·深度学习·云计算·gpu
盘古开天16662 天前
PPO算法代码实战(三):PyTorch从零实现PPO求解CartPole
人工智能·pytorch·算法
Είναι η κοπέλα2 天前
PyTorch 安装与验证
人工智能·pytorch·python
盼小辉丶3 天前
PyTorch强化学习实战(27)——进化策略在强化学习中的应用
人工智能·pytorch·深度学习·强化学习
weixin_447195293 天前
【无标题】
pytorch·python
bigdata-余建新3 天前
week2
人工智能·pytorch·深度学习
HwJack203 天前
【共创稿事节】HarmonyOS 7材质与光照:真实感的营造与性能的平衡
pytorch·harmonyos·材质
头发够用的程序员4 天前
TensorRT 自定义算子插件实战(一):从零手写 customScaledTanh
c++·人工智能·pytorch·python·深度学习·边缘计算·jetson