一个深度学习项目一般由数据
、模型
、损失
、优化
、训练
和预测
等部分构成,对于其中的“优化”部分,我们最熟悉的可以说就是 梯度下降(gradient descent) 算法了。然而,在实际的深度学习架构中,我们却经常看到的是Adam优化器,那么Adam和梯度下降算法有什么关系呢?又有哪些梯度下降算法的变体呢?以及又有哪些优化梯度下降算法的策略呢?
本文参考Sebastian Ruder的论文:“An overview of gradient descent optimization algorithms”,解答以上问题。
根据计算目标函数(损失函数)时所使用的数据量的不同,有三种梯度下降算法的变体。根据数据量的大小,我们在参数更新的准确度和更新所需时间之间进行权衡。
Batch gradient descent,即批量梯度下降算法,计算整个数据集关于参数 θ \theta θ的损失函数的梯度:
θ = θ − η ⋅ ∇ θ J ( θ ) (1) \theta = \theta - \eta \cdot\nabla_{\theta}J(\theta)\tag{1} θ=θ−η⋅∇θJ(θ)(1)
批量梯度下降算法代码示例:
for i in range(nb_epochs):
params_grad = evaluate_gradient(loss_function, data, params)
params = params - learning_rate * params_grad
批量梯度下降算法保证对于凸误差曲面
(即误差函数为凸函数),收敛到全局最小值
(global minimum);对于非凸误差曲面
(即误差函数为非凸函数),收敛到局部最小值
(local minimum)。
很明显,批量梯度下降在计算中,每一次的更新都需要在整个数据集上进行计算,因此该算法速度较慢,且当数据量较大时,计算内存是个棘手问题。同时,批量梯度下降算法不能在线更新模型,即不能在计算中加入新数据。
Stochastic gradient descent(SGD),即随机梯度下降算法,对每一个训练样本 x ( i ) x^{(i)} x(i)和标签 y ( i ) y^{(i)} y(i)执行参数更新:
θ = θ − η ⋅ ∇ θ J ( θ ; x ( i ) ; y ( i ) ) (2) \theta = \theta -\eta \cdot \nabla_{\theta}J(\theta; x^{(i)}; y^{(i)})\tag{2} θ=θ−η⋅∇θJ(θ;x(i);y(i))(2)
随机梯度下降算法代码示例:
for i in range(nb_epochs):
np.random.shuffle(data)
for example in data:
params_grad = evaluate_gradient(loss_function, example, params)
params = params - learning_rate * params_grad
可以看出,不同于批量梯度下降,随机梯度下降在每输入一个训练样本后,就更新参数,这样计算速度较快,而且可以在线更新模型。
然而,SGD以高方差频繁进行更新,使得目标函数波动严重,如图1所示。
SGD的波动,一方面,使得它能够跳到新的、可能更好的局部最小值。另一方面,这最终也使得收敛到确切的最小值复杂化,因为SGD会保持超调(overshooting)。
然而,事实表明,当学习率缓慢减小时,SGD能够表现出和批量梯度下降同样的收敛性,即对于非凸优化和凸优化分别收敛到局部最小值和全局最小值。
Mini-batch gradient descent,即小批量梯度下降算法,对每小批量 n n n个训练样本执行参数更新:
θ = θ − η ⋅ ∇ θ J ( θ ; x ( i ; i + n ) ; y ( i ; i + n ) ) (3) \theta = \theta -\eta \cdot \nabla_{\theta}J(\theta; x^{(i; i+n)}; y^{(i; i+n)})\tag{3} θ=θ−η⋅∇θJ(θ;x(i;i+n);y(i;i+n))(3)
小批量梯度下降算法代码示例:
for i in range(nb_epochs):
np.random.shuffle(data)
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function, batch, params)
params = params - learning_rate * params_grad
小批量梯度下降降低了参数更新的方差,从而使得收敛更加稳定。通常情况下,mini-batch size在50到256之间,当然也可以根据应用的不同而不同。小批量梯度下降是训练神经网络的经典算法,同时,使用小批量梯度下降算法时也经常使用SGD。
然而,普通的小批量梯度下降算法并不能保证很好的收敛,也面临着一些挑战需要解决:
针对以上面临的挑战,深度学习社区提出了一些解决方法,下面简要介绍这些算法。
SDG在峡谷中是有困难的,在峡谷的斜坡上振荡,只沿着底部犹豫地走向局部最优,如图2a所示。
Momentum(动量)是一个有助于在相关方向上加速SGD以及抑制震荡的方法,如图2b所示。Momentum通过将过去时间步长的更新向量的 γ \gamma γ部分添加到当前的更新向量中来实现这一点,公式如下:
v t = γ v t − 1 + η ∇ θ J ( θ ) θ = θ − v t (4) \begin{aligned} v_t &= \gamma v_{t-1}+\eta \nabla_{\theta}J(\theta)\\ \theta &= \theta - v_t \end{aligned}\tag{4} vtθ=γvt−1+η∇θJ(θ)=θ−vt(4)
可以看出,参数 θ \theta θ的更新在本来梯度的基础上,又中加入了 γ \gamma γ 倍的前一个时间步的更新量,使得参数更新积累了动量,提升了更新速度。同时,在参数更新时,动量项增加对梯度指向相同方向的维度的动量项,并减少对梯度改变方向的维度的更新,这与梯度下降算法的思想一致。因此,该方法加速了收敛,同时减小了震荡。
然而,通过Momentum方法虽然加速了参数在斜坡面上的更新,但是,其更新方向却是盲目。我们希望有一个更加聪明的方法,使得参数在更新时,能够知道自己要去哪里,以及在山坡再次倾斜前能够知道减速。
Nesterov accelerated gradient(NAG)就是赋予动量项这种预知能力的方法。其具体实现方法是,将 θ − γ v t − 1 \theta-\gamma v_{t-1} θ−γvt−1看作是参数的大致下一个位置,即参数要去的地方。通过计算损失函数关于 θ − γ v t − 1 \theta-\gamma v_{t-1} θ−γvt−1的值来更新当前参数 θ \theta θ:
v t = γ v t − 1 + η ∇ θ J ( θ − γ v t − 1 ) θ = θ − v t (5) \begin{aligned} v_t &= \gamma v_{t-1}+\eta \nabla_{\theta}J(\theta-\gamma v_{t-1})\\ \theta &= \theta - v_t \end{aligned}\tag{5} vtθ=γvt−1+η∇θJ(θ−γvt−1)=θ−vt(5)
既然已经能够调整我们的更新,以适应误差函数的斜率,从而加速SGD,我们便希望能够对每一个参数进行调整,以根据它们的重要性来进行更大或更小的更新。
Adagrad是一种基于梯度优化的算法,基本思想是:更新学习率,对于出现较少的参数执行更大的更新,对于频繁出现的参数执行更小的更新。公式如下:
θ t + 1 = θ t − η G t + ϵ ⊙ ∇ θ t J ( θ t ) (6) \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}}\odot\nabla_{\theta_t}J(\theta_t)\tag{6} θt+1=θt−Gt+ϵη⊙∇θtJ(θt)(6)
其中, G t ∈ R d × d G_t \isin \mathbb{R}^{d\times d} Gt∈Rd×d是一个对角线矩阵,对角线上的元素 i i i是直到时间步 t t t的 θ i \theta_i θi的梯度的平方和; ϵ \epsilon ϵ是平滑项,防止分母为零; ⊙ \odot ⊙是逐位置矩阵向量乘积。
Adagrad的一个主要的好处就是避免了手动调整学习率,然而,其缺点在于分母中梯度平方的累计。由于每次增加项都是正值,因此在训练中累计和持续增加。这反过来导致学习率持续缩减,以致于最终变为无限小。这就使得算法不能再接受新的知识了,即不能实现参数的更新了。
Adadelta是Adagrad算法的扩展,以解决其单调减小学习率的问题。Adadelta将所要积累的过去梯度限制在一些固定的尺寸 w w w中,而不是积累所有过去的平方梯度。
并不是低效地存储 w w w个先前的平方梯度,Adadelta将梯度之和递归地定义为所有过去平方梯度的衰减平均值(decaying average)。
E [ g 2 ] t = γ E [ g 2 ] t − 1 + ( 1 − γ ) g t 2 (7) E[g^2]_t= \gamma E[g^2]_{t-1}+(1-\gamma)g^2_t\tag{7} E[g2]t=γE[g2]t−1+(1−γ)gt2(7)
在Adagrad中,
Δ θ t = − η G t + ϵ ⊙ g t \Delta\theta_t = -\frac{\eta}{\sqrt{G_t+\epsilon}}\odot g_t Δθt=−Gt+ϵη⊙gt
在Adadelta中,将对角矩阵 G t G_t Gt换为之前平方梯度的衰减平均值 E [ g 2 ] t E[g^2]_t E[g2]t,得:
Δ θ t = − η E [ g 2 ] t + ϵ g t (8) \Delta\theta_t = -\frac{\eta}{\sqrt{E[g^2]_t+\epsilon}}g_t \tag{8} Δθt=−E[g2]t+ϵηgt(8)
上式分母正好为梯度 g g g的均方根(RMS)误差,因此可简写为:
Δ θ t = − η R M S [ g ] t g t (9) \Delta\theta_t = -\frac{\eta}{RMS[g]_t}g_t \tag{9} Δθt=−RMS[g]tηgt(9)
由于一些原因,Adadelta的作者又定义了关于参数的衰减平均值,并将学习率 η \eta η换为了 R M S [ Δ θ ] t − 1 RMS[\Delta\theta]_{t-1} RMS[Δθ]t−1,从而得到Adadelta的参数更新规则:
Δ θ t = − R M S [ Δ θ ] t − 1 R M S [ g ] t g t θ t + 1 = θ t + Δ θ t (10) \begin{aligned} \Delta\theta_t = -\frac{RMS[\Delta\theta]_{t-1}}{RMS[g]_t}g_t \\ \theta_{t+1}=\theta_t+\Delta\theta_t \end{aligned}\tag{10} Δθt=−RMS[g]tRMS[Δθ]t−1gtθt+1=θt+Δθt(10)
可以看出,Adadelta不需要设置默认学习率,学习率已经从参数更新规则中消除掉了。
Adaptive Moment Estimation(Adam),即自适应矩估计,是另一种为每个参数计算自适应学习率的方法。除了像Adadelta一样存储了之前平方梯度的指数衰减平均值 v t v_t vt,Adam也保持了之前梯度的指数衰减平均值 m t m_t mt,类似于动量:
m t = β 1 m t − 1 + ( 1 − β 1 ) g t v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 (11) \begin{aligned} m_t = \beta_1 m_{t-1}+(1-\beta_1)g_t \\ v_t = \beta_2 v_{t-1} + (1-\beta_2)g^2_t \end{aligned}\tag{11} mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)gt2(11)
m t m_t mt和 v t v_t vt分别是梯度的一阶矩(均值)和二阶矩(方差)的估计值,因此该方法的名字中使用了moment(矩)。当 m t m_t mt和 v t v_t vt初始化为零向量时,Adam作者观察到,它们偏向于零,特别是在初始时间步,以及当衰减率很小时(即, β 1 \beta_1 β1和 β 2 \beta_2 β2接近于1)。
他们通过计算经过偏差校正的一阶距和二阶矩矩估计值来抵消这些偏差:
m t ^ = m t 1 − β 1 t v t ^ = v t 1 − β 2 t (12) \begin{aligned} \hat{m_t} =\frac{m_t}{1-\beta^t_1}\\ \hat{v_t} =\frac{v_t}{1-\beta^t_2} \end{aligned}\tag{12} mt^=1−β1tmtvt^=1−β2tvt(12)
然后,使用它们来更新参数,方法和Adadelta一样。从而得到Adam的参数更新规则:
θ t + 1 = θ t − η v t ^ + ϵ m t ^ (13) \theta_{t+1}=\theta_t - \frac{\eta}{\sqrt{\hat{v_t}}+\epsilon}\hat{m_t}\tag{13} θt+1=θt−vt^+ϵηmt^(13)
Adam作者提出 β 1 \beta_1 β1的默认值为0.9, β 2 \beta_2 β2的默认值为0.999,以及 ϵ \epsilon ϵ的默认值为10-8 。他们的经验表明,Adam实践工作效果良好,并且优于其他自适应性学习算法。
下面的两张图能够给我们带来对于以上所介绍的优化算法在优化过程上的感知,推荐同时参考https://cs231n.github.io/neural-networks-3/中关于此内容的动态图,生动形象!
在图a中,我们能看到不同的优化算法在损失面轮廓上的时间演化路径。所有的算法从同一点出发,通过不同的路径达到最小值。可以看到,Adagrad、Adadelta和RMSprop立即朝着正确的方向前进,并很快相似地收敛;而Momentum和NAG确实是“overshooting”,就像是一个球滚下了山一样。图b展示了不同的优化算法在鞍点(saddle point)的行为,可以形象地看出,SGD、Momentum和NAG很难打破鞍点的对称性,SDG最终被困入其中,Momentum和NAG最终还是成功逃脱了;而Adagrad、Adadelta和RMSprop从一开始便冲向负斜坡,并由Adadelta带头冲锋。
通常情况下,我们不希望输入到模型的训练数据是按照具有一定意义的顺序排列的,因为那样可能会使优化算法有所偏差。因此,在每一个epoch之后将训练数据“洗一下牌”(shuffling)是一个不错的做法,如在上述的SGD和mini-batch gradient descent算法中,均用到了shuffling方法。
另一方面,有些情况下,我们需要解决较为困难的问题,需要训练数据按照一定意义的顺序来排布,以提升效果,使模型更好地收敛。这时,我们就需要使用称之为Curriculum Learning的方法,来将训练数据按照一定意义的顺序进行排列。
为了促进模型学习,我们一般会将初始参数标准化为均值为0,方差为1的标准正态分布。然而随着训练的进行,参数被更新到了不同的尺度,便失去了之前的标准化。随着网络层数的加深,这会减低训练速度,同时放大变化。
Batch normalization,即批量标准化,为每一个小批次重新建立标准化数据。通过在模型架构中使用标准化,我们可以使用更高的学习率,以及不需要太多关注于参数初始化。另外,批量标准化也起到正则化的效果,因此可以降低(有时甚至可以去除)对Dropout的需要。
正如Geoff Hinton所说:“Early stopping(is) beautiful free lunch”(早停法是漂亮的免费午餐)。Early stopping的思想就是,在模型训练中,监测模型在验证集上的误差,当模型在验证集上的效果提升得不够时,我们可以提前停止(也要有点耐心)模型的训练。
Neelakantan等人为每个梯度更新添加了遵循高斯分布 N ( 0 , σ t 2 ) N(0,\sigma^2_t) N(0,σt2)的噪声:
g t , i = g t , i + N ( 0 , σ t 2 ) (14) g_{t,i} = g_{t,i} + N(0, \sigma^2_t)\tag{14} gt,i=gt,i+N(0,σt2)(14)
并通过下面的策略对方差 σ \sigma σ进行处理:
σ t 2 = η ( 1 + t ) γ (15) \sigma^2_t = \frac{\eta}{(1+t)^\gamma}\tag{15} σt2=(1+t)γη(15)
他们表明,添加这个噪声可以使得网络对缺乏初始化的情况表现得更加稳健,而且对训练特别是深层复杂的网络是有帮助的。他们怀疑,添加的噪声给了模型更多的机会逃脱和找到新的局部最小值,而这些最小值对于更深层次的模型是更多的。
本文参考Sebastian Ruder的论文,对梯度下降算法的变体、梯度下降优化算法以及随机梯度下降算法优化策略进行了介绍。通过梳理,我们得知,梯度下降算法有三种变体:批量梯度下降、随机梯度下降和小批量梯度下降。Adam是梯度下降优化算法之一,除此之外,还有Momentum、Nesterov accelerated gradient、Adagrad和Adadelta等梯度优化算法。对于随机梯度下降算法,介绍了几种优化策略,分别是:Shuffling and Curriculum Learning、Batch normalization、Early stopping和Gradient noise,为我们在提升深度学习模型训练上提供了切实可用的方法。
学习笔记,以作分享,如有不妥,敬请指出。