―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天的气温,我们有:
根据上面的公式我们能够画出这条线,如下图所示(图片来自ng deep learning课):
下面来正式的看一下 指数加权平均(Exponentially weighted average) 的定义:
可以认为 近似是 天的平均气温,所以上面公式设置了,当 时,则可以认为 近似是它前10天的平均气温。
比如,按照上面的公式,我们计算 ,
从上面的公式能够看出,实际上是个指数衰减函数。,所以这个就解释了上面的 。
(ps:关于这一点为什么要用0.35及,我是不太明白的,搜了下资料也没找到更好的资料,希望路过的有知道的大神,在下面评论交流。)
下面再看下 取不同值时,曲线的拟合情况(图片来自ng deep learning课):
从上图能够看出:
- 当 较大时( 相当于每一点前50天的平均气温),曲线波动相对较小更加平滑(绿色曲线),因为对很多天的气温做了平均处理,正因为如此,曲线还会右移。
- 同理,当 较小时( 相当于每一点前2天的平均气温),曲线波动相对激烈,但是它可以更快的适应温度的变化。
下面直接看实现指数加权平均(Exponentially weighted average)的伪代码:
V0 = 0 repeat { get next theta_t V_theta = beta * V_theta + (1 - beta)* theta_t }
关于指数加权平均的优缺点: 当 时,我们可以近似的认为当前的数值是过去10天的平均值,但是显然如果我们直接计算过去10天的平均值,要比用指数加权平均来的更加准确。但是如果直接计算过去10天的平均值,我们要存储过去10天的数值,而加权平均只要存储一个数值即可,而且只需要一行代码即可,所以在机器学习中用的很多。
二、带偏差修正的指数加权平均(bias correction in exponentially weighted average)
上一节中介绍了指数加权平均,用的公式是:
当我们令 时,我们想得到下图中的“绿线”,实际上我们得到的是下图中的“紫线”。对比这两条线,能够发现在“紫线”的起点相比“绿线”非常的低,举个例子,如果第一天的温度是40℃,按照上面的公式计算,,按照这个公式预估第一天的温度是0.8℃,显然距离40℃,差距非常大。同样,继续计算,第二天的温度也没有很好的拟合,也就是说 指数加权平均 不能很好地拟合前几天的数据,因此需要 偏差修正,解决办法就是,再令 。因此, 带偏差修正的指数加权平均(bias correction in exponentially weighted average) 公式为:
再来按照上述公式对前两天温度进行计算,,能够发现温度没有偏差。并且当 时,,这样在后期 就和 没有修正的指数加权平均一样了。
在机器学习中,多数的指数加权平均运算并不会使用偏差修正。因为大多数人更愿意在初始阶段,用一个捎带偏差的值进行运算。不过,如果在初试阶段就开始考虑偏差,指数加权移动均值仍处于预热阶段,偏差修正可以做出更好的估计。
介绍完上面的准备知识,下面正式进入正题。
三、动量(momentum)
我们使用SGD(stochastic mini-batch gradient descent,深度学习中一般称之为SGD)训练参数时,有时候会下降的非常慢,并且可能会陷入到局部最小值中,如下图所示(图片来自李宏毅《一天搞懂深度学习》)。
动量的引入就是为了加快学习过程,特别是对于高曲率、小但一致的梯度,或者噪声比较大的梯度能够很好的加快学习过程。动量的主要思想是积累了之前梯度指数级衰减的移动平均(前面的指数加权平均),下面用一个图来对比下,SGD和动量的区别:
区别: SGD每次都会在当前位置上沿着负梯度方向更新(下降,沿着正梯度则为上升),并不考虑之前的方向梯度大小等等。而动量(moment)通过引入一个新的变量 去积累之前的梯度(通过指数衰减平均得到),得到加速学习过程的目的。
最直观的理解就是,若当前的梯度方向与累积的历史梯度方向一致,则当前的梯度会被加强,从而这一步下降的幅度更大。若当前的梯度方向与累积的梯度方向不一致,则会减弱当前下降的梯度幅度。
用一个图来形象的说明下上面这段话(图片来自李宏毅《一天搞懂深度学习》):
下面给出动量(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的唯一区别就是多了一步红色框框起来的步骤。
但是,我们仔细观察这个算法,你会发现一个很大的缺点,这个算法会导致运行速度巨慢无比,因为这个算法每次都要计算这样就导致这个算法的运行速度比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/
这些框架用的全是同一个公式:(提示:把上面的公式第一步代入第二步可以得到相同的公式:)。我这里直接使用keras风格公式了,其中 是动量参数, 是学习率。
这样写的高明之处在于,我们没必要去计算 了,直接利用当前已求得的 去更新参数即可。这样就节省了一半的时间。
写到这里大家肯定会有一个疑问,为什么这个公式就是正确的,这和原公式(论文中和《deep learning》书中提到的公式)形式明显不一样。下面我来推导证明这两个公式为什么是一样的:
先看原始公式:
下面是推导过程(参考资料:1、cs231n中Nesterov Momentum段中However, in practice people prefer to express the update….这段话 2、9987 用Theano实现Nesterov momentum的正确姿势):
首先令 ,则
则:
下面替换回来,令 ,则上述公式为:
下面是实现的代码(初始化和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 的公式为: