Pytorch常用损失函数拆解
作者 | 小新
来源 | https:// lhyxx.top
本文从理论和实践两方面来全面梳理一下常用的损失函数。(避免自己总是一瓶子不满半瓶子晃荡……)。要么理论满分,编码时不会用;要么编码是会调包,但是不明白其中的计算原理。本文来科普一下。
我们将每个损失函数分别从理论和pytorch中的实现两个方面来拆解一下。
另外,解释一下torch.nn.Module 和 torch.nn.functional(俗称F)中损失函数的区别。
Module的损失函数例如CrossEntropyLoss、NLLLoss等是封装之后的损失函数类,是一个类,因此其中的变量可以自动维护。经常是对F中的函数的封装。而F中的损失函数只是单纯的函数。
当然我们也可以自己构造自己的损失函数对象。有时候损失函数并不需要太复杂,没有必要特意封装一个类,直接调用F中的函数也是可以的。使用哪种看具体实现需求而定。
CrossEntropyLoss
交叉熵损失,是分类任务中最常用的一个损失函数。
理论
直接上理论公式:
\operatorname{Loss}(\hat{x}, x)=-\sum_{i=1}^{n} x \log (\hat{x}) \\
其中 x 是真实标签, \hat{x} 是预测的类分布(通常是使用softmax将模 型输出转换为概率分布), 也就是 x 与 \hat{x} 中的元素分别表示对应类 别的概率。
举个例子,清晰明了:
x=[0,1,0] # 假设该样本属于第二类 \hat{x}=[0.1,0.5,0.4] # 因为是分布, 所以属于各个类的和为 1 \operatorname{Loss}(\hat{x}, x)=-0 \times \log (0.1)-1 \times \log (0.5)-0 \times \log (0.4)=\log (0.5)
pytorch-实现
from torch.nn import CrossEntropyLoss
举例:
实际使用中需要注意几点:
-
torch.nn.CrossEntropyLoss(input, target)
中的标签target
使用的不是one-hot形式,而是类别的序号。形如target = [1, 3, 2]
表示3个样本分别属于第1类、第3类、第2类。
-
torch.nn.CrossEntropyLoss(input, target)
的input
是 没有归一化的每个类的得分 ,而不是softmax之后的分布。
举例,输入的形式大概就像相面这种格式:
\begin{aligned} &\text { target }=[1,3,2] \\ &\text { input }=[[0.13,-0.18,0.87],[0.25,-0.04,0.32],[0.24,-0.54,0.53]] \end{aligned} \\
然后就将他们扔到CrossEntropyLoss函数中,就可以得到损失。
loss = CrossEntropyLoss(input, target)
我们看CrossEntropyLoss函数里面的实现,是下面这样子的:
def forward(self, input, target):
return F.cross_entropy(input, target, weight=self.weight,
ignore_index=self.ignore_index, reduction=self.reduction)
是调用的torch.nn.functional(俗称F)中的cross_entropy()函数。
参数
-
input:预测值,(batch,dim),这里dim就是要分类的
总类别数
-
target:真实值,(batch),这里为啥是1维的?
因为真实值并不是用one-hot形式表示,而是直接传类别id。
-
weight:指定权重,(dim),可选参数,可以给每个类指定一个权重。通常在训练数据中不同类别的样本数量差别较大时,可以使用权重来平衡。
-
ignore_index:指定忽略一个真实值,(int),也就是手动忽略一个真实值。
-
reduction:在[none, mean, sum]中选,string型。none表示不降维,返回和target相同形状;mean表示对一个batch的损失求均值;sum表示对一个batch的损失求和。
其中参数weight、ignore_index、reduction要在实例化CrossEntropyLoss对象时指定,例如:
loss = torch.nn.CrossEntropyLoss(reduction='none')
我们再看一下 F中的cross_entropy的实现 :
return nll_loss(log_softmax(input, dim=1), target, weight, None, ignore_index, None, reduction)
可以看到就是先调用
log_softmax
,再调用
nll_loss
。
log_softmax
就是先
softmax
再取
log
:
\log _{s} o f t \max (x)=\log (\operatorname{softmax}(x))
nll_loss
是negative log likelihood loss:
详细介绍见下面
torch.nn.NLLLoss
,计算公式如下:
\operatorname{nll}_{l} \operatorname{oss}(\hat{x}, \text { class })=-\hat{x}[\text { class }] \\
例如假设 \hat{x}=[1,2,3] , class =2 ,则 n\operatorname{ll}_{l}\operatorname{oss}(\hat{x} ,class )=-\hat{x}[ class ]=-\hat{x}[2]=-2
源码中给了个用法例子:
# input is of size N x C = 3 x 5
input = torch.randn(3, 5, requires_grad=True)
# each element in target has to have 0 <= value < C
target = torch.tensor([1, 0, 4])
output = F.nll_loss(F.log_softmax(input), target)
output.backward()
因此,其实CrossEntropyLoss损失,就是 softmax + log + nll_loss 的集成。
CrossEntropyLoss(input, target) = nll_loss(log_softmax(input, dim=1), target)
CrossEntropyLoss中的target必须是LongTensor类型。
实验如下:
pred = torch.FloatTensor([[2, 1], [1, 2]])
target = torch.LongTensor([1, 0])
loss_fun = nn.CrossEntropyLoss()
loss = loss_fun(pred, target)
print(loss) # 输出为tensor(1.3133)
loss2 = F.nll_loss(F.log_softmax(pred, dim=1), target)
print(loss2) # 输出为tensor(1.3133)
数学形式就是:
\operatorname{Loss}(\text { input, target })=-\log \frac{\exp (\text { input }[\text { target }])}{\Sigma_{j} \exp (\text { input }[j])} \\
torch-nn-BCELoss
理论
CrossEntropy损失函数适用于总共有N个类别的分类。 当N=2时,即二分类任务,只需要判断是还是否的情况,就可以使用二分类交叉熵损失:BCELoss 二分类交叉熵损失。上公式 (y是真实标签,x是预测值) :
\operatorname{loss}(x, y)=-\frac{1}{n} \Sigma_{i}\left(y_{i} \times \log \left(x_{i}\right)+\left(1-y_{i}\right) \times \log \left(1-x_{i}\right)\right) \\
其实 这个函数就是CrossEntropyLoss的当类别数N=2时候的特例 。因为类别数为2,属于第一类的概率为y,那么属于第二类的概率自然就是(1-y)。因此套用与CrossEntropy损失的计算方法,用对应的标签乘以对应的预测值再求和,就得到了最终的损失。
实践
torch.nn.BCELoss(x, y)
x形状(batch,*),y形状与x相同。
x与y中每个元素,表示的是该维度上属于(或不属于)这个类的概率。
另外,pytorch中的BCELoss可以为每个类指定权重。通常,当训练数据中正例和反例的比例差别较大时,可以为其赋予不同的权重,weight的形状应该是一个一维的,元素的个数等于类别数。
实际使用如下例,计算BCELoss(pred, target):
pred = torch.FloatTensor([0.4, 0.1]) # 可以理解为第一个元素分类为是的概率为0.4,第二个元素分类为是的概率为0.1。
target = torch.FloatTensor([0.2, 0.8]) # 实际上第一个元素分类为是的概率为0.2,第二个元素分类为是的概率为0.8。
loss_fun = nn.BCELoss(reduction='mean') # reduction可选 none, sum, mean, batchmean
loss = loss_fun(pred, target)
print(loss) # tensor(1.2275)
a = -(0.2 * np.log(0.4) + 0.8 * np.log(0.6) + 0.8 * np.log(0.1) + 0.2 * np.log(0.9))/2
print(a) # 1.2275294114572126
可以看到,计算BCELoss(pred,target)与上面理论中的公式一样。
内部实现
pytorch 中的
torch.nn.BCELoss
类,实际上就是调用了
F.binary_cross_entropy(input, target, weight=self.weight, reduction=self.reduction)
torch.nn.BCEWithLogitsLoss
理论
该函数实际上与BCELoss相同,只是BCELoss的输入x,在输入之前需要先手动经过sigmoid激活函数映射到(0, 1)区间,而 该函数将sigmoid与BCELoss整合到一起了 。
也就是先将输入经过sigmoid函数,然后计算BCE损失。
实践
torch.nn.BCEWithLogitsLoss(x, y)
x与y的形状要求与BCELoss相同。
pred = torch.FloatTensor([0.4, 0.1])
target = torch.FloatTensor([0.2, 0.8])
loss_fun = nn.BCEWithLogitsLoss(reduction='mean') # reduction可选 none, sum, mean, batchmean
loss = loss_fun(pred, target)
print(loss) # tensor(0.7487)
# 上面的过程与下面的过程结果相同
loss_fun = nn.BCELoss(reduction='mean') # reduction可选 none, sum, mean, batchmean
loss = loss_fun(torch.sigmoid(pred), target) # 先经过sigmoid,然后与target计算BCELoss
print(loss) # tensor(0.7487)
可以看出,先对输入pred调用sigmoid,在调用BCELoss,结果就等于直接调用BCEWithLogitsLoss。
torch.nn.L1Loss
理论
L1损失很简单,公式如下:
\operatorname{loss}(x, y)=\frac{1}{n} \Sigma_{i}\left|x_{i}-y_{i}\right| \\
x是预测值,y是真实值。
实践
torch.nn.L1Loss(x, y)
x形状:任意形状
y形状:与输入形状相同
pred = torch.FloatTensor([[3, 1], [1, 0]])
target = torch.FloatTensor([[1, 0], [1, 0]])
loss_fun = nn.L1Loss()
loss = loss_fun(pred, target)
print(loss) # tensor(0.7500)
其中L1Loss的内部实现为:
def forward(self, input, target):
return F.l1_loss(input, target, reduction=self.reduction)
我们可以看到,其实还是对F.l1_loss的封装。
torch.nn.MSELoss
理论
L1Loss可以理解为向量的1-范数,MSE均方误差就可以理解为向量的2-范数,或矩阵的F-范数。
\operatorname{loss}(x, y)=\frac{1}{n} \Sigma\left(x_{i}-y_{i}\right)^{2} \\
x是预测值,y是真实值。
实践
torch.nn.MSELoss(x, y)
x任意形状,y与x形状相同。
pred = torch.FloatTensor([[3, 1], [1, 0]])
target = torch.FloatTensor([[1, 0], [1, 0]])
loss_fun = nn.MSELoss()
loss = loss_fun(pred, target)
print(loss) # tensor(1.2500)
其中MSELoss内部实现为:
def forward(self, input, target):
return F.mse_loss(input, target, reduction=self.reduction)
本质上是对F中mse_loss函数的封装。
torch.nn.NLLLoss
理论
NLLLoss(Negative Log Likelihood Loss),其数学表达形式为:
\operatorname{loss}(x, y)=-\log (x[y]) \\
前面讲到CrossEntropyLoss中用的nll_loss,实际上,该损失函数就是对
F.nll_loss
的封装,功能也和
nll_loss
相同。
正如前面所说,先把输入x进行
softmax
,在进行
log
,再输入该函数中就是
CrossEntropyLoss
。
实践
torch.nn.NLLLoss(x, y)
x是预测值,形状为(batch,dim)
y是真实值,形状为(batch)
形状要求与CrossEntropyLoss相同。
pred = torch.FloatTensor([[3, 1], [2, 4]])
target = torch.LongTensor([0, 1]) #target必须是Long型
loss_fun = nn.NLLLoss()
loss = loss_fun(pred, target)
print(loss) # tensor(-3.5000)
其内部实现实际上就是调用了F.nll_loss():
def forward(self, input, target):
return F.nll_loss(input, target, weight=self.weight, ignore_index=self.ignore_index, reduction=self.reduction)
torch.nn.KLDivLoss
理论
KL散度通常用来衡量两个连续分布之间的距离。两个分布越相似,KL散度越接近0。
KL散度又叫相对熵,具体理论可以参考: https:// lhyxx.top/2019/09/15/%E 4%BF%A1%E6%81%AF%E8%AE%BA%E5%9F%BA%E7%A1%80-%E7%86%B5/
\operatorname{loss}(x, y)=\frac{1}{n} \Sigma_{i}\left(x_{i} \times \log \frac{x_{i}}{y_{i}}\right) \\
注意,这里 x 与 y 都是分布,分布就意味着其中所有元素求和概率为1。
\text { 例如, } x=[0.1,0.2,0.7], y=[0.5,0.2,0.3] \\
则:
\operatorname{loss}(x, y)=\frac{1}{3} \times\left(0.1 \times \log \frac{0.1}{0.5}+0.2 \times \log \frac{0.2}{0.2}+0.7 \times \log \frac{0.7}{0.3}\right)=0.43216 \\
本例中计算的 log 都是以e为底的。
实践
torch.nn.KLDivLoss(input, target)
试验测试
torch.nn.KLDivLoss
,计算
KL(pred|target)
:
pred = torch.FloatTensor([0.1, 0.2, 0.7])
target = torch.FloatTensor([0.5, 0.2, 0.3])
loss_fun = nn.KLDivLoss(reduction='sum') # reduction可选 none, sum, mean, batchmean
loss = loss_fun(target.log(), pred)