深度学习中优化方法――momentum、Nesterov Momentum、AdaGrad、Adadelta、RMSprop、Adam

匿名 (未验证) 提交于 2019-12-03 00:18:01
―momentum、Nesterov Momentum、AdaGrad、Adadelta、RMSprop、Adam―

        我们通常使用梯度下降来求解神经网络的参数,关于梯度下降前面一篇博客已经很详细的介绍了(几种梯度下降方法对比)。我们在梯度下降时,为了加快收敛速度,通常使用一些优化方法,比如:momentum、RMSprop和Adam等。这篇博客主要介绍:

  • 指数加权平均(Exponentially weighted average)
  • 带偏差修正的指数加权平均(bias correction in exponentially weighted average)
  • momentum
  • Nesterov Momentum
  • Adagrad
  • Adadelta
  • RMSprop
  • Adam

在介绍这几种优化方法之前,必须先介绍下 指数加权平均(Exponentially weighted average) ,因为这个算法是接下来将要介绍的三个算法的重要组成部分。


一、 指数加权平均(Exponentially weighted average)

        指数加权平均是处理时间序列的常用工具,下面用一个例子来引入指数加权平均的概念。下图是一个180天的气温图(图片来自ng Coursera deep learning 课):

如果我们想找一条线去拟合这个数据,该怎么去做呢。我们知道某一天的气温其实和前几天(前一段时间)相关的,并不是一个独立的随机事件,比如夏天气温都会普遍偏高些,冬天气温普遍都会低一些。我们用[θ1θ2...θn]表示第1,2,…,n天的气温,我们有:

根据上面的公式我们能够画出这条线,如下图所示(图片来自ng deep learning课):

下面来正式的看一下 指数加权平均(Exponentially weighted average) 的定义:

Vt=βVt1+(1β)θt

可以认为 Vt 近似是 11β 天的平均气温,所以上面公式设置了β=0.9,当 t>10 时,则可以认为 Vt 近似是它前10天的平均气温。
比如,按照上面的公式,我们计算 V100

V100=0.1θ100+0.9V99=0.1θ100+0.9(0.9V98+0.1θ99)=0.1θ100+0.90.1θ99+0.92V98=0.1θ100+0.90.1θ99+0.92(0.9V97+0.1θ98)=0.1θ100+0.90.1θ99+0.920.1θ98+0.93V97=0.1θ100+0.90.1θ99+0.920.1θ98+0.930.1θ97+0.94V96=....=0.1θ100+0.90.1θ99+0.920.1θ98+0.930.1θ97+....+0.9990.1θ1+0.9100V0

从上面的公式能够看出,实际上是个指数衰减函数。0.9100.351e,所以这个就解释了上面的 11β
(ps:关于这一点为什么要用0.35及1e,我是不太明白的,搜了下资料也没找到更好的资料,希望路过的有知道的大神,在下面评论交流。)
下面再看下 β 取不同值时,曲线的拟合情况(图片来自ng deep learning课):

从上图能够看出:

  • β 较大时(β=0.98 相当于每一点前50天的平均气温),曲线波动相对较小更加平滑(绿色曲线),因为对很多天的气温做了平均处理,正因为如此,曲线还会右移。
  • 同理,当 β 较小时(β=0.5 相当于每一点前2天的平均气温),曲线波动相对激烈,但是它可以更快的适应温度的变化。

下面直接看实现指数加权平均(Exponentially weighted average)的伪代码:

V0 = 0 repeat {     get next theta_t     V_theta = beta * V_theta + (1 - beta)* theta_t } 

关于指数加权平均的优缺点:β=0.9 时,我们可以近似的认为当前的数值是过去10天的平均值,但是显然如果我们直接计算过去10天的平均值,要比用指数加权平均来的更加准确。但是如果直接计算过去10天的平均值,我们要存储过去10天的数值,而加权平均只要存储Vt1一个数值即可,而且只需要一行代码即可,所以在机器学习中用的很多。


二、带偏差修正的指数加权平均(bias correction in exponentially weighted average)

        上一节中介绍了指数加权平均,用的公式是:

Vt=βVt1+(1βθt )

