一、机器学习中的优化思想
在之前的学习中,我们建立神经网络时总是先设定好 与 的值(或者由我们调用的PyTorch类帮助我们随机生成权重向量 ),接着通过加和求出 ,再在 上嵌套sigmoid或者softmax函数,最终获得神经网络的输出。我们的代码及计算流程,总是从神经网络的左侧向右侧计算的。之前我们提到过,这是神经网络的正向传播过程。但很明显,这并不是神经网络算法的全流程,这个流程虽然可以输出预测结果,但却无法保证神经网络的输出结果与真实值接近。
之前我们在线性回归上走过这个全流程。对线性回归,我们的损失函数是SSE,优化算法是最小二乘法和梯度下降法,两者都是对机器学习来说非常重要的优化算法。但遗憾的是,最小二乘法作为入门级优化算法,有较多的假设和先决条件,不足以应对神经网络需要被应用的各种复杂环境。梯度下降法应用广泛,不过也有很多问题需要改进。接下来,我将主要以分类深层神经网络为例来介绍神经网络中所使用的入门级损失函数及优化算法。
二、回归:误差平方和SSE
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 #没有设置随机数种子,所以每次运行的数字都会不一致
#在MSELoss中有重要的参数,reduction
#当reduction = "mean" (默认也是mean),则输出MSE
#当reduction = "sum",则输出SSE
criterion = MSELoss(reduction = "mean") #实例化
criterion(yhat,y)
criterion = MSELoss(reduction = "sum")
criterion(yhat,y)
三、二分类交叉熵损失函数
1 极大似然估计求解二分类交叉熵损失
现在,我们已经将模型拟合中的“最小化损失”问题,转换成了对函数求解极值的问题。这就是一个,基于逻辑回归的返回值 的概率性质以及极大似然估计得出的损失函数。在这个函数上,我们只要追求最小值,就能让模型在训练数据上的拟合效果最好,损失最低。
在极大似然估计中,我们只要在对数似然函数上对权重 求导,再令导数为0,就可以求解出最合适的,但是对于像交叉熵这样复杂的损失函数,加上神经网络中复杂的权重组合,令所有权重的导数为0并一个个求解方程的难度很大。因此我们要使用优化算法,这部分我们下一章展开来聊。
2 用tensor实现二分类交叉熵损失
现在,让我们在PyTorch中来实现二分类交叉熵损失函数。首先使用基本的tensor方法来试试看,以加深我们对二分类交叉熵损失的印象:
import torch
import time
N = 3*pow(10,3)
torch.random.manual_seed(420)
X = torch.rand((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的。你可以试着比较在样本量为300W时,以下两行代码运行的时间差异:
#你可以试着比较在样本量为300W时,以下两行代码运行的时间差异。这段代码不需要GPU。
#如果你的电脑内存或计算资源有限,可以试着将样本量调小为30W或3W
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)
y = torch.randint(low=0,high=2,size=(N,1),dtype=torch.float32)
zhat = torch.mm(X,w)
sigma = torch.sigmoid(zhat)
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)
start = time.time()
L2 = -(1/N)*sum((1-y)*torch.log(1-sigma)+y*torch.log(sigma))
now = time.time() #seconds
print(now - start)
从运行结果来看,除了加减乘除,我们应该尽量避免使用任何Python原生的计算方法。如果可能的话,让PyTorch处理一切。
3 用PyTorch中的类实现二分类交叉熵损失
在PyTorch当中,我们有多种方式可以调用二分类交叉熵损失函数。
import torch.nn as nn
#调用nn模块下的类
criterion = nn.BCELoss() #实例化
loss = criterion(sigma,y)
loss
criterion2 = nn.BCEWithLogitsLoss() #实例化
loss = criterion2(zhat,y)
loss
可以看出,两个类的结果是一致的。根据PyTorch官方的公告,他们更推荐使用BCEWithLogitsLoss这个内置了sigmoid函数的类。内置的sigmoid函数可以让精度问题被缩小(因为将指数运算包含在了内部),以维持算法运行时的稳定性,即是说当数据量变大、数据本身也变大时,BCELoss类产生的结果可能有精度问题。所以,当我们的输出层使用sigmoid函数时,我们就可以使BCEWithLogitsLoss作为损失函数。
与MSELoss相同,二分类交叉熵的类们也有参数reduction,默认是”mean“,表示求解所有样本平均的损失,也可换为”sum”,要求输出整体的损失。以及,还可以使用选项“none”,表示不对损失结果做任何聚合运算,直接输出每个样本对应的损失矩阵。
criterion2 = nn.BCEWithLogitsLoss(reduction = "mean")
loss = criterion2(zhat,y)
loss
criterion2 = nn.BCEWithLogitsLoss(reduction = "sum")
loss = criterion2(zhat,y)
loss
criterion2 = nn.BCEWithLogitsLoss(reduction = "none")
loss = criterion2(zhat,y)
loss
第二种方法很少用,我们了解一下即可:
方法2:functional库中的计算函数
function F.binary_cross_entropy_with_logits
function F.binary_cross_entropy
和nn中的类们相似,名称中带有Logits的是内置了sigmoid功能的函数,没有带Logits的,是只包含交叉熵损失的函数。对于含有sigmoid功能的函数,我们需要的输入是zhat与标签,不含sigmoid的函数我们则需要输入sigma与标签。同样的,这两个函数对输入有严格的要求,输入的预测值必须与标签结构一致、数据类型一致。我们来看看他们的运行结果:
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。通常来说,我们都使用类,不使用函数。虽然代码会因此变得稍稍有点复杂,但为了代码的稳定性与日后维护,使用类是更好的选择。当然,在进行代码演示和快速测算的时候,使用函数或者类都没有问题。
四、多分类交叉熵损失函数
1 由二分类推广到多分类
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时应该怎么做?应该设置为矩阵吗?
y = torch.randint(low=0,high=3,size=(N,),dtype=torch.float32)
zhat = torch.mm(X,w)
#从这里开始调用softmax和NLLLoss
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,大家可以自行尝试其代码并查看返回结果。
无论时二分类还是多分类,PyTorch都提供了包含输出层激活函数和不包含输出层激活函数的类两种选择。在实际神经网络建模中,类可以被放入定义好的Model类中去构建神经网络的结构,因此是否包含激活函数,就需要由用户来自行选择。
- 重视展示网络结构和灵活性,应该使用不包含输出层激活函数的类
通常在Model类中,init中层的数量与forward函数中对应的激活函数的数量是一致的,如果我们使用内置sigmoid/logsoftmax功能的类来计算损失函数,forward函数在定义时就会少一层(输出层),
网络结构展示就不够简单明了,对于结构复杂的网络而言,结构清晰就更为重要。同时,如果激活函数是单独写的,要修改激活函数就变得很容易,如果混在损失函数中,要修改激活函数时就得改掉整个损失函数的代码,不利于维护。
- 重视稳定性和运算精度,使用包含输出层激活函数的类
如果在一个Model中,很长时间我们都不会修改输出层的激活函数,并且模型的稳定运行更为要紧,我
们就使用内置了激活函数的类来计算损失函数。同时,就像之前提到的,内置激活函数可以帮助我们推升运算的精度。
因此,选择哪种损失函数的实现方式,最终还要看我们的需求。
有了损失函数,我们终于要开始进行求解了。下一部分我们来讲解神经网络的入门级优化算法:小批量随机梯度下降。