几种优化方法总结---Momentum,AdaGrad,RMSProp,Adam

SGD

SGD的全称是stochastic gradient descent,随机梯度下降,是相对batch gradient descent提出来的。

batch gradient descent:将所有训练样本全部投入计算dw,db。好处:这种计算方式求出来的dw和db是最真实的。坏处:如果训练样本特别大,比如100,000个样本,这样会导致很长时间才能计算出一次倒数然后进行一次梯度下降,导致训练特别缓慢。另外计算机的内存也无法存储这么大的矩阵进行计算。

stochastic gradient descent:用每一个训练样本计算dw和db。好处:一个样本计算导数非常快,可以很快的进行一次梯度下降。坏处:一个样本计算的导数与真实导数相差非常大,可能会导致在最小值附近徘徊而无法接近最小值,另外由于只用一个样本计算导数,导致遍历所有样本需要进行一个很大的循环,从整体上而言失去了矩阵计算带来的运算加速。

所以可以使用一种折中的方法,mini-batch gradient descent,现在提到SGD一般指的是这种方法。mini batch一般选择16,32,64等,根据计算机内存决定。这样计算一次导数比较快,所以可以比较快的进行一次梯度下降,而且也有矩阵计算带来的运算加速。

SGD公式表示

\Theta_t = \Theta_t - \eta *d\Theta_{t-1}

\Theta表示所有的参数,t代表第t次梯度下降。

tensorflow接口调用

tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)

SGD优化效率不高

以下图为例,如果使用SGD优化方式,初始点在红色点出,优化过程会出现图中的zigzag的线路,以非常迂回的方式到达最优点。

为何会出现上图这样的曲线?

对上图而言,假设横轴是w1,纵轴是w2,那么这个图形如果从立体上来想象,应该是横轴的梯度比较平滑开口很大,纵轴的梯度比较陡峭开口很小。那么dw2比较大,dw1会比较小。所以在梯度更新的时候纵轴方向梯度更新会很大,横轴方向梯度变化会比较小,这样很可能w2因为步子大跳到了另一个方向,而w1因为步子小而正常的向前迈进了一小步。所以就会形成上图中的折线。

SGD+Momentum

Momentum是动量,通过指数加权平均来累积之前的动量,从而给下降的过程来带一点冲力。

公式表达

v_t = \beta *v_{t-1} +(1-\beta )*d\Theta_t

\Theta_t = \Theta_t - \eta*v_t

v_t就是通过指数加权平均的方式累积的动量,\beta是一个超参数,一般使用0.9。

其实还有很多其他的表达方式,比如tensorflow源码中的实现方式:

  ```
  accumulation = momentum * accumulation + gradient
  variable -= learning_rate * accumulation
  ```

区别就在于gradient之前是否要乘以(1-\beta)。

另外还有一种表示方法,比如keras和cs231n都是使用这一种方式

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

tensorflow接口调用

tf.train.MomentumOptimizer(learning_rate, momentum).minimize(loss)

为何加上Momentum会使SGD加速?

还是以下面这张图片来说明

如果\beta设置为0.9,按照指数加权平均的意义,大概是平均了10次的导数值,对于dw1因为每次导数都是同一个方向,所以加权平均后变化不大,但是对于dw2的方向是一直在变化,一次向上一次向下,而如果取了之前的10次数据进行了平均,正负数抵消部分,会让dw2变化的波动变小。所以可能会沿着黄色的曲线趋近最小值,减少了大幅度的波动而加速了优化过程。

Nesterov

Nesterov是对标准动量的方式加了一个修正因子,因为用指数加权平均求出v_t后,梯度下降是针对这个v_t进行的,所以修正的方式就是将这个v_t代入函数中计算,然后用这个新的结果来对下一次的\Theta求导。

公式表达

就是相对之前的Momentum的公式稍作了改动

表达1

x_ahead = x + mu * v_prev
# evaluate dx_ahead (the gradient at x_ahead instead of at x)
v = mu * v_prev - learning_rate * dx_ahead
x += v

差别就在于dx与dx_ahead的差别,这就是修正的方式。但是这里有个问题,如果我们更新了x_head,要对它求导数,就需要将函数用x_head代入进行前向计算,然后再反向求导,这样导致计算过程多了一倍。所以一般代码实现中并不是这样操作的,而是使用如下的公式:

表达2

v_prev = v # back this up
v = mu * v_prev - learning_rate * dx # velocity update stays the same
x += -mu * v_prev + (1 + mu) * v # position update changes form

将公式整理一下可以得到下面的式子:

表达3

x += mu * mu * v_prev - learning_rate * (1 + mu) * dx

这样就避免了对dx_head求导,但是为何可以从表达1推导出表达3呢?可以参考下面的过程

已知:

\hat{x}_t = x_t + \mu * v_t (1)

