Bootstrap

交叉熵和softmax函数和sigmoid函数(在二分类中的比较)

1.softmax

假设在进入softmax函数之前,已经有模型输出 C C C值,其中 C C C是要预测的类别数,模型可以是全连接网络的输出 a a a,其输出个数为 C C C,即输出为 a 1 , a 2 , . . . , a C a_1, a_2, ..., a_C a1,a2,...,aC
所以对每个样本,它属于类别 i i i的概率为:
y i = e a i ∑ k = 1 C e a k ∀ i ∈ 1 … C y_{i}=\frac{e^{a_{i}}}{\sum_{k=1}^{C} e^{a_{k}}} \quad \forall i \in 1 \ldots C yi=k=1Ceakeaii1C
通过上式可以保证 ∑ i = 1 C y i = 1 \sum_{i=1}^{C}y_i=1 i=1Cyi=1,即属于各个类别的概率和为1。上式即为softmax计算公式。

1.1 softmax的计算数值稳定性

python中softmax函数为:

import numpy as np
def softmax(x):
    exp = np.exp(x)
    return exp/np.sum(exp)

当数值较大时会出现如下结果

>>> softmax([1000, 2000, 3000, 4000, 5000])
array([ nan,  nan,  nan,  nan,  nan])

这是因为在求exp(x)时候溢出了,一种简单有效避免该问题的方法就是让exp(x)中的x值不要那么大或那么小,在softmax函数的分式上下分别乘以一个非零常数:
y i = e a i ∑ k = 1 C e a k = E e a i ∑ k = 1 C E e a k = e a i + log ⁡ ( E ) ∑ k = 1 C e a k + log ⁡ ( E ) = e a i + F ∑ k = 1 C e a k + F y_{i}=\frac{e^{a_{i}}}{\sum_{k=1}^{C} e^{a_{k}}}=\frac{E e^{a_{i}}}{\sum_{k=1}^{C} E e^{a_{k}}}=\frac{e^{a_{i}+\log (E)}}{\sum_{k=1}^{C} e^{a_{k}+\log (E)}}=\frac{e^{a_{i}+F}}{\sum_{k=1}^{C} e^{a_{k}+F}} yi=k=1Ceakeai=k=1CEeakEeai=k=1Ceak+log(E)eai+log(E)=k=1Ceak+Feai+F

这里 l o g ( E ) log(E) log(E) 是个常数,所以可以令它等于 F F F。加上常数之后,等式与原来还是相等的,所以我们可以考虑怎么选取常数 F F F。我们的想法是让所有的输入在0附近,这样的值不会太大,所以可以让 F F F的值为:
F = m a x ( a 1 , a 2 , . . . , a C ) F=max(a_1, a_2,...,a_C) F=max(a1,a2,...,aC)
这样子将所有的输入平移到0附近(当然需要假设所有输入之间的数值上较为接近),同时,除了最大值,其他输入值都被平移成负数, e e e为底的指数函数,越小越接近0,这种方式比得到nan的结果更好。

def softmax(x):
    shift_x = x - np.max(x)
    exp_x = np.exp(shift_x)
    return exp_x / np.sum(exp_x)

>>> softmax([1000, 2000, 3000, 4000, 5000])
array([ 0.,  0.,  0.,  0.,  1.])

当然这种做法也不是最完美的,因为softmax函数不可能产生0值,但这总比出现nan的结果好,并且真实的结果也是非常接近0的。

2. sigmoid和softmax比较

2.1 sigmoid函数

g ( x ) = 1 1 + e − x g(x)=\frac{1}{1+e^{-x}} g(x)=1+ex1

sigmoid函数输出值的范围和softmax函数一样都是位于[0, 1]

2.2 sigmoid 函数和 softmax 函数比较

对于二分类,同一个样本的输出,sigmoid输出如式(1)所示,softmax输出如式(2)所示
 output  ( x 1 ) = 1 1 + e − x 1         ( 1 )  output  ( x 1 ) = e x 1 e x 1 + e x 2 = 1 1 + e − ( x 1 − x 2 )         ( 2 ) \begin{aligned} &\text { output }\left(x_{1}\right)=\frac{1}{1+e^{-x_{1}}} \,\,\,\,\,\,\,(1) \\ &\text { output }\left(x_{1}\right)=\frac{e^{x_{1}}}{e^{x_{1}}+e^{x_{2}}}=\frac{1}{1+e^{-\left(x_{1}-x_{2}\right)}} \,\,\,\,\,\,\,(2) \end{aligned}  output (x1)=1+ex11(1) output (x1)=ex1+ex2ex1=1+e(x1x2)1(2)
式(2)中 x 1 − x 2 x_1-x_2 x1x2 可以替换成 z 1 z_1 z1 这样的话,式(1)和式(2)完全相同,所以理论上来说两者是没有任何区别的

两者区别
对于二值分类问题,softmax输出两个值,这两个值和为1;对于sigmoid来说,输出也是两个值,不过没有可加性,两个值各自是0到1的某个数;

