写在开头:这里主要要记录在平时学习中遇到的一些问题与学习的一些东西,整体的风格按照参考文献的大佬进行。
内容介绍
今天主要对momentum、NesterovMomentum、AdaGrad、RMSprop以及Adam是什么进行讲解,并对这些和比较新的AdaBound算法结果进行对比,通常会使用梯度下降来得到神经网络的参数,但是有时为了加速优化收敛,需要使用优化方法。今天就将对笔者在最近学习深度学习中遇到的优化方法,进行一个分享。如果有感兴趣的朋友想了解这几种优化方法的一些基础核心内容,可以点击这里查看指数加权平均和带偏差修正的指数加权平均。
开始分享
1.momentum
当笔者第一次使用Torch框架的时候,在调用optim.SGD时意外发现了一个参数,没错这个参数就是momentum,在一度以为SGD就是我接触的最棒梯度下降时解决的办法时,笔者对momentum的出现产生了好奇,并在Torch实操案例MNIST中添加了关于momentum的说明,在使用SGD的时候我们不难发现下降的速度比较的慢,而且从理论上来看容易遇到鞍点,就是一个局部最小点,放在一个二维来说就是如下图一样,(图片来自李宏毅《一天搞懂深度学习》),
那么这个时候怎么办,加入动量,能够帮提高我们的效率,特别是对于高曲率、小但一致的梯度,或者噪声比较大的梯度能够很好的加快学习过程。动量就是一个对于之前的速度与当前的加速度加权考虑的一个综合平均值,能够有效的保持当前的一个速度,对冲当前加速度的影响。
比较好理解的解释就是,加入动量,能够保持原有趋势,在原有趋势上,调整更新的步幅。如果梯度方向与动量方向相同则加强这个方向的步幅,反之则进行削弱。其具体算法过程,如下(参考《deep learning》):
在更新速度的时候,对速度也就是动量减去负梯度,相当于就是负梯度更新与动量更新的均衡。从torch的源码中的解释可以看到,动量的SGD的定义,笔者会在文章的最后通过代码来展示各个方法不同的效果。
Considering the specific case of Momentum, the update can be written as
.. math::
v_{t+1} = \mu * v_{t} + g_{t+1} \\
p_{t+1} = p_{t} - lr * v_{t+1}
where p, g, v and :math:`\mu` denote the parameters, gradient,
velocity, and momentum respectively.
2.NesterovMomentum
既然有了Momentum的方法为什么还需要一个Nesterov的动量方法呢?我们给出一张示意图,从图上可以看到,左边就是我们的普通的动量学习法,右边就是Nesterov的方法,如果从结果出发我们可以理解为修正了动量的改进方向。再来看一看算法进行理解,
与动量算法不同的是,该方法先对参数进行一个临时的更新,也就是站在近似的更新点上来计算梯度,相当于就是跨了一步走。但并不能证明提高了收敛率。
在cs231n中告诉我们在实践中人们我那个网更习惯于使用参数只在最后进行更新,就像前面的动量一样,于是我们可以得到一个新的计算方法,
将上述公式展开写就是
这个地方的证明是借鉴的这里,
首先有
我们定义第一步的向量近似更新步骤,
则代入动量的更新式中得到,
于是有,
最后将
即可得到,
通过这个公式我们就可以简单的计算参数,而不用计算整个损失函数再去求导,直接用对应参数的损失函数的导数就可以求得参数更新。
3.AdaGrad
学习率难以设置的问题体制都存在,学习率对模型的性能有显著的影响。损失通常会对参数空间中的某些方向高度敏感。动量算法可以在一定程度上缓解这些问题,但这样做的代价是引入了另一个超参数。于是有人就想可以尝试着每个参数设置不同的学习率,来适应这样一个敏感度,有一些增量或者说小批量的算法来自适应模型参数的学习率。
AdaGrad独自适应所有模型参数的学习率,缩放每个参数反比与其他所有梯度历史平方值总和的平方根首先我们来看看AdaGrad算法的步骤,
在给定参数后,首先对m个样本计算梯度,然后计算梯度的内积并累加到之前的和上,然后对累加和开平方再求倒数,这里的
是保证累计梯度不至于太小为0,这样的设计,我们可以理解一下,如果在累加和中参数比较大的,那么倒数后就比较小,那么就会尽量小的更新参数,反之会更大的更新参数,做到了对不同的参数使用不同的学习率进行更新。
但这个模型仍然存在一些问题就是累计梯度平方会使得学习效率过早或过量的减小,并且AdaGrad在应用凸问题时快速收敛,当用于非凸函数神经网络时,可能就不太行。根据整个平方根历史收缩学习率,可能使得学习率在达到这样图结构前就变得太小了。
4.RMSProp
为了解决AdaGrad过早的让学习率变得太小的问题,就设想缩小累加和的影响,于是可以联想到当的梯度权重增加,对往期的累加和权重缩小的办法,利用加权来衰减往期的影响。具体算法如下,
RMSProp算法就在累计梯度的时候加入了
来不断衰减之前的影响。在实际用法中,RMSProp被证明是一种有效且使用的深度神经网络优化算法,也是比较常用的。当然还可以将Nesterov与RMSProp进行结合,在参数求梯度前进行模拟更新,具体算法如下,在这个方法似乎不是很常用。
5.Adam
Adam更像是综合了Momentum和RMSProp的一种学习率自适应优化算法。在Adam中对于有偏一阶矩估计来说就是Momentum的速度更新部分,有偏二阶矩估计就是我们的RMSProp的部分,然后对两个参数进行修正,并得到一个综合的更新结果,然后来更新参数。这些都是比较好理解的。
6.如何选择?
目前还是没有一个最终的结论,但是根据Schaul et al在2014年得到的结论是RMSProp和AdaDelta(这里没有讲AdaDelta,其实是对AdaGrad的更新,日后会继续了解)鲁棒性良好,目前至于怎么使用主要取决于熟练程度,比如调参。在Ruder, S.在2016年得到的结论中认为RMSprop、Adadelta和Adam都是非常相似的算法,在类似的情况下表现得都很好,,并认为Adam因为存在修正所以比RMSProp变得更稀疏,Adam综合来看是个不错的选择。
7.程序结果对比
为了更加直观的了解到各个算法的一个运算速度,我们调用莫烦的代码,稍作修改来展示我们的结果,并且加入了2019提出的的梯度优化算法Adabound的测试,项目代码在下面
论文地址:https://openreview.net/pdf?id=Bkg3g2R9FX
个人主页版:https://www.luolc.com/publications/adabound/
GitHub地址:https://github.com/Luolc/AdaBound
原文链接:https://blog.csdn.net/weixin_42147780/article/details/88092725
Adabound作者说的是有Adam的收敛速度,有SGD的收敛效果和鲁棒性,对于超参数的敏感度不高。Adam如果超参数小于0.001或者说大于某个极端值会使得很难收敛,而Adabound就解决了这样一个问题。
整个的代码逻辑是1.加载包与参数;2.加载loader数据;3.构建网络;4.设计优化器与损失函数;5.编写训练函数;6.绘图。根据默认一般的参数设置,我们选择学习率为0.01
#加载包
import torch
import torch.utils.data as Data #用来处理数据batch
import torch.nn.functional as F #用来调用优化函数
import matplotlib.pyplot as plt
import adabound
#加载参数
learning_rate = 0.001
BATCH_SIZE = 32
EPOCH = 12
#创建数据
x = torch.unsqueeze(torch.linspace(-1, 1, 1000), dim=1)
y = x.pow(2) + 0.1*torch.normal(torch.zeros(*x.size()))
#观察数据
plt.scatter(x.numpy(), y.numpy())
plt.show()
#将数据进行扰乱分割后储存
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):
x = F.relu(self.hidden(x)) #Relu激活函数
x = self.predict(x)
return x
# 定义实操
if __name__ == '__main__':
#定义不同的优化函数网络名
net_SGD = Net()
net_Momentum = Net()
net_NesterovMomentum = Net()
net_RMSprop = Net()
net_Adam = Net()
net_AdaBound = Net()
nets = [net_SGD, net_Momentum, net_NesterovMomentum, net_RMSprop, net_Adam, net_AdaBound]
#定义不同的优化器
opt_SGD = torch.optim.SGD(net_SGD.parameters(), lr=learning_rate)
opt_Momentum = torch.optim.SGD(net_Momentum.parameters(), lr=learning_rate, momentum=0.8)
opt_NesterovMomentum = torch.optim.SGD(net_NesterovMomentum.parameters(),lr=learning_rate, momentum=0.8, nesterov=True)
opt_RMSprop = torch.optim.RMSprop(net_RMSprop.parameters(), lr=learning_rate, alpha=0.9)
opt_Adam = torch.optim.Adam(net_Adam.parameters(), lr=learning_rate, betas=(0.9, 0.99))
opt_AdaBound = adabound.AdaBound(net_AdaBound.parameters(), lr=1e-3, final_lr=0.1)
optimizers = [opt_SGD, opt_Momentum, opt_NesterovMomentum, opt_RMSprop, opt_Adam, opt_AdaBound]
loss_func = torch.nn.MSELoss() #定义损失函数
losses_his = [[], [], [], [], [], []] #记录损失
# 训练函数
for epoch in range(EPOCH):
print('Epoch: ', epoch)
for step, (b_x, b_y) in enumerate(loader): #每一步进行训练
for net, opt, l_his in zip(nets, optimizers, losses_his):
output = net(b_x) #输出batch的结果
loss = loss_func(output, b_y) #计算损失
opt.zero_grad() #清除上一个batch的梯度
loss.backward() #向后传播计算梯度
opt.step() #梯度更新
l_his.append(loss.data.numpy()) #记录损失
labels = ['SGD', 'Momentum','NesterovMomentum', 'RMSprop', 'Adam', 'AdaBound']
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.3))
plt.show()
从图上我们可以看到,NesterovMomentum的效果最差,其次是Momentum,然后是SGD,这三个其实效果差不多,然后就是Adam和RMSProp,最后就是的AdaBound。这个只是这个数据集不具有说服力,比较简单,但也可以反映出Adam在学习率为极端值的时候收敛不行。后期有机会会看看AdaBound论文再做分享。
结语
好了今天的分享到此结束,大致了解了一下目前常见的一些优化算法,不过对于Adadelta和Adabnound会在后面学习了后再继续分享。
谢谢阅读。
参考
1.对于优化方法的总结
2.《deep learning》
3.Fei-Fei Li et al. 《CS231n Convolutional Neural Networks for Visual Recognition》