深度神经网络及训练
本篇博文是上一篇博文【PyTorch】深度学习基础:神经网络的后续,上一篇主要是讨论了传统神经网络范畴上的内容。
本篇博文主要介绍深度神经网络、梯度下降算法、优化器及正则化等训练优化技巧。
深度神经网络
1. 从传统神经网络到深度神经网络
①标志:2006年,Geoffrey Hinton提出了一种名叫“深度信念网络”的神经网络,可以使用“贪婪逐层预训练”的策略有效地进行神经网络的训练。
这些方法在其他神经网络上也得到了了很好的应用,把这些新兴神经网络统称为深度学习,因为这些神经网络的模型可以含有多个隐含层。
深度学习主要包括深度神经网络、卷积神经网络、循环神经网络、LSTM及强化学习。
②背景:
- 神经网络很难训练,参数调试需要很多技巧
- 诸如SVM等其他机器学习方法取得了长足进步
以上导致了神经网络研究热潮的衰退,从而促进了深度神经网络的兴起。
③深度学习解决了神经网络的训练问题:
- 硬件设备进步,提高了数值和矩阵运算的速度
- 标注的数据集的规模增大,避免因为参数过多训练不充分的问题
- 新型神经网络的提出
- 优化算法的进步
2. 神经网络为什么难以训练?
神经网络在层数较多的时候训练很容易出现问题,除了计算资源不足以及训练数据规模较小的问题以外,还有两大重点——
梯度消失和梯度爆炸。
(1)梯度消失
根据反向传播原理,接近输出层的隐含层的权值更新相对正常;在反方向上,权值更新越来越不明显,以此类推,接近输入层的隐含层的权值更新几乎消失——即使经过了很多次训练,仍然接近初始化的权值。
那么靠近输入层的隐含层相当于只是对输入层做了一个同一映射,在神经网络中不起任何作用。
以下做一个简要的推导来论证这一问题,假设神经网络中每一层只有一个神经元,且每一层的输出yi = σ(zi) = σ(wixi+bi),σ表示使用的激活函数。
p.s. 且要注意yi = xi+1,上一层的输出是下一层输入的值。
那么根据链式法则来推导最终输出关于第一层网络偏置项b的偏导:
其中σ激活函数我们选择的是sigmoid函数
sigmoid导数曲线如下所示
如图所示,导数的最大值为0.25;
一般来说,对神经网络权值的初始化值通常都小于1,而|σ’(x)|<1,根据上面链式求导的连乘效果,越多的小于1的小数相乘结果只会越来越小。
神经网络的层数越多,连乘式子就会越多,求导结果就会越小,所以梯度消失的情况就会出现。
(2)梯度爆炸
梯度爆炸出现的原因与梯度消失大致相同,只不过它是因为选择的激活函数|σ’(x)|>1,随着神经网络的层数增多,连乘式的结果也会越来越大,从而在反向传播中出现梯度爆炸的情况。
总而言之,梯度消失与梯度爆炸都是因为网络层数太深,权值更新不稳定造成的,本质上是因为梯度反向传播中的连乘效应。
(3)改进策略
讨论神经网络的优化思路,有以下两个角度:
- 对损失函数的优化问题——梯度下降
- 提高模型的泛化能力——模型正则化方法。
梯度下降
深度学习训练算法都是以梯度下降算法及其改进算法为核心的。
梯度。函数的一个向量,指向函数值上升最快的方法。
1. 随机梯度下降
(1)批量梯度下降
每次梯度下降使用整个训练集进行损失计算和梯度求解。
- 每次更新都朝着正确的方法进行,保证收敛于极值点
- 收敛速度快,迭代次数少
- 每次都需要遍历整个数据集,计算量大,消耗内存多,不利于分布式训练
(2)随机梯度下降
随机选择一个样本来更新模型参数
- 每次学习所需计算量小,速度快
- 每次更新不一定朝着梯度下降最快的方向进行,收敛速度慢,需要更多的迭代次数才可能收敛
2. Mini-Batch梯度下降
小批量梯度下降(Mini-Batch),是介于批量梯度下降和随机梯度下降算法之间的一种选择,被深度学习广泛采用。
小批量梯度下降算法使用一个以上但又不是全部的训练样本,每次更新从训练集中随机选择m(m<n)个样本进行学习。
p.s. 一般而言,每次更新选择{50,256}个样本进行学习,但是也要根据具体问题而选择,实践中可以进行多次试验,选择一个更新速度与更新次数都较为适合的样本数。
(1)小批量梯度下降算法需要样本随机抽取
因为计算梯度时需要样本满足相互独立的条件,现实中数据自然排列,前后样本具有一定的关联性。
因此需要把样本顺序随机打乱,以便满足样本独立性的要求。
(2)mini-batch是对批量梯度下降和随机梯度下降的综合
①mini-batch在更新速度和更新次数之间取得了一个平衡。
②相对于随机梯度下降,mini-batch降低了收敛扰动性,降低了参数更新的方差,使得更新更加稳定。
③相对于批量梯度下降,提高了每次学习的速度,不用担心内存瓶颈问题。
(3)mini-batch方法的实现
批量梯度下降和随机梯度下降算法都可以看做是Mini-Batch梯度下降的特例
当mini-batch中的size取为1,则是随机梯度下降
当mini-batch中的size取为整个数据集的大小,则是批量梯度下降
mini-batch方法是作为数据加载函数torch.utils.data.DataLoader
中的一个系数batch_size
出现的。
注意:
DataLoader
函数只涉及数据集的划分,不涉及梯度下降算法。
class torch.utils.data.DataLoader(dataset,batch_size = 1,shuffle = False,
sampler = None,batch_sampler = None,num_workers = 0,collate_fn = <function default_collate>,pin_memory = False,drop_last = False)
#建立一个具体实例
train_loader = torch.utils.data.DataLoader(dataset = train_dataset,batch_size = batch_size,shuffle = True)
在上述函数中,实现数据加载功能,根据Mini-Batch方法和采样机制,对数据集进行划分,并在数据集上提供单进程或多进程迭代器。
-
dataset
Dataset的类型,指出要加载的数据集 -
batch_size
指出每个batch需要加载多少样本,默认值为1 -
shuffle
指出是否在每个epoch都需要对数据进行打乱 -
sampler
从数据集中采样样本的策略 -
batch_sampler
与sampler相似,只不过一次会返回一批指标 -
num_workers
加载数据时所使用的子进程数目。默认值为0,表示在主进程中加载数据 -
collate_fn
定义合并样本列表以形成一个mini_batch -
pin_memory
若设置为true,则数据加载器会将张量复制到CUDA固定内存中,然后返回它们。 -
drop_last
若设置为true,最后一个不完整的batch将会被丢弃。
优化器
在上一部分,我们讲述了梯度下降的概念和方法,针对梯度下降我们可以继续进行优化。
在PyTorch中,有一个优化器(Optimizer)的概念,具体的包为torch.optim
从加速梯度下降的角度进行优化:Momentum
从改进学习率的角度进行优化:RMSProp,AdaGrad和Adam。
1. SGD
在深度学习和PyTorch实践中,SGD就是所谓的mini-bacth梯度下降算法。
“随机梯度下降方法及其变种是深度学习中应用最多的优化方法”
2. Momentum
下面这篇博文图文并茂,言简意赅地介绍了动量法的含义以及其公式推导,读者可以参考。
《深度学习优化函数详解(4)-- momentum 动量法》 p.s. 下文中有关图片或推导过程部分来自这篇博文。
考虑SGD(mini-batch的随机梯度下降)的实际过程,其实就像是一辆匀速行驶的小车,每到一个关键节点,找到当前最优的行驶方向后继续匀速向下行驶。
但是很多人喜欢把梯度下降的过程比喻成一个小球从山顶往山谷滚动,因为小球具有了速度和加速度,所以在滚动的过程中,小球的速度会越来越快,加速冲向山谷。
用数学来模拟这段物理过程,如下:
即——算法在更新模型参数时,对于那些当前的梯度方向与上一次梯度方向相同的参数进行加强(也就是这些方向上更快);对于那些当前的梯度方向与上一次梯度方向不同的参数进行削减(也就是在这些方向进行减缓)。
正因为此,动量(Momentum)方法可以获得更快的收敛速度与减少扰动。
在PyTorch中,通过调用torch.optim.SGD
来实现动量方法,这里要注意SGD和动量方法的调用是同一个函数,依靠参数momentum进行区分。
class torch.optim.SGD(params,lr = <objectobject>,momentum = 0,
dampening = 0,weight_decay = 0,nesterov = False)
- params:用于优化的迭代次数
- lr:学习率,默认为1e-3
- momentum:动量因子,用于动量梯度下降算法,默认为0
- dampening:抑制因子,用于动量算法,默认为0
- weight_decay:权值衰减系数,L2系数,默认为0
- nesterov:nesterov动量方法使能
3. AdaGrad
学习率是SGD中一个关键的但是又难以设置的参数,对于神经网络模型有较大影响。
因此——如何自适应地设置模型参数的学习率是深度学习的研究方向之一。
p.s.可参考博文《深度学习优化函数详解(6)-- adagrad》
AdaGrad算法,根据每个参数的所有梯度历史平方值总和的平方根,成反比地缩放参数,以此独立地调整所有模型参数的学习率。
AdaGrad算法只在某些深度学习模型上表现不错,从训练开始时积累的梯度平方会导致有效学习率过早和过量减小。
从而:损失最大偏导的参数相应地有一个快速下降的学习率,损失较小偏导的参数在学习率上的下降幅度相对较小。
在PyTorch中通过调用torch.optim.Adgrad
函数使用AdaGrad方法
class torch.optim.Adagrad(params,lr = 0.01,lr_decay = 0,
weight_decay = 0)
- params:用于优化的迭代参数
- lr:学习率,默认为1e-3
- lr_decay:学习率衰减因子,默认为0
- weight_decay:权值衰减系数,L2参数,默认为0
4. RMSProp
AdaGrad在凸函数中可以快速收敛,但实际神经网络的损失函数难以满足这个条件。
Hilton将AdaGrad中的梯度平方计算方式修改成指数衰减平均,从而产生了RMSProp方法。
该方法因为使用了指数衰减平均,丢弃了遥远过去的历史,可以避免学习率下降过快的问题。目前其实深度学习从业者常采用的优化方式之一。
在PyTorch中,通过调用torch.optim.RMSProp
函数来实现RMSProp方法
class torch.optim.RMSProp(params,lr = 0.1,alpha = 0.99,eps = 1e-08,
weight_decay = 0,momentum = 0,centered = False)
- params:用于优化的迭代参数
- lr:学习率,默认为1e-3
- momentum:动量因子,默认为0
- alpha:平滑常量,默认为0.99
- eps:添加到分母的因子,用于改善分子稳定性
- centered:如果为真,则计算中心化的RMSProp,梯度根据它的方差进行归一化
- weight_decay:权值衰减系数,L2系数,默认为0
5. Adam
Adam可以视作是动量方法和RMSProp方法的结合版:
在Adam中,动量并入在梯度一阶矩的估计中;
而且Adam中还包括偏置修正,修正从原点初始化的一阶矩和二阶矩的估计。
经过修正后的偏置进行矫正后,每一次迭代学习率都会有一个确定的范围,从而使得参数比较平稳。
在PyTorch中,Adam方法调用torch.optim.Adam
class torch.optim.Adam(params,lr = 0.001,betas = (0.9,0.999),
eps = 1e-08,weight_decay = 0)
上述各参数含义与之前相同,不再赘述,其中betas是用于计算梯度平均和平方的参数,注意其默认值。
6. 选择正确的优化算法
(1)具有学习率自适应的SGD算法
以上列举、讲述的优化算法(SGD,具有动量的SGD,RMSProp,AdaDelta,Adam)是很流行并且使用很高的算法。
这些具有学习率自适应的算法在实践中的使用效果很好:
- 算法健壮
- 如果数据特征很稀疏,那么使用学习率自适应的算法无需在迭代过程中对学习速率进行人工调整
- 具有更快的收敛速度且可以更好地应对一个更深或者更复杂的网络
(2)训练小trick
- 为了保证学习过程是无偏的,每次迭代中都要随机打乱训练集中的样本
- 在验证集上如果连续的多次迭代过程中损失函数不再显著地降低,应该提前结束训练
- 对梯度增加随机噪声可以增加模型的健壮性,这样会有更高的可能性跳过局部极值点并去寻找一个更好的极值点,适用于深层次的网络
(3)梯度下降与优化器的梳理
7. 优化器的使用实例
p.s. 代码是照着参考书籍自己敲了一遍,微微改动了一点印刷错误和版本区别。
笔者也是初学PyTorch,期望通过敲代码的方式熟悉这个模块。
'''
不同优化器的使用示例
'''
import torch
import torch.utils.data as Data
import torch.nn.functional as F
from torch.autograd import Variable
import matplotlib.pyplot as plt
import numpy as np
torch.manual_seed(1)
LR = 0.01
BATCH_SIZE = 20
EPOCH = 10
#生成数据
x = torch.unsqueeze(torch.linspace(-1,1,1500),dim = 1)
y = x.pow(3) + 0.1 * torch.normal(torch.zeros(x.size()))
#数据绘图
plt.scatter(x.numpy(),y.numpy())
plt.show()
#把数据转换为torch类型
torch_dataset = Data.TensorDataset(x, y)
loader = Data.DataLoader(dataset = torch_dataset,batch_size = BATCH_SIZE,shuffle = True,num_workers = 2)
#定义模型
class Net(torch.nn.Module):
def __init__(self):
super(Net,self).__init__()
self.hidden = torch.nn.Linear(1,20)#隐含层
self.predict = torch.nn.Linear(20,1)#输出层
def forward(self,x):
#pdb.set_trace()
x = F.relu(self.hidden(x))#定义隐含层的激活函数
x = self.predict(x)#线性输出
return x
#不同的网络模型
net_SGD = Net()
net_Momentum = Net()
net_RMSProp = Net()
net_AdaGrad = Net()
net_Adam = Net()
nets = [net_SGD,net_Momentum,net_RMSProp,net_AdaGrad,net_Adam]
#不同的优化器
opt_SGD = torch.optim.SGD(net_SGD.parameters(),lr = LR)
opt_Momentum = torch.optim.SGD(net_Momentum.parameters(),lr = LR,momentum = 0.8)
opt_AdaGrad = torch.optim.Adagrad(net_AdaGrad.parameters(),lr = LR)
opt_RMSProp = torch.optim.RMSprop(net_RMSProp.parameters(),lr = LR,alpha = 0.9)
opt_Adam = torch.optim.Adam(net_Adam.parameters(),lr = LR,betas = (0.9,0.99))
optimizers = [opt_SGD ,opt_Momentum,opt_AdaGrad,opt_RMSProp,opt_Adam]
loss_func = torch.nn.MSELoss()
losses_his = [[],[],[],[],[]]#用于记录loss用
#模型训练
for epoch in range(EPOCH):
print('Epoch: ',epoch)
for step,(batch_x,batch_y) in enumerate(loader):
b_x = Variable(batch_x)
b_y = Variable(batch_y)
for net,opt,l_his in zip(nets,optimizers,losses_his):
output = net(b_x)#得到前向计算的结果
loss = loss_func(output,b_y)#计算损失值
opt.zero_grad()#梯度清零
loss.backward()#后向算法,计算梯度值
opt.step()#运用梯度
l_his.append(loss.data.item())#记录loss值
labels = ['SGD','Momentum','AdaGrad','RMSProp','Adam']
for i,l_his in enumerate(losses_his):
plt.plot(l_his,label = labels[i])
plt.legend(loc = 'best')
plt.xlabel('Steps')
plt.ylabel('Loss')
plt.ylim((0,0.2))
plt.show()
数据集可视化:
不同优化器收敛可视化比较:
正则化
深度神经网络在训练中主要有两个方面的思路的优化:
其一就是从梯度下降算法的角度,关于这一点我们前面介绍了很多优化器
其二就是提高模型的泛化能力,可以通过正则化措施来实现。
其中,泛化能力就是模型既能在训练集上表现良好,又能在测试集上表现良好
1. 欠拟合与过拟合
- 欠拟合:模型训练不足,在训练集上的loss值已经较大。
- 过拟合:模型将数据的扰动也学习进去了,在训练集上表现很优越但是在验证集上表现较差。
正则化就是在欠拟合和过拟合问题中保持平衡的方法之一。
- 正则化:在目标函数中引入额外的信息来惩罚过大的权重参数,建立一个新的优化函数J(θ)+λR(W)
J(θ):用于训练神经网络模型在训练数据上表现是否良好的目标函数
λR(W):正则化项
λ:正则化系数,λ∈[0,∞]
p.s. 在深度学习中,参数包括每一层的权重和偏置项,但是通常只对权重项进行正则化惩罚,而不对偏置项进行处理。
2. 参数规范惩罚
(1)L2参数正则化
L2又称权值衰减,只针对权值w,不针对偏置项b。
正则化定义:
正则化作用:
可以使得权值w变小,这也是“权值衰减”的名字由来。
过拟合的时候,在某些小区间内,函数值的变化很剧烈,这就说明在小区间内函数有较大的导数值。
通过减小权值系数的方式,可以减小小区间内函数导数值的大小,从而使得函数曲线趋于平滑。
在某种程度上可以减少过拟合的情况。
(2)L1参数正则化
正则化定义:
正则化作用:
产生更加稀疏的解,稀疏性可用于特征选择机制。
(3)PyTorch实现
只对L2参数正则化进行了实现,没有实现L1正则化。
在torch.optim
中可调用的优化器函数中,weight_decay
参数就是L2正则化。
3. Batch Normalization(批标准化)
关于BN层的原理,通过以下博文进行了大致了解,有些细节依然存疑,但这不是本篇博文的重点,留待下次笔者再详细探究。
《深度学习(二十九)Batch Normalization 学习笔记》
一言以蔽之,BN层是在层与层之间新加入的一层网络结构,利用隐含层输出结果的均值和方差来标准化每一层特征的分布,以解决在模型训练期间数据分布会发生变化的问题。
(1)神经网络训练过程中的问题:
- 需要我们人为选择参数,比如学习率、参数初始化、权重衰减系数、Drop out比例等
- 数据分布影响模型的泛化能力
在神经网络训练开始前,都要对输入数据做一个归一化处理;
原因在于神经网络学习过程本质就是为了学习数据分布,一旦训练数据与测试数据的分布不同,那么网络的泛化能力也大大降低。
- 数据分布影响模型训练速度
①一旦每批训练数据的分布各不相同(batch 梯度下降),那么网络就要在每次迭代都去学习适应不同的分布,这样将会大大降低网络的训练速度
②只要网络的前面几层发生微小的改变,那么后面几层就会被累积放大下去。
一旦网络某一层的输入数据的分布发生改变,那么这一层网络就需要去适应学习这个新的数据分布,所以如果训练过程中,训练数据的分布一直在发生变化,那么将会影响网络的训练速度。
(2)BN层引入的优势
- 使得模型训练收敛的速度更快
- 提高模型泛化能力
- 使得模型隐层的输出特征分布更加稳定,利于模型的学习
(3)BN算法概述
①神经元的归一化与重构
先求均值和标准差,每一个原始输出进行归一化操作。
再利用γ和β系数对归一化后的值进行重构。
【原因】
如果是仅仅使用上面的归一化公式,对网络某一层A的输出数据做归一化,然后送入网络下一层B,这样是会影响到本层网络A所学习到的特征的。
相当于把一个任意可能的分布强行拉成标准正态分布。
于是我们想到再进行变换重构:
②BN层+激活函数层的前向传播
BN层通常放在激活函数层之前。
(4)PyTorch实现
在PyTorch中,有相应的类对BN层进行封装。
class torch.nn.BatchNorm1d(num_features,eps = 1e-05,momentum = 0.1,affine = True)
class torch.nn.BatchNorm2d(num_features,eps = 1e-05,momentum = 0.1,affine = True)
class torch.nn.BatchNorm3d(num_features,eps = 1e-05,momentum = 0.1,affine = True)
以上类别分别针对小批量的2d,3d输入进行批标准化。
-
num_features
:来自期望输入的特征数 -
eps
:为保证数值稳定性,给分母加上的数值 -
momentum
:动态均值和动态方差所使用的动量 -
affine
:布尔值,为真时说明给该层添加可学习的仿射变换参数
4. Dropout
(1)思想
Dropout是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃,这样可以让模型更加健壮——不会太依赖于局部的特征(局部的特征可能会被丢弃)。
因为是随机丢弃,所以每一个小批量都在训练不同的网格。
Dropout使得一个全连接的网络结构变成稀疏连接的网络结构。
(2)步骤
在实践中,通常把神经元的输出设置为0来“关闭”神经元。
- 建立一个维度与本层神经元相同的矩阵D
- 根据概率p将D中的元素设置为0
设置为0的神经元表示神经元失效,不再参与后续计算
- 将本层激活函数的输出与D相乘作为新的输出值
- 新的输出再除以p,保证训练和测试满足同一分布
(3)PyTorch实现
class torch.nn.Dropout(p = 0.5,inplace = False)
class torch.nn.Dropout2d(p = 0.5,inplace = False)
- p:将元素置为0的概率
- inplace:若设置为True,则直接对input进行处理。