本节开始将以分类神经网络为例,展示神经网络的学习和训练过程。在介绍PyTorch
的基本工具AutoGrad
库时,我们系统地介绍过数学中的优化问题和优化思想,我们介绍了最小二乘法以及梯度下降法这两个入门级优化算法的具体操作,并使用AutoGrad
库实现了他们。接下来,我们将从梯度下降法向外拓展,介绍神经网络的损失函数、常用优化算法等信息,实现神经网络的学习和迭代。本节主要讲解神经网络常用的损失函数,并在PyTorch
中实现这些函数。
37 机器学习中的优化思想
在之前的学习中,我们建立神经网络时总是先设定好 w w w和 b b b的值(或者由我们调用的
Pytorch
类帮助我们随机生成权重向量 w w w),接着通过加和求出 z z z,再在 z z z上嵌套sigmoid
或者softmax
函数,最终获得神经网络的输出。
我们的代码及计算流程总是从神经网络的左侧向右侧进行计算,这是神经网络的 正向传播过程 。但很明显这不是神经网络算法的全流程,这个流程虽然可以输出预测结果,但无法保证神经网络的输出结果与真实值接近。
在之前的学习中,我们了解到线性回归的任务就是构造一个预测函数来映射输入的特征矩阵 X X X和标签值 y y y的线性关系。构造预测函数核心就是 找出模型的权重向量 w w w,并令线性回归的输出结果与真实值相近 ,也就是求解线性方程组中的 w w w和 b b b。
对神经网络而言也是如此,我们的核心任务是 求解一组最适合的 w w w和 b b b,令神经网络的输出结果和真实值相近 。找寻这个 w w w和 b b b的过程就是 “学习” ,也叫做 “训练” 或者 “建模” 。
那我们如何评价 w w w和 b b b是否合适呢?又如何衡量输出结果与真实值之间的差异大小呢?
此时就需要使用机器学习中通用的优化流程了。在讲解autograd时其实已经提过这个优化流程:
-
提出基本模型,明确目标
基本模型就是自建的神经网络架构,我们需要求解的就是神经网络架构中的权重向量 w w w。
-
确定损失函数/目标函数
我们需要定义某个评估指标,用以衡量模型权重为 w w w的情况下,预测结果与真实结果的差异。当真实值与预测值差异越大时,我们就认为神经网络学习过程中丢失了许多信息,丢失的这部分被形象地称为“损失”,因此评估真实值与预测值差异的函数我们称为“损失函数”。
-
确定适合的优化算法
-
利用优化算法,最小化损失函数,求解最佳权重 w w w(训练)
对线性回归,我们的损失函数是
SSE
,优化算法是最小二乘法和梯度下降法,两者都是对机器学习来说非常重要的优化算法。但遗憾的是,最小二乘法作为入门级优化算法,有较多的假设和先决条件,不足以应对神经网络需要被应用的各种复杂环境。梯度下降法应用广泛,不过也有很多问题需要改进。
接下来将以分类深层神经网络为例介绍神经网络中所使用的入门级损失函数及优化算法。
38 回归:误差平方和SSE
对于回归类神经网络而言,最常见的损失函数是SSE
(Sum of the Squared Errors
):
S
S
E
=
∑
i
=
1
m
(
z
i
−
z
^
i
)
2
SSE = \sum_{i=1}^{m} (z_i - \hat{z}_i)^2
SSE=i=1∑m(zi−z^i)2
对于全部样本的 平均 损失,则可以写作:
M
S
E
=
1
m
∑
i
=
1
m
(
z
i
−
z
^
i
)
2
MSE = \frac{1}{m} \sum_{i=1}^{m} (z_i - \hat{z}_i)^2
MSE=m1i=1∑m(zi−z^i)2
在Pytorch
中,我们可以通过以下代码调用MSE
:
from torch.nn import MSELoss # 类
yhat = torch.randn(size = (50, ), dtype = torch.float32)
y = torch.randn(size = (50, ), dtype = torch.float32)
criterion = MSELoss() # 实例化
loss = criterion(yhat, y)
# 没有设置随机数种子,所以每次运行的数字都会不一致
loss
# output :
tensor(1.9213)
在MSELoss
中有重要的参数,reduction
当reduction = "mean"
(默认也是mean
),则输出MSE
当reduction = "sum"
,则输出SSE
criterion = MSELoss(reduction = 'mean') # 实例化
criterion(yhat, y)
# output :
tensor(2.0026)
criterion = MSELoss(reduction = 'sum')
criterion(yhat, y)
# output :
tensor(128.8871)
39 二分类交叉熵损失函数
在这一节将介绍二分类神经网络的损失函数:二分类交叉熵损失函数(Binary Cross Entropy Loss
),也叫做对数损失(log loss
)。这个损失函数被广泛地使用在任何输出结果是二分类的神经网络中,不止限于单层神经网络,还可拓展到多分类中。大多数时候,除非特殊声明为二分类,否则提到交叉熵损失,我们会默认算法的分类目标是多分类。
二分类交叉熵损失函数是由极大似然估计推导出来的,对于有
m
m
m个样本的数据集而言,在全部样本上的平均损失写作:
L
(
w
)
=
−
∑
i
=
1
m
(
y
i
⋅
ln
(
σ
i
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
σ
i
)
)
L(w) = -\sum_{i=1}^{m} \left( y_i \cdot \ln(\sigma_i) + (1 - y_i) \cdot \ln(1 - \sigma_i) \right)
L(w)=−i=1∑m(yi⋅ln(σi)+(1−yi)⋅ln(1−σi))
在单个样本的损失写作:
L
(
w
)
i
=
−
(
y
i
⋅
ln
(
σ
i
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
σ
i
)
)
L(w)_i = -\left( y_i \cdot \ln(\sigma_i) + (1 - y_i) \cdot \ln(1 - \sigma_i) \right)
L(w)i=−(yi⋅ln(σi)+(1−yi)⋅ln(1−σi))
其中,
l
n
ln
ln是以自然底数
e
e
e为底的对数函数,
w
w
w表示求解出来的一组权重(在等号的右侧,
w
w
w在
σ
\sigma
σ里),
m
m
m是样本的个数,
y
i
y_i
yi是样本
i
i
i上真实的标签,
σ
i
\sigma_i
σi是样本
i
i
i上基于参数
w
w
w计算出来的sigmoid
函数的返回值,
x
i
x_i
xi是样本
i
i
i各个特征的取值。我们的目标就是求解出使
L
(
w
)
L(w)
L(w)最小的
w
w
w取值。
注意,在神经网络中,特征张量 X X X是自变量,权重是 w w w。但在损失函数中,权重 w w w是损失函数的自变量,特征 x x x和真实标签 y y y都是已知的,相当于是常数。
39.1 极大似然估计求解二分类交叉熵损失
二分类交叉熵损失函数是怎么来的呢?为什么这个函数能够代表二分类时,真实值与预测值的差异呢?
极大似然估计(Maximum Likelihood Estimate,MLE)的感性认识
如果一个事件的发生概率很大,那这个事件应该很容易发生。相应的,如果依赖于权重 w w w的任意事件的发生就是我们的目标,那我们只要寻找令其发生概率最大化的权重 w w w就可以了。
寻找相应的权重 w w w,使得目标事件的发生概率最大,就是极大似然估计的基本方法。
其步骤如下:
- 构筑似然函数 P ( w ) P(w) P(w),用于评估目标事件发生的概率,该函数被设计成目标事件发生时,概率最大。
- 对整体似然函数取对数,构成对数似然函数 l n P ( w ) lnP(w) lnP(w)。
- 在对数似然函数上对权重 w w w求导,并使导数为 0 0 0,对权重进行求解。
在二分类的例子中,我们的“任意事件”就是每个样本的分类都正确 ,对数似然函数的负数就是我们的损失函数。
在这里,我们基于极大似然估计法来推导交叉熵损失,这个推导过程能够帮助我们充分了解交叉熵损失的含义,以及为什么 L ( w ) L(w) L(w)的最小化能够实现模型在数据集上的拟合最好。
我们来看看逻辑回归的对数似然函数是怎样构筑的。
构筑对数似然函数
二分类神经网络的标签是[0, 1],此标签服从伯努利分布(即0-1分布),因此可得:
样本
i
i
i在由特征向量
x
i
x_i
xi和权重向量
w
w
w组成的预测函数中,样本标签被预测为1的概率为:
P
1
=
P
(
y
^
i
=
1
∣
x
i
,
w
)
=
σ
P_1 = P(\hat{y}_i = 1 | x_i, w) = \sigma
P1=P(y^i=1∣xi,w)=σ
对二分类而言,
σ
\sigma
σ就是sigmoid函数的结果。
样本
i
i
i在由特征向量
x
i
x_i
xi和权重向量
w
w
w组成的预测函数中,样本标签被预测为0的概率为:
P
0
=
P
(
y
^
i
=
0
∣
x
i
,
w
)
=
1
−
σ
P_0 = P(\hat{y}_i = 0 | x_i, w) = 1-\sigma
P0=P(y^i=0∣xi,w)=1−σ
当
P
1
P_1
P1的值为1的时候,代表样本
i
i
i的标签被预测为1;当
P
0
P_0
P0的值为1的时候,代表样本
i
i
i的标签被预测为0。
P
1
P_1
P1与
P
0
P_0
P0相加一定是等于1的。
假设样本 i i i的真实标签 y i y_i yi为1,并且 P 1 P_1 P1也为1的话,那就说明我们将 i i i的标签预测为1的概率很大,与真实值一致,那模型的预测就是准确的,拟合程度很高,信息损失很少。相反,如果 P 1 P_1 P1接近0,就说明信息损失很多。当 y i y_i yi为0时也是同样的道理。
所以,当 y i y_i yi为1的时候,我们希望 P 1 P_1 P1非常接近1,当 y i y_i yi为0的时候,我们希望 P 0 P_0 P0非常接近1,这样,模型的效果就很好,信息损失就很少。
真实标签 y i y_i yi | 被预测为1的概率 P 1 P_1 P1 | 被预测为0的概率 P 0 P_0 P0 | 样本被预测为? | 与真实值一致吗? | 拟合状况 | 信息损失 |
---|---|---|---|---|---|---|
1 | 0 | 1 | 0 | 否 | 坏 | 大 |
1 | 1 | 0 | 1 | 是 | 好 | 小 |
0 | 0 | 1 | 0 | 是 | 好 | 小 |
0 | 1 | 0 | 1 | 否 | 坏 | 大 |
将两种取值的概率整合,我们可以定义如下等式:
P
(
y
^
i
∣
x
i
,
w
)
=
P
1
y
i
∗
P
0
1
−
y
i
P(\hat{y}_i | x_i, w) = P_1^{y_i} * P_0^{1-y_i}
P(y^i∣xi,w)=P1yi∗P01−yi
这个等式同时代表了
P
1
P_1
P1和
P
0
P_0
P0,在数学上,它被叫做逻辑回归的假设函数。
当样本 i i i的真实标签 y i y_i yi为1的时候, 1 − y i 1-y_i 1−yi就等于0, P 0 P_0 P0的0次方就是1,所以 P ( y ^ i ∣ x i , w ) P(\hat{y}_i | x_i, w) P(y^i∣xi,w)就等于 P 1 P_1 P1,这个时候,如果 P 1 P_1 P1为1,模型的效果就很好,损失就很小。
同理,当样本 i i i的真实标签 y i y_i yi为0的时候, P ( y ^ i ∣ x i , w ) P(\hat{y}_i | x_i, w) P(y^i∣xi,w)就等于 P 0 P_0 P0,此时如果 P 0 P_0 P0非常接近1,模型的效果就很好,损失就很小。
所以,为了达成让模型拟合好,损失小的目的,我们每时每刻都希望 P ( y ^ i ∣ x i , w ) P(\hat{y}_i | x_i, w) P(y^i∣xi,w)的值等于1。而 P ( y ^ i ∣ x i , w ) P(\hat{y}_i | x_i, w) P(y^i∣xi,w)的本质是样本 i i i由特征向量 x i x_i xi和权重 w w w组成的预测函数中,预测出所有可能的 y ^ i \hat{y}_i y^i的概率,因此1是它的最大值。 也就是说,每时每刻,我们都在追求 P ( y ^ i ∣ x i , w ) P(\hat{y}_i | x_i, w) P(y^i∣xi,w)的最大值。 而寻找相应的参数 w w w,使得每次得到的预测概率最大,正是极大似然估计的基本方法,不过 P ( y ^ i ∣ x i , w ) P(\hat{y}_i | x_i, w) P(y^i∣xi,w)是对单个样本而言的,因此我们还需要将其拓展到多个样本上。
P
(
y
^
i
∣
x
i
,
w
)
P(\hat{y}_i | x_i, w)
P(y^i∣xi,w)是对单个样本
i
i
i而言的函数,对一个训练集
m
m
m个样本来说,我们可以定义如下等式来表达所有样本在特征张量
X
X
X和权重向量
w
w
w组成的预测函数中,预测出所有可能的
y
^
\hat{y}
y^的概率
P
P
P为:
P
=
∏
i
=
1
m
P
(
y
^
i
∣
x
i
,
w
)
=
∏
i
=
1
m
(
P
1
y
i
∗
P
0
1
−
y
i
)
=
∏
i
=
1
m
(
σ
i
y
i
∗
(
1
−
σ
i
)
1
−
y
i
)
\begin{align*} P &= \prod_{i=1}^m P(\hat{y}_i | x_i, w) \\ &= \prod_{i=1}^m (P_1^{y_i} * P_0^{1-y_i}) \\ &= \prod_{i=1}^m (\sigma_i^{y_i} * (1 - \sigma_i)^{1-y_i}) \end{align*}
P=i=1∏mP(y^i∣xi,w)=i=1∏m(P1yi∗P01−yi)=i=1∏m(σiyi∗(1−σi)1−yi)
这个函数就是逻辑回归的似然函数 。对该概率
P
P
P取以
e
e
e为底的对数,再由
l
o
g
(
A
∗
B
)
=
l
o
g
A
+
l
o
g
B
log(A*B)=logA+logB
log(A∗B)=logA+logB和
l
o
g
A
B
=
B
l
o
g
A
logA^B=BlogA
logAB=BlogA可得到逻辑回归的对数似然函数:
ln
P
=
ln
∏
i
=
1
m
(
σ
i
y
i
∗
(
1
−
σ
i
)
1
−
y
i
)
=
∑
i
=
1
m
ln
(
σ
i
y
i
∗
(
1
−
σ
i
)
1
−
y
i
)
=
∑
i
=
1
m
(
ln
σ
i
y
i
+
ln
(
1
−
σ
i
)
1
−
y
i
)
=
∑
i
=
1
m
(
y
i
∗
ln
(
σ
i
)
+
(
1
−
y
i
)
∗
ln
(
1
−
σ
i
)
)
\begin{align*} \ln P &= \ln \prod_{i=1}^m (\sigma_i^{y_i} * (1 - \sigma_i)^{1-y_i}) \\ &= \sum_{i=1}^m \ln (\sigma_i^{y_i} * (1 - \sigma_i)^{1-y_i}) \\ &= \sum_{i=1}^m (\ln \sigma_i^{y_i} + \ln (1 - \sigma_i)^{1-y_i}) \\ &= \sum_{i=1}^m (y_i * \ln(\sigma_i) + (1 - y_i) * \ln(1 - \sigma_i)) \end{align*}
lnP=lni=1∏m(σiyi∗(1−σi)1−yi)=i=1∑mln(σiyi∗(1−σi)1−yi)=i=1∑m(lnσiyi+ln(1−σi)1−yi)=i=1∑m(yi∗ln(σi)+(1−yi)∗ln(1−σi))
这就是我们的二分类交叉熵函数。为了数学上的便利以及更好地定义“损失”的含义,我们希望将极大值问题转换成极小值问题,因此我们对
l
n
P
lnP
lnP取负,并且让权重
w
w
w作为函数的自变量,就得到了我们的损失函数
L
(
w
)
L(w)
L(w):
L
(
w
)
=
−
∑
i
=
1
m
(
y
i
⋅
ln
(
σ
i
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
σ
i
)
)
L(w) = -\sum_{i=1}^{m} (y_i \cdot \ln(\sigma_i) + (1 - y_i) \cdot \ln(1 - \sigma_i))
L(w)=−i=1∑m(yi⋅ln(σi)+(1−yi)⋅ln(1−σi))
当 L ( w ) L(w) L(w)为0时, l n P lnP lnP就等于0, P P P就得到最大概率1。
现在,我们已经将模型拟合中的“最小化损失”问题,转换成了对函数求解极值的问题。这就是一个,基于逻辑回归的返回值 σ \sigma σ的概率性质以及极大似然估计得出的损失函数。在这个函数上,我们只要追求最小值,就能让模型在训练数据上的拟合效果最好,损失最低。
39.2 用tensor实现二分类交叉熵损失
现在,让我们在PyTorch
中来实现二分类交叉熵损失函数。首先使用基本的tensor
方法来试试看,以加深我们对二分类交叉熵损失的印象:
import torch
import time
N = 3 * pow(10, 3) # 样本数3000
torch.random.manual_seed(420) # 设置随机数种子
X = torch.random((N, 4), dtype = torch.float32)
w = torch.rand((4, 1), dtype = torch.float32, requires_grad = True)
y = torch.randint(low = 0, high = 2, size = (N, 1), dtype = torch.float32)
zhat = torch.mm(X, w)
sigma = torch.sigmoid(zhat)
Loss = -(1/N) * torch.sum((1-y) * torch.log(1 - sigma) + y * torch.log(sigma))
注意,在写损失函数这样的复杂函数时,除了普通的加减乘除以外的全部计算,都要使用torch
中的函数,因为tensor
的运算速度是远远超过普通Python
代码,甚至是NumPy
的。
N = 3 * pow(10,6)
torch.random.manual_seed(420)
X = torch.rand((N,4),dtype=torch.float32)
w = torch.rand((4,1),dtype=torch.float32,requires_grad=True)
# 左闭右开,只会取0和1
y = torch.randint(low=0,high=2,size=(N,1),dtype=torch.float32)
zhat = torch.mm(X,w)
sigma = torch.sigmoid(zhat)
# 使用torch
start = time.time()
L1 = -(1/N)*torch.sum((1-y)*torch.log(1-sigma)+y*torch.log(sigma))
now = time.time() #seconds
print(now - start)
# output :
0.013980865478515625
# 使用python
start = time.time()
L2 = -(1/N)*sum((1-y)*torch.log(1-sigma)+y*torch.log(sigma))
now = time.time() #seconds
print(now - start)
# output :
16.02258539199829
从运行结果来看,除了加减乘除,我们应该尽量避免使用任何Python
原生的计算方法。如果可能的话,让PyTorch处理一切。
39.3 用Pytorch中的类实现二分类交叉熵损失
在PyTorch
当中,我们有多种方式可以调用二分类交叉熵损失函数。
方法1:nn模块中的类
对于二分类交叉熵损失,nn
提供了两个类:BCEWithLogitsLoss
以及BCELoss
。虽然pytorch
官方没有直接明确,但实际上两个函数所需要输入的参数不同。
BCEWithLogitsLoss
内置了sigmoid
函数与交叉熵函数,它会自动计算输入值的sigmoid
值,因此需要输入zhat
与真实标签,且顺序不能变化,zhat
必须在前。
相对的,BCELoss
中只有交叉熵函数,没有sigmoid
层,因此需要输入sigmoid
与真实标签,且顺序不能变化。
同时,这两个函数都要求预测值与真实标签的 数据类型以及结构(shape)必须相同 ,否则运行就会报错。
接下来,我们来看看这两个类是如何使用的:
import torch.nn as nn
# 调用nn模块下的类
criterion = nn.BCELoss() # 实例化
loss = criterion(sigma, y)
criterion2 = torch.nn.BCEWithLogitsLoss() # 实例化
loss = criterion2(zhat, y)
根据pytorch
官方的公告,他们更推荐使用BCEWithLogitsLoss
这个内置了sigmoid
函数的类。内置的sigmoid
可以让精度问题被缩小(因为将指数运算包含在了内部),以维持算法运行时的稳定性。所以,当我们的输出层使用sigmoid
函数时,我们就可以使用BCEWithLogitsLoss
作为损失函数。
与MSELoss
相同,二分类交叉熵的类也有参数reduction
,默认是"mean
",表示求解所有样本平均的损失,也可换为"sum
",要求输出整体的损失。以及,还可以使用选项"none
",表示不对损失结果做任何聚合运算,直接输出每个样本对应的损失矩阵。
criterion2 = torch.nn.BCEWithLogitsLoss(reduction = 'mean')
loss = criterion2(zhat, y)
loss
# output :
tensor(0.8951)
criterion2 = torch.nn.BCEWithLogitsLoss(reduction = 'sum')
loss = criterion2(zhat, y)
loss
# output :
tensor(2685.3774)
criterion2 = torch.nn.BCEWithLogitsLoss(reduction = 'none')
loss = criterion2(zhat, y)
loss.shape
# output :
torch.Size([3000, 1])
第二种方法很少用,我们了解一下即可:
方法2:functional库中的计算函数
function F.binary_cross_entropy_with_logits
function F.binary_cross_entropy
和nn
中的类们类似,名称中带有Logits
的是内置了sigmoid
功能的函数,没有带Logits
的,是只包含交叉熵损失的函数。
from torch.nn import functional as F
# 直接调用functional库中的计算函数
F.binary_cross_entropy_with_logits(zhat, y)
F.binary_cross_entropy(sigma, y)
在这里两个函数的运行结果是一致的。同样的,pytorch
官方推荐的是内置sigmoid
功能的函数binary_cross_entropy_with_logits
。通常来说,我们都使用类,不使用函数。虽然代码会因此变得稍稍有点复杂,但为了代码的稳定性与日后维护,使用类是更好的选择。
40 多分类交叉熵损失函数
40.1 由二分类推广到多分类
二分类交叉熵损失可以被推广到多分类上,但在实际处理时,二分类与多分类却有一些关键的区别。依然使用极大似然估计的推导流程,首先我们来确定单一样本概率最大化后的似然函数。
对于多分类的状况而言,标签不再服从伯努利分布(0-1
分布),因此我们可以定义,样本
i
i
i在由特征向量
x
i
x_i
xi和权重向量
w
w
w组成的预测函数中,样本标签被预测为类别
k
k
k的概率为:
P
k
=
P
(
y
^
i
=
k
∣
x
i
,
w
)
=
σ
P_k = P(\hat{y}_i = k | x_i, w) = \sigma
Pk=P(y^i=k∣xi,w)=σ
对于多分类算法而言,
σ
\sigma
σ就是softmax
函数返回的对于类别的值。
假设一种最简单的情况:我们现在有三分类[1, 2, 3],则样本
i
i
i被预测为三个类别的概率分别为:
P
1
=
P
(
y
^
i
=
1
∣
x
i
,
w
)
=
σ
1
P
2
=
P
(
y
^
i
=
2
∣
x
i
,
w
)
=
σ
2
P
3
=
P
(
y
^
i
=
3
∣
x
i
,
w
)
=
σ
3
\begin{align*} P_1 &= P(\hat{y}_i = 1 | x_i, w) = \sigma_1 \\ P_2 &= P(\hat{y}_i = 2 | x_i, w) = \sigma_2 \\ P_3 &= P(\hat{y}_i = 3 | x_i, w) = \sigma_3 \end{align*}
P1P2P3=P(y^i=1∣xi,w)=σ1=P(y^i=2∣xi,w)=σ2=P(y^i=3∣xi,w)=σ3
假设样本的真实标签为1
,我们就希望
P
1
P_1
P1最大,同理,如果样本的真实标签为其他值,我们就希望其他值所对应的概率最大。
在二分类中,我们将
y
y
y和
(
1
−
y
)
(1-y)
(1−y)作为概率
P
P
P的指数,以此来融合真实标签0和为1的两种状况。但在多分类中,我们的真实标签可能是任意整数,无法使用
y
y
y和
(
1
−
y
)
(1-y)
(1−y)这样的结构来构建似然函数。所以我们认为,如果多分类的标签也可以使用0
和1
来表示就好了,这样我们就可以继续使用真实标签作为指数的方式。
因此,我们对多分类的标签做出了如下变化:
原本的真实标签y
是含有[1, 2, 3]
三个分类的列向量,现在我们把它变成了标签矩阵,每个样本对应一个向量(这正是机器学习中的独热编码one-hot
)。在矩阵中,每一行依旧对应样本,但却由三分类衍生出了三个新的列,分别代表:真实标签是否等于1
、等于2
以及等于3
。
在矩阵中,我们使用“1”标注出样本的真实标签的位置,使用0表示样本的真实标签不是这个标签。不难注意到,这个标签矩阵的结构其实是和softmax
函数输出的概率矩阵的结构一致,并且一一对应的。
回顾二分类的似然函数:
P
(
y
^
i
∣
x
i
,
w
)
=
P
1
y
i
∗
P
0
1
−
y
i
P(\hat{y}_i | x_i, w) = P_1^{y_i} * P_0^{1-y_i}
P(y^i∣xi,w)=P1yi∗P01−yi
当我们把标签整合为标签矩阵后,我们就可以将单个样本在总共
k
k
k个分类情况整合为以下的似然函数:
P
(
y
^
i
∣
x
i
,
w
)
=
P
1
y
i
(
k
=
1
)
∗
P
2
y
i
(
k
=
2
)
∗
P
3
y
i
(
k
=
3
)
∗
…
∗
P
K
y
i
(
k
=
K
)
P(\hat{y}_i | x_i, w) = P_1^{y_i(k=1)} * P_2^{y_i(k=2)} * P_3^{y_i(k=3)} * \ldots * P_K^{y_i(k=K)}
P(y^i∣xi,w)=P1yi(k=1)∗P2yi(k=2)∗P3yi(k=3)∗…∗PKyi(k=K)
其中
P
P
P就是样本标签被预测为某个具体值的概率,而右上角的指数就是标签矩阵中对应的值,即这个样本的真实标签是否为当前标签的判断(是就是1
,否就是0
)。
也可将式子简写为:
P
(
y
^
i
∣
x
i
,
w
)
=
P
j
y
i
(
k
=
j
)
,
j
为样本
i
所对应的真实标签的编号
P(\hat{y}_i | x_i, w) = P_j^{y_{i(k=j)}}, \quad j \text{为样本} i \text{所对应的真实标签的编号}
P(y^i∣xi,w)=Pjyi(k=j),j为样本i所对应的真实标签的编号
对一个训练集的
m
m
m个样本来说,我们可以定义如下等式来表达所有样本在特征张量
X
X
X和权重向量
w
w
w组成的预测函数中,预测出所有可能的
y
^
\hat{y}
y^的概率
P
P
P为:
P
=
∏
i
=
1
m
P
(
y
^
i
∣
x
i
,
w
)
=
∏
i
=
1
m
P
j
y
i
(
k
=
j
)
=
∏
i
=
1
m
σ
j
y
i
(
k
=
j
)
\begin{align*} P &= \prod_{i=1}^m P(\hat{y}_i | x_i, w) \\ &= \prod_{i=1}^m P_j^{y_{i(k=j)}} \\ &= \prod_{i=1}^m \sigma_j^{y_{i(k=j)}} \end{align*}
P=i=1∏mP(y^i∣xi,w)=i=1∏mPjyi(k=j)=i=1∏mσjyi(k=j)
这是多分类状况下的似然函数。与二分类一致,似然函数解出来后,我们需要对似然函数求对数:
ln
P
=
ln
∏
i
=
1
m
σ
j
y
i
(
k
=
j
)
=
∑
i
=
1
m
ln
(
σ
j
y
i
(
k
=
j
)
)
=
∑
i
=
1
m
y
i
(
k
=
j
)
ln
σ
i
\begin{align*} \ln P &= \ln \prod_{i=1}^m \sigma_j^{y_{i(k=j)}} \\ &= \sum_{i=1}^m \ln(\sigma_j^{y_{i(k=j)}}) \\ &= \sum_{i=1}^m y_{i(k=j)} \ln \sigma_i \end{align*}
lnP=lni=1∏mσjyi(k=j)=i=1∑mln(σjyi(k=j))=i=1∑myi(k=j)lnσi
其中
σ
\sigma
σ就是softmax
函数返回的对应类别的值。在对整个公式取负,就得到了多分类状况下的损失函数:
L
(
w
)
=
−
∑
i
=
1
m
y
i
(
k
=
j
)
ln
σ
i
L(w) = -\sum_{i=1}^{m} y_{i(k=j)} \ln \sigma_i
L(w)=−i=1∑myi(k=j)lnσi
这个函数就是我们之前提到过的交叉熵函数 。不难看出,二分类的交叉熵函数其实是多分类的一种特殊情况。
交叉熵函数十分特殊,虽然我们求解过程中,取对数的操作是在确定了似然函数后才进行的,但从计算结果看,对数操作其实只对softmax
函数的结果
σ
\sigma
σ起效。因此在实际操作中,我们把
l
n
(
s
o
f
t
m
a
x
(
z
)
)
ln(softmax(z))
ln(softmax(z))这样的函数单独定义了一个功能叫做logsoftmax
,pytorch
可以直接通过nn.logsoftmax
类直接调用这个功能。
同时,我们把对数之外的。乘以标签、加和、取负等等过程打包起来,称之为负对数似然函数(Negative Log Likelihood function
),在pytorch
中可以使用nn.NLLLoss
来调用。也就是说,在计算损失函数时,我们不再需要使用单独的softmax函数了。
40.2 用pytorch实现多分类交叉熵损失
在PyTorch
中实现交叉熵函数的时候,有两种办法:
调用logsoftmax和NLLLoss实现
import torch
import torch.nn as nn
N = 3 * pow(10, 2)
torch.random.manual_seed(420)
X = torch.rand((N, 4), dtype = torch.float32)
w = torch.rand((4, 3), dtype = torch.float32, requires_grad = True)
y = torch.randint(low=0,high=3,size=(m,),dtype=torch.float32)
zhat = torch.mm(X,w)
# 从这里开始调用softmax和NLLLoss
# dim = 1表示要对矩阵的每一行标签都进行softmax计算
logsm = nn.LogSoftmax(dim=1) # 实例化
logsigma = logsm(zhat)
criterion = nn.NLLLoss() #实例化
# 由于交叉熵损失需要将标签转化为独热形式,因此不接受浮点数作为标签的输入
# 对NLLLoss而言,需要输入logsigma
criterion(logsigma,y.long())
更加简便的方法是:
直接调用CrossEntropyLoss
criterion = nn.CrossEntropyLoss()
# 对打包好的CorssEnrtopyLoss而言,只需要输入zhat
criterion(zhat,y.long())
两种输出方法得到的损失函数结果是一致的。与其他损失函数一致,CrossEntropyLoss
也有参数reduction
,可以设置为mean
、sum
以及None
,