1. 写在前面
最近在补ML和DL的相关基础,发现有些非常重要的知识还是了解的太表面,甚至可以说不知其然也不知其所以然了,所以这段时间想借着找工作的这个机会,通过学习一些优秀的文章和经典书籍,来慢慢的把这块短板也补上来。
今天是深度学习优化算法这块的一个总结,在深度学习中,影响深度深度神经网络训练效果或者说可以加速神经网络训练的重要策略一般有:
合适的初始化策略: 如果初始化的不好,有可能导致网络没法最终收敛或者中途无法训练(比如初始化0就成了线性,不适当的参数初始化还会导致梯度消失或者爆炸等), 这个之前在pytorch初始化那里都整理过了。
合适的激活函数: 激活函数的引进给网络带来了非线性,使得网络有更加强大的拟合能力,但是激活函数也不是随便用的,常用的有Sigmoid,tanh,Relu及它的一些变体,而这些激活函数各有各自的特点和应用场景,比如Sigmoid,tanh这种,往往适合做概率值处理或者LSTM的门控机制,而Relu这种,就适合深层网络的训练等等,这个后面也会写一篇文章统一整理下。
批量归一化策略: Batch Normalization前面写了一篇文章整理过,非常的强大和实用,有了它,可以选择更大的学习率,加速网络训练,可以少考虑初始化带来的烦恼,少考虑使用正则的烦恼,解决了网络隐层层输出的数据分布偏移问题等,more powerful的一个东西,具体可以看这篇
正则化技术: 这个一般是为了防止网络的拟合能力太强带来的过拟合问题,在深度学习网络中,常用的正则化技术除了L2,L1这种,还有Dropout,包括上面的那个。 后面有时间,也会补上深度学习的正则化这一块。
优化算法: 这个也是非常重要的一环,也是我这篇文章想要总结的内容。
优化算法可谓是非常重要一个环节,决定着算法模型是否能够找到最优解或者快速的找到最优解,讨论的是模型在参数更新的时候,怎么去更新的问题。这次主要是先整理梯度下降以及从这个基础上衍生出来的改进算法,花书上这部分的内容太多,还有牛顿法,拟牛顿法这些涉及到二阶导数的家族,有些来不及看,后面再慢慢进行补充。 先整理一些耳熟能详但又不是那么清楚的一些。
依然是几个面试题打头:
训练数据量特别大时,经典的梯度下降法存在什么问题,如何改进?
随机梯度下降存在的问题以及改进的两大方向?
介绍下常用的那些优化算法(SGD,Momentum,Nesterov,AdaGrad, RMSProp, AdaDelta, Adam)以及它们的参数更新原理,解决了SGD的什么问题?
2. 梯度下降
首先,先再快速的回顾下啥叫梯度下降, 微积分中,使用梯度表示函数增长最快的方向,因此,神经网络中使用负梯度来指示目标函数下降最快的方向。为啥这个能表示目标函数下降最快的方式呢? 之前也整理过,我们可以把目标函数泰勒展开,会发现当参数更新方向与梯度方向相反的时候,更新速度是最快的,具体可以参考我之前这篇文章
- 梯度实际上是损失函数对网络中每个参数的偏导所组成的向量;
- 梯度仅仅指示了对于每个参数各自增长最快的方向,因此,梯度无法保证全局方向就是函数为了达到最小值应该前进的方向;
- 使用梯度的具体计算方法即反向传播。
梯度下降也是一种优化算法, 通过迭代的方式寻找使模型目标函数达到最小值时的最优参数, 当目标函数为凸函数的时候,梯度下降的解是全局最优解,但在一般情况下,梯度下降无法保证全局最优。
梯度下降法最常用的形式是批量梯度下降(Batch Gradient Descent, BGS), 其做法就是在更新参数的时候使用所有的样本来进行更新。
一般是采用所有训练数据的平均损失来近似目标函数:
优点:由于每一步迭代使用了全部样本,因此当损失函数收敛过程比较稳定。对于凸函数可以收敛到全局最小值,对于非凸函数可以收敛到局部最小值。
问题:经典的梯度下降法在每次对模型参数更新时,需要遍历所有的训练数据,M MM很大时,需要耗费相当大的计算资源,实际应用中基本不可行,不能投入新数据实时更新模型。
3. 随机梯度下降
随机梯度下降(Stochastic Gradient Descent, SGD)是用单个训练样本的损失来近似平均损失,即
因此,随机梯度下降法用单个训练数据即可对模型参数进行一次更新,大大加快了收敛速率,该方法也适用于数据源源不断到来的在线更新场景。
问题:随机梯度下降每步仅仅采样一个或者少量样本来估计当前梯度,计算速度快,内存开销少,但每一步接受的信息量有限,常常对梯度的估计出现偏差,造成目标函数曲线收敛的很不稳定,伴有剧烈波动,有时候甚至不收敛。
为了降低随机梯度的方差,从而使得迭代算法更加稳定,也为了充分利用高度优化的矩阵运算操作,实际应用中,会同时处理若干训练数据,这就是小批量梯度下降(mini-batch Gradient Descent)。目标函数和梯度如下:
m表示一个批次里面的样本个数。 这个也是目前常用的方式(注意这里是在训练样本上个数的选择), 但是在用的时候要注意三个问题:
如何选取参数m : 不同应用中, 最优的m 通常会不一样,往往需要调参确定。 一般m 取2的幂次时能充分利用矩阵运算操作。 所以可以在2的幂次中选取最优值,32,64,128等,如果感觉内存比较小,可以小一点。
如何选择m 个训练数据? 为了避免数据的特定顺序对收敛带来的影响,一般在每次遍历训练数据之前,先对数据随机打乱,然后每次迭代时,按顺序挑选m 个训练数据。 也就是每个epoch的时候都会随机打乱一次训练集。
如何选取学习速率α \alphaα: MBGD不能保证很好的收敛性,如果学习率太小,收敛速度会很慢,如果太大,损失函数就会在极小值出不停的震荡甚至偏离,所以通常用衰减学习率的方案: 一开始采用较大学习率,当误差曲线进入平台期后,减小学习率做更精细的调整。 最优的学习速率也通常需要调参。
所以,MBGD是解决训练数据量过大的一个不错的方案, 而SGD更适用于在线的实时更新,BGD一般很少用于实际中。
But,上面我说了,这是在讨论更新的时候,一次用多少样本更新合适,我们说一般常用的就是SGD(一次拿一个样本来更新参数)或者是MBGD(一次拿几个样本来更新参数)。但是存在的问题依然也是比较明显的,因为这毕竟是拿准确性去换速度嘛。
SGD放弃了梯度的准确性, 仅采用一部分样本或者一个样本来估计当前梯度,因此SGD会出现对梯度估计常常出现偏差,造成目标函数收敛不稳定甚至不收敛的情况。BGD是能保证准确性的,毕竟每一步都是载入整个训练集。
无论是经典的梯度下降还是随机梯度下降,都可能陷入局部极值点。这个对于SGD来说,还不是最要命的,毕竟这个现象普遍存在,这种情况可以随机初始化参数,多训练几遍模型试试看。
对SGD来说,最要命的是SGD可能会遇到“峡谷”和“鞍点”两种困境
峡谷类似⼀个带有坡度的狭长小道,左右两侧是 “峭壁”;在峡谷中,准确的梯度方向应该沿着坡的方向向下,但粗糙的梯度估计使其稍有偏离就撞向两侧的峭壁,然后在两个峭壁间来回震荡。
鞍点的形状类似⼀个马鞍,⼀个方向两头翘,⼀个方向两头垂,而中间区域近似平地;⼀旦优化的过程中不慎落入鞍点,优化很可能就会停滞下来(坡度不明显,很可能走错方向,如果梯度为0的区域,SGD无法准确察觉出梯度的微小变化,结果就停下来)。为了形象,还找了个图:
所以接下来的一些算法,就是针对于SGD的这两个要命问题进行的一系列改进了,首先先把握住改进的两个大方向: 惯性保持和环境感知
惯性保持: 加入动量, 代表:Momentum, Nesterov Accerlerated Gradient
环境感知: 根据不同参数的一些经验性判断, 自适应的确定每个参数的学习速率,这是一种自适应学习率的优化算法。代表:AdaGrad, AdaDelta, RMSProp
还有把上面两个方向结合的: Adam, AdaMax, Nadam
这样就把常用的优化算法给分类梳理完毕。 下面就看看这两个大方向是怎么改的,又是解决SGD的什么问题? 介绍具体的优化算法了。
在介绍之前,我们先回顾下随机梯度下降算法中的参数更新公式,因为下面的算法都是在这个公式上进行的一些改进。
4. 改进方向一:惯性保持
4.1 动量(Momentum)方法
动量方法解决的问题:
- 随机梯度下降中的“峡谷”和“鞍点”问题
- SGD加速,特别是高曲率,小幅但是方向一致,或者带噪声的梯度
《百面机器学习》上的那个比喻很形象:
如果把原始的 SGD 想象成⼀个纸团在重力作用向下滚动,由于质量小受到山壁弹力的干扰大,导致来回震荡。或者在鞍点处因为质量小速度很快减为 0,导致无法离开这块平地。动量方法相当于把纸团换成了铁球。不容易受到外力的干扰,轨迹更加稳定,同时因为在鞍点处因为惯性的作用,更有可能离开平地。
动量算法积累了之前梯度指数级衰减的移动平均,并且继续沿该方向移动,参数更新公式:
所以当前迭代点的下降方向不仅仅取决于当前的梯度,还受到前面所有迭代点的影响。动量方法以一种廉价的方式模拟了二阶梯度(牛顿法)。撤了这么半天理论,拿个图来看看效果:
使用动量的SGD算法流程:
4.2 NAG算法
Nesterov Accelerated Gradient 提出了⼀个针对动量算法的改进措施。动量算法是把历史的梯度和当前的梯度进进行合并,来计算下降的⽅向。而Nesterov 提出,让迭代点先按照历史梯度走⼀步,然后再合并。更新规则如下,改变主要在于梯度的计算上:
1 | optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, |
完整的Nesterov动量算法流程:
5. 改进方向二:环境感知
由于SGD中随机采样Batch会引入噪声源,在极小点处的梯度并不会小时。 因此,随着梯度的降低,有必要逐步减小学习率。
SGD对环境的感知是指在参数空间中,根据不同参数的一些经验性判断,自适应的确定参数的学习速率,不同参数的更新步幅是不同的。
5.1 AdaGrad算法
Adaptive Gradient算法的思想是独立地适应模型的每个参数(自动改变学习速率):一直较大偏导的参数相应有一个较小的学习率,初始学习率会下降的较快;而一直小偏导的参数则对应一个较大的学习率,初始学习率会下降的较慢。具体来说,每个参数的学习率会缩放各参数反比于其历史梯度平方值总和的平方根,更新公式:
问题:历史梯度在分母上的累积会越来越大, 所以学习率会越来越小, 使得中后期网络的学习能力越来越弱。
经验上已经发现,对于训练深度神经网络模型而言,从训练开始时积累梯度平方会导致有效学习率过早和过量的减小。AdaGrad 在某些深度学习模型上效果不错,但不是全部。
具体的算法流程:
5.2 RMSProp
RMSProp 是 Geoff Hinton 提出的一种自适应学习率方法。RMSprop 和 Adadelta都是为了解决Adagrad 学习率过度衰减问题的。AdaGrad 根据平方梯度的整个历史来收缩学习率,可能使得学习率在达到局部最小值之前就变得太小而难以继续训练。RMSProp 算法修改 AdaGrad 以在非凸设定下效果更好,改变梯度积累为指数加权的移动平均。
AdaGrad 旨在应用于凸问题时快速收敛。当应用于非凸函数训练神经网络时,学习轨迹可能穿过了很多不同的结构,最终到达一个局部是凸碗的区域。AdaGrad 根据平方梯度的整个历史收缩学习率,可能使得学习率在达到这样的凸结构前就变得太小了。RMSProp 使用指数衰减平均以丢弃遥远过去的历史,使其能够在找到凸碗状结构后快速收敛,它就像一个初始化于该碗状结构的 AdaGrad 算法实例。
RMSProp类似于Momentum中的做法,与Momentum的效果一样,某一维度的导数比较大,则指数加权平均就大,某一维度的导数比较小,则其指数加权平均就小,这样就保证了各维度导数都在一个量级,进而减少了摆动。允许使用一个更大的学习率。
RMSProp 还加入了⼀个超参数 ρ 用于控制衰减速率:
优点:相比于AdaGrad,这种方法更好的解决了深度学习中过早的结束学习的问题,适合处理非平稳目标,对RNN效果很好。
缺点: 引入了新的超参衰减系数ρ
1 | optimizer = tf.train.RMSPropOptimizer(learning_rate=learning_rate, |
经验上,RMSProp已被证明是一种有效且实用的深度神经网络优化算法,目前经常采用。采用动量的RMSProp算法:
5.3 AdaDelta算法
6. Adam算法(Adaptive Moment Estimation)
Adam算法之所以单独写出来,是因为它将惯性保持和环境感知这两个优点集于一身。
Adam记录梯度的一阶矩,即过往梯度与当前梯度的平均,体现了保持惯性—历史梯度的指数衰减平均
Adam记录梯度的二阶矩, 即过往梯度平方与当前梯度平方的平均,RMSProp的方式,体现了环境感知的能力,为不用参数产生自适应的学习速率 — 历史梯度平方的指数衰减平均。
一阶矩和二阶矩类似于滑动窗口内求平均的思想进行融合,即当前梯度和近一段时间内的梯度平均值,时间久远的梯度为当前平均值贡献呈指数衰减。参数更新方式如下:
1 | optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate) |
Adam算法流程如下:
在训练深度神经网络模型时,Adam 优化算法可以被优先选择,它通常比其他优化算法快并且效果好;Adam 算法有三个参数,一般使用默认的参数就可以了,但是如果需要调整的话,建议熟悉一下它的理论,然后根据实际情况设置参数。
为了使得内容更加的完整,这里再来了解一点新的东西。
7. 理解更多的细节
7.1 理解指数滑动平均
上面的很多优化算法里面都见到了一个词叫做指数滑动平均,这个东西到底是干嘛的?这里理解下这个,便于更好的理解上面的改进算法。
指数加权平均在时间序列中经常用于求取平均值的一个方法,它的思想是这样,我们要求取当前时刻的平均值,距离当前时刻越近的那些参数值,它的参考性越大,所占的权重就越大,这个权重是随时间间隔的增大呈指数下降,所以叫做指数滑动平均,公式如下:
看上面这个温度图像,横轴是第几天,然后纵轴是温度, 假设我想求第100天温度的一个平均值,那么根据上面的公式:
可以发现,如果这个β 很高, 比如0.98, 最终得到的温度变化曲线就会平缓一些,因为多平均了几天的温度, 缺点就是曲线进一步右移, 因为现在平均的温度值更多, 要平均更多的值, 指数加权平均公式,在温度变化时,适应的更缓慢一些,所以会出现一些延迟,因为如果β =0.98,这就相当于给前一天加了太多的权重,只有0.02当日温度的权重,所以温度变化时,温度上下起伏,当β 变大时,指数加权平均值适应的更缓慢一些, 换了0.5之后,由于只平均两天的温度值,平均的数据太少,曲线会有很大的噪声,更有可能出现异常值,但这个曲线能够快速适应温度的变化。 所以这个β 过大过小,都会带来问题。 一般取0.9.
这样,就可以发现,当前梯度的更新量会考虑到当前梯度, 上一时刻的梯度,前一时刻的梯度,这样一直往前,只不过越往前权重越小而已。这就是动量的含义了,考虑了之前的梯度保持着一种惯性。而像RMSProp里面的历史梯度平方的指数加权衰减,AdaDelta里面的,Adam里面的指数加权等,其实都是这个意思,考虑前面的梯度或者梯度的平方,求一个平均值来更新当前参数。
7.2 理解下指数加权平均的偏差修正
偏差修正可以让平均数运算更加准确, 看看是怎么做的。这里拿吴恩达老师深度学习的一个例子:
所以预测的初始阶段,才开始预测的热身练习,偏差修正可以帮助更好的预测温度,后期的话热身过去了,偏差修正就起不到作用了
7.3 理解里面的超参数选择
上面各个优化算法中,常用的是Adam,SGD,RMSProp等, 这些优化算法中,有两个比较重要的超参数需要进行调参确定,就是学习率ϵ 和指数衰减系数ρ , 而我们如何为这两个参数选择合适的范围呢?
超参数的随机取值可以提高搜索效率,但是这个随机取值可不是乱随机的,而是应该选择合理的范围,在某个范围内取值,而选择合理范围就需要选择合适的标尺,用于探究这些超参。这个很重要。
假设我们选取的是隐藏单元这种或者神经网络层数这种,那么我们可以确定一个大致的范围,比如隐藏单元50-100,然后在这个范围随机取点尝试, 隐藏层2-8, 从这里面随机取层试验,这两个超参数这样取值是合理的。 但是上面的那两个超参确定范围不能这么玩。
假设我们已知一个学习率的范围是0.00011, 但是我们如果想在这个范围内取比较好的值,就不能像上面那个一样,在这个区间随机的取值了。因为如果用上面这种方式随机的取值,我们会发现90%的数据都落到 0.11之间。
这是因为当ρ 接近1时,所得结果的灵敏度会变化,即使ρ 有很小的变化,如果ρ 在0.9-0.9005之间取值,无关紧要,但ρ 在0.999-0.9995之间取值,这会对算法产生巨大影响,当ρ 接近1,β的细微变化变的很敏感,所以整个取值过程中,需要更加密集的取值,在ρ 接近1的区间内。
就是说虽然还是随机取值,但是不能随便取了,因为这个关系着算法,和上面的ϵ 一样,虽然可以在线性数轴上取值,但是那样这个ϵ 的一个小波动就可能对算法产生很大的影响,即每个点的影响不同,至少要加点权那样子,所以不能随便取。
这个也是比较重要的一个点了。
8. 如何选择优化算法
这里是回答怎么选择优化算法的问题了,《深度学习》里面指出目前这个没有一个确定的定论,如何选,取决于做的任务以及对优化算法的熟练程度。这里整理一个常用参考标准,当然也不是定论:
- 对于稀疏数据,尽量使用学习率可自适应的优化方法,不用手动调节,而且最好采用默认值。
- SGD通常训练时间更长,但是在好的初始化和学习率调度方案的情况下(很多论文都用SGD),结果更可靠。并且适用于在线的实时更新,推荐里面可是常用
- 如果在意更快的收敛,并且需要训练较深较复杂的网络时,推荐使用学习率自适应的优化方法。
- Adadelta,RMSprop,Adam是比较相近的算法,在相似的情况下表现差不多 。Adam 就是在 RMSprop 的基础上加了 bias-correction 和 momentum,随着梯度变得稀疏,Adam 比 RMSprop 效果会好。整体来讲,Adam 是最好的选择。
PS: 上面的这些优化器,在TensorFlow或者pytorch里面都有包已经集成好了,我们可以直接拿来用。
下面一张动图看看各个优化器的效果啦, 放松下 😉
参考:
- 《深度学习》 – 花书
- 《百面机器学习》
- 深度学习中的优化问题以及常用优化算法
- 深度模型中的优化
- 深度学习中的优化算法总结
- 系统学习Pytorch笔记七:优化器和学习率调整策略
- 吴恩达老师的《深度学习课程》