更快的优化器
动量优化
想象一下,一个保龄球在光滑的表面上沿着平缓的坡度滚动:它开始速度很慢,在很快会获得动量,直到最终达到终极速度(如果有摩擦或空气阻力)。相比之下,常规的梯度下降法只是在斜坡上采取小的、常规的步骤,因此算法将花费更多时间到达底部
回想一下梯度下降通过直接减去权重的成本函数$J(\theta)$的梯度乘以学习率$\eta(\nabla_\theta J(\theta))$来更新权重$\theta$。等式是:$\theta\gets\theta-\eta\nabla_\theta J(\theta)$。它不关心较早的梯度是什么。如果局部梯度很小,则它会走得非常缓慢。
动量优化非常关心先前的梯度是什么:在每次迭代时,它都会从动量向量$m$(乘以学习率$\eta$)中减去局部梯度,并通过添加该动量向量来更新权重。换句话说,梯度是用于加速度而不是速度。为了模拟某种摩擦机制并防止动量变得过大,该算法引入了一个新的超参数$\beta$,称为动量,必须将其设置为0(高摩擦)和1(无摩擦)之间。典型的动量值为0.9。
动量算法:
- $m\gets\beta m-\eta\nabla_\theta J(\theta)$
- $\theta\gets\theta+m$
你可以轻松地验证,如果梯度保持恒定,则最终速度(即权重更新的最大大小)等于该梯度乘以学习率$\eta$在乘以$1/(1+\beta)$(忽略符号)。例如,如果$\beta=0.9$,则最终速度等于梯度乘以学习率的10倍!这使得动量优化比梯度下降要更快地从平台逃脱。梯度下降相当快地沿着陡峭的斜坡下降,但是沿着山谷下降需要很长时间。相反,动量优化将沿着山谷滚动得越来越快,直到达到谷底(最优解)。在不使用批量归一化的深层神经网络中,较高的层通常会得到比率不同的输入,因此使用动量优化会有所帮助。它还可以帮助绕过局部优化问题。
由于这种动量势头,优化器可能会稍微过调,然后又回来,再次过调,在稳定于最小点之前会多次振荡。这是在系统中有一些摩擦力的原因之一:它消除了这些振荡,从而加快了收敛速度。
在Keras中实现动量优化,只需要使用SGD优化器并设置其超参数momentum:
optimizer=keras.optimizers.SGD(lr=1e-4,momentum=.9)动量优化的一个缺点是它增加了另一个超参数来调整。但是,动量0.9通常在实践中效果很好,几乎总是比常规的“梯度下降”更快。
Nesterov加速度
Nesterov加速梯度(Nesterov Accelerated Gradient,NAG)方法也被称为 Nesterov 动量优化,它不是在局部位置$\theta$,而是在$\theta+\beta m$处沿动量方向稍微提前处测量成本函数的梯度
Nesterov 加速梯度算法:
- $m\gets\beta m-\eta\nabla_\theta J(\theta+\beta m)$
- $\theta\gets\theta+m$
这种小的调整有效是因为通常动量向量会指向正确的方向(即朝向最优解),因为使用在该方向上测得更远的梯度而不是原始位置上的梯度会稍微准确些。
Nesterov 更新会最终稍微接近最优解。一段时间后,这些小的改进累加起来,NAG 就比常规的动量优化要快得多。此外,当动量推动权重跨越谷底时,$\nabla_1$继续推动越过谷底,而$\nabla_2$推回谷底。这有助于减少振荡,因为 NAG 收敛更快。
NAG 通常比常规动量更快,要使用它,只需要在创建 SGD 优化器时设置nesterov=True 即可:
optimizer=keras.optimizers.SGD(lr=1e-4,momentum=.9,nesterov=True)常规与 Nesterov 动量优化:前者使用在动量步骤之前计算梯度,而后者使用在动量步骤后计算的梯度
AdaGrad
再次考虑拉长的碗状问题:梯度下降从快速沿最陡的坡度下降开始,该坡度没有直接指向全局最优解,然后非常缓慢地下沉到谷底。如果算法可以纠正其方向,使它更多地指向全局最优解,那将是很好的。AdaGrad 算法通过沿最陡峭的维度按比例缩小梯度向量
AdaGrad 算法:
- $s\gets s+\nabla_\theta J(\theta)\otimes\nabla_\theta J(\theta)$
- $\theta\gets\theta-\eta\nabla_\theta J(\theta)\oslash\sqrt{s+\epsilon}$
第一步将梯度的平方累加到向量${s}$中($\otimes$符号表示逐元素相乘)。此向量化形式等效于针对向量${s}$中的每个元素$s_i$计算$s_i\gets s_i+(\partial J(\theta)/\partial\theta_i)^2$。换句话说,每个$s_i$累加关于参数$\theta_i$的成本函数偏导数的平方。如果成本函数沿第$i$个维度陡峭,则$s_i$将在每次迭代中变得越来越大。
第二步几乎与“梯度下降”相同,但有一个很大的区别:梯度向量按比例因子$\sqrt{{s}+\epsilon}$缩小了($\oslash$符号代表逐元素相除,而$\epsilon$是避免被除以零的平滑项,通常设置为$10^{-10}$)。此向量化形式等效于对所有参数$\theta_i$同时计算$\theta_i\gets\theta_i-\eta\ \partial J(\theta)/\partial\theta_i\ /\ \sqrt{{s_i}+\epsilon}$ 。
简而言之,该算法会降低学习率,但是对于陡峭的维度,它的执行速度要比对缓慢下降的维度的执行速度更快。这称为自适应学习率。它有助于将结果更新更直接的指向全局最优解。另一个好处是,它几乎不需要调整学习率超参数$\eta$。
AdaGrad与梯度下降法:前者可以较早地修正其方向,使之指向最优解
对于简单的二次问题,AdaGrad经常表现良好,但是在训练神经网络时,它往往停止得太早。学习率按比例缩小,以至于算法在最终达到全局最优解之前完全停止了。因此,即使Keras有AdaGrad优化器。
RMSProp
AdaGrad有下降太快,永远不会收敛到全局最优解的风险。RMSProp算法通过只是累加最近迭代中的梯度(而不是自训练开始以来的所有梯度)来解决这个问题。它通过在第一步中使用指数衰减。
RMSProp算法:
- $s\gets\beta s\ \nabla_\theta J(\theta)\otimes\nabla_\theta J(\theta)$
$\theta\gets\theta-\eta\ \nabla_\theta J(\theta)\oslash\sqrt{s+\epsilon}$
衰减率$\beta$通常设置为0.9。默认值通常效果很好。
Keras有RMSProp优化器:optimizer=keras.optimizers.RMSProp(lr=1e-4,rho=.9)rho参数对应于公式中的$\beta$,除了非常简单的问题外,该优化器几乎总是比AdaGrad表现更好。实际上,直到Adam优化出现之前,它一直是许多研究人员首选的优化算法。
Adam 和 Nadam 优化
Adam代表自适应矩估计,结合了动量优化和RMSProp的思想:就像动量优化一样,它跟踪过去梯度的指数衰减平均值。就像RMSProp一样,它跟踪过去平方梯度的指数衰减平均值。
Adma算法:- $m\gets\beta_1m-(1-\beta_1)\ \nabla_\theta J(\theta)$
- $s\gets\beta_2s+(1-\beta_2)\ \nabla_\theta J(\theta)\otimes\nabla_\theta J(\theta)$
- $\hat{m}\gets\frac{m}{1-\beta_1^t}$
- $\hat{s}\gets\frac{s}{1-\beta_2^t}$
- $\theta\gets\theta-\eta\hat{m}\oslash\sqrt{\hat{s}+\epsilon}$
在此等式中,$t$表示迭代次数(从1开始)。
如果只看步骤1、2和5,你会发现Adam与动量优化和RMSProp非常相似。唯一的区别是步骤1计算的是指数衰减的平均值,而不是指数衰减的总和,但除了常数因子(衰减平均值是衰减总和的$1-\beta_1$倍)外,它们实际上是等效的。第三步和第四步在技术上有些细节:由于m和s初始化为0,因此在训练开始时它们会偏向0,这两个步骤将有助于在训练开始时提高m和s
动量衰减超参数$\beta_1$通常初始化为0.9,而缩放衰减超参数$\beta_2$通常被初始化为0.999。平滑项$\epsilon$通常会初始化为一个很小的数字。这些是Adam类的默认值(准确说,epsilon的默认值为None,它告诉Keras使用keras.backend.espilon(),默认值为$10^{-10}$。可以使用keras.backend.set_epsilon()来改变)。这是使用Keras来创建Adam优化器的方法:
optimizer=keras.optimizers.Adam(lr=1e-4,beta_1=.9,beta_2=.999)由于Adam是一种自适应学习率算法(如AdaGrad和RMSProp),因此对学习率超参数$\eta$需要较少的调整。通常可以使用默认值$\eta=.001$,这使得Adam比梯度下降更易于使用。
Adam的两个变体
AdaMax
在Adam算法的步骤2中,Adma累加了s中的梯度平方(对于最近的梯度,权重更大)。在第5步中,如果忽略$\epsilon$和第三步和第四步,Adma将以s的平凡根按比例缩小参数更新。简而言之,Adam按时间衰减梯度的$\ell_2$范数按比例缩小参数更新($\ell_2$范数是平方和的平方根)。与Adam在用一篇论文中介绍的AdaMax将$\ell_2$范数替换为$\ell_\infty$(一种表达最大值的新颖方法)。具体来说,它用$s\gets max(\beta_2s,\nabla_\theta J(\theta))$替换公式中的步骤2,它删除第4步,在第5步中,将梯度更新按比例s缩小,这是时间衰减梯度的最大值。实际上,这可以使AdaMax比Adam更稳定,但这确实取决于数据集,通常Adam的表现更好。因此,用Adam在某些任务上遇到问题,这时可以尝试的另一种优化器。
Nadam
Nadam优化是Adam加上 Nesterov 技巧,因此其收敛速度通常比Adam稍快。研究人员Timothy Dozat在各种任务上对许多不同的优化器进行了比较,发现Nadam总体上胜过Adam,但有时不如RMSProp。
自适应优化方法(包括RMSProp、Adam和Nadam优化)通常很棒,可以快速收敛到一个好的解决方案,因此,当对模型的性能感到失望时,尝试改用Nesterov 加速梯度:你的数据可能对自适应梯度过敏。
到目前为止讨论的所有优化技术都仅仅依赖于一阶片导数(Jacobians)。一些优化文献还包含了基于二阶片导数(Hessian)的一些算法。这些算法很难应用于深度神经网络,因为每个输出有$n^2$的Hessian(其中n是参数的数量),而不是每个输出只有n个Jacobians。由于DNN通常具有成千上万的参数,因此二阶优化算法甚至不适合储存在内存中,即使可以,计算Hessian也太慢了
训练稀疏模型
所有的优化算法都产生了密集模型,这意味着大多数参数都是非零的。如果运行时需要一个非常快的模型,或者需要占用更少的内存,就更需要一个稀疏模型。
实现这一点的一个简单方法是像往常一样训练模型,然后去掉很小的权重(将它们设置为零)。但这通常不会导致非常稀疏的模型,而且可能降低模型的性能。
一个更好的选择是在训练时使用强$\ell_1$正则化,因为它会迫使优化器产生尽可能的为零的权重
学习率调度
找到一个好的学习率很重要。如果设置得太高,训练可能会发散。如果设置地太低,训练最终会收敛到最优解,但是这将花费很长时间。如果它设置得稍微有点高,它一开始会很快,但是最终会围绕最优解振荡,不会真正稳定下来。如果算力有限,则可能必须先终端训练,然后才能正确收敛,从而产程次优解。
与恒定学习率相比,对学习率进行调度可以更快地找到一个最优解
幂调度
将学习率设置为迭代次数$t$的函数:$\eta(t)=\eta_0/(1+t/s)^c$。初始学习率$\eta_0$、幂$c$(通常设置为1)和步骤$s$是超参数。学习率在每一步都会下降。在$s$个步骤之后,它下降到$\eta_0/2$。再在$s$个步骤之后,它下降到$\eta_0/3$,以此类推,此调度开始下降,然后越来越慢。幂调度需要调整$\eta_0$和$s$可能还有$c$。
指数调度
将学习率设置为$\eta(t)=\eta_0\ 0.1^{\frac{t}{s}}$。学习率每s步将逐渐下降10倍。幂调度越来越缓慢地降低学习率,而指数调度则使学习率每s步降低10倍。
分段恒定调度
对一些轮次使用恒定的学习率(例如对5个轮次,$\eta_0=0.1$),对于另外一些轮次使用较小的学习率(例如,对于50轮次,$\eta_0=0.001$)。以此类推。
性能调度
每N步测量一次验证误差(就像提前停止一样),并且当误差停止下降时,将学习率降低$\lambda$倍
1周期调度1
与其他方法相反,1周期调度从提高初始学习率$\eta_0$开始,在训练过程中增长至$\eta_1$。然后,它在训练的后半部分将学习率再次线性降低到$\eta_0$,通过将学习率降低几个数量级(仍然是线性的)来完成最后几个轮次。使用与找到最优学习率相同的方法来选择最大学习率$\eta_1$,而初始学习率$\eta_0$大约要低10倍。
使用动量优化来训练深度神经网络进行语音识别时一些最受欢迎的学习率调度的性能。在这种情况下,性能调度和指数调度都表现良好。更倾向于指数调度,因为它易于微调
在Keras中实现幂调度是最简单的选择:只需在创建优化器时设置超参数decay即可:
optimizer=keras.optimizers.SGD(lr=0.01,decay=1e-4)decay是s(学习率除以多个数字单位所需要的步数)的倒数,Keras假定c=1.
指数调度和分段调度也非常简单,需要定义一个函数,该函数采用当前轮次并返回学习率。
指数调度:
def exponential_decay_fn(epoch):
return 0.01*0.1**(epoch/20)如果不想对$\eta_0$和$s$进行硬编码,则可以创建一个返回配置函数的函数:
def exponential_decay(lr0,s):
def exponential_decay_fn(epoch):
return lr0*0.1**(epoch/s)
return exponential_decay_fn接下来,创建一个LearningRateScheduler回调函数,为其提供调度函数,然后将此回调函数传递给fit()方法:
lr_scheduler=keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history=model.fit(X_train_scaled,y_train,callbacks=[lr_scheduler])LearningRateScheduler将在每个轮次开始时更新优化器的learning_rate属性。通常每轮次更新一次学习率
调度函数可以选择将当前学习率作为第二个参数。例如,以下调度调度函数将以前的学习率乘以$0.1^{1/20}$,这将导致相同的指数衰减(但现在衰减从轮次0开始而不是1开始):
def exponential_decay_fn(epoch,lr):
return lr*0.1**(1/20)此实现依赖于优化器的出是学习率。
保存模型时,优化器及其学习率也会随之保存。有了新的调度函数,只需加载经过训练的模型,从中断的地方开始训练。但是如果调度函数使用了epoch参数,就会有所区别:epoch不会被保存,并且每次调用fit()方法都会将其重置为0。如果要继续训练一个中断的模型,则可能会导致一个很高的学习率,这可能会损害模型的权重。一种解决方法是手动设置fit()方法的initial_epoch参数,使epoch从正确的值开始。
对于分段恒定调度,可以使用以下调度函数,然后创建带有此函数的LearningRateScheduler回调函数,并将其传递给fit()方法
def piecewise_constant_fn(epoch):
if epoch<5:
return 0.01
elif epoch<15:
return 0.005
else:
return 0.001 对于性能调度,需要使用ReduceLROnPlateau回调函数。例如,如果连续5个轮次的最好验证损失都没有改善时,它将使学习率乘以0.5
lr_scheduler=keras.callbacks.ReduceLROnPlateau(factor=.5,patiences=5)
最后,tf.keras提供了另一种实现学习率调度的方法:使用keras.optimizers.schedule中可以使用的调度之一来定义学习率,然后将该学习率传递给任意优化器。这种方法在每个步骤更新学习率而不是每个轮次。例如,以下是实现与之前定义的exponential_decay_fn()函数相同的指数调度的方法:s=20*len(X_train)//32 #batch_size=32,每轮X_train//32步,共20轮
learning_rate=keras.optimizers.schedules.ExponentialDecay(0.01,s,0.1)
optimizer=kersa.optimizers.SGD(leraning_rate)
保存模型时,学习率及其调度(包括其状态)也将被保存。