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

Hoper.J 2024-08-05 14:05:03 阅读 80

文章目录

前置知识nn.CrossEntropyLoss() 交叉熵损失参数数学公式带权重的公式(weight)标签平滑(label_smoothing)

要点

附录参考链接

前置知识

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

nn.CrossEntropyLoss() 交叉熵损失

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

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

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

参数

weight (Tensor, 可选): 一个形状为

(

C

)

(C)

(C) 的张量,表示每个类别的权重。如果提供了这个参数,损失函数会根据类别的权重来调整各类别的损失,适用于类别不平衡的问题。默认值是 Nonesize_average (bool, 可选): 已弃用。如果 reduction 不是 'none',则默认情况下损失是取平均(True);否则,是求和(False)。默认值是 Noneignore_index (int, 可选): 如果指定了这个参数,则该类别的索引会被忽略,不会对损失和梯度产生影响。默认值是 -100reduce (bool, 可选): 已弃用。请使用 reduction 参数。默认值是 Nonereduction (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)。

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

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=1C​exp(xij​)exp(xic​)​

其中 $ p_{ic} $ 表示第 $ i $ 个样本属于第 $ c $ 类别的预测概率。负对数似然(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​ 上的预测概率。总损失

计算所有样本的平均损失( 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=N1​i=1∑N​ℓi​=N1​i=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=N1​i=1∑N​wyi​​⋅ℓi​=∑i=1N​wyi​​∑i=1N​wyi​​⋅(−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∑C​yic′​⋅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。

要点

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

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.")

>>> 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()。它是将 softmaxlog 这两个操作结合在一起,以提高数值稳定性和计算效率。具体的数学定义如下:

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(∑j​exp(xj​)exp(xi​)​)=xi​−log(j∑​exp(xj​))

在代码中,F.log_softmax 的等价操作可以用以下步骤实现:

计算 softmax。计算 softmax 的结果的对数。

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.")

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

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()}")

>>> 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 没变,这也就验证了之前的说法。目标标签 target 期望两种格式:

类别索引: 类别的整数索引,而不是 one-hot 编码。范围在

[

0

,

C

)

[0, C)

[0,C) 之间,其中

C

C

C​ 是类别数。如果指定了 ignore_index,则该类别索引也会被接受(即便可能不在类别范围内)

使用示例:

# 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()

类别概率: 类别的概率分布,适用于需要每个批次项有多个类别标签的情况,如标签平滑等。

使用示例:

# 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.

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

附录

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

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')code>

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')code>

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')code>

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), "标签平滑的数学公式验证失败"

>>> PyTorch 计算的交叉熵损失: 0.45524317026138306

>>> 根据公式实现的交叉熵损失: 0.4552431106567383

>>> PyTorch 计算的带权重的交叉熵损失: 0.5048722624778748

>>> 根据公式实现的带权重的交叉熵损失: 0.50487220287323

>>> PyTorch 计算的标签平滑的交叉熵损失: 0.5469098091125488

>>> 根据公式实现的标签平滑的交叉熵损失: 0.5469098091125488

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

参考链接

CrossEntropyLoss - Docs



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。