例如:
我们预测文本分为A类和B类,神经网络输出为[x, y],在经过softmax后输出可能为[0.3, 0.7],代表算法认为是A类的概率为0.3, 是B类的概率0.7,相加为1;对于sigmoid输出可能是(0.4, 0.8),他们相加不为1,解释来说sigmoid认为输出第一位为A的概率是0.4, 不为A(为B)的概率0.6, 第二位为A的概率0.8,不为A的概率0.2; 也就是说sigmoid函数的结果是分到正确类别的概率P和未分到正确类别的概率(1-P)

2.3 分类中的使用

二分类
当用Sigmoid函数的时候,最后一层全连接层的神经元个数为1,而用Softmax函数的时候,最后一层全连接层的神经元个数是2。Softmax是对两个类别(正反两类,通常定义为0/1的label)建模,所以对于NLP模型而言(比如泛BERT模型),Bert输出层需要通过一个nn.Linear()全连接层压缩至2维,然后接Softmax(或者直接接上torch.nn.CrossEntropyLoss);而Sigmoid只对一个类别建模(通常就是正确的那个类别),所以Bert输出层需要通过一个nn.Linear()全连接层压缩至1维,然后接Sigmoid(或者直接使用torch.nn.BCEWithLogitsLoss)

因此对于二分类而言,使用sigmoid函数和使用softmax函数都行,关键在于最后全链接层神经元个数,用sigmoid函数必须为1,用softmax函数必须为2

多分类
Bert输出层需要通过一个nn.Linear()全连接层压缩至m维(m个类别),然后接Softmax(或者直接接上torch.nn.CrossEntropyLoss)

多标签
多标签其实就是多个二分类,计算当前样本属于每个类别的概率,Bert输出层需要通过一个nn.Linear()全连接层压缩至m维(总共有m个标签),然后用sigmoid计算属于每个标签的概率,最后一班取概率大于0.5的标签

分类问题名称输出层使用激活函数对应损失函数
二分类sigmoid函数二分类交叉熵损失函数
多分类softmax函数多类别交叉熵损失函数
多标签分类sigmoid函数二分类交叉熵损失函数

错误示例
将Softmax和Sigmoid混为一谈,在把BERT输出层压缩至2维的情况下,却用Sigmoid对结果进行计算。这样我们得到的结果其意义是什么呢?假设我们现在BERT输出层经nn.Linear()压缩后,得到一个二维的向量:

[-0.9419267177581787, 1.944047451019287]

对应类别分别是(0,1)。我们经过Sigmoid运算得到:tensor([0.2805, 0.8748]) 前者0.2805指的是分类类别为0的概率,0.8748指的是分类类别为1的概率。二者相互独立,可看作两次独立的实验(显然在这里不适用,因为0-1类别之间显然不是相互独立的两次伯努利事件)。所以显而易见的,二者加和并不等于1。

若用softmax进行计算,可得:tensor([0.0529, 0.9471]) 这里两者加和是1,才是正确的选择。

3. 交叉熵损失函数

3.1 多类别交叉熵损失函数

交叉熵的推导,分类问题为什么不用均方误差
1.将标签进行one_hot编码
2.神经网络输出后接softmax层,即预测值取softmax输出来进行交叉熵计算
3.使用交叉熵公式计算:
H ( y ( i ) , y ^ ( i ) ) = − ∑ j = 1 q y j ( i ) log ⁡ y ^ j ( i ) H\left( \boldsymbol{y}^{\left( i \right)},\boldsymbol{\hat{y}}^{\left( i \right)} \right) =-\sum_{j=1}^q{y_{j}^{\left( i \right)}}\log \hat{y}_{j}^{\left( i \right)} H(y(i),y^(i))=j=1qyj(i)logy^j(i)
上面是一个样本的交叉熵计算;batch_size样本交叉熵计算如下:
ℓ ( Θ ) = 1 n ∑ i = 1 n H ( y ( i ) , y ^ ( i ) ) \ell \left( \Theta \right) =\frac{1}{n}\sum_{i=1}^n{H}\left( \boldsymbol{y}^{\left( i \right)},\boldsymbol{\hat{y}}^{\left( i \right)} \right) (Θ)=n1i=1nH(y(i),y^(i))
即将每个样本计算交叉熵,然后求均值

实例
假设有3个类别,对某一个样本来计算交叉熵:
设真实类别为3----->[0,0,1]
神经网络初始输出为[3,6,9];将其经过softmax层后---->[0.003, 0.047, 0.95]
那么交叉熵损失计算为:

-(0*lg0.003+0*lg0.047+1*lg0.95)= 0.02

如果是计算一个batch_size个样本的损失,则将每个样本计算交叉熵损失,然后求均值

3.2 二分类交叉熵损失函数

l o s s ( y , y ^ ) = − 1 n ∑ i n ( y i log ⁡ ( y i ^ ) + ( 1 − y i ) l o g ( 1 − y i ^ ) ) loss(y, \hat{y})=-\frac{1}{n}\sum_{i}^{n}(y_i\log(\hat{y_i})+(1-y_i)log(1-\hat{y_i})) loss(y,y^)=n1in(yilog(yi^)+(1yi)log(1yi^))

