一、优化与深度学习
优化与估计
尽管优化方法可以最小化深度学习中的损失函数值,但本质上优化方法达到的目标与深度学习的目标并不相同。
- 优化方法目标:训练集损失函数值
- 深度学习目标:测试集损失函数值(泛化性)
现实世界中,训练集数据和测试集数据的分布存在差异,因此可能训练和测试过程中误差不尽相同。
优化在深度学习中的挑战
1. 局部最小值
举例:f(x)=xcosπx
2. 鞍点
举例:一维函数 f(x)=x3
对于高维变量输入,我们通过二阶导数Hessian矩阵来说明:
A=⎣⎢⎢⎢⎢⎢⎡∂x12∂2f∂x2∂x1∂2f⋮∂xn∂x1∂2f∂x1∂x2∂2f∂x22∂2f⋮∂xn∂x2∂2f⋯⋯⋱⋯∂x1∂xn∂2f∂x2∂xn∂2f⋮∂xn2∂2f⎦⎥⎥⎥⎥⎥⎤
对于多维变量 x=[x1,x2,…,xn], 当一阶偏导∂x∂f=0 ,二阶偏导有正有负时,则 x 这时可称为函数f(⋅)的鞍点。
我们以二维变量举例:z=f(x)=x12−x22
3. 梯度消失
深度学习中的诸如sigmoid,tanh等激活函数在偏离中心的区域梯度逐渐消失,趋近于0。多层这样的激活函数的堆叠,导致深层神经网络训练过程中会经常面临梯度消失的难题。我们以tanh函数图例说明:
二、凸优化基础
凸集
在凸集中的示意图中我们可以总结出凸集的几何性质:
- 凸集中任意两点连线上的点仍在凸集中
- 凸集的交集仍旧是凸集
- 凸集的并集不一定是凸集
凸函数
对于凸函数凸性的描述,我们可以引申凸集第一条性质得到如下的不等式:
λf(x)+(1−λ)f(x′)≥f(λx+(1−λ)x′)
我们举三个函数的例子来直观的说明函数凸性和上述不等式的对应关系:
上面的不等式说明任意两点的割线始终位于函数上方,我们泛化到任意点集{xi},使用数学归纳法,可以证明凸函数满足Jensen不等式:
i∑αif(xi)≥f(i∑αixi)
在概率论中,如果把 αi 看成取值为 xi 的离散变量 x 的概率分布, 就可以得到:
Ex[f(x)]≥f(Ex[x])
其中, E[⋅] 表示期望。
凸函数的性质
-
无局部极小值
证明:(反证法) 假设存在x∈X是局部最小值,则存在全局最小值x′∈X , 使得f(x)>f(x′) , 则对λ∈(0,1] :
f(x)>λf(x)+(1−λ)f(x′)≥f(λx+(1−λ)x′)
可以观察到f(x)在x的邻域内并非达到局部最小,至此证毕。
-
与凸集的关系
结论:对于凸函数f(x) ,定义集合Sb:={x∣x∈X and f(x)≤b} ,则集合Sb 为凸集。
证明:对于点x,x′∈Sb , 有f(λx+(1−λ)x′)≤λf(x)+(1−λ)f(x′)≤b , 故λx+(1−λ)x′∈Sb 对映凸集的第一条性质。
这里举一个非凸函数来可视化这条性质:
f(x,y)=0.5x2+cos(2πy)
从等高线图中可以看出,当都满足小于某一特定值时,可能形成无交集的两部分,取值集合不具有凸性。
-
凸函数与二阶导数
结论:f′′(x)≥0⟺f(x)是凸函数
证明:必要性(⇐)
对于凸函数:
21f(x+ϵ)+21f(x−ϵ)≥f(2x+ϵ+2x−ϵ)=f(x)
故:
f′′(x)=ε→0limϵϵf(x+ϵ)−f(x)−ϵf(x)−f(x−ϵ)f′′(x)=ε→0limϵ2f(x+ϵ)+f(x−ϵ)−2f(x)≥0
充分性(⇒)
令 a<x<b 为 f(x) 上的三个点,由拉格朗日中值定理:
f(x)−f(a)=(x−a)f′(α) for some α∈[a,x] and f(b)−f(x)=(b−x)f′(β) for some β∈[x,b]
根据单调性,有 f′(β)≥f′(α), 故:
f(b)−f(a)=f(b)−f(x)+f(x)−f(a)=(b−x)f′(β)+(x−a)f′(α)≥(b−a)f′(α)
有限制条件凸函数的优化方法
举例:
xminimizef(x) subject to ci(x)≤0 for all i∈{1,…,N}
1. 拉格朗日乘子法
L(x,α)=f(x)+i∑αici(x) where αi≥0
2. 添加惩罚项
欲使ci(x)≤0 , 将项αici(x) 加入目标函数,如多层感知机章节中的2λ∣∣w∣∣2
3. 投影法
ProjX(x)=x′∈Xargmin∥x−x′∥2
三、梯度下降
理想的梯度下降发生在局部凸函数中。
一维梯度下降
证明:沿梯度反方向移动自变量可以减小函数值
泰勒展开:
f(x+ϵ)=f(x)+ϵf′(x)+O(ϵ2)
代入沿梯度方向的移动量ηf′(x),η∈(0,1]:
f(x−ηf′(x))=f(x)−ηf′2(x)+O(η2f′2(x))f(x−ηf′(x))≲f(x)x←x−ηf′(x)其中η被称为学习率/步长,当学习率过大时,梯度下降无法收敛到局部最小值。
当学习率过小时,收敛速度缓慢。
除面临如何选择合适的学习率的问题,梯度下降还经常被局部最小值所困扰:
多维梯度下降
仅在一维梯度下降中就面临学习率选择,局部最小值陷阱的问题。在多维梯度下降中,这两个问题变得更复杂。对于多维梯度下降,我们可以用数学语言表示成:
∇f(x)=[∂x1∂f(x),∂x2∂f(x),…,∂xd∂f(x)]⊤f(x+ϵ)=f(x)+ϵ⊤∇f(x)+O(∥ϵ∥2)x←x−η∇f(x)
以二维梯度下降举例:
f(x)=x12+2x22
eta = 0.1
def f_2d(x1, x2):
return x1 ** 2 + 2 * x2 ** 2
def gd_2d(x1, x2):
return (x1 - eta * 2 * x1, x2 - eta * 4 * x2)
def train_2d(trainer, steps=20):
x1, x2 = -5, -2
results = [(x1, x2)]
for i in range(steps):
x1, x2 = trainer(x1, x2)
results.append((x1, x2))
print('epoch %d, x1 %f, x2 %f' % (i + 1, x1, x2))
return results
对于多维梯度下降,我们通过考虑二阶导数来实现自动选择学习率,这里我们首先介绍牛顿法:
牛顿法
在 x+ϵ 处泰勒展开(此处使用二次展开,并使用peano余项):
f(x+ϵ)=f(x)+ϵ⊤∇f(x)+21ϵ⊤∇∇⊤f(x)ϵ+O(∥ϵ∥3)
最小值点处满足:∇f(x)=0 即我们希望∇f(x+ϵ)=0 , 对上式关于 ϵ 求导,忽略高阶无穷小,有:∇f(x)+Hfϵ=0 and hence ϵ=−Hf−1∇f(x)
这样我们就获得了牛顿法对变量的更新公式。
牛顿法的收敛性分析
只考虑在函数为凸函数(或在局部凸函数的范围内), 且最小值点上 f′′(x∗)>0时的收敛速度:
令 xk 为第 k 次迭代后 x 的值, ek:=xk−x∗ 表示 xk 到最小值点 x∗ 的距离,由f′(x∗)=0,展开成Lagrange余项形式:
0=f′(xk−ek)=f′(xk)−ekf′′(xk)+21ek2f′′′(ξk)for some ξk∈[xk−ek,xk]
两边除以 f′′(xk) , 有:
ek−f′(xk)/f′′(xk)=21ek2f′′′(ξk)/f′′(xk)
代入更新方程 xk+1=xk−f′(xk)/f′′(xk) , 得到:
xk−x∗−f′(xk)/f′′(xk)=21ek2f′′′(ξk)/f′′(xk)xk+1−x∗=ek+1=21ek2f′′′(ξk)/f′′(xk)
当 21f′′′(ξk)/f′′(xk)≤c 时,有:
ek+1≤cek2
我们称之为具有二阶收敛速度。
预处理(Heissan阵辅助梯度下降)
在牛顿法中,我们需要计算Heissan矩阵来进行变量的更新,Heissan矩阵大小是d×d,其中d代表变量的维度,计算复杂度为O(d2)。我们改用Heissan阵的对角阵diag(Hf)来代替Heissan阵进行变量更新,diag(Hf)的计算复杂度降至O(d),同时也能对各维度变量变化尺度进行归一化的处理,保证各方向下降速率基本一致。
x←x−ηdiag(Hf)−1∇x
梯度下降与线性搜索(共轭梯度法)(略)
随机梯度下降
在实际的优化过程中,对于求解Heissan阵这样耗费计算的步骤仍然是无法实现的,因此对牛顿法进行进一步的松弛,我们得到简单计算即可迭代的随机梯度下降算法。
随机梯度下降参数更新
对于有 n 个样本对训练数据集,设 fi(x) 是第 i 个样本的损失函数, 则目标函数为:
f(x)=n1i=1∑nfi(x)
其梯度为:
∇f(x)=n1i=1∑n∇fi(x)
使用该梯度的一次更新的时间复杂度为O(n),这种梯度下降的更新算法也被称为最速梯度下降法。我们每次更新都要计算数据集中全部样本的梯度。
而随机梯度下降是最速梯度下降的变种,每次迭代仅对一个样本计算梯度,其时间复杂度为O(1):
x←x−η∇fi(x)
且有:
Ei∇fi(x)=n1i=1∑n∇fi(x)=∇f(x)
举例:
f(x1,x2)=x12+2x22
eta = 0.1
lr = (lambda: 1)
def f(x1, x2):
return x1 ** 2 + 2 * x2 ** 2
def gradf(x1, x2):
return (2 * x1, 4 * x2)
def sgd(x1, x2):
global lr
(g1, g2) = gradf(x1, x2)
(g1, g2) = (g1 + np.random.normal(0.1), g2 + np.random.normal(0.1))
eta_t = eta * lr()
return (x1 - eta_t * g1, x2 - eta_t * g2)
动态学习率
在随机梯度下降的过程中,我们可以发现学习率应当随着学习过程的进行而逐渐减小。这里借用李宏毅老师的课件来说明学习率设定的重要性:
下面介绍几种设置动态学习率的方法:
η(t)=ηi if ti≤t≤ti+1η(t)=η0⋅e−λtη(t)=η0⋅(βt+1)−α piecewise constant exponential polynomial
小批量随机梯度下降
在使用随机梯度下降算法的过程中我们会发现,更新的梯度受所选定的样本影响很大,即方差很大。会导致梯度下降过程中出现偏离局部最小值方向的更新。但如果使用所有样本进行梯度计算又会占用高计算资源,因此我们采用一种折衷的办法来进行随机梯度下降。也就是批量梯度下降(与小批量梯度下降的原理相同)。随机从样本中选取一部分数据计算当前的平均梯度方向,迭代速度比最速梯度下降法更快,比随机梯度下降每次更新梯度方向精度更高。
随机梯度下降:loss: 0.245968, 0.463836 sec per epoch,2 epoch
小批量随机梯度下降:loss: 0.243900, 0.065017 sec per epoch,batch_size=10,2 epoch
四、优化算法
梯度下降优化法经历了SGD→SGDM→NAG→AdaGrad→AdaDelta→RMSProp→Adam→Nadam
这样的发展历程。之所以会不断地提出更加优化的方法,究其原因,是引入了动量(Momentum)这个概念。最初,人们引入一阶动量来给梯度下降法加入惯性(即,越陡的坡可以允许跑得更快些)。后来,在引入二阶动量之后,才真正意味着“自适应学习率”优化算法时代的到来。
动量法(Momentum)
在 Dive into DL Section 11.4 中,目标函数有关自变量的梯度代表了目标函数在自变量当前位置下降最快的方向。因此,梯度下降也叫作最速下降(steepest descent)。在每次迭代中,梯度下降根据自变量当前位置,沿着当前位置的梯度更新自变量。然而,如果自变量的迭代方向仅仅取决于自变量当前位置,这可能会带来一些问题。对于noisy gradient,我们需要谨慎的选取学习率和batch size, 来控制梯度方差和收敛的结果。
gt=∂w∣Bt∣1i∈Bt∑f(xi,wt−1)=∣Bt∣1i∈Bt∑gi,t−1.
An ill-conditioned Problem
Condition Number of Hessian Matrix:
condH=λminλmax
where λmax,λmin is the maximum amd minimum eignvalue of Hessian matrix.
让我们考虑一个输入和输出分别为二维向量x=[x1,x2]⊤和标量的目标函数:
f(x)=0.1x12+2x22
condH=0.24=20→ill-conditioned
最大学习率限制条件
- For f(x), according to convex optimizaiton conclusions, we need step size η<L1 to have the fastest convergence, where L=maxx∇2f(x).
- To guarantee the convergence, we need to have η<L2 .
Supp: Preconditioning
在二阶优化中,我们使用Hessian matrix的逆矩阵(或者pseudo inverse)来左乘梯度向量 i.e.Δx=H−1g,这样的做法称为precondition,相当于将 H 映射为一个单位矩阵,拥有分布均匀的Spectrum,也即我们去优化的等价标函数的Hessian matrix为良好的identity matrix。
与Dive into DL Section 11.4一节中不同,这里将x12系数从1减小到了0.1。下面实现基于这个目标函数的梯度下降,并演示使用学习率为0.4时自变量的迭代轨迹。
可以看到,同一位置上,目标函数在竖直方向(x2轴方向)比在水平方向(x1轴方向)的斜率的绝对值更大。因此,给定学习率,梯度下降迭代自变量时会使自变量在竖直方向比在水平方向移动幅度更大。那么,我们需要一个较小的学习率从而避免自变量在竖直方向上越过目标函数最优解。然而,这会造成自变量在水平方向上朝最优解移动变慢。
下面我们试着将学习率调得稍大一点,此时自变量在竖直方向不断越过最优解并逐渐发散。当学习率为0.6时,L=maxx∇2f(x)=[4,0.2],L2=[0.5,10],已经超出范围,因此在其中一个维度上无法收敛。
Solution to ill-condition
- Preconditioning gradient vector: applied in Adam, RMSProp, AdaGrad, Adelta, KFC, Natural gradient and other secord-order optimization algorithms.
- Averaging history gradient: like momentum, which allows larger learning rates to accelerate convergence; applied in Adam, RMSProp, SGD momentum.
Momentum Algorithm
动量法的提出是为了解决梯度下降的上述问题。设时间步 t 的自变量为 xt,学习率为 ηt。
在时间步 t=0,动量法创建速度变量 m0,并将其元素初始化成 0。在时间步 t>0,动量法对每次迭代的步骤做如下修改:
mtxt←βmt−1+ηtgt,←xt−1−mt,
Another version:
mtxt←βmt−1+(1−β)gt,←xt−1−αtmt,
αt=1−βηt
其中,动量超参数 β满足 0≤β<1。当 β=0 时,动量法等价于小批量随机梯度下降。
在解释动量法的数学原理前,让我们先从实验中观察梯度下降在使用动量法后的迭代轨迹。
eta, beta = 0.4, 0.5
def momentum_2d(x1, x2, v1, v2):
v1 = beta * v1 + eta * 0.2 * x1
v2 = beta * v2 + eta * 4 * x2
return x1 - v1, x2 - v2, v1, v2
epoch 20, x1 -0.062843, x2 0.001202
可以看到使用较小的学习率 η=0.4 和动量超参数 β=0.5 时,动量法在竖直方向上的移动更加平滑,且在水平方向上更快逼近最优解。下面使用较大的学习率 η=0.6,此时自变量也不再发散
η=0.6: epoch 20, x1 0.007188, x2 0.002553
指数加权平均(Exponential Moving Average)
为了从数学上理解动量法,让我们先解释一下指数加权移动平均(exponential moving average)。给定超参数 0≤β<1,当前时间步 t 的变量 yt 是上一时间步 t−1 的变量 yt−1 和当前时间步另一变量 xt 的线性组合:
yt=βyt−1+(1−β)xt.
我们可以对 yt 展开:
yt=(1−β)xt+βyt−1=(1−β)xt+(1−β)⋅βxt−1+β2yt−2=(1−β)xt+(1−β)⋅βxt−1+(1−β)⋅β2xt−2+β3yt−3=(1−β)i=0∑tβixt−i
(1−β)i=0∑tβi=1−β1−βt(1−β)=(1−βt)
指数加权平均的优势
我们可以看到指数加权平均的求解过程实际上是一个递推的过程,那么这样就会有一个非常大的好处,每当我要求从0到某一时刻(n)的平均值的时候,我并不需要像普通求解平均值的作为,保留所有的时刻值,类和然后除以n。
而是只需要保留0~(n-1)时刻的平均值和n时刻的值即可。也就是每次只需要保留常数值,然后进行运算即可,这对于深度学习中的海量数据来说,是一个很好的减少内存和空间的做法。
Supp
Approximate Average of 1−β1 Steps
令 n=1/(1−β),那么 (1−1/n)n=β1/(1−β)。因为
n→∞lim(1−n1)n=exp(−1)≈0.3679,
所以当 β→1时,β1/(1−β)=exp(−1),如 0.9520≈exp(−1)。如果把 exp(−1) 当作一个比较小的数,我们可以在近似中忽略所有含 β1/(1−β) 和比 β1/(1−β) 更高阶的系数的项。例如,当 β=0.95 时,
yt≈0.05i=0∑190.95ixt−i.
因此,在实际中,我们常常将 yt 看作是对最近 1/(1−β) 个时间步的 xt 值的加权平均。例如,当 γ=0.95 时,yt 可以被看作对最近20个时间步的 xt 值的加权平均;当 β=0.9 时,yt 可以看作是对最近10个时间步的 xt 值的加权平均。而且,离当前时间步 t 越近的 xt 值获得的权重越大(越接近1)。
由指数加权移动平均理解动量法
现在,我们对动量法的速度变量做变形:
mt←βmt−1+(1−β)(1−βηtgt).
Another version:
mt←βmt−1+(1−β)gt.
xt←xt−1−αtmt,
αt=1−βηt
由指数加权移动平均的形式可得,速度变量 vt 实际上对序列 {ηt−igt−i/(1−β):i=0,…,1/(1−β)−1} 做了指数加权移动平均。换句话说,相比于小批量随机梯度下降,动量法在每个时间步的自变量更新量近似于将前者对应的最近 1/(1−β) 个时间步的更新量做了指数加权移动平均后再除以 1−β。所以,在动量法中,自变量在各个方向上的移动幅度不仅取决当前梯度,还取决于过去的各个梯度在各个方向上是否一致。在本节之前示例的优化问题中,所有梯度在水平方向上为正(向右),而在竖直方向上时正(向上)时负(向下)。这样,我们就可以使用较大的学习率,从而使自变量向最优解更快移动。
SGDM
相对于小批量随机梯度下降,动量法需要对每一个自变量维护一个同它一样形状的速度变量,且超参数里多了动量超参数。实现中,我们将速度变量用更广义的状态变量states
表示。
def init_momentum_states():
v_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
v_b = torch.zeros(1, dtype=torch.float32)
return (v_w, v_b)
def sgd_momentum(params, states, hyperparams):
for p, v in zip(params, states):
v.data = hyperparams['momentum'] * v.data + hyperparams['lr'] * p.grad.data
p.data -= v.data
AdaGrad
在之前介绍过的优化算法中,目标函数自变量的每一个元素在相同时间步都使用同一个学习率来自我迭代。举个例子,假设目标函数为f,自变量为一个二维向量[x1,x2]⊤,该向量中每一个元素在迭代时都使用相同的学习率。例如,在学习率为η的梯度下降中,元素x1和x2都使用相同的学习率η来自我迭代:
x1←x1−η∂x1∂f,x2←x2−η∂x2∂f.
在动量法中我们看到当x1和x2的梯度值有较大差别时,需要选择足够小的学习率使得自变量在梯度值较大的维度上不发散。但这样会导致自变量在梯度值较小的维度上迭代过慢。动量法依赖指数加权移动平均使得自变量的更新方向更加一致,从而降低发散的可能。本节我们介绍AdaGrad算法,它根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率,从而避免统一的学习率难以适应所有维度的问题 [1]。
Algorithm
AdaGrad算法会使用一个小批量随机梯度gt按元素平方的累加变量st。在时间步0,AdaGrad将s0中每个元素初始化为0。在时间步t,首先将小批量随机梯度gt按元素平方后累加到变量st:
st←st−1+gt⊙gt,
其中⊙是按元素相乘。接着,我们将目标函数自变量中每个元素的学习率通过按元素运算重新调整一下:
xt←xt−1−st+ϵη⊙gt,
其中η是学习率,ϵ是为了维持数值稳定性而添加的常数,如10−6。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。
Feature
需要强调的是,小批量随机梯度按元素平方的累加变量st出现在学习率的分母项中。因此,如果目标函数有关自变量中某个元素的偏导数一直都较大,那么该元素的学习率将下降较快;反之,如果目标函数有关自变量中某个元素的偏导数一直都较小,那么该元素的学习率将下降较慢。然而,由于st一直在累加按元素平方的梯度,自变量中每个元素的学习率在迭代过程中一直在降低(或不变)。所以,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。
下面我们仍然以目标函数f(x)=0.1x12+2x22为例观察AdaGrad算法对自变量的迭代轨迹。我们实现AdaGrad算法并使用和上一节实验中相同的学习率0.4。可以看到,自变量的迭代轨迹较平滑。但由于st的累加效果使学习率不断衰减,自变量在迭代后期的移动幅度较小。
def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2
def adagrad_2d(x1, x2, s1, s2):
g1, g2, eps = 0.2 * x1, 4 * x2, 1e-6
s1 += g1 ** 2
s2 += g2 ** 2
x1 -= eta / math.sqrt(s1 + eps) * g1
x2 -= eta / math.sqrt(s2 + eps) * g2
return x1, x2, s1, s2
def init_adagrad_states():
s_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
s_b = torch.zeros(1, dtype=torch.float32)
return (s_w, s_b)
def adagrad(params, states, hyperparams):
eps = 1e-6
for p, s in zip(params, states):
s.data += (p.grad.data**2)
p.data -= hyperparams['lr'] * p.grad.data / torch.sqrt(s + eps)
RMSProp
AdaGrad因为调整学习率时分母上的变量st一直在累加按元素平方的小批量随机梯度,所以目标函数自变量每个元素的学习率在迭代过程中一直在降低(或不变)。因此,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。为了解决这一问题,RMSProp算法对AdaGrad算法做了修改。该算法源自Coursera上的一门课程,即“机器学习的神经网络”。
Algorithm
我们之前提及过指数加权移动平均。不同于AdaGrad算法里状态变量st是截至时间步t所有小批量随机梯度gt按元素平方和,RMSProp算法将这些梯度按元素平方做指数加权移动平均。具体来说,给定超参数0≤γ0计算
vt←βvt−1+(1−β)gt⊙gt.
和AdaGrad算法一样,RMSProp算法将目标函数自变量中每个元素的学习率通过按元素运算重新调整,然后更新自变量
xt←xt−1−vt+ϵα⊙gt,
其中η是学习率,ϵ是为了维持数值稳定性而添加的常数,如10−6。因为RMSProp算法的状态变量st是对平方项gt⊙gt的指数加权移动平均,所以可以看作是最近1/(1−β)个时间步的小批量随机梯度平方项的加权平均。如此一来,自变量每个元素的学习率在迭代过程中就不再一直降低(或不变)。
照例,让我们先观察RMSProp算法对目标函数f(x)=0.1x12+2x22中自变量的迭代轨迹。回忆在AdaGrad算法一节使用的学习率为0.4的AdaGrad算法,自变量在迭代后期的移动幅度较小。但在同样的学习率下,RMSProp算法可以更快逼近最优解。
RMSProp 实现
def init_rmsprop_states():
s_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
s_b = torch.zeros(1, dtype=torch.float32)
return (s_w, s_b)
def rmsprop(params, states, hyperparams):
gamma, eps = hyperparams['beta'], 1e-6
for p, s in zip(params, states):
s.data = gamma * s.data + (1 - gamma) * (p.grad.data)**2
p.data -= hyperparams['lr'] * p.grad.data / torch.sqrt(s + eps)
我们将初始学习率设为0.01,并将超参数γ设为0.9。此时,变量st可看作是最近1/(1−0.9)=10个时间步的平方项gt⊙gt的加权平均。
AdaDelta
除了RMSProp算法以外,另一个常用优化算法AdaDelta算法也针对AdaGrad算法在迭代后期可能较难找到有用解的问题做了改进。有意思的是,AdaDelta算法没有学习率这一超参数。
Algorithm
AdaDelta算法也像RMSProp算法一样,使用了小批量随机梯度gt按元素平方的指数加权移动平均变量st。在时间步0,它的所有元素被初始化为0。给定超参数0≤ρ0,同RMSProp算法一样计算
st←ρst−1+(1−ρ)gt⊙gt.
与RMSProp算法不同的是,AdaDelta算法还维护一个额外的状态变量Δxt,其元素同样在时间步0时被初始化为0。我们使用Δxt−1来计算自变量的变化量:
gt′←st+ϵΔxt−1+ϵ⊙gt,
其中ϵ是为了维持数值稳定性而添加的常数,如10−5。接着更新自变量:
xt←xt−1−gt′.
最后,我们使用Δxt来记录自变量变化量gt′按元素平方的指数加权移动平均:
Δxt←ρΔxt−1+(1−ρ)gt′⊙gt′.
可以看到,如不考虑ϵ的影响,AdaDelta算法与RMSProp算法的不同之处在于使用Δxt−1来替代超参数η。
AdaDelta实现
AdaDelta算法需要对每个自变量维护两个状态变量,即st和Δxt。我们按AdaDelta算法中的公式实现该算法。
def init_adadelta_states():
s_w, s_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
delta_w, delta_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
return ((s_w, delta_w), (s_b, delta_b))
def adadelta(params, states, hyperparams):
rho, eps = hyperparams['rho'], 1e-5
for p, (s, delta) in zip(params, states):
s[:] = rho * s + (1 - rho) * (p.grad.data**2)
g = p.grad.data * torch.sqrt((delta + eps) / (s + eps))
p.data -= g
delta[:] = rho * delta + (1 - rho) * g * g
Adam
Adam算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均。下面我们来介绍这个算法。
Algorithm
Adam算法使用了动量变量mt和RMSProp算法中小批量随机梯度按元素平方的指数加权移动平均变量vt,并在时间步0将它们中每个元素初始化为0。给定超参数0≤β1<1(算法作者建议设为0.9),时间步t的动量变量mt即小批量随机梯度gt的指数加权移动平均:
mt←β1mt−1+(1−β1)gt.
和RMSProp算法中一样,给定超参数0≤β2<1(算法作者建议设为0.999),
将小批量随机梯度按元素平方后的项gt⊙gt做指数加权移动平均得到vt:
vt←β2vt−1+(1−β2)gt⊙gt.
由于我们将m0和s0中的元素都初始化为0,
在时间步t我们得到mt=(1−β1)∑i=1tβ1t−igi。将过去各时间步小批量随机梯度的权值相加,得到 (1−β1)∑i=1tβ1t−i=1−β1t。需要注意的是,当t较小时,过去各时间步小批量随机梯度权值之和会较小。例如,当β1=0.9时,m1=0.1g1。为了消除这样的影响,对于任意时间步t,我们可以将mt再除以1−β1t,从而使过去各时间步小批量随机梯度权值之和为1。这也叫作偏差修正。在Adam算法中,我们对变量mt和vt均作偏差修正:
m^t←1−β1tmt,
v^t←1−β2tvt.
接下来,Adam算法使用以上偏差修正后的变量m^t和m^t,将模型参数中每个元素的学习率通过按元素运算重新调整:
gt′←v^t+ϵηm^t,
其中η是学习率,ϵ是为了维持数值稳定性而添加的常数,如10−8。和AdaGrad算法、RMSProp算法以及AdaDelta算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。最后,使用gt′迭代自变量:
xt←xt−1−gt′.
Adam实现
我们按照Adam算法中的公式实现该算法。其中时间步t通过hyperparams
参数传入adam
函数。
def init_adam_states():
v_w, v_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
s_w, s_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
return ((v_w, s_w), (v_b, s_b))
def adam(params, states, hyperparams):
beta1, beta2, eps = 0.9, 0.999, 1e-6
for p, (v, s) in zip(params, states):
v[:] = beta1 * v + (1 - beta1) * p.grad.data
s[:] = beta2 * s + (1 - beta2) * p.grad.data**2
v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
p.data -= hyperparams['lr'] * v_bias_corr / (torch.sqrt(s_bias_corr) + eps)
hyperparams['t'] += 1