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) 的张量,表示每个类别的权重。如果提供了这个参数,损失函数会根据类别的权重来调整各类别的损失,适用于类别不平衡的问题。默认值是 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)。
交叉熵损失的计算步骤如下:
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 $ 类别的预测概率。负对数似然(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=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。
要点
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()。它是将 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
的等价操作可以用以下步骤实现:
计算 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
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。