注意: 上面是对一个样本的计算, i i i是一个样本中的分值
对所有m个样本:
l o s s = 1 m ∑ l o s s ( y , y ^ ) loss = \frac{1}{m}\sum loss(y, \hat{y}) loss=m1loss(y,y^)

步骤
1.将标签进行one_hot编码
2.神经网络输出后接sigmoid层,即预测值取sigmoid输出来进行交叉熵计算
3.使用交叉熵公式计算

例子
神经网络输出数据:3个样本

       [[ 1.9072,  1.1079,  1.4906],
        [-0.6584, -0.0512,  0.7608],
        [-0.0614,  0.6583,  0.1095]]

经过sigmoid函数后:

       [[0.8707, 0.7517, 0.8162],
        [0.3411, 0.4872, 0.6815],
        [0.4847, 0.6589, 0.5273]]

设真实标签为:

				[[0, 1, 1], 
				 [1, 1, 1], 
				 [0, 0, 0]]

使用上面公式计算:
(1)对样本1:

r11 = 0 * math.log(0.8707) + (1-0) * math.log((1 - 0.8707))
r12 = 1 * math.log(0.7517) + (1-1) * math.log((1 - 0.7517))
r13 = 1 * math.log(0.8162) + (1-1) * math.log((1 - 0.8162))
r1 = -(r11 + r12 + r13) / 3
#0.8447112733378236

(2)对样本2:

r21 = 1 * math.log(0.3411) + (1-1) * math.log((1 - 0.3411))
r22 = 1 * math.log(0.4872) + (1-1) * math.log((1 - 0.4872))
r23 = 1 * math.log(0.6815) + (1-1) * math.log((1 - 0.6815))
r2 = -(r21 + r22 + r23) / 3
#0.7260397266631787

(3)对样本3:

r31 = 0 * math.log(0.4847) + (1-0) * math.log((1 - 0.4847))
r32 = 0 * math.log(0.6589) + (1-0) * math.log((1 - 0.6589))
r33 = 0 * math.log(0.5273) + (1-0) * math.log((1 - 0.5273))
r3 = -(r31 + r32 + r33) / 3
#0.8292933181294807

总共损失

bceloss = (r1 + r2 + r3) / 3 
0.8000147727101611

对应pytorch中的内置函数

loss = nn.BCELoss()
print(loss(nn.sigmoid(input), target))
tensor(0.8000, grad_fn=<BinaryCrossEntropyBackward>)

可以把sigmoid和bce的过程放到一起,使用内建的BCEWithLogitsLoss函数

loss = nn.BCEWithLogitsLoss()
print(loss(input, target))
tensor(0.8000, grad_fn=<BinaryCrossEntropyWithLogitsBackward>)

4. torch中交叉熵损失函数的使用

4.1 nn.CrossEntropyLosss()(多分类)

输入x: 不需要经过softmax,函数内部有softmax
输入target: 不需要进行one-hot,只需输入标签索引即可

import torch
import numpy as np
loss = torch.nn.CrossEntropyLoss()
# x表示输出分数,不需要经过softmax
x = torch.FloatTensor([[1,2,3]])
print("x:", x)
# y是真实的标签,不需要表示成onehot
y = torch.LongTensor([2])
# 手动计算loss
x_softmax = torch.softmax(x, dim=1)
print("x_softmax:", x_softmax)
print("loss by hand:", -np.log(0.6652))

l = loss(x, y)
print("real loss:", l)

输出

x: tensor([[1., 2., 3.]])
x_softmax: tensor([[0.0900, 0.2447, 0.6652]])
loss by hand: 0.40766753166336445
real loss: tensor(0.4076)

注意:输出是一个batch中所有loss均值。

  • CrossEntropyLoss() = softmax + log + NLLLoss() = log_softmax + NLLLoss() NLLLoss是负对数似然函数损失
  • nn.CrossEntropyLossF.cross_entropy 计算结果是等价的。两个函数都结合了 LogSoftmax and NLLLoss 运算;两者区别见 PyTorch 中,nn 与 nn.functional 有什么区别?

4.2 nn.BCEWithLogitsLoss(pred, target)(二分类/多标签)

  • 输入pred: 不需要经过sigmoid,即直接是全链接的输出
  • target: 不需要经过onehot, 直接是正确的类别
  • pred.shape == target.shape
import torch
import torch.nn as nn

model = nn.Linear(10, 1)
criterion = nn.BCEWithLogitsLoss()

x = torch.randn(16, 10)
y = torch.empty(16).random_(2)  # (16, )

out = model(x)  # (16, 1)
out = out.squeeze(dim=-1)  # (16, )

loss = criterion(out, y)
  • nn.BCEWithLogitsLoss(pred, target) = sigmoid+torch.nn.BCELoss()
  • nn.BCELoss()F.binary_cross_entropy 计算结果是等价的

5. 参考

[1]. Softmax函数与交叉熵
[2]. Pytorch分类问题中的交叉熵损失函数使用
[3]. pytorch中常用的损失函数用法说明
[4]. PyTorch学习笔记——二分类交叉熵损失函数

;