在前面的文章中,我们介绍了单层和多层神经网络的正向传播过程,同时也使用了torch.nn
来实现这一过程。不难发现,模型中的权重对模型输出的预测结果起着很重要的作用。而前面我们在实现正向传播的过程中,手动设定或者随机生成了模型参数的值,并没有对其进行训练和优化。从这篇文章开始,我们将重点讲解神经网络的训练,本文将主要介绍神经网络训练中的损失函数。
模型训练的一般过程包含以下几个步骤:
- 定义模型;
- 定义损失函数/目标函数:有目标才能指导模型训练优化的方向;
- 确定优化算法:不同的优化算法迭代效率和效果会有不同;
- 利用优化算法,最小化损失函数,求解最佳模型参数。
在机器学习和深度学习中,我们通过比较预测值和真实值的差异,来衡量一个模型表现的好坏。而预测值和真实值的差异,可以通过定义损失函数来表示。下面我们将介绍在回归问题、二分类问题、多分类问题中,所使用的损失函数。由于损失函数是与模型权重相关的,我们使用标记符号来代表。
误差平方和 Sum of Squared Error (SSE)
在回归问题中,我们可以用误差平方和SSE来衡量预测值与真实值的差异,其定义为:
其中,为样本个数,为样本预测值,为真实值。可以看出,误差平方和SSE的值与样本个数有关,为了消除样本数量的影响,我们可以使用均方误差MSE (Mean Squared Error),其定义如下:
- 注:在有些教材中,MSE定义中前面的系数为,这是为了在计算MSE导数时,可以消除系数的影响;以上两种定义均可使用,只需在同一项目中保持一致即可。
用torch.nn中的类实现MSE
import torch
from torch.nn import MSELoss
y_hat = torch.randn(size=(500,), dtype=torch.float32) # 预测值
y = torch.randn(size=(500,), dtype=torch.float32) # 真实值
criterion = MSELoss(reduction="mean") # 实例化一个MSELoss类
loss = criterion(y_hat, y)
loss
在`MSELoss`类中有个重要参数是:reduction(默认值为"mean")
- `reduction="mean"`输出MSE;
- `reduction="sum"`输出SSE;
criterion = MSELoss(reduction="sum") # 实例化一个MSELoss类
loss = criterion(y_hat, y)
loss
tensor(2.0436)
二分类交叉熵损失函数 Binary Cross Entropy Loss
对于二分类问题,我们可以使用二分类交叉熵损失函数(Binary Cross Entropy Loss),其公式如下:
二分类交叉熵损失函数来源于统计学上的极大似然估计(Maximun Likelihood Estimate)。下面是对如何从极大似然估计获得二分类交叉熵损失函数的解释说明,该部分内容需要一定的统计学基础,也可直接跳过。
假设我们的样本满足以下两个条件:
1) 每一个样本是独立的;
2) 对于样本,它的值是一个随机变量,且服从伯努利分布:
服从上述概率分布的:取值为1的概率是;取值为0的概率是。即:
其中表示的概率,的值可带入0或1。
对于所有的样本,似然函数 Likelihood 为:
对数似然函数 Log-Likelihood 为:
由于,而是由样本特征及权重构成,因此,Log-Likelihood函数值与权重有关。极大似然估计,就是求解能使Log-Likelihood函数取得最大值的那一个权重。
我们的二分类交叉熵损失函数,就是取负数的Log-Likelihood函数,也可以叫做 Negtive Log-Likelihood(NLL)函数:
使Log-Likelihood取得最大值的,就是使二分类交叉熵损失函数取得最小值的,也就是我们所需要的的最优解。
用tensor自定义二分类交叉熵损失函数
import torch
import time
m = 3*pow(10, 3)
torch.manual_seed(516)
X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,1), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,1), dtype=torch.float32)
z_hat = torch.mm(X, w)
sigma = torch.sigmoid(z_hat)
loss = -1/m*torch.sum(y*torch.log(sigma) + (1-y)*torch.log(1-sigma))
loss
注意,在用tensor手写损失函数等复杂函数时,除了简单的加减乘除,其余运算都需要使用tensor中定义的运算,这样大大加快运算速度。我们可以对比在数据样本量大的时候,在定义损失函数中使用torch.sum()
和使用sum()
的区别。
import torch
import time
m = 3*pow(10, 6) # 300万的样本量
torch.manual_seed(516)
X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,1), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,1), dtype=torch.float32)
z_hat = torch.mm(X, w)
sigma = torch.sigmoid(z_hat)
start = time.time()
loss1 = -1/m*torch.sum(y*torch.log(sigma) + (1-y)*torch.log(1-sigma))
end = time.time()
print("使用torch.sum()时间:", end-start)
start = time.time()
loss2 = -1/m*sum(y*torch.log(sigma) + (1-y)*torch.log(1-sigma))
end = time.time()
print("使用sum()时间:", end-start)
使用torch.sum()时间: 0.0221560001373291
使用sum()时间: 16.319851875305176
可以看出,使用了torch.sum()
函数大大节约了运算时间。
用torch.nn中的类实现二分类交叉熵损失函数
-
BCEWithLogitsLoss
:内置了sigmoid函数,参数输入线性加和值z_hat
和真实值y
; -
BCELoss
:参数输入sigma
和真实值y
;
import torch
from torch.nn import BCEWithLogitsLoss
from torch.nn import BCELoss
m = 3*pow(10, 3)
torch.manual_seed(516)
X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,1), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,1), dtype=torch.float32)
z_hat = torch.mm(X, w)
sigma = torch.sigmoid(z_hat)
criterion1 = BCEWithLogitsLoss()
loss1 = criterion1(z_hat, y)
print(loss1)
criterion2 = BCELoss()
loss2 = criterion2(sigma, y)
print(loss2)
tensor(0.8185, grad_fn=<BinaryCrossEntropyWithLogitsBackward>)
tensor(0.8185, grad_fn=<BinaryCrossEntropyBackward>)
PyTorch官方更推荐使用BCEWithLogitsLoss这个内置了sigmoid函数的类。内置的sigmoid函数可以让精度问题被缩小(因为将指数运算包含在了内部),以维持算法运行时的稳定性。所以,当我们的输出层使用sigmoid函数时,我们就可以使用BCEWithLogitsLoss作为损失函数。
类似MSELoss
类,BCEWithLogitsLoss
和BCELoss
中也有参数reduction(默认值为"mean"),可以替换成"sum"或"none"。
多分类交叉熵损失函数 Cross Entropy Loss
在多分类问题中,我们需要预测的样本的标签是一个类别代号。比如,在一个水果分类问题中,我们需要区分葡萄、杏子、和荔枝三类水果,我们可以用类别1,类别2,类别3 来分别代表葡萄,杏子,荔枝。
样本 | 类别 |
---|---|
样本1 | 1 |
样本2 | 2 |
样本3 | 3 |
样本4 | 2 |
... | ... |
样本m | 1 |
在给多分类问题定义损失函数时,我们需要将上述每个样本的标量标签,拓展为一个向量标签,拓展方式如下表:
样本 | 类别1 | 类别2 | 类别3 |
---|---|---|---|
样本1 | 1 | 0 | 0 |
样本2 | 0 | 1 | 0 |
样本3 | 0 | 0 | 1 |
样本4 | 0 | 1 | 0 |
... | ... | ... | ... |
样本m | 1 | 0 | 0 |
每个样本的标签是由一个向量构成,向量的长度等于类别总数,向量的第个元素对应分类类别,向量元素的值由0和1构成,元素值为1所对应的类别编号即为该样本的类别编号。比如上表中,样本的向量标签为,在类别1的位置元素值为1,表示样本的分类类别为1。
至此,我们可以使用交叉熵损失函数(Cross Entropy Loss)来衡量多分类问题中预测值与真实值的差异,其公式如下:
其中,为样本对应的真实类别编号。
同样的,交叉熵损失函数也是由极大似然估计推导出来的。不同于二分类问题,多分类问题假设样本的标签服从多项式分布。下面是推导过程的简要概述,不感兴趣的也可直接跳过。
对于样本,其标签概率分布满足:
若为样本对应的真实类别编号,即、 ,上述式子可以简化为:
似然函数 Likelihood 为:
对数似然函数 Log-Likelihood 为:
注:在上述推导中,为了公式的简化,有一些不严谨之处,
- 小为类别取值,大代表总共有个分类(按道理说若代表总共有个类别,则不应该再使用代表某个具体类别,但我们使用的类别编号与类别本身相同,因此简化了表达);
- 对于不同的样本,真实类别是不同的,因此不是一个固定的值,而公式中并未特别体现这一点。
用torch.nn中的类实现交叉熵损失函数
使用NLLLoss
和LogSoftmax
实现
import torch
from torch.nn import NLLLoss
from torch.nn import LogSoftmax
m = 3*pow(10, 2)
torch.manual_seed(516)
X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,3), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,1), dtype=torch.float32)
z_hat = torch.mm(X, w)
logsm = LogSoftmax(dim=1) # 实例化;dim=1: 对矩阵中每一行数据求解softmax
logsigma = logsm(z_hat)
criterion = NLLLoss()
loss = criterion(logsigma, y)
loss
RuntimeError: 1D target tensor expected, multi-target not supported
从报错信息中可以看出,在NLLLoss
中,只接受一维tensor的y值。因此,我们在下面代码中修改y的定义。
import torch
from torch.nn import NLLLoss
from torch.nn import LogSoftmax
m = 3*pow(10, 2)
torch.manual_seed(516)
X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,3), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,), dtype=torch.float32)
# y被定义为一维张量
z_hat = torch.mm(X, w)
logsm = LogSoftmax(dim=1) # 实例化;dim=1: 对矩阵中每一行数据求解softmax
logsigma = logsm(z_hat)
criterion = NLLLoss()
loss = criterion(logsigma, y)
loss
RuntimeError: expected scalar type Long but found Float
从报错信息中可以看出,在NLLLoss
中,只接受整型类型的y值(交叉熵损失需要将标签转化为独热形式,因此不接受浮点数作为标签的输入),因此,我们在计算NLLLoss值时,使用转换为整型的y。
criterion = NLLLoss()
loss = criterion(logsigma, y.long())
loss
tensor(2.5821, grad_fn=<NllLossBackward>)
为何我们在定义y维度时,直接将二维张量改为一维张量,而在定义y数据类型时,不直接将其定义为整型呢?
- 将y定义为一维张量,可以适应绝大多数pytorch中的运算而不报错,相反,若每次调用都需要转换维度,将会大大降低运算效率;
- 在大多数情况下,pytorch运算中都要求保持张量数据类型的一致,而特征X和权重w通常是float类型的。因此,我们只在需要将y的数据类型转换时转换。
直接使用CrossEntroyLoss
实现
import torch
from torch.nn import CrossEntropyLoss
m = 3*pow(10, 2)
torch.manual_seed(516)
X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,3), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,), dtype=torch.float32)
z_hat = torch.mm(X, w)
criterion = CrossEntropyLoss()
loss = criterion(z_hat, y.long())
loss
tensor(2.5821, grad_fn=<NllLossBackward>)