当我们令 β=0.98 时,我们想得到下图中的“绿线”,实际上我们得到的是下图中的“紫线”。对比这两条线,能够发现在“紫线”的起点相比“绿线”非常的低,举个例子,如果第一天的温度是40℃,按照上面的公式计算,V1=0.98V0+(10.98)θ1=0.980+0.0240=0.8,按照这个公式预估第一天的温度是0.8℃,显然距离40℃,差距非常大。同样,继续计算,第二天的温度也没有很好的拟合,也就是说 指数加权平均 不能很好地拟合前几天的数据,因此需要 偏差修正,解决办法就是,再令 Vt=Vt1βt。因此, 带偏差修正的指数加权平均(bias correction in exponentially weighted average) 公式为:

Vt=βVt1+(1βθt)Vt=Vt1βt

再来按照上述公式对前两天温度进行计算,V1=V11β1=0.810.98=40,能够发现温度没有偏差。并且当t 时,βt0,这样在后期 Vt 就和 没有修正的指数加权平均一样了。

在机器学习中,多数的指数加权平均运算并不会使用偏差修正。因为大多数人更愿意在初始阶段,用一个捎带偏差的值进行运算。不过,如果在初试阶段就开始考虑偏差,指数加权移动均值仍处于预热阶段,偏差修正可以做出更好的估计。

介绍完上面的准备知识,下面正式进入正题。


三、动量(momentum)
        我们使用SGD(stochastic mini-batch gradient descent,深度学习中一般称之为SGD)训练参数时,有时候会下降的非常慢,并且可能会陷入到局部最小值中,如下图所示(图片来自李宏毅《一天搞懂深度学习》)。

动量的引入就是为了加快学习过程,特别是对于高曲率、小但一致的梯度,或者噪声比较大的梯度能够很好的加快学习过程。动量的主要思想是积累了之前梯度指数级衰减的移动平均(前面的指数加权平均),下面用一个图来对比下,SGD和动量的区别:

区别: SGD每次都会在当前位置上沿着负梯度方向更新(下降,沿着正梯度则为上升),并不考虑之前的方向梯度大小等等。而动量(moment)通过引入一个新的变量 v 去积累之前的梯度(通过指数衰减平均得到),得到加速学习过程的目的。

最直观的理解就是,若当前的梯度方向与累积的历史梯度方向一致,则当前的梯度会被加强,从而这一步下降的幅度更大。若当前的梯度方向与累积的梯度方向不一致,则会减弱当前下降的梯度幅度。

用一个图来形象的说明下上面这段话(图片来自李宏毅《一天搞懂深度学习》):

下面给出动量(momentum)的公式:

β的值越大,则之前的梯度对现在的方向影响越大。β一般取值为0.5,0.9,0.99。ng推荐取值0.9。
这个公式是ng的在Coursera课上给出的。

关于这个公式目前主要有两种形式*一种是原论文里的公式:(原论文:On the importance of initialization and momentum in deep learning

使用这个公式的有:
1、 goodfellow和bengio的《deep learning》书:(8.3.2节 momentum

2、 CS231N课(链接:http://cs231n.github.io/neural-networks-3/):

# Momentum update v = mu * v - learning_rate * dx # integrate velocity x += v # integrate position

3、 keras:(链接:https://github.com/keras-team/keras/blob/master/keras/optimizers.py

4、Intel的chainer框架,链接:https://github.com/chainer/chainer/blob/v4.0.0/chainer/optimizers/momentum_sgd.py

def update_core_cpu(self, param):         grad = param.grad         if grad is None:             return         v = self.state['v']         if isinstance(v, intel64.mdarray):             v.inplace_axpby(self.hyperparam.momentum, -                             self.hyperparam.lr, grad)             param.data += v         else:             v *= self.hyperparam.momentum             v -= self.hyperparam.lr * grad             param.data += v

*第二种是类似ng给的:
1、论文《An overview of gradient descent optimization algorithms》:

2、TensorFlow源码里的版本(链接:tensorflow/tensorflow/python/training/momentum.py):

3、pytorch源码里的版本(链接:https://pytorch.org/docs/master/_modules/torch/optim/sgd.html#SGD):

4、当时修hinton的nn课时,hinton给出的版本与TensorFlow这个是一样的,链接见我的博客:

这个版本是和ng在课上给出的版本是一致的,只不过会影响下learning_rate的取值。
综上,应该是哪个版本都可以。大家根据自己的喜好使用。

由于我个人更喜欢ng/TensorFlow/pytorch那个形式,下面无论是代码还是伪代码我都会统一用ng的版本,下面给出momentum的伪代码:

initialize VdW = 0, vdb = 0 //VdW维度与dW一致,Vdb维度与db一致 on iteration t:     compute dW,db on current mini-batch     VdW = beta*VdW + (1-beta)*dW     Vdb = beta*Vdb + (1-beta)*db     W = W - learning_rate * VdW     b = b - learning_rate * Vdb

具体的代码实现为:

def initialize_velocity(parameters):     """     Initializes the velocity as a python dictionary with:                 - keys: "dW1", "db1", ..., "dWL", "dbL"                  - values: numpy arrays of zeros of the same shape as the corresponding gradients/parameters.     Arguments:     parameters -- python dictionary containing your parameters.                     parameters['W' + str(l)] = Wl                     parameters['b' + str(l)] = bl      Returns:     v -- python dictionary containing the current velocity.                     v['dW' + str(l)] = velocity of dWl                     v['db' + str(l)] = velocity of dbl     """     L = len(parameters) // 2 # number of layers in the neural networks     v = {}     # Initialize velocity     for l in range(L):         v["dW" + str(l + 1)] = np.zeros(parameters["W" + str(l+1)].shape)         v["db" + str(l + 1)] = np.zeros(parameters["b" + str(l+1)].shape)     return v
def update_parameters_with_momentum(parameters, grads, v, beta, learning_rate):     """     Update parameters using Momentum     Arguments:     parameters -- python dictionary containing your parameters:                     parameters['W' + str(l)] = Wl                     parameters['b' + str(l)] = bl     grads -- python dictionary containing your gradients for each parameters:                     grads['dW' + str(l)] = dWl                     grads['db' + str(l)] = dbl     v -- python dictionary containing the current velocity:                     v['dW' + str(l)] = ...                     v['db' + str(l)] = ...     beta -- the momentum hyperparameter, scalar     learning_rate -- the learning rate, scalar     Returns:     parameters -- python dictionary containing your updated parameters      v -- python dictionary containing your updated velocities     """     L = len(parameters) // 2 # number of layers in the neural networks     # Momentum update for each parameter     for l in range(L):         # compute velocities         v["dW" + str(l + 1)] = beta * v["dW" + str(l + 1)] + (1 - beta) * grads['dW' + str(l + 1)]         v["db" + str(l + 1)] = beta * v["db" + str(l + 1)] + (1 - beta) * grads['db' + str(l + 1)]         # update parameters         parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * v["dW" + str(l + 1)]         parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * v["db" + str(l + 1)]     return parameters, v

下面用sklearn中的breast_cancer数据集来对比下只用SGD(stochastic mini-batch gradient descent)和SGD with momentum:

在测试集上的准确率是 0.91228。

在测试集上的准确率是 0.9210。


四、Nesterov Momentum
        Nesterov Momentum是对Momentum的改进,可以理解为nesterov动量在标准动量方法中添加了一个校正因子用一张图来形象的对比下momentum和nesterov momentum的区别(图片来自:http://ttic.uchicago.edu/~shubhendu/Pages/Files/Lecture6_pauses.pdf):

根据Sutskever et al (2013)的论文(On the importance of initialization and momentum in deep learning),nesterov momentum的公式如下:

goodfellow et al 的《deep learning》书中给的nesterov momentum算法:

和momentum的唯一区别就是多了一步红色框框起来的步骤。

但是,我们仔细观察这个算法,你会发现一个很大的缺点,这个算法会导致运行速度巨慢无比,因为这个算法每次都要计算θiL(f(x(i);θ+αv),y(i))Ҫfpbpһ这样就导致这个算法的运行速度比momentum要慢两倍,因此在实际实现过程中几乎没人直接用这个算法,而都是采用了变形版本。

我们先来看看几个框架中nesterov momentum是怎么实现的:
keras:https://github.com/keras-team/keras/blob/master/keras/optimizers.py#L146

Lasagne:lasagne.updates.nesterov_momentum

chainer:

Neural Network Libraries:http://nnabla.readthedocs.io/en/latest/python/api/solver.html

CS231N:http://cs231n.github.io/neural-networks-3/

这些框架用的全是同一个公式:(提示:把上面的公式第一步代入第二步可以得到相同的公式:w=w+β2V(1+β)αgrad)。我这里直接使用keras风格公式了,其中 β 是动量参数,α 是学习率。

VdW=βVdWαdWVdb=βVdbαdbW=W+βVdWαdWb=b+βVdbαdb

这样写的高明之处在于,我们没必要去计算 θiL(f(x(i);θ+αv),y(i)) 了,直接利用当前已求得的 dθ 去更新参数即可。这样就节省了一半的时间。

写到这里大家肯定会有一个疑问,为什么这个公式就是正确的,这和原公式(论文中和《deep learning》书中提到的公式)形式明显不一样。下面我来推导证明这两个公式为什么是一样的:
先看原始公式:

Vt+1=βVtαθtL(θt+βVt)θt+1=θt+Vt+1

下面是推导过程(参考资料:1、cs231n中Nesterov Momentum段中However, in practice people prefer to express the update….这段话 2、9987 用Theano实现Nesterov momentum的正确姿势):
首先令 θt=θt+βVt,则 Vt+1=βVtαθtL(θt)
则:

θt+1=θt+1+βVt+1=θt+Vt+1+βVt+1=θt+(1+β)Vt+1=θtβVt+(1+β)[βVtαθtL(θt)]=θt+β2Vt(1+β)αθtL(θt)

下面替换回来,令 θt=θt,则上述公式为:θt+1=θt+β2Vt(1+β)αθtL(θt)

下面是实现的代码(初始化和momentum一样就不贴出初始化的代码了):

#nesterov momentum def update_parameters_with_nesterov_momentum(parameters, grads, v, beta, learning_rate):     """     Update parameters using Momentum     Arguments:     parameters -- python dictionary containing your parameters:                     parameters['W' + str(l)] = Wl                     parameters['b' + str(l)] = bl     grads -- python dictionary containing your gradients for each parameters:                     grads['dW' + str(l)] = dWl                     grads['db' + str(l)] = dbl     v -- python dictionary containing the current velocity:                     v['dW' + str(l)] = ...                     v['db' + str(l)] = ...     beta -- the momentum hyperparameter, scalar     learning_rate -- the learning rate, scalar      Returns:     parameters -- python dictionary containing your updated parameters     v -- python dictionary containing your updated velocities      '''     VdW = beta * VdW - learning_rate * dW     Vdb = beta * Vdb - learning_rate * db     W = W + beta * VdW - learning_rate * dW     b = b + beta * Vdb - learning_rate * db     '''     """      L = len(parameters) // 2  # number of layers in the neural networks      # Momentum update for each parameter     for l in range(L):         # compute velocities         v["dW" + str(l + 1)] = beta * v["dW" + str(l + 1)] - learning_rate * grads['dW' + str(l + 1)]         v["db" + str(l + 1)] = beta * v["db" + str(l + 1)] - learning_rate * grads['db' + str(l + 1)]         # update parameters         parameters["W" + str(l + 1)] += beta * v["dW" + str(l + 1)]- learning_rate * grads['dW' + str(l + 1)]         parameters["b" + str(l + 1)] += beta * v["db" + str(l + 1)] - learning_rate * grads["db" + str(l + 1)]      return parameters

论文《An overview of gradient descent optimization algorithms》中写道:This anticipatory update prevents us from going too fast and results in increased responsiveness, which has significantly increased the performance of RNNs on a number of tasks


五、AdaGrad(Adaptive Gradient)
        通常,我们在每一次更新参数时,对于所有的参数使用相同的学习率。而AdaGrad算法的思想是:每一次更新参数时(一次迭代),不同的参数使用不同的学习率。AdaGrad 的公式为:

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!