文章目录
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=1Ceakeai∀i∈1…C
通过上式可以保证
∑
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+e−x1
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+e−x11(1) output (x1)=ex1+ex2ex1=1+e−(x1−x2)1(2)
式(2)中
x
1
−
x
2
x_1-x_2
x1−x2 可以替换成
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=1∑qyj(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=1∑nH(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^)=−n1i∑n(yilog(yi^)+(1−yi)log(1−yi^))
注意: 上面是对一个样本的计算,
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=m1∑loss(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.CrossEntropyLoss
与F.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学习笔记——二分类交叉熵损失函数