v_{t+1}= \mu * v_t- \alpha * d\hat{x}_t (2)

x_{t+1} = x_t + v_{t+1} (3)

由(1)得到==>\hat{x}_{t+1} = x_{t+1} + \mu * v_{t+1}

代入(3)得到==>\hat{x}_{t+1} = x_t+v_{t+1}+ \mu * v_{t+1}

代入(1)和(3)==>\hat{x}_{t+1} = \hat{x}_t - \mu * v_t+(1+\mu) * (\mu * v_t- \alpha * d\hat{x}_t)

==>\hat{x}_{t+1} = \hat{x}_t + \mu^{2} * v_t- \alpha *(1+\mu)* d\hat{x}_t,如果令\hat{x}_{t+1} = x_{t+1}就相当于表达3的式子

tensorflow接口

tf.train.MomentumOptimizer(learning_rate, momentum, use_nesterov=True).minimize(loss)

与Momentum的接口一样,只是对一个参数指定为True

无论是Momentum还是Nesterov,他们的目的都是改变导数的方向,通过指数加权平均的方式让导数的方向变化边缓,从而达到加速优化的目的。

AdaGrad

AdaGrad是adaptive gradient的缩写,主要目的是对不同的参数设置不同的学习率,如果参数的导数比较大,设置学习率小一些,如果参数的导数比较小,设置学习率大一些,用这种方式来减少抖动。

公式

# Assume the gradient dx and parameter vector x
cache += dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

从公式中可以看出学习率由以前的learning_rate变成了learning_rate/(np.sqrt(cache) + eps),eps一般是10^{-7},是为了防止除以0而添加的一个非常小的偏差。

如果dx比较大,那么cache会大一些,然后leanring_rate除以比较大的cache的平方根就会让dx的学习率变小;相反如果dx比较小,cache会小一些,learning_rate除以比较小的cache的平方根会让dx的学习率大一些。

tensorflow调用

tf.train.AdagradOptimizer(learning_rate).minimize(loss)

但是AdaGrad有个很严重的缺陷,就是一直累加dx的平方会导致cache无法避免的越来越大,从而导致学习率衰减到接近0而失去学习的能力。

Adadelta

Adadelta是对AdaGrad的改进,使用指数加权平均来替代cache无限制的累加dx的平方。

公式

Sdw = rho*Sdw + (1 - rho)*(dW)^2
Vdw = sqrt((delta_w + epsilon) / (Sdw + epsilon))*dW
W -= Vdw
delta_w = rho*delta_w + (1 - rho)*(Vdw)^2

可以看出去掉了学习率,进行自适应。

tensorflow接口

tf.train.AdadeltaOptimizer(learning_rate=1.0).minimize(loss)

tensorflow中还是保留了学习率的设置,但是如果要忽略学习率可以将学习率直接设置为1.0

learning_rate: A `Tensor` or a floating point value. The learning rate.
To match the exact form in the original paper use 1.0.

 

RMSProp

RMS是root mean square的缩写,RMSProp也是AdaGrad的一种改进,但是相对Adadelta来说更加简单和直观。

公式

cache = decay_rate * cache + (1 - decay_rate) * dx**2
x += - learning_rate * dx / (np.sqrt(cache) + eps)

也是使用了指数加权平均来计算cache,这样可以防止cache过大而使学习率降的太低。decay默认是0.9

tensorflow接口

tf.train.RMSPropOptimizer(learning_rate, decay).minimize(loss)

 

Adam

Adam就是将Momentum和RMSProp结合起来的算上,也就是说既从方向上来修正导数也从步伐大小上来修正导数。

公式

# t is your iteration counter going from 1 to infinity
m = beta1*m + (1-beta1)*dx
mt = m / (1-beta1**t)
v = beta2*v + (1-beta2)*(dx**2)
vt = v / (1-beta2**t)
x += - learning_rate * mt / (np.sqrt(vt) + eps)

m是Momentum的计算,beta1一般是0.9,mt是对m进行偏差修正,v是RMSProp的计算,beta2一般是0.999,vt是对v进行偏差修正。

tensorflow接口

tf.train.AdamOptimizer(learning_rate, beta1, beta2).minimize(loss)

总结

这么多优化方法使用哪种更合适?实际上是无法比较这些算法中的优劣的,有些优化算法可能在某些问题中更好,在有些问题中表现一般。但是现在用的比较多的就是Adam,因为它融合了两类优化算法的思想。但是也有文章表明Adam可能由于学习率不断的变化,时大时小而在最后比较难达到最优解。所以有些论文中可能会使用Adam在训练初期使用,后期再使用SGD进行精调。相关内容可以参考:Adam的两宗罪

猜你喜欢

转载自blog.csdn.net/stesha_chen/article/details/84966510
今日推荐