【深度学习】TensorFlow学习之路四
本系列文章主要是对OReilly的Hands-On Machine Learning with Scikit-learn and TensorFlow一书深度学习部分的阅读摘录和笔记。
训练一个规模庞大,层次较深的神经网络会相当消耗时间。为了加快神经网络的训练速度,我们已经介绍过,可以从以下几个方面入手进行优化:
- 选择较合理的参数初始化方案,比如Xavier或He初始化方案;
- 选择计算较快的激活函数,比如Relu及其衍生激活函数;
- 使用Batch标准化;
- 基于已经存在的,解决相似问题的神经网络的初始几层进行训练;
- 选择更快速的梯度下降算法。
其中最后一点,就是本篇要详细介绍的几种可以优化梯度下降的方法
一、动量下降(Momentum)
如果我们把梯度下降过程想象成一个小球沿着曲面下滚的过程,最开始时小球的速度会比较慢,但在下降过程中,小球会不断积累动量,加速滑到最底部,这就是动量梯度下降的基本过程。相比较而言,梯度下降则更像是以匀速下降,所以下降过程会比动量下降慢很多。
梯度下降的计算过程为:每一次计算该点目标函数J对于参数
θ
\theta
θ的梯度
∇
θ
J
(
θ
)
\nabla_{\theta}J(\theta)
∇θJ(θ),然后
θ
\theta
θ下降一个梯度的幅度,即
θ
=
θ
−
η
∇
θ
J
(
θ
)
\theta = \theta - \eta\nabla_{\theta}J(\theta)
θ=θ−η∇θJ(θ)。而动量下降的不同在于,每次计算的是该点累计的动量m,然后
θ
\theta
θ下降一个动量的幅度,公式为:
m
=
β
m
+
η
∇
θ
J
(
θ
)
θ
=
θ
−
m
m = \beta m + \eta\nabla_{\theta}J(\theta) \\ \theta = \theta - m
m=βm+η∇θJ(θ)θ=θ−m
其中m就是每次累计的动量,
β
\beta
β是一个超参,是一个0-1的取值,可以理解为是个控制摩擦项的参数,越小代表阻力越大,即对动量的损耗也越大。
那到底动量下降能比梯度下降快多少呢?我们可以做一个理论测算,假设每一步的梯度不变
m
=
β
m
+
η
∇
=
β
(
β
(
.
.
.
)
+
η
∇
)
+
η
∇
=
β
n
∗
m
1
+
η
∇
+
β
η
∇
+
β
2
η
∇
+
.
.
.
+
β
n
η
∇
m = \beta m + \eta\nabla \\=\beta (\beta (...) + \eta\nabla) + \eta\nabla \\= \beta^n*m_1 + \eta\nabla + \beta\eta\nabla + \beta^2\eta\nabla +... +\beta^n\eta\nabla
m=βm+η∇=β(β(...)+η∇)+η∇=βn∗m1+η∇+βη∇+β2η∇+...+βnη∇
当n趋于无穷大时,m会最终收敛于
η
∇
(
1
1
−
β
)
\eta\nabla(\frac{1}{1-\beta})
η∇(1−β1),如果
β
\beta
β取0.9,那m就是梯度
∇
\nabla
∇的十倍,所以动量下降一般最终会趋向于10倍于梯度下降的速度去收敛,由此使得动量下降能更快地走出梯度平原和局部最优解的位置。但另一方面,由于动量太大,动量下降算法会在最终最优解的地方来回摆动,所以我们需要
β
\beta
β项来加一些摩擦。
用TensorFlow执行动量下降非常简单:
optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.9)
二、Nesterov加速梯度
Nesterov加速梯度方法是对动量下降进行了一些小小的改进,动量下降计算的是在当前点的动量,而Nesterov加速梯度方法则是在动量方向上前进一小步后再计算动量,公式如下:
m
=
β
m
+
η
∇
θ
J
(
θ
+
β
m
)
θ
=
θ
−
m
m = \beta m + \eta\nabla_{\theta}J(\theta+\beta m) \\ \theta = \theta - m
m=βm+η∇θJ(θ+βm)θ=θ−m
和动量下降唯一的不同就是计算梯度的时候是在
θ
+
β
m
\theta+\beta m
θ+βm点上计算的。为什么这点改进会进一步加快收敛速度呢?因为动量向量m往往指向正确的方向,即指向最优点方向,所以在这个方向上前进一小步后再算梯度,再进一步算动量会更加精确。如下图所示,蓝色箭头表示当前点的梯度方向,绿色箭头表示在梯度方向上前进一小步后的梯度方向,明显在更新动量的时候,使用绿色箭头进行更新会让动量方向更加准确指向最优点。每一步,Nesterov加速梯度方法都会对动量方向进行一点点修正,那在不断下降过程中,收敛就会越来越快。
另外,当优化到越过最优点后,动量下降中的动量方向会进一步让参数走得更远,而Nesterov加速梯度方法会将动量方向修正到反向,加速震荡收敛的过程。
TensorFlow中使用该方法也非常简单,只需要在刚刚动量下降的函数中加个参数use_nesterov=True即可:
optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.9, use_nesterov=True)
三、AdaGrad
Ada是adaptive的缩写,AdaGrad的算法基本思想就是调整梯度方向使其更具适应性。看如下图中的情况:
当不同变量间取值范围差异过大时,目标函数就会是这种扁长型,梯度方向就会被量级大的变量所带偏,导致每一步梯度的指向不一定是最优的。AdaGrad方法就是在算梯度的时候先把每个维度做个标准化,这样梯度虽然更平缓,但是却更能指向最优解方向(如图),具体公式如下:
s
=
s
+
∇
θ
J
(
θ
)
⊗
∇
θ
J
(
θ
)
θ
=
θ
−
η
∇
θ
J
(
θ
)
⊘
s
+
ϵ
s = s + \nabla_{\theta}J(\theta)\otimes\nabla_{\theta}J(\theta) \\ \theta = \theta - \eta\nabla_{\theta}J(\theta) \oslash \sqrt{s+\epsilon}
s=s+∇θJ(θ)⊗∇θJ(θ)θ=θ−η∇θJ(θ)⊘s+ϵ
解释一下,第一步相当于算一下当前梯度每个维度上的方差,不过这里方差s是累加计算的,这样一方面是起到标准化的作用,一方面是逐步缩小学习率,有利于找到最优解。如果目标函数在某个维度上特别陡,那s值就会在迭代过程中越来越大。
⊗
\otimes
⊗符号表示向量中的每个对应元素相乘。第二步就是梯度的每个维度先标准化一下,再执行梯度下降,
⊘
\oslash
⊘表示每个对应位置的元素相除。
ϵ
\epsilon
ϵ是个非常小的数,加它是为了防止除以0。
AdaGrad方法处理结构比较简答的网络效果还可以,但在处理深度神经网络的时候,由于梯度被标准化,所以总是会在找到最优解之前就停止了,因此,虽然TensorFlow里面有AdagradOptimizer函数,但不建议用来训练深度网络。
四、RMSProp
RMSProp方法是对AdaGrad方法的一种改进,他通过加了一个衰减系数
β
\beta
β使得算法只累加最近几次迭代的梯度,具体公式如下:
s
=
β
s
+
(
1
−
β
)
∇
θ
J
(
θ
)
⊗
∇
θ
J
(
θ
)
θ
=
θ
−
η
∇
θ
J
(
θ
)
⊘
s
+
ϵ
s = \beta s + (1-\beta) \nabla_{\theta}J(\theta) \otimes \nabla_{\theta}J(\theta) \\ \theta = \theta - \eta\nabla_{\theta}J(\theta) \oslash \sqrt{s+\epsilon}
s=βs+(1−β)∇θJ(θ)⊗∇θJ(θ)θ=θ−η∇θJ(θ)⊘s+ϵ
衰减系数
β
\beta
β一般设置为0.9,在TensorFlow中的实现如下:
optimizer = tf.train.RMSPropOptimizer(learning_rate=learning_rate,
momentum=0.9, decay=0.9, epsilon=1e-10)
这种方法通常都会比AdaGrad算法表现更好,一般也会比动量下降方法效果好,一度是最优选择,直到Adam优化方法的出现。
五、Adam优化算法
Adam是Adaptive momentum estimation的缩写,看名字就知道是融合了动量下降和RMSProp的一种算法。该算法的具体计算过程如下:
1.
m
=
β
1
m
+
(
1
−
β
1
)
∇
θ
J
(
θ
)
2.
s
=
β
2
s
+
(
1
−
β
2
)
∇
θ
J
(
θ
)
⊗
∇
θ
J
(
θ
)
3.
m
=
m
1
−
β
1
T
4.
s
=
s
1
−
β
2
T
5.
θ
=
θ
−
η
m
⊘
s
+
ϵ
1.\ m = \beta_1 m + (1-\beta_1)\nabla_{\theta}J(\theta) \\ 2. \ s = \beta_2 s + (1-\beta_2) \nabla_{\theta}J(\theta) \otimes \nabla_{\theta}J(\theta) \\ 3. \ m = \frac{m}{1-\beta_1^T} \\ 4. \ s = \frac{s}{1-\beta_2^T} \\ 5. \ \theta = \theta-\eta m \oslash \sqrt{s+\epsilon}
1. m=β1m+(1−β1)∇θJ(θ)2. s=β2s+(1−β2)∇θJ(θ)⊗∇θJ(θ)3. m=1−β1Tm4. s=1−β2Ts5. θ=θ−ηm⊘s+ϵ
其中T代表了迭代的轮数。如果只看1,2,5步,会发现Adam算法和动量下降或RMSProp过程基本一样。其中4和5步是一个技术细节的操作,因为m和s初始化时都是0,所以最开始训练的几轮里,m和s都会偏向于0,4-5步的操作会放大m和s,从而加快收敛过程,越迭代到后面,4-5步的作用也越小。
动量衰减系数
β
1
\beta_1
β1一般设置为0.9,参数
β
2
\beta_2
β2一般设置为0.999,
ϵ
\epsilon
ϵ一般设置为1e-8,这些都是TensorFlow里AdamOptimizer函数的默认值:
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
六、学习率优化方案
找到一个合适的学习率并非那么容易,如果学习率过大,会导致最终算法不收敛;如果学习率过小,会导致训练时间过长;如果学习率稍微大了一些,又会导致结果最最优解附近来回震荡,找不到最优解(AdaGrad和RMSProp和Adam算法不会出现这种状况)。
为了解决这些问题,我们完全可以不使用一个固定的学习率,而是让学习率在最开始的时候大一些,下降的快一些,到后面快找到最优解了就把学习率减小,以便于找到最优解。这里直接介绍一个最推荐的学习率优化方案,指数方案:
η
(
t
)
=
η
0
∗
1
0
−
t
/
r
\eta(t) = \eta_0*10^{-t/r}
η(t)=η0∗10−t/r
这个公式表示每过r轮,学习率就会减小为原来的十分之一。在TensorFlow中实现如下:
#eta0赋值
initial_learning_rate = 0.1
#r赋值
decay_steps = 10000
decay_rate = 1/10
#记录当前训练到多少轮
global_step = tf.Variable(0, trainable=False)
#构造一个变动的学习率
learning_rate = tf.train.exponential_decay(initial_learning_rate, global_step, decay_steps, decay_rate)
optimizer = tf.train.MomentumOptimizer(learning_rate, momentum=0.9)
training_op = optimizer.minimize(loss, global_step=global_step)
这里设定了
η
0
\eta_0
η0为0.1,r为10000,并且创建了一个global_step变量去追踪记录当前的训练轮数,然后通过tf.train.exponential_decay函数生成了一个变动的学习率。
当然,AdaGrad和RMSProp和Adam三种算法会自动在迭代过程中减小学习率,所以不必做这种学习率优化方案。