PyTorch中NLLLoss|CrossEntropy|BCELoss记录

文章目录
  1. NLLLoss
  2. CrossEntropy Loss
  3. BCELoss

在分类任务中,有几个常用的损失函数,包括NLLLoss, CrossEntropy以及BCELosss,内容比较基础, 这里以pytorch的函数为例,回顾下细节和使用方法作为记录。

NLLLoss

NLLLoss其实就是负对数似然损失(Negative Log Likelihood Loss),直接将最大化似然取负数,就是最小化损失了。取对数是将为了方便累加,一般用在多分类问题上,定义如下: \[ nll(x, y)= -logx[y] \] 其中, x为输入的向量,维度为类别数目,可以理解为每一个类别的score,y为真实类别的标量。

举例,如果 \(x=[0.1, 0.3, 0.5], y=1\) ,含义就是一共有3个类别,每一类的预测概率或者分数为0.1,0.3,0.5(这里未归一化), 真实标签为1, 那么nll(x,y)=-log0.3.

不过在pytorch的实现中,并没有log,只有一个负号,如下:

1
2
3
4
5
6
7
import torch
import torch.nn.functional as F
# 两条数据,一共三类;每一类的score
x = torch.tensor([[0.1, 0.3, 0.5], [0.2,0.4,0.6]])
y = torch.tensor([2, 1])
print(F.nll_loss(x, y, reduction='none'))
# tensor([-0.5, -0.4])

所以要想真正的使用NLLLoss的用在分类的话, 需要提前对输入取softmax和log,这样才是负对数似然损失。(但这其实就是cross_entropy了)

CrossEntropy Loss

CrossEntropyLoss交叉熵损失函数应该是在分类任务中出现频次最多的损失函数了,其实就是上述NLLLoss的完整版,可以直接用在分类任务中。

即:对于输入x向量,首先进行softmax操作,得到归一化的每一类的概率,之后进行log操作,最后执行NLLLoss,也就是说,

CE就是 log_softmax 与 nll_loss的结合得到的损失函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
import torch.nn.functional as F
# 两条数据,一共三类;每一类的score
x = torch.tensor([[0.4, 0.3, 0.7], [1.2,0.4,0.6]])
# 两条数据的真实label。
y = torch.tensor([2, 1])
print(F.cross_entropy(x, y, reduction='none'))
# tensor([0.8801, 1.4922])
log_softmax_x = F.log_softmax(x, dim=1)
# tensor([[-1.1801, -1.2801, -0.8801],
# [-0.6922, -1.4922, -1.2922]])
print(F.nll_loss(log_softmax_x, y, reduction='none'))
# tensor([0.8801, 1.4922])

所以分类直接用 CrossEntropy Loss, 二分类多分类都可以

这里多说一句,交叉熵的具体计算如下: \[ CE = -\sum_xp(x)\log q(x) \] 其中,p(x)为标签的真实分布,一般为one-hot向量,q(x)为模型预测预测的标签分布,因此这里可以理解为已经经过了softmax操作。

既然p的值只有真实标签的index的地方为1,其余都是0,因此0的地方就不用管了,只需要考虑真实标签的位置的值就行,也就是: \[ CE = - log(q_*) \] 其中,\(q_*\) 就是真实类别处的值,这就是上面的NLLLoss的形式(x[y])。所以CE就是logsoftmax + nllloss即可。

BCELoss

BCELoss可以理解为二分类的CE,就 Binary Cross Entropy Loss, 也就是上面的Cross Entropy中的标签只有两个值0或者1,正负样本。

这样展开求和其实就是: \[ BCE = -[ylogx + (1-y)log(1-x)] \] 所以在使用BCELoss之前,一般将x的值都计算到0-1之间(一般使用sigmoid),即当前数据的标签为正样本的概率,另一类负样本的概率就是1-x了。这个其实就是逻辑回归的损失函数了。

举个例子:

1
2
3
4
5
6
7
8
import torch
import torch.nn.functional as F
# 两条数据点,正样本的概率分别为0.3 和 0.9
x = torch.tensor([[0.3], [0.9]])
# 两条数据的真实标签,第一个数据为正样本,第二个为负样本
y = torch.tensor([[1.], [0.]])
print(F.binary_cross_entropy(x,y, reduction='none'))
# tensor([[1.2040], [2.3026]])

以第一条为例,手动计算就是:\(1*log0.3 + 0*log0.7=-1.2040\) 也就是损失函数。

另外torch中另一个相关的 损失函数是BCEWithLogitsLoss ,这个其实就是sigmoid+BCELoss 将sigmoid操作加进去了。

既然已经有了cross entropy, 为什么还要专门多一个binary的呢,我个人在多标签分类中用BCELoss比较多一些(Multi-label) 。

在多标签分类中,每一类其实都是一个二分类任务(即是否将其分为该类别),这种情况下,用BCELoss是最合适的。

举个例子:文本话题分类中,一句话可能有多个topic :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch
import torch.nn.functional as F

# 两条数据,5个标签的score
x = torch.randn(2, 5)
# tensor([[ 0.9992, -0.0551, -0.7823, 2.4850, -0.5739],
# [-1.1502, -1.8042, -0.6534, 0.5632, -0.9339]])

x = torch.sigmoid(x) # 得到每一类标签的概率值
# tensor([[0.7309, 0.4862, 0.3138, 0.9231, 0.3603],
# [0.2404, 0.1413, 0.3422, 0.6372, 0.2821]])

y = torch.tensor([[0, 1, 1, 0, 0], [1, 1, 0, 0, 1]],
dtype=torch.float)

print(F.binary_cross_entropy(x, y, reduction='none'))
#tensor([[1.3127, 0.7211, 1.1589, 2.5650, 0.4468],
# [1.4253, 1.9566, 0.4189, 1.0139, 1.2654]])
# 每条数据中,每个二分类的损失
# 如果要得到平均损失,直接reduction='mean'即可

另外在计算损失函数的时候,有时候会有mask的需求,比如在文本生成中,句子长度不同,需要做padding,因此padding部分的loss是无效的,不能参加运算,因此,需要把mask部分去掉,一般使用 torch的masked_select即可,举例如下:

1
2
3
4
5
6
7
8
9
10
loss = torch.randn(2,3)
loss.required_grad_()
#tensor([[-0.1425, -0.9712, -0.5876, -0.6469, 0.1424],
# [ 1.0024, 0.6287, 1.5695, -1.0321, 0.1236]], requires_grad=True)
mask = torch.tensor( [[1, 1, 1, 1, 0], [1, 1, 1, 0, 0]],
dtype=torch.bool)
loss.selected_mask(mask)
# tensor([-0.1425, -0.9712, -0.5876, -0.6469, 1.0024, 0.6287, 1.5695],
# grad_fn=<MaskedSelectBackward>)
# 这样就将mask为true的选取出来了,后续可以求和或者平均

总结一下的话,这三个分类loss之间关系很大,本质都是从似然函数得出的。其中NLLLoss是最基础,当然用的也较少。不如直接使用Cross Entropy,再到多标签分类的BinaryCE等。