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), 其做法就是在更新参数的时候使用所有的样本来进行更新。
一般是采用所有训练数据的平均损失来近似目标函数:
L
(
θ
)
=
1
M
∑
i
=
1
M
L
(
f
(
x
i
,
θ
)
,
y
i
)
∇
L
(
θ
)
=
1
M
∑
i
=
1
M
∇
L
(
f
(
x
i
,
θ
)
,
y
i
)
L(\theta)=\frac{1}{M} \sum_{i=1}^{M} L\left(f\left(x_{i}, \theta\right), y_{i}\right) \\ \nabla L(\theta)=\frac{1}{M} \sum_{i=1}^{M} \nabla L\left(f\left(x_{i}, \theta\right), y_{i}\right)
L(θ)=M1i=1∑ML(f(xi,θ),yi)∇L(θ)=M1i=1∑M∇L(f(xi,θ),yi)
M
M
M是训练样本的个数, 模型的参数更新公式:
θ
t
+
1
=
θ
t
−
a
∇
L
(
θ
t
)
\theta_{t+1}=\theta_{t}-a \nabla L\left(\theta_{t}\right)
θt+1=θt−a∇L(θt)
优点:由于每一步迭代使用了全部样本,因此当损失函数收敛过程比较稳定。对于凸函数可以收敛到全局最小值,对于非凸函数可以收敛到局部最小值。
问题: 经典的梯度下降法在每次对模型参数更新时,需要遍历所有的训练数据,
M
M
M很大时,需要耗费相当大的计算资源,实际应用中基本不可行,不能投入新数据实时更新模型。
3. 随机梯度下降
随机梯度下降(Stochastic Gradient Descent, SGD)是用单个训练样本的损失来近似平均损失,即
L
(
θ
;
x
i
,
y
i
)
=
L
(
f
(
x
i
,
θ
)
,
y
i
)
∇
L
(
θ
;
x
i
,
y
i
)
=
∇
L
(
f
(
x
i
,
θ
)
,
y
i
)
\begin{array}{l} L\left(\theta ; x_{i}, y_{i}\right)=L\left(f\left(x_{i}, \theta\right), y_{i}\right) \\ \nabla L\left(\theta ; x_{i}, y_{i}\right)=\nabla L\left(f\left(x_{i}, \theta\right), y_{i}\right) \end{array}
L(θ;xi,yi)=L(f(xi,θ),yi)∇L(θ;xi,yi)=∇L(f(xi,θ),yi)
因此,随机梯度下降法用单个训练数据即可对模型参数进行一次更新,大大加快了收敛速率,该方法也适用于数据源源不断到来的在线更新场景。
问题:随机梯度下降每步仅仅采样一个或者少量样本来估计当前梯度,计算速度快,内存开销少,但每一步接受的信息量有限,常常对梯度的估计出现偏差,造成目标函数曲线收敛的很不稳定,伴有剧烈波动,有时候甚至不收敛。
为了降低随机梯度的方差,从而使得迭代算法更加稳定,也为了充分利用高度优化的矩阵运算操作,实际应用中,会同时处理若干训练数据,这就是小批量梯度下降(mini-batch Gradient Descent)。目标函数和梯度如下:
L
(
θ
)
=
1
m
∑
j
=
1
m
L
(
f
(
x
i
j
,
θ
)
,
y
i
j
)
∇
L
(
θ
)
=
1
m
∑
j
=
1
∞
∇
L
(
f
(
x
i
,
θ
)
,
y
i
)
\begin{array}{l} L(\theta)=\frac{1}{m} \sum_{j=1}^{m} L\left(f\left(x_{i j}, \theta\right), y_{i j}\right) \\ \nabla L(\theta)=\frac{1}{m} \sum_{j=1}^{\infty} \nabla L\left(f\left(x_{i}, \theta\right), y_{i}\right) \end{array}
L(θ)=m1∑j=1mL(f(xij,θ),yij)∇L(θ)=m1∑j=1∞∇L(f(xi,θ),yi)
m
m
m表示一个批次里面的样本个数。 这个也是目前常用的方式(注意这里是在训练样本上个数的选择), 但是在用的时候要注意三个问题:
- 如何选取参数 m m m: 不同应用中, 最优的 m m m通常会不一样,往往需要调参确定。 一般 m m m取2的幂次时能充分利用矩阵运算操作。 所以可以在2的幂次中选取最优值,32,64,128等,如果感觉内存比较小,可以小一点。
- 如何选择 m m m个训练数据? 为了避免数据的特定顺序对收敛带来的影响,一般在每次遍历训练数据之前,先对数据随机打乱,然后每次迭代时,按顺序挑选 m 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的什么问题? 介绍具体的优化算法了。
在介绍之前,我们先回顾下随机梯度下降算法中的参数更新公式,因为下面的算法都是在这个公式上进行的一些改进。
g
←
1
m
∇
θ
∑
i
J
(
f
(
x
(
i
)
;
θ
)
,
y
(
i
)
)
θ
←
θ
−
ϵ
g
\begin{array}{l} g \leftarrow \frac{1}{m} \nabla_{\theta} \sum_{i} J\left(f\left(\boldsymbol{x}^{(i)} ; \theta\right), y^{(i)}\right) \\ \theta \leftarrow \theta-\epsilon g \end{array}
g←m1∇θ∑iJ(f(x(i);θ),y(i))θ←θ−ϵg
4. 改进方向一:惯性保持
4.1 动量(Momentum)方法
动量方法解决的问题:
- 随机梯度下降中的“峡谷”和“鞍点”问题
- SGD加速,特别是高曲率,小幅但是方向一致,或者带噪声的梯度
《百面机器学习》上的那个比喻很形象:
如果把原始的 SGD 想象成⼀个纸团在重力作用向下滚动,由于质量小受到山壁弹力的干扰大,导致来回震荡。或者在鞍点处因为质量小速度很快减为 0,导致无法离开这块平地。动量方法相当于把纸团换成了铁球。不容易受到外力的干扰,轨迹更加稳定,同时因为在鞍点处因为惯性的作用,更有可能离开平地。
动量算法积累了之前梯度指数级衰减的移动平均,并且继续沿该方向移动,参数更新公式:
v
←
α
v
−
ϵ
∇
θ
(
1
m
∑
i
=
1
m
L
(
f
(
x
(
i
)
;
θ
)
,
y
(
i
)
)
)
θ
←
θ
+
v
\begin{array}{l} \boldsymbol{v} \leftarrow \alpha \boldsymbol{v}-\epsilon \nabla_{\boldsymbol{\theta}}\left(\frac{1}{m} \sum_{i=1}^{m} L\left(\boldsymbol{f}\left(\boldsymbol{x}^{(i)} ; \boldsymbol{\theta}\right), \boldsymbol{y}^{(i)}\right)\right) \\ \boldsymbol{\theta} \leftarrow \boldsymbol{\theta}+\boldsymbol{v} \end{array}
v←αv−ϵ∇θ(m1∑i=1mL(f(x(i);θ),y(i)))θ←θ+v
重点:
-
从形式上看,动量算法引入了变量 v v v充当速度角色,以及相关的超参数 α \alpha α,决定了之前的梯度贡献衰减的有多快
-
原始的SGD每次更新的步长只是梯度乘以学习率。现在,步长还取决于历史梯度序列的大小和排列。 当许多连续的梯度指向相同的方向时,步长就会不断的增大。如果动量算法总是能观测到梯度 g g g ,那么它会在方向 − g -g −g 上不停加速,直到达到最终速度,其中步长大小为: ϵ ∥ g ∥ 1 − α \frac{\epsilon\|g\|}{1-\alpha} 1−αϵ∥g∥, 如果动量超参数设置为 α = 0.9 \alpha=0.9 α=0.9, 则对应最大速度10倍于梯度下降算法。
optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.9)
-
速度 v v v累积了当前梯度元素 ∇ θ ( 1 m ∑ i = 1 m J ( f ( x ( i ) ; θ ) , y ( i ) ) ) \nabla_{\theta}\left(\frac{1}{m} \sum_{i=1}^{m} J\left(f\left(\boldsymbol{x}^{(i)} ; \theta\right), y^{(i)}\right)\right) ∇θ(m1∑i=1mJ(f(x(i);θ),y(i))), 相对于 ϵ \epsilon ϵ, 如果 α \alpha α越大,之前的梯度对现在方向影响也越大。实践中一般 α \alpha α取值为0.5, 0.9, 0.99. 和学习率一样, α \alpha α也会随着时间变化,一般初始值是一个较小的值,随后会慢慢变大。随着时间推移,改变 α \alpha α没有 ϵ \epsilon ϵ重要。
所以当前迭代点的下降方向不仅仅取决于当前的梯度,还受到前面所有迭代点的影响。动量方法以一种廉价的方式模拟了二阶梯度(牛顿法)。撤了这么半天理论,拿个图来看看效果:
使用动量的SGD算法流程:
4.2 NAG算法
Nesterov Accelerated Gradient 提出了⼀个针对动量算法的改进措施。动量算法是把历史的梯度和当前的梯度进进行合并,来计算下降的⽅向。而Nesterov 提出,让迭代点先按照历史梯度走⼀步,然后再合并。更新规则如下,改变主要在于梯度的计算上:
v
←
α
v
−
ϵ
∇
θ
(
1
m
∑
i
=
1
m
J
(
f
(
x
(
i
)
;
θ
+
α
v
)
,
y
(
i
)
)
)
θ
←
θ
+
v
\begin{array}{l} v \leftarrow \alpha v-\epsilon \nabla_{\theta}\left(\frac{1}{m} \sum_{i=1}^{m} J\left(f\left(\boldsymbol{x}^{(i)} ; \theta+\alpha v\right), y^{(i)}\right)\right) \\ \theta \leftarrow \theta+v \end{array}
v←αv−ϵ∇θ(m1∑i=1mJ(f(x(i);θ+αv),y(i)))θ←θ+v
参数
α
\alpha
α和
ϵ
\epsilon
ϵ发挥了和标准动量方法中类似的所用,Nesterov动量和标准动量之间的区别在于梯度计算上。 NAG把梯度计算放在了对参数施加当前速度之后, 往前走了一步
J
(
f
(
x
(
i
)
;
θ
+
α
v
)
J(f(x^{(i)}; \theta + \alpha v)
J(f(x(i);θ+αv),这个地方标准动量那里是
J
(
f
(
x
(
i
)
;
θ
)
J(f(x^{(i)}; \theta)
J(f(x(i);θ)。这个提前量的设计让算法有了对前方环境的预判能力,可以理解为Nesterov动量往标准动量方法中添加了一个校正因子。
在凸优化问题使用批量梯度下降,Nesterov动量将
k
k
k步之后的额外误差收敛率从
O
(
1
k
)
O(\frac{1}{k})
O(k1)提高到
O
(
1
k
2
)
O(\frac{1}{k^2})
O(k21), 对SGD没有改进收敛率。
optimizer = tf.train.MomentumOptimizer(learning_rate=learning_rate,
momentum=0.9, use_nesterov=True)
完整的Nesterov动量算法流程:
5. 改进方向二:环境感知
由于SGD中随机采样Batch会引入噪声源,在极小点处的梯度并不会小时。 因此,随着梯度的降低,有必要逐步减小学习率。
SGD对环境的感知是指在参数空间中,根据不同参数的一些经验性判断,自适应的确定参数的学习速率,不同参数的更新步幅是不同的。
5.1 AdaGrad算法
Adaptive Gradient算法的思想是独立地适应模型的每个参数(自动改变学习速率):一直较大偏导的参数相应有一个较小的学习率,初始学习率会下降的较快;而一直小偏导的参数则对应一个较大的学习率,初始学习率会下降的较慢。具体来说,每个参数的学习率会缩放各参数反比于其历史梯度平方值总和的平方根,更新公式:
g
←
1
m
∇
θ
∑
i
J
(
f
(
x
(
i
)
;
θ
)
,
y
(
i
)
)
r
←
r
+
g
⊙
g
θ
←
θ
−
ϵ
δ
+
r
⊙
g
\begin{array}{l} g \leftarrow \frac{1}{m} \nabla_{\theta} \sum_{i} J\left(f\left(\boldsymbol{x}^{(i)} ; \theta\right), y^{(i)}\right) \\ r \leftarrow r+g \odot g \\ \theta \leftarrow \theta-\frac{\epsilon}{\delta+\sqrt{r}} \odot g \end{array}
g←m1∇θ∑iJ(f(x(i);θ),y(i))r←r+g⊙gθ←θ−δ+rϵ⊙g
r
r
r为累积平方梯度。学习率相当于
ϵ
δ
+
r
\frac{\epsilon}{\delta+\sqrt{r}}
δ+rϵ。对于更新不频繁的参数,单次步长会更大, 而对于更新频繁的参数,步长较小,使得学习到的参数更加稳定,不至于被单个样本影响太多。 这里更新频率与步长大小,就是通过这个累积的平方梯度来体现的。 这个数越小,说明更新频率慢,那么在分母上,就相当于增大了学习步长,可以往前走的多一点。
问题:历史梯度在分母上的累积会越来越大, 所以学习率会越来越小, 使得中后期网络的学习能力越来越弱。
经验上已经发现,对于训练深度神经网络模型而言,从训练开始时积累梯度平方会导致有效学习率过早和过量的减小。AdaGrad 在某些深度学习模型上效果不错,但不是全部。
具体的算法流程:
5.2 RMSProp
RMSProp 是 Geoff Hinton 提出的一种自适应学习率方法。RMSprop 和 Adadelta都是为了解决Adagrad 学习率过度衰减问题的。AdaGrad 根据平方梯度的整个历史来收缩学习率,可能使得学习率在达到局部最小值之前就变得太小而难以继续训练。RMSProp 算法修改 AdaGrad 以在非凸设定下效果更好,改变梯度积累为指数加权的移动平均。
AdaGrad 旨在应用于凸问题时快速收敛。当应用于非凸函数训练神经网络时,学习轨迹可能穿过了很多不同的结构,最终到达一个局部是凸碗的区域。AdaGrad 根据平方梯度的整个历史收缩学习率,可能使得学习率在达到这样的凸结构前就变得太小了。RMSProp 使用指数衰减平均以丢弃遥远过去的历史,使其能够在找到凸碗状结构后快速收敛,它就像一个初始化于该碗状结构的 AdaGrad 算法实例。
RMSProp类似于Momentum中的做法,与Momentum的效果一样,某一维度的导数比较大,则指数加权平均就大,某一维度的导数比较小,则其指数加权平均就小,这样就保证了各维度导数都在一个量级,进而减少了摆动。允许使用一个更大的学习率。
RMSProp 还加入了⼀个超参数
ρ
\rho
ρ用于控制衰减速率:
g
←
1
m
∇
θ
∑
i
J
(
f
(
x
(
i
)
;
θ
)
,
y
(
i
)
)
r
←
ρ
r
+
(
1
−
ρ
)
g
⊙
g
θ
←
θ
−
ϵ
δ
+
r
⊙
g
\begin{array}{l} g \leftarrow \frac{1}{m} \nabla_{\theta} \sum_{i} J\left(f\left(\boldsymbol{x}^{(i)} ; \theta\right), y^{(i)}\right) \\ r \leftarrow \rho r+(1-\rho) g \odot g \\ \theta \leftarrow \theta-\frac{\epsilon}{\sqrt{\delta+r}} \odot g \end{array}
g←m1∇θ∑iJ(f(x(i);θ),y(i))r←ρr+(1−ρ)g⊙gθ←θ−δ+rϵ⊙g
优点:相比于AdaGrad,这种方法更好的解决了深度学习中过早的结束学习的问题,适合处理非平稳目标,对RNN效果很好。
缺点: 引入了新的超参衰减系数
ρ
\rho
ρ
optimizer = tf.train.RMSPropOptimizer(learning_rate=learning_rate,
momentum=0.9, decay=0.9, epsilon=1e-10)
RMSProp建议的初始值: 全局学习率 ϵ = 1 e − 3 \epsilon=1e-3 ϵ=1e−3,对应上面的learning_rate参数。 衰减速率 ρ = 0.9 \rho=0.9 ρ=0.9,对应的是上面的decay参数。而momentum这个参数是之前的动量 α \alpha α, 因为具体用的时候可能要结合这动量的一些算法来用。
RMSProp的算法流程如下:
经验上,RMSProp已被证明是一种有效且实用的深度神经网络优化算法,目前经常采用。采用动量的RMSProp算法:
5.3 AdaDelta算法
AdaDelta和RMSPRop一样使用指数衰减平均来丢弃遥远的历史,但AdaDelta算法没有学习率这一超参数。 AdaDelta算法维护一个额外的变量
Δ
θ
\Delta \theta
Δθ, 来计算自变量变化量按元素平方的指数加权移动平均:
g
←
1
m
∇
θ
∑
i
J
(
f
(
x
(
i
)
;
θ
)
,
y
(
i
)
)
r
←
ρ
r
+
(
1
−
ρ
)
g
⊙
g
g
′
←
δ
+
Δ
θ
δ
+
r
⊙
g
θ
←
θ
−
g
′
Δ
θ
←
ρ
Δ
θ
+
(
1
−
ρ
)
g
′
⊙
g
′
\begin{array}{l} g \leftarrow \frac{1}{m} \nabla_{\theta} \sum_{i} J\left(f\left(\boldsymbol{x}^{(i)} ; \theta\right), y^{(i)}\right) \\ r \leftarrow \rho r+(1-\rho) g \odot g \\ g^{\prime} \leftarrow \frac{\sqrt{\delta+\Delta \theta}}{\sqrt{\delta+r}} \odot g \\ \theta \leftarrow \theta-g^{\prime} \\ \Delta \theta \leftarrow \rho \Delta \theta+(1-\rho) g^{\prime} \odot g^{\prime} \end{array}
g←m1∇θ∑iJ(f(x(i);θ),y(i))r←ρr+(1−ρ)g⊙gg′←δ+rδ+Δθ⊙gθ←θ−g′Δθ←ρΔθ+(1−ρ)g′⊙g′
可以看到,如不考虑
δ
\delta
δ的影响,AdaDelta算法和RMSProp算法的不同之处在于使用
Δ
θ
\sqrt{\Delta \theta}
Δθ来代替超参数
ϵ
\epsilon
ϵ。这样就引入了一个超参,又干掉了一个超参。 但貌似用的并不是很多。
6. Adam算法(Adaptive Moment Estimation)
Adam算法之所以单独写出来,是因为它将惯性保持和环境感知这两个优点集于一身。
- Adam记录梯度的一阶矩,即过往梯度与当前梯度的平均,体现了保持惯性—历史梯度的指数衰减平均
- Adam记录梯度的二阶矩, 即过往梯度平方与当前梯度平方的平均,RMSProp的方式,体现了环境感知的能力,为不用参数产生自适应的学习速率 — 历史梯度平方的指数衰减平均。
一阶矩和二阶矩类似于滑动窗口内求平均的思想进行融合,即当前梯度和近一段时间内的梯度平均值,时间久远的梯度为当前平均值贡献呈指数衰减。参数更新方式如下:
g
←
1
m
∇
θ
∑
i
J
(
f
(
x
(
i
)
;
θ
)
,
y
(
i
)
)
t
←
t
+
1
r
←
ρ
1
r
+
(
1
−
ρ
1
)
g
s
←
ρ
2
s
+
(
1
−
ρ
2
)
g
⊙
g
r
^
←
r
1
−
ρ
1
t
s
^
←
s
1
−
ρ
2
t
Δ
θ
←
−
ϵ
r
^
s
^
+
δ
θ
←
θ
+
Δ
θ
\begin{array}{l} g \leftarrow \frac{1}{m} \nabla_{\theta} \sum_{i} J\left(f\left(\boldsymbol{x}^{(i)} ; \theta\right), y^{(i)}\right) \\ t \leftarrow t+1 \\ r \leftarrow \rho_{1} r+\left(1-\rho_{1}\right) g \\ s \leftarrow \rho_{2} s+\left(1-\rho_{2}\right) g \odot g \\ \hat{r} \leftarrow \frac{r}{1-\rho_{1}^{t}} \\ \hat{s} \leftarrow \frac{s}{1-\rho_{2}^{t}} \\ \Delta \theta \leftarrow-\epsilon \frac{\hat{r}}{\sqrt{\hat{s}}+\delta} \\ \theta \leftarrow \theta+\Delta \theta \end{array}
g←m1∇θ∑iJ(f(x(i);θ),y(i))t←t+1r←ρ1r+(1−ρ1)gs←ρ2s+(1−ρ2)g⊙gr^←1−ρ1trs^←1−ρ2tsΔθ←−ϵs^+δr^θ←θ+Δθ
这里加了偏差修正,
s
s
s和
r
r
r初始化为0, 且
ρ
1
\rho_1
ρ1和
ρ
2
\rho_2
ρ2推荐的初始值都很接近1(0.9和0.999), 这时候注意到, 在时间步
t
t
t,我们得到
r
t
=
(
1
−
ρ
1
)
∑
i
=
1
t
ρ
1
t
−
i
g
i
r_{t}=\left(1-\rho_{1}\right) \sum_{i=1}^{t} \rho_{1}^{t-i} g_{i}
rt=(1−ρ1)∑i=1tρ1t−igi, 将过去各时间步小批量随机梯度的权值相加,得到
(
1
−
ρ
1
)
∑
i
=
1
t
ρ
1
t
−
i
=
1
−
ρ
1
t
\left(1-\rho_{1}\right) \sum_{i=1}^{t} \rho_{1}^{t-i}=1-\rho_{1}^{t}
(1−ρ1)∑i=1tρ1t−i=1−ρ1t。可以看出, 当
t
t
t较小时,过去各个时间步小批量随机梯度权值之和会较小。假设取
ρ
1
=
0.9
\rho_1=0.9
ρ1=0.9,可以得到
t
=
1
t=1
t=1时
r
1
=
0.1
g
1
r_1=0.1g_1
r1=0.1g1。 因此我们要偏差修正,也就是上面的
r
^
,
s
^
\hat r, \hat s
r^,s^,使得过去各时间步小批量随机梯度权值之和为1。Adam通常被认为对超参数的选择相当鲁棒。
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
Adam算法流程如下:
在训练深度神经网络模型时,Adam 优化算法可以被优先选择,它通常比其他优化算法快并且效果好;Adam 算法有三个参数,一般使用默认的参数就可以了,但是如果需要调整的话,建议熟悉一下它的理论,然后根据实际情况设置参数。
为了使得内容更加的完整,这里再来了解一点新的东西。
7. 理解更多的细节
7.1 理解指数滑动平均
上面的很多优化算法里面都见到了一个词叫做指数滑动平均,这个东西到底是干嘛的?这里理解下这个,便于更好的理解上面的改进算法。
指数加权平均在时间序列中经常用于求取平均值的一个方法,它的思想是这样,我们要求取当前时刻的平均值,距离当前时刻越近的那些参数值,它的参考性越大,所占的权重就越大,这个权重是随时间间隔的增大呈指数下降,所以叫做指数滑动平均,公式如下:
v
t
=
β
∗
v
t
−
1
+
(
1
−
β
)
∗
θ
t
v_{t}=\beta * v_{t-1}+(1-\beta) * \theta_{t}
vt=β∗vt−1+(1−β)∗θt
v
t
v_{t}
vt是当前时刻的一个平均值,这个平均值有两项构成,一项是当前时刻的参数值
θ
t
\theta_{t}
θt, 所占的权重是
1
−
β
1-\beta
1−β, 这个
β
\beta
β是个参数。 另一项是上一时刻的一个平均值, 权重是
β
\beta
β。
当然这个公式看起来还是很抽象,丝毫没有看出点指数滑动的意思, 那么还是用吴恩达老师PPT里的一个例子解释一下吧:
看上面这个温度图像,横轴是第几天,然后纵轴是温度, 假设我想求第100天温度的一个平均值,那么根据上面的公式:
v
100
=
β
∗
v
99
+
(
1
−
β
)
∗
θ
100
=
(
1
−
β
)
∗
θ
100
+
β
∗
(
β
∗
v
98
+
(
1
−
β
)
∗
θ
99
)
=
(
1
−
β
)
∗
θ
100
+
(
1
−
β
)
∗
β
∗
θ
99
+
(
β
2
∗
v
98
)
=
(
1
−
β
)
∗
θ
100
+
(
1
−
β
)
∗
β
∗
θ
99
+
(
1
−
β
)
∗
β
2
∗
θ
98
+
(
β
3
∗
v
97
)
=
(
1
−
β
)
∗
β
0
∗
θ
100
+
(
1
−
β
)
∗
β
1
∗
θ
99
+
(
1
−
β
)
∗
β
2
∗
θ
98
+
(
β
3
∗
v
97
)
=
∑
i
N
(
1
−
β
)
∗
β
i
∗
θ
N
−
i
\begin{array}{l} v_{100}=\beta * v_{99}+(1-\beta) * \theta_{100} \\ =(1-\beta) * \theta_{100}+\beta *\left(\beta * v_{98}+(1-\beta) * \theta_{99}\right) \\ =(1-\beta) * \theta_{100}+(1-\beta) * \beta * \theta_{99}+\left(\beta^{2} * v_{98}\right) \\ = (1-\beta) * \theta_{100}+(1-\beta) * \beta * \theta_{99}+(1-\beta) * \beta^{2} * \theta_{98}+\left(\beta^{3} * v_{97}\right) \\ = (1-\beta) * \beta^{0} * \theta_{100}+(1-\beta) * \beta^{1} * \theta_{99}+(1-\beta) * \beta^{2} * \theta_{98}+\left(\beta^{3} * v_{97}\right) \\ = \sum_{i}^{N}(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\beta}^{i} * \boldsymbol{\theta}_{N-i} \end{array}
v100=β∗v99+(1−β)∗θ100=(1−β)∗θ100+β∗(β∗v98+(1−β)∗θ99)=(1−β)∗θ100+(1−β)∗β∗θ99+(β2∗v98)=(1−β)∗θ100+(1−β)∗β∗θ99+(1−β)∗β2∗θ98+(β3∗v97)=(1−β)∗β0∗θ100+(1−β)∗β1∗θ99+(1−β)∗β2∗θ98+(β3∗v97)=∑iN(1−β)∗βi∗θN−i
最下面这一行就是通式了,我们发现,距离当前时刻越远的那些
θ
\theta
θ值,它的权重是越来越小的,因为
β
\beta
β小于1, 所以间隔越远,小于1的这些数连乘,权重越来越小,而且是呈指数下降,因为这里是
β
i
\beta ^i
βi。
我们可以发现,beta越小,就会发现它关注前面一段时刻的距离就越短,比如这个0.8, 会发现往前关注20天基本上后面的权重都是0了,意思就是说这时候是平均的过去20天的温度, 而0.98这个,会发现,关注过去的天数会非常长,也就是说这时候平均的过去50天的温度。所以beta在这里控制着记忆周期的长短,或者平均过去多少天的数据,这个天数就是
1
1
−
β
\frac{1}{1-\beta}
1−β1, 通常beta设置为0.9, 物理意义就是关注过去10天左右的一个温度。 这个参数也是比较重要的, 还是拿吴恩达老师PPT的一张图片:
看上图,是不同beta下得到的一个温度变化曲线
- 红色的那条,是beta=0.9, 也就是过去10天温度的平均值
- 绿色的那条,是beta=0.98, 也就是过去50天温度的平均值
- 黄色的那条,beta=0.5, 也就是过去2天的温度的平均
可以发现,如果这个 β \beta β很高, 比如0.98, 最终得到的温度变化曲线就会平缓一些,因为多平均了几天的温度, 缺点就是曲线进一步右移, 因为现在平均的温度值更多, 要平均更多的值, 指数加权平均公式,在温度变化时,适应的更缓慢一些,所以会出现一些延迟,因为如果 β \beta β=0.98,这就相当于给前一天加了太多的权重,只有0.02当日温度的权重,所以温度变化时,温度上下起伏,当 β \beta β变大时,指数加权平均值适应的更缓慢一些, 换了0.5之后,由于只平均两天的温度值,平均的数据太少,曲线会有很大的噪声,更有可能出现异常值,但这个曲线能够快速适应温度的变化。 所以这个 β \beta β过大过小,都会带来问题。 一般取0.9.
而Momentum梯度下降,基本的想法就是计算梯度的指数加权平均数,并利用该梯度更新权重。
- 普通的梯度下降: w i + 1 = w i − l r ∗ g ( w i ) {w}_{i+1}={w}_{i}-{l} {r} * {g}\left({w}_{i}\right) wi+1=wi−lr∗g(wi)
- Momentum梯度下降:
v i = m ∗ v i − 1 + g ( w i ) w i + 1 = w i − l r ∗ v i v_i = m * v_{i-1} + g(w_i) \\ w_{i+1} = w_{i} - lr * v_i vi=m∗vi−1+g(wi)wi+1=wi−lr∗vi
这里的
m
m
m就是momentum系数,
v
i
v_i
vi表示更新量,
g
(
w
i
)
g(w_i)
g(wi)是
w
i
w_i
wi的梯度。 这里的
v
i
v_i
vi就是既考虑了当前的梯度,也考虑了上一次梯度的更新信息, 如果还是很抽象,那么再推导一下就可以:
v
100
=
m
∗
v
99
+
g
(
w
100
)
=
g
(
w
100
)
+
m
∗
(
m
∗
v
98
+
g
(
w
99
)
)
=
g
(
w
100
)
+
m
∗
g
(
w
99
)
+
m
2
∗
v
98
=
g
(
w
100
)
+
m
∗
g
(
w
99
)
+
m
2
∗
g
(
w
98
)
+
m
3
∗
v
97
\begin{aligned} v_{100} &=m * v_{99}+g\left(w_{100}\right) \\ &=g\left(w_{100}\right)+m *\left(m * v_{98}+g\left(w_{99}\right)\right) \\ &=g\left(w_{100}\right)+m * g\left(w_{99}\right)+m^{2} * v_{98} \\ &=g\left(w_{100}\right)+m * g\left(w_{99}\right)+m^{2} * g\left(w_{98}\right)+m^{3} * v_{97} \end{aligned}
v100=m∗v99+g(w100)=g(w100)+m∗(m∗v98+g(w99))=g(w100)+m∗g(w99)+m2∗v98=g(w100)+m∗g(w99)+m2∗g(w98)+m3∗v97
这样,就可以发现,当前梯度的更新量会考虑到当前梯度, 上一时刻的梯度,前一时刻的梯度,这样一直往前,只不过越往前权重越小而已。这就是动量的含义了,考虑了之前的梯度保持着一种惯性。而像RMSProp里面的历史梯度平方的指数加权衰减,AdaDelta里面的,Adam里面的指数加权等,其实都是这个意思,考虑前面的梯度或者梯度的平方,求一个平均值来更新当前参数。
7.2 理解下指数加权平均的偏差修正
偏差修正可以让平均数运算更加准确, 看看是怎么做的。这里拿吴恩达老师深度学习的一个例子:
当我们执行这个算法的时候,
β
=
0.9
\beta=0.9
β=0.9的时候是红色那条线,
β
=
0.98
\beta=0.98
β=0.98的时候是绿色那条线,但是真实的运行,实际上不是绿色那条,是紫色的那条线,起始点更低一些,为啥呢?
具体运行的时候, v 0 = 0 v_0=0 v0=0, 那么求 v 1 v_1 v1的时候,左边那块是0,只有0.02的 θ 1 \theta_1 θ1, 所以如果第一天的温度是40华氏度,那么求出的 v 1 = 8 v_1=8 v1=8, 因此得到的真实值要比40小了很多,所以第一天的估测不准。再看看上面的 v 2 v_2 v2,把求出的 v 1 v_1 v1代入下面那个公式,这样计算的 v 2 v_2 v2要远小于第一天和第二天的数据。所以在预测的初始阶段,直接用加权平均的方式是不准的,因为初始的时候前面没有积累下东西。
那么怎么处理呢? 偏差修正,也就是在估测的初期,我不用 v t v_t vt来表示平均温度,而是在这个基础上再除以 1 − β t 1-\beta ^t 1−βt,用 v t 1 − β t \frac{v_t}{1-\beta ^t} 1−βtvt这个来表示平均温度, 这时候求出来的前两天的平均维度就正常了很多,如上图右边。而随着 t t t的增加,由于 β \beta β是小于1的,所以慢慢的会趋于0, 到了后面,就恢复成 v t v_t vt了。
所以预测的初始阶段,才开始预测的热身练习,偏差修正可以帮助更好的预测温度,后期的话热身过去了,偏差修正就起不到作用了
7.3 理解里面的超参数选择
上面各个优化算法中,常用的是Adam,SGD,RMSProp等, 这些优化算法中,有两个比较重要的超参数需要进行调参确定,就是学习率 ϵ \epsilon ϵ和指数衰减系数 ρ \rho ρ, 而我们如何为这两个参数选择合适的范围呢?
超参数的随机取值可以提高搜索效率,但是这个随机取值可不是乱随机的,而是应该选择合理的范围,在某个范围内取值,而选择合理范围就需要选择合适的标尺,用于探究这些超参。这个很重要。
假设我们选取的是隐藏单元这种或者神经网络层数这种,那么我们可以确定一个大致的范围,比如隐藏单元50-100,然后在这个范围随机取点尝试, 隐藏层2-8, 从这里面随机取层试验,这两个超参数这样取值是合理的。 但是上面的那两个超参确定范围不能这么玩。
假设我们已知一个学习率的范围是0.0001~1, 但是我们如果想在这个范围内取比较好的值,就不能像上面那个一样,在这个区间随机的取值了。因为如果用上面这种方式随机的取值,我们会发现90%的数据都落到 0.1~1之间。
这会导致
ϵ
\epsilon
ϵ的取值有很大的不均匀性,因为
ϵ
\epsilon
ϵ最优可能的最优值在0.0001~1之间。这时候上面的均匀取值法就不适合了,而是应该采用学习率对数均匀取值。具体操作就是给定了学习率的范围[0.0001, 1], 我们先求这个范围的对数取值
a
=
l
o
g
10
0.0001
=
−
4
,
b
=
l
o
g
10
1
=
0
a=log_{10}0.0001=-4, b=log_{10}1=0
a=log100.0001=−4,b=log101=0, 那么就从[-4,0]
随机取一个值
r
r
r(python: r=-4 * np.random.rand()
)。 然后
ϵ
=
1
0
r
\epsilon=10^r
ϵ=10r, 深度学习不同于传统的机器学习,实践也证明在一个超参数空间范围内,随机地去参数的组合要比顺序地取参数组合效果要好。
同理, 对于指数衰减系数 ρ \rho ρ也得用一种这样的策略,但是还不太一样。假设我们知道 ρ \rho ρ的合理取值范围是[0.9, 0.999], 取0.9就像在10个值中计算平均值,有点类似于计算10天的温度平均值,而后者相当于1000天的温度平均值,所以这个和上面那个类似,就不能在线性数轴上搜索了。
这是因为当 ρ \rho ρ接近1时,所得结果的灵敏度会变化,即使 ρ \rho ρ有很小的变化,如果 ρ \rho ρ在0.9-0.9005之间取值,无关紧要,但 ρ \rho ρ在0.999-0.9995之间取值,这会对算法产生巨大影响,当 ρ \rho ρ接近1,β的细微变化变的很敏感,所以整个取值过程中,需要更加密集的取值,在 ρ \rho ρ接近1的区间内。
就是说虽然还是随机取值,但是不能随便取了,因为这个关系着算法,和上面的 ϵ \epsilon ϵ一样,虽然可以在线性数轴上取值,但是那样这个 ϵ \epsilon ϵ的一个小波动就可能对算法产生很大的影响,即每个点的影响不同,至少要加点权那样子,所以不能随便取。
那么这个 ρ \rho ρ的值应该怎么确定呢? 依然是采用对数均匀取值,但是不是直接考虑 ρ \rho ρ, 而是考虑 1 − ρ 1-\rho 1−ρ。这个的范围是[0.001-0.1]之间, 于是我们可以根据确定学习率的方式确定 1 − ρ 1-\rho 1−ρ。
- 取对数确定两个端点值 a = l o g 10 0.001 = − 3 , b = l o g 10 0.1 = − 1 a=log_{10}0.001=-3,b=log_{10}0.1=-1 a=log100.001=−3,b=log100.1=−1, 所以对数区间[-3,-1]
- 在[-3,-1]区间随机的取值 r r r,就得到了 1 − ρ = 1 0 r 1-\rho=10^r 1−ρ=10r
- 这就得到了 ρ = 1 − 1 0 r \rho=1-10^r ρ=1−10r
这个也是比较重要的一个点了。
8. 如何选择优化算法
这里是回答怎么选择优化算法的问题了,《深度学习》里面指出目前这个没有一个确定的定论,如何选,取决于做的任务以及对优化算法的熟练程度。这里整理一个常用参考标准,当然也不是定论:
- 对于稀疏数据,尽量使用学习率可自适应的优化方法,不用手动调节,而且最好采用默认值。
- SGD通常训练时间更长,但是在好的初始化和学习率调度方案的情况下(很多论文都用SGD),结果更可靠。并且适用于在线的实时更新,推荐里面可是常用
- 如果在意更快的收敛,并且需要训练较深较复杂的网络时,推荐使用学习率自适应的优化方法。
- Adadelta,RMSprop,Adam是比较相近的算法,在相似的情况下表现差不多 。Adam 就是在 RMSprop 的基础上加了 bias-correction 和 momentum,随着梯度变得稀疏,Adam 比 RMSprop 效果会好。整体来讲,Adam 是最好的选择。
PS: 上面的这些优化器,在TensorFlow或者pytorch里面都有包已经集成好了,我们可以直接拿来用。
下面一张动图看看各个优化器的效果啦, 放松下 😉
参考:
- 《深度学习》 – 花书
- 《百面机器学习》
- 深度学习中的优化问题以及常用优化算法
- 深度模型中的优化
- 深度学习中的优化算法总结
- 系统学习Pytorch笔记七:优化器和学习率调整策略
- 吴恩达老师的《深度学习课程》