梯度下降
回归本源
好了,现在让我们从头说起,从另一个角度看待梯度下降算法。
考虑某连续可微函数 f:R→R,根据Taylor展开有
f(x+ε)=f(x)+εf′(x)+o(ε2)
固定步长 η>0,取 ε=−ηf′(x),代入上式有
f(x−ηf′(x))=f(x)−ηf′2(x)+o(η2f′2(x))
这意味着,如果使用 x−ηf′(x) 代替 x,函数值可能会下降,这就是一维函数梯度下降的基本思想。其中的 η 称为学习率,是一个可控制的超参数。
现在考虑某多元连续可微函数 f:Rd→R,其梯度定义为
∇f(x)=[∂x1∂f,⋯,∂xd∂f]T
对此函数应用多元Taylor展开有
f(x+ε)=f(x)+εT∇f(x)+o(∥ε∥2)
结合梯度的几何意义以及上式,函数最陡下降的方向由梯度的反方向 −∇f(x) 给出。因此固定步长 η>0,取 ε=−η∇f(x),则用 x+ε 代替 x 以实现梯度下降。
牛顿法
选择一个合适的学习率 η 是很棘手的问题。如果我们可以自动确定 η,或者完全不必选择学习率就好了。基于这种思想,人们提出了几种自适应方法,其中最为著名的便是牛顿法。
对于某多元连续可微函数 f:Rd→R,考虑更高阶的Taylor展开
f(x+ε)=f(x)+εT∇f(x)+21εTHε+o(∥ε∥3)
其中 H=∇2f(x) 表示该处的Hessian矩阵。当我们固定 x 时,上式可以看做一个关于 ε 的函数。要想使得 f(x+ε) 最小,只需要令 f 对 ε 的偏导数为 0 即可,即有
∇f(x)+Hε=0⇒ε=−H−1∇f(x)
因此,我们需要找到一种方法求Hessian矩阵的逆。然而,直接求 Hessian 矩阵的逆在计算上极其昂贵,且当 Hessian 矩阵非正定时会导致算法向错误方向发散,因此通常采用阻尼牛顿法来改进。
我们不直接求解 H−1∇f(x),而是引入阻尼因子 μ,对 Hessian 矩阵修正变为 (H+μI)−1∇f(x)。这一修正利用单位矩阵 I 的特性将 Hessian 矩阵的特征值整体抬升,从而强制矩阵变为正定,确保搜索方向始终指向函数值下降的区域。
这样做的原因之一还有:泰勒展开仅在局部范围内对原函数拟合良好,直接使用计算出的极值点作为下一步位置往往步长过大,容易导致震荡甚至发散。通过控制步长,我们可以保守地利用二阶导数提供的曲率信息,在保证算法稳定收敛的前提下,尽可能获得比传统一阶梯度下降更快的收敛速度。
可以证明,牛顿法对于凸函数优化问题而言,可以达到线性收敛速度。
随机梯度下降
随机梯度更新
事实上,我们在整个深度学习篇中使用的梯度下降方法实为随机梯度下降法。我们考虑某个目标函数被定义为每个样本的损失函数的平均值。给定 n 个样本的训练数据集,令 fi(x) 为索引 i 训练样本的损失函数,其中 x 是参数向量。因此,目标函数记作
f(x)=n1i=1∑nfi(x)
显然有
∇f(x)=n1i=1∑n∇fi(x)
如果使用梯度下降法,则每个自变量迭代需要消耗 O(n) 的复杂度。当训练数据集较大时,这种方法是不可取的。为此,随机梯度下降通过对数据样本随机均匀的采样一个索引 i∈{1,2,⋯,n},并通过下式更新 x
x←x−η∇fi(x)
这样做就用 O(1) 的代价实现了迭代。我们不难证明随机梯度 ∇fi(x) 是完整梯度 ∇f(x) 的无偏估计,即
E[∇fi(x)]=j=1∑nP(i=j)∇fj(x)=n1i=1∑n∇fi(x)=∇f(x)
这表明随机梯度是对梯度的良好估计。
动态学习率
为了控制算法收敛的复杂性,我们可以使用关于时间 t 的动态学习率 η(t)。一些常见的动态学习率如下
- 分段常数:η(t)=ηi(ti⩽t⩽ti+1)
- 指数衰减:η=η0e−λt
- 多项式衰减:η(t)=η0(βt+1)−α
关于如何设置学习率,还有更多的选择。例如,我们可以从较小的学习率开始,然后使其迅速上涨,再让它降低,尽管这会更慢。我们甚至可以在较小和较大的学习率之间切换。现在,让我们专注于可以进行全面理论分析的学习率计划,即凸环境下的学习率。对一般的非凸问题,很难获得有意义的收敛保证,因为总的来说,最大限度地减少非线性非凸问题是NP困难的。
小批量随机梯度下降
实际应用中,我们更倾向于读取数据的小批量,而不是观测单个数据来更新参数。这是由于处理单个观测值需要我们执行许多单一矩阵乘法,这耗费相当大,而且对应深度学习框架也要巨大的开销。也就是说,每当我们执行更新操作 x←x−ηigt 时消耗巨大。其中
gt=∇xf(x)
我们可以通过将其应用于一个小批量观测值来提高此操作的计算效率。也就是说,我们将梯度 gt 替换为一个小批量而不是单个观测值。考虑一个随机小批量 Bt,则有
gt=∇x∣Bt∣1i∈Bt∑fi(x)
这样做既保证了估计的无偏性,也减少了估计的标准差,并加速了训练过程。
动量法
泄露平均值
上一节中我们讨论了小批量随机梯度下降作为加速计算的手段,下文我们用SGD简称此方法。目前为止,我们使用的参数优化方法都是基于纯粹的梯度下降法介绍的,尽管我们介绍了诸如动态学习率等优化手段,但其还是暴露出两个最显著的问题
- 下降路径曲折:不同维度的梯度存在差异,导致下降路径多呈之字形
- 贪心性:不能保证到达全局最优点
SGD等梯度法进关注当前时刻的梯度 gt,而我们希望利用某种机制记录下梯度的长期变化趋势,这可以通过对过去所有时刻的梯度进行加权求和得到。在介绍其之前,我们先讲清楚一个重要的符号标记。
在 t 时刻的参数变化依赖于前一时刻的参数 xt−1,因此实际算出的梯度是 t−1 时刻的参数梯度,但为了表示其用于更新 t 时刻的参数,我们将梯度记为 gt,t−1,下文为了方便起见还是记作 gt 的形式。因此,在 t 时刻的泄露平均值(Leaky Average)表示为
vt=βvt−1+gt
其中参数 β∈(0,1),vt 也被称为当前时刻的动量(Momentum),将其展开得到
vt=τ=0∑t−1βτgt−τ=gt+βgt−1+β2gt−2+⋯
可以看出,越久远时刻的梯度拥有更小的权,这与人类的记忆规律相吻合。
动量法
取较大的 β 相当于长期平均值,而较小的 β 相对于梯度法只是略有修正。采用泄露平均值形式的梯度替换不再指向特定实例下降最陡的方向,而是指向过去梯度的加权平均值的方向。这使我们能够实现对单批量计算平均值的大部分好处,而不产生实际计算其梯度的代价。
因此,动量法使我们能够解决上面描述的梯度下降问题。其更新方程被描述为
vt←βvt−1+gt(v0=0)
xt←xt−1−ηtvt
考虑 t→+∞ 的情况,根据上式不难解出参数的步长将稳定于 1−βηg,这表明实际步长将变为原梯度的数倍,这也就是为什么我们要选取一个较小的 η,避免步长过大从而难以收敛。
动量法的实现非常简单,只是需要存储额外的动态向量 v。某些定义中将动量的更新式子表示为 vt←βvt−1+(1−β)gt,我们在本篇中先以正文的定义为主。
凸问题分析
我们以二次凸问题为例,证明动量法的正确性。考虑某个存在唯一极值的二次函数
h(x)=21xTQx+xTc+b
根据条件可知 Q≻0(正定矩阵),两边求梯度可以得到最优解和对应极值为
x∗=−Q−1c,h(x∗)=b−21cTQ−1c
将原函数改写为零点式为
h(x)=21(x−Q−1c)TQ(x−Q−1c)+b−21cTQ−1c
对于正定矩阵 Q,将其进行特征值分解为 Q=P−1ΛP,利用二次型令 z=P(x−Q−1c) 有
h(z)=21zTΛz+b′(b′=b−21cTQ−1c)
由于 P 是个正交矩阵,因此其不会影响梯度,即有
zt=zt−1−Λzt−1=(I−Λ)zt−1
根据动量法,可以把上式改写为
vt←βvt−1+Λzz−1
zt←zt−1−η(βvt−1+Λzz−1)=(I−ηΛ)zt−1−ηβvt−1
自适应梯度
自适应学习率
假设我们在训练一个语言模型,考虑关于稀疏特征的模型训练。例如,我们在某个自然语言数据集中看到 黑洞辐射 的概率要远小于看到 学习 的概率。一般地,对于某个不常出现的特征,只有其出现时才会对相关参数执行更新。
另一方面,我们不希望学习率 η 过大,因此最终可能会得到这样的训练结果——常见特征的参数相当迅速地收敛到最佳值,而对于不常见的特征,我们仍缺乏足够的观测以确定其最佳值。换句话说,学习率要么对于常见特征而言降低太慢,要么对于不常见特征而言降低太快。
因此,我们希望通过某种机制,自动调整对于不同特征的梯度下降速度。不同的参数是需要不同的学习率的:具有损失较大偏导的参数相应地有一个快速下降的学习率,而具有小偏导的参数在学习率上有相对较小的下降。
AdaGrad算法
自适应梯度算法(Adaptive Gradient, AdaGrad)给出了一种简单而有效的解决方案,即考虑过去所有时刻的梯度累积平方和。对于时间步为 t 的累积量 st,其更新公式表示为
st←st−1+gt∘gt
该累积量记录了参数在不同特征下历史梯度的累积大小。对于某个梯度较大的特征,我们希望其步长变小,反之亦然。因此考虑如下参数更新公式
xt←xt−1−st+εη∘gt
其中为防止分母为 0,定义某个小常数向量 ε。
- 若某个特征的历史梯度很大,则 st 对应维度较大,对应上式更小的更新量
- 若某个特征的历史梯度很小,则 st 对应维度较小,对应上式更大的更新量
AdaGrad算法对于稀疏特征特别有效,在此情况下由于不常出现的问题,学习率需要更慢地降低。然而,在深度学习问题上,AdaGrad算法有时在降低学习率方面可能表现的过于剧烈。
RMSProp算法
AdaGrad算法的确有效解决了稀疏特征模型的训练问题,但其中有一个潜在风险。根据累积量 st 的公式,随着训练进行下去,其值只会变得越来越大,这就导致训练后期的步长将越来越小甚至几乎为 0。
不过还是有聪明人结合了动量法中泄露平均值的性质,对AdaGrad算法进行了小小的改进。具体而言,考虑因子 γ>0,采用泄露平均值的形式重新改写累积量 st 的公式为
st=γst−1+(1−γ)gt∘gt
保持其他流程和AdaGrad算法不变,这样就得到了RMSProp算法。
Nesterov-RMSProp算法
在RMSProp算法只改变学习率的基础上,再参照动量法设计Nesterov动量改变梯度就可以得到另一种AdaGrad算法的变种。在传统动量法中,我们将动量的更新看做旧参数的梯度加权,即
vt=βvt−1+∇x∣Bt∣1i∈Bt∑fi(x)
Nesterov动量通过将动量和旧参数相加合并再求梯度的形式,给出了一种全新的动量更新公式
x~←xt−1+αvt−1
gt←∇x~∣Bt∣1i∈Bt∑fi(x~)
st←γst−1+(1−γ)gt∘gt
vt←αvt−1−st+εη∘gt
xt←xt−1+vt
Nesterov动量的RMSProp算法即采用了新的动量更新方式的梯度自适应算法。其和RMSProp算法的不同之处在于采用了不同的梯度定义和动量更新公式。
Adadelta算法
Adadelta算法是AdaGrad算法的另一种变体,主要区别在于前者减少了学习率适应坐标的数量。简而言之,Adadelta使用两个状态变量来存储梯度信息
- st:存储梯度平方的泄露平均值
- Δxt:存储模型本身参数变化量平方的泄露平均值
对于前者,我们采用和RMSProp算法一样的更新方式
st=ρst−1+(1−ρ)gt∘gt
对于后者,其更新方式类似定义为
Δxt=ρΔxt−1+(1−ρ)gt′∘gt′(Δx0=0)
其中的 gt′ 是调整后的梯度,其计算公式为
gt′=st+εΔxt−1+ε∘gt
最终,我们利用调整后的梯度执行更新,即有
xt=xt−1−gt′
可以看出,Adadelta没有学习率参数,它使用参数本身的变化率来调整学习率。
Adam算法
优化总结
在前文中,我们介绍了多种优化策略,他们从不同角度缓解了梯度下降算法的痛点
- SGD:随机梯度下降在解决优化问题时比梯度下降更有效
- 小批量SGD:在一个小批量中使用更大的观测值集,可以优化效率
- 动量法:通过动量机制汇总过去梯度的历史以加速训练收敛
- AdaGrad:通过不同特征维度的自适应步长缩放优化稀疏特征模型训练
- RMSProp:通过泄露平均值缓解AdaGrad中 st 的单调累加问题
- Adadelra:通过多个参数记录历史梯度信息
Adam算法将所有这些技术汇总到一个高效的学习算法中,因此作为深度学习中使用的更强大和有效的优化算法之一,它非常受欢迎。 但是它并非没有问题,有时Adam算法可能由于方差控制不良而发散。 在后人的完善工作中,通过给Adam算法提供一个称为Yogi的热补丁来解决这些问题。
算法组成
Adam算法的关键组成部分之一在于:它使用指数加权移动平均值来估算梯度的动量和二次矩。定义
vt←β1vt−1+(1−β1)gt
st←β2st−1+(1−β2)gt∘gt
这里的 β1,β2 是非负加权参数。常将它们设置为 β1=0.9,β2=0.999 并初始化 v0=s0=0。这种初始化方式可能会导致模型训练初期拥有较大的偏差,因此定义修正后的标准化向量
v^t=1−β1tvt,s^t=1−β2tst
这是根据等比求和公式 τ=0∑tβiτ=1−βi1−βit 得到的,我们来说明为什么要这样修正。以 β1=0.9 为例,这表明在更新 vt 时,我们关注 90% 的历史梯度信息和 10% 的当前梯度信息。对于初始化 v0=0,代入 t=1 可以得到 v1=0.1g1,如果我们直接用 v1 去更新参数,那么意味着我们只利用了梯度的 10%,剩下的 90% 历史梯度由于初始化为 0 被浪费掉了。这会导致两个严峻的问题
- 模型的起步训练太慢
- 初始化偏差可能过大导致不收敛
我们想要让 v1 真实反映 t=1 时的梯度,因此需要乘以 1/0.1=10 以修正。一般的,对于 t 时刻的梯度均值估计 vt,将其展开可以得到
vt=(1−β1)τ=0∑tβit−τgτ
由于我们对历史梯度做加权平均,所有的权重之和应当为 1,但等比求和的结果表明这些权重之和为 1−β1t,因此需要除以该因子保证权重归一化。换言之,只有修正后的 v^t 才是原梯度的无偏估计。
在此基础之上,参照RMSProp算法的方式缩放梯度
gt′=s^t+εηv^t
与RMSProp不同,Adam算法在分母使用 s^t+ε 而不是 st+ε 进行缩放。前者在实践中效果略好一些,因此与RMSProp算法有所区分。通常,我们选择 ε=10−6,这是为了在数值稳定性和逼真度之间取得良好的平衡。
最后简单更新 xt←xt−1−gt′ 即可。
由于Adam算法设计的优越性,深度学习框架一般自带Adam算法,代码只需要传递配置参数即可。
1 2
| import torch trainer = torch.optim.Adam
|
Yogi更新
Adam算法也存在一些问题:即使在凸环境下,当 st 的二次矩估计值爆炸时,它可能无法收敛。Zaheer 等提出了改进更新和参数初始化的方法,在介绍之前先重新改写 st 的更新公式
st=β2st−1+(1−β2)gt∘gt=st−1+(1−β2)(gt2−st−1)
当 gt2 的某个维度值很大或更新很稀疏时,st 可能会太快的遗忘历史值,而一个有效的方法是以逐元素乘积的形式添加一个修正因子 sgn(gt2−st−1),并对原式稍作修改,得到Yogi更新公式
st=st−1+(1−β2)gt2∘sgn(gt2−st−1)
其中 sgn:R→{0,±1} 为符号函数,用于返回自变量的符号。