CrossEntropyLoss
公式
J l o s s = − ∑ i = 1 N ∑ k = 1 K y i k ∗ l o g ( h θ ( x i ) k ) J_{loss} = -\sum_{i = 1}^{N} \sum_{k = 1}^{K} y_{ik}*log(h_{\theta}(x_{i})_{k}) Jloss=−∑i=1N∑k=1Kyik∗log(hθ(xi)k)
h θ ( x i ) j = e x p ( x i ) j ∑ k = 1 K e x p ( x i ) k h_{\theta}(x_{i})_{j} = \frac{exp(x_{i})_{j}}{\sum_{k = 1}^{K}exp(x_{i})_{k}} hθ(xi)j=∑k=1Kexp(xi)kexp(xi)j
其中
- N N N : 样本点的数量
- K K K : 类别的数量
- y i c y_{ic} yic : 样本目标的 o n e − h o t one-hot one−hot 编码。如果样本 x i x_{i} xi 的真实类别等于 k k k 取1,否则取0。
- h θ ( x i ) k h_{\theta}(x_{i})_{k} hθ(xi)k : 观测样本 x i x_{i} xi 属于类别 k k k 的预测概率。
样例
下面以一个图片分类问题为例,理解上面的公式。这个例子,根据图片中动物的轮廓、颜色等特征,来预测动物的类别(猫、狗、马)。
类别名称 | 类别编号 | one-hot |
---|---|---|
猫 | 0 | [1, 0, 0] |
狗 | 1 | [0, 1, 0] |
马 | 2 | [0, 0, 1] |
有一张马的图片,它的真实类别编号为 2, one-hot 编码为 [ 0 , 0 , 1 ] [0, 0, 1] [0,0,1] 。假定模型的预测概率为 [ 0.1 , 0.12 , 0.78 ] [0.1, 0.12, 0.78] [0.1,0.12,0.78],如下为计算交叉熵的过程。
类别名称 | 预测概率 | 类别的one-hot | 符号函数值 yicyic | 单项交叉熵计算过程 | 单项交叉熵结果 |
---|---|---|---|---|---|
猫 | 0.1 | [1, 0, 0] | 0 | -0 * log(0.1) | 0 |
狗 | 0.12 | [0, 1, 0] | 0 | -0 * log(0.12) | 0 |
马 | 0.78 | [0, 0, 1] | 1 | -1 * log(0.78) | 0.2485 |
总的交叉熵为0.2485。
Pytorch 中的CrossEntropy的形式
pytorch中torch.nn模块和torch.nn.functional中均有对应的交叉熵的模块,但是两者是相通的。不过torch.nn.functional中可以理解为是一个接口,而torch.nn是具体类的定义。
- torch.nn.functional.cross_entropy
def cross_entropy(input, target, weight=None, size_average=None, ignore_index=-100,
reduce=None, reduction='mean'):
# type: (Tensor, Tensor, Optional[Tensor], Optional[bool], int, Optional[bool], str) -> Tensor
if size_average is not None or reduce is not None:
reduction = _Reduction.legacy_get_string(size_average, reduce)
return nll_loss(log_softmax(input, 1), target, weight, None, ignore_index, None, reduction)
看上面代码也能知道input和target是必选项,并且是Tensor类型的。最后一行说明functional.cross_entropy实际计算过程就是先计算Tensor的log_softmax,然后再计算nll_loss。
那么问题来了,log_softmax是怎么计算的,干了些什么,用上面表格的数据来举例来说:
Tensor的log_softmax函数和functional的函数作用一样,都是先对数据进行softmax,然后进行log函数,这里的log以e为底,即ln。log_softmax和softmax中的数字表示按照什么维度计算。0代表按列计算,softmax函数计算后的数据按列加起来为1;1代表按行计算,softmax函数计算后的数据按行加起来为1。
import torch.nn.functional as F
import torch
truth = torch.tensor([[1, 0, 0]], dtype=torch.float)
predicted1 = torch.tensor([[0.5, 0.4, 0.1]], dtype=torch.float)
print(truth.softmax(0))
print(truth.softmax(1))
print(F.log_softmax(predicted1, 1))
print(truth.log_softmax(-1))
上面代码的输出结果为:
tensor([[1., 1., 1.]])
tensor([[0.5017, 0.2491, 0.2491]])
tensor([[-0.9459, -1.0459, -1.3459]])
tensor([[-0.6897, -1.3897, -1.3897]])
第一行的输出结果可以理解,按照列计算只有一个数据,所以计算后每列的只有1个1。
第二行的输出结果可以通过计算得到:
e 1 e 1 + e 0 + e 0 = e e + 2 = 0.5017 \frac{e^{1}}{e^{1}+e^{0}+e^{0}}=\frac{e}{e+2}=0.5017 e1+e0+e0e1=e+2e=0.5017
e 0 e 1 + e 0 + e 0 = 1 e + 2 = 0.2491 \frac{e^{0}}{e^{1}+e^{0}+e^{0}}=\frac{1}{e+2}=0.2491 e1+e0+e0e0=e+21=0.2491
e 0 e 1 + e 0 + e 0 = 1 e + 2 = 0.2491 \frac{e^{0}}{e^{1}+e^{0}+e^{0}}=\frac{1}{e+2}=0.2491 e1+e0+e0e0=e+21=0.2491
第四行的输出结果通过计算得到:
l
n
(
0.5017
)
=
−
0.68975
ln(0.5017)=−0.68975
ln(0.5017)=−0.68975
l
n
(
0.2491
)
=
−
1.38990
ln(0.2491)=−1.38990
ln(0.2491)=−1.38990
l
n
(
0.2491
)
=
−
1.38990
ln(0.2491)=−1.38990
ln(0.2491)=−1.38990
知道了log_softmax
是干嘛的,现在来了解nll_loss
是干嘛的。
truth = torch.tensor([0], dtype=torch.int64)
predicted1 = torch.tensor([[0.5, 0.4, 0.1]], dtype=torch.float)
loss = nn.NLLLoss()
print(F.log_softmax(predicted1, 1))
print(loss(F.log_softmax(predicted1, 1), truth))
print(F.cross_entropy(predicted1, truth))
上面的输出结果为:
tensor([[-0.9459, -1.0459, -1.3459]])
tensor(0.9459)
tensor(0.9459)
可以看到truth
中的值就是log_softmax
结果后的数组的idx,即truth
的0
对应-0.9459
的位置,将-0.9459
取出后取负数便为NLLLoss的结果。同样地,若truth
的值为1
,那么第二行和第三行的输出结果为1.0459
。
Label Smoothing
Label Smoothing
也称之为标签平滑,其实是一种防止过拟合的正则化方法。传统的分类loss采用softmax loss
,先对全连接层的输出计算
s
o
f
t
m
a
x
softmax
softmax,视为各类别的置信度概率,再利用交叉熵计算损失。
S o f t m a x Softmax Softmax函数,或称归一化指数函数,该函数的形式通常按下面的式子给出:
σ ( z ) j = e z j ∑ k = 1 K e z k , j ∈ [ 1 , K ] \sigma(z)_{j} = \frac{e^{z_{j}}}{\sum_{k = 1}^{K}e^{z_{k}}},j \in [1,K] σ(z)j=∑k=1Kezkezj,j∈[1,K]
在这个过程中尽可能使得各样本在正确类别上的输出概率为1,这要使得对应的 z j z_{j} zj值为 + ∞ +\infty +∞,这拉大了其与其他类别间的距离。
例如:
对于表情分类,假设
h
a
p
p
y
happy
happy 的
o
n
e
−
h
o
t
one-hot
one−hot 编码是
[
1
,
0
,
0
,
0
,
0
]
[1,0,0,0,0]
[1,0,0,0,0] ,若要预测的结果完全正确,则预测
z
j
z_{j}
zj的值应该为
[
+
∞
,
−
∞
,
−
∞
,
−
∞
,
−
∞
]
[+\infty , -\infty , -\infty , -\infty , -\infty]
[+∞,−∞,−∞,−∞,−∞] 。这拉大了不同类别之间的距离,并且有时候类与类之间的并不是毫无关联,如果鼓励输出的概率间相差过大,会导致一定程度上的过拟合。
因此Label Smoothing的想法是让目标不再是one-hot
编码,而是变为如下形式:
q i = { 1 − ε , if i = y ε / ( K − 1 ) , o t h e r w i s e q_{i} = \begin{cases} 1 - \varepsilon, & \text{if } i = y \\ \varepsilon/(K - 1), & otherwise \end{cases} qi={1−ε,ε/(K−1),if i=yotherwise
其中 ε \varepsilon ε为一个较小的常数, K K K 是类别数,这使得softmax损失中的概率优目标不再为1和0,同时z值的最优解也不再是正无穷大,而是一个具体的数值。
σ ( z ) j = e z j ∑ k = 1 K e z k → ( 1 − ε ) \sigma(z)_{j} = \frac{e^{z_{j}}}{\sum_{k = 1}^{K}e^{z_{k}}} \rightarrow (1 - \varepsilon) σ(z)j=∑k=1Kezkezj→(1−ε)
假设
ε
=
0.1
\varepsilon = 0.1
ε=0.1 ,
h
a
p
p
y
happy
happy 的编码就是
[
0.9
,
0.025
,
0.025
,
0.025
,
0.025
]
[0.9,0.025,0.025,0.025,0.025]
[0.9,0.025,0.025,0.025,0.025] ,预测
z
j
z_{j}
zj 的值
[
3.06679091
−
0.51669774
−
0.51669774
−
0.51669774
−
0.51669774
]
[ 3.06679091 -0.51669774 -0.51669774 -0.51669774 -0.51669774]
[3.06679091−0.51669774−0.51669774−0.51669774−0.51669774] , 较少了不同类别之间的距离,这在一定程度上避免了过拟合,也缓解了错误标签带来的影响。
timm 库中的实现
import torch
import torch.nn as nn
import torch.nn.functional as F
class LabelSmoothingCrossEntropy(nn.Module):
"""
NLL loss with label smoothing.
"""
def __init__(self, smoothing=0.1):
super(LabelSmoothingCrossEntropy, self).__init__()
assert smoothing < 1.0
self.smoothing = smoothing
self.confidence = 1. - smoothing
def forward(self, x: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
logprobs = F.log_softmax(x, dim=-1)
nll_loss = -logprobs.gather(dim=-1, index=target.unsqueeze(1))
nll_loss = nll_loss.squeeze(1)
smooth_loss = -logprobs.mean(dim=-1)
loss = self.confidence * nll_loss + self.smoothing * smooth_loss
return loss.mean()
if __name__ == '__main__':
# Example of using LabelSmoothingCrossEntropy
criterion = LabelSmoothingCrossEntropy(smoothing=0.1)
input = torch.Tensor([[-2.2280, -1.2998, -1.7275, -1.2504, -1.8609],
[-1.9066, -2.8856, -1.1829, -1.3866, -1.4301],
[-1.0275, -1.8822, -2.0455, -1.1622, -3.0423]])
target = torch.Tensor([1, 4, 3]).long()
loss = criterion(input, target)
loss.backward()
print(loss.item())
解释:
def forward(self, x: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
"""
logprobs
tensor([[-2.2280, -1.2998, -1.7275, -1.2504, -1.8609],
[-1.9066, -2.8856, -1.1829, -1.3866, -1.4301],
[-1.0275, -1.8822, -2.0455, -1.1622, -3.0423]])
"""
logprobs = F.log_softmax(x, dim=-1)
"""
index
tensor([[1],
[4],
[3]])
"""
index=target.unsqueeze(1)
"""
-logprobs.gather(dim=-1, index = index)
tensor([[1.2998],
[1.4301],
[1.1622]])
"""
nll_loss = -logprobs.gather(dim=-1, index = index)
"""
nll_loss
tensor([1.2998,1.4301,1.1622])
"""
nll_loss = nll_loss.squeeze(1)
"""
smooth_loss
tensor([1.6733, 1.7584, 1.8319])
"""
smooth_loss = -logprobs.mean(dim=-1)
loss = self.confidence * nll_loss + self.smoothing * smooth_loss
return loss.mean()
其中:
logprobs = F.log_softmax(x, dim=-1)
nll_loss = -logprobs.gather(dim=-1, index=target.unsqueeze(1))
nll_loss = nll_loss.squeeze(1)
这三行计算原始的交叉熵损失。
smooth_loss = -logprobs.mean(dim=-1)
计算预测的值进行 l o g _ s o f t m a x log\_softmax log_softmax 之后的平均值。
loss = self.confidence * nll_loss + self.smoothing * smooth_loss
计算LabelSmoothing
之后的损失。(这里想不明白看接下来的推导)
推导
J l s _ l o s s = ( 1 − ε ) ∗ ( ∑ k = 1 K y k ∗ − l o g ( h k ) ) ) + ε ∗ ( ∑ k = 1 K − l o g ( h ( x k ) ) K ) = ( 1 − ε ) ∗ ( ∑ k = 1 K y k ∗ − l o g ( h k ) ) ) + ε K ∗ ( ∑ k = 1 K − l o g ( h ( x k ) ) ) = ( 1 − ε ) ∗ ( − l o g ( h k ) ) ) , k = i + ε K ∗ ( ∑ k = 1 K − l o g ( h ( x k ) ) ) = ( 1 − ε + ε K ) ∗ ( − l o g ( h k ) ) ) , k = i + ε K ∗ ( ∑ k = 1 K − l o g ( h ( x k ) ) ) , k ≠ i \begin{align} J_{ls\_loss} & = (1 - \varepsilon)*( \sum_{k = 1}^{K}y_{k}*-log(h_{k}))) + \varepsilon * (\sum_{k =1}^{K}\frac{-log(h(x_{k}))}{K}) \\ & = (1 - \varepsilon)*( \sum_{k = 1}^{K}y_{k}*-log(h_{k}))) + \frac{\varepsilon}{K} * (\sum_{k =1}^{K}-log(h(x_{k}))) \\ & = (1 - \varepsilon)*( -log(h_{k}))) , k = i \\ & + \frac{\varepsilon}{K} * (\sum_{k =1}^{K}-log(h(x_{k})))\\ & = (1 - \varepsilon + \frac{\varepsilon}{K})*( -log(h_{k}))) , k = i \\ & + \frac{\varepsilon}{K} * (\sum_{k =1}^{K}-log(h(x_{k}))),k \neq i \\ \end{align} Jls_loss=(1−ε)∗(k=1∑Kyk∗−log(hk)))+ε∗(k=1∑KK−log(h(xk)))=(1−ε)∗(k=1∑Kyk∗−log(hk)))+Kε∗(k=1∑K−log(h(xk)))=(1−ε)∗(−log(hk))),k=i+Kε∗(k=1∑K−log(h(xk)))=(1−ε+Kε)∗(−log(hk))),k=i+Kε∗(k=1∑K−log(h(xk))),k=i
可以看出和原来的公式有所不同,这里
q i = { 1 − ε + ε K , if i = y ε / K , o t h e r w i s e q_{i} = \begin{cases} 1 - \varepsilon + \frac{\varepsilon}{K}, & \text{if } i = y \\ \varepsilon/K, & otherwise \end{cases} qi={1−ε+Kε,ε/K,if i=yotherwise
要使 L o s s Loss Loss 最小,就要使
σ ( z j ) = e z j ∑ k = 1 K e z k → ( 1 − ε + ε K ) , j = i σ ( z j ) = e z j ∑ k = 1 K e z k → ( ε K ) , j ≠ i \begin{align} & \sigma(z_{j}) = \frac{e^{z_{j}}}{\sum_{k = 1}^{K}e^{z_{k}}} \rightarrow (1 - \varepsilon + \frac{\varepsilon}{K} ) ,& j = i \\ & \sigma(z_{j}) = \frac{e^{z_{j}}}{\sum_{k = 1}^{K}e^{z_{k}}} \rightarrow (\frac{\varepsilon}{K} ) ,& j \neq i \\ \end{align} σ(zj)=∑k=1Kezkezj→(1−ε+Kε),σ(zj)=∑k=1Kezkezj→(Kε),j=ij=i
举例
假设有二分类问题, ε = 0.2 \varepsilon = 0.2 ε=0.2 ,happy 和 sad 。损失函数由上文可知如下:
J l s _ l o s s = − ( 0.1 ∗ l n ( 1 − x ) ) + 0.9 ∗ l n ( x ) J_{ls\_loss} = -(0.1*ln(1-x)) + 0.9*ln(x) Jls_loss=−(0.1∗ln(1−x))+0.9∗ln(x)
可以得到如下图像:
极值点的 x x x 坐标为 0.9。
Reference
Pytorch:交叉熵损失(CrossEntropyLoss)以及标签平滑(LabelSmoothing)的实现_labelsmoothingcrossentropy-CSDN博客 \
Softmax函数 - 维基百科,自由的百科全书 (wikipedia.org)
交叉熵损失函数(cross-entropy loss function)原理及Pytorch代码简介_损失函数 dkl-CSDN博客