文章目录
优化器在机器学习、深度学习中往往起着举足轻重的作用,同一个模型,因选择不同的优化器,性能有可能相差很大,甚至导致一些模型无法训练。所以了解各种优化器的基本原理非常重要。下面介绍各种常用优化器或算法的主要原理,及各自的优点或不足。
3.1、传统梯度优化的不足(BGD,SGD,MBGD)
BGD、SGD、MBGD分别为批量梯度下降算法、随机梯度下降算法、小批量梯度下降算法。BGD在训练的时候选用所有的训练集进行计算,SGD在训练的时候只选择一个数据进行训练,而MBGD在训练的时候只选择小部分数据进行训练。这三个优化算法在训练的时候虽然所采用的的数据量不同,但是他们在进行参数优化的时候是相同的。
在训练的时候一般都是使用小批量梯度下降算法,即选择部分数据进行训练,在此把这三种算法统称为传统梯度更新算法,因为他们在更新参数的时候采用相同的方式,而更优的优化算法从梯度方向和学习率方面对参数更新方式进行优化。
传统梯度更新算法为最常见、最简单的一种参数更新策略。其基本思想是:先设定一个学习率,参数沿梯度的反方向移动。假设需要更新的参数为,梯度为,则其更新策略可表示为:
这种梯度更新算法简洁,当学习率取值恰当时,可以收敛到全面最优点(凸函数)或局部最优点(非凸函数)。但其还有很大的不足点:
- 对超参数学习率比较敏感(过小导致收敛速度过慢,过大又越过极值点)。
- 学习率除了敏感,有时还会因其在迭代过程中保持不变,很容易造成算法被卡在鞍点的位置。
- 在较平坦的区域,由于梯度接近于0,优化算法会因误判,在还未到达极值点时,就提前结束迭代,陷入局部极小值。
3.1.1 一维梯度下降
下面展示如何实现梯度下降以及上述这些不足点。为了简单,选用目标函数。尽管我们知道时能取得最小值(这里x为模型参数)。
import numpy as np
import torch
from d2l import torch as d2l
%matplotlib inline
#目标函数
def f(x):
return x**2
#目标函数的梯度(导数)
def f_grad(x):
return 2*x
接下来,使用作为初始值,并假设。使用梯度下降迭代法迭代共10次,可以看到的值最终将接近最优解。
#进行梯度下降
def gd(eta,f_grad):
x=10.0
results=[x]
for i in range(10):
x-=eta*f_grad(x)
results.append(float(x))
print(f'epoch 10,x:{x:f}')
return results
results = gd(0.2,f_grad)
epoch 10,x:0.060466
对优化的过程进行可视化。
def show_trace(results,f):
n=max(abs(min(results)),abs(max(results)))
f_line=torch.arange(-n,n,0.01)
d2l.set_figsize()
d2l.plot([f_line,results],[[f(x) for x in f_line],[f(x) for x in results]],'x','f(x)',fmts=['-','-o'])
show_trace(results,f)
学习率
学习率决定了目标函数是否能够收敛到局部最小值,以及何时收敛到最小值。学习率可由算法设计者设置。请注意,如果使用的学习率太小,将导致的更新非常缓慢,需要更多的迭代。下面将学习率设置为0.05。如下图所示,尽管经历了10个步骤,我们仍然离最优解很远。
show_trace(gd(0.05,f_grad),f)
epoch 10,x:3.486784
相反,当使用过高的学习率,的迭代不能保证降低的值,例如,当学习率为时,超出了最优解并逐渐发散。
show_trace(gd(1.1,f_grad),f)
epoch 10,x:61.917364
局部极小值
为了演示非凸函数的梯度下降,考虑函数,其中为某常数。这个函数有无穷多个最小值。如果学习率选择不当,我们最终只会得到一个最优解。下面的例子说明了高学习率如何导致较差的局部最小值。
c=torch.tensor(0.15*np.pi)
#目标函数
def f(x):
return x*torch.cos(c*x)
#目标函数的梯度
def f_grad(x):
return torch.cos(c*x)-c*x*torch.sin(c*x)
show_trace(gd(2,f_grad),f)
epoch 10,x:-1.528166
3.1.2 多维梯度下降
在对单元梯度下降有了了解之后,下面看看多元梯度下降,即考虑的情况。相应的它的梯度也是多元的,是一个由d个偏导数组成的向量:
然后选择合适的学率进行梯度下降:
下面通过代码可视化它的参数更新过程。构造一个目标函数,并有二维向量作为输入,标量作为输出。梯度由给出。从初始位置[-5,-2]通过梯度下降观察x的轨迹。
首先需要定义两个辅助函数第一个是train_2d()
函数,用指定的训练机优化2D目标函数;第二个是show_trace_2d()
,用于显示x的轨迹。
#用指定的训练机优化2D目标函数
def train_2d(trainer,steps=20,f_grad=None):
x1,x2,s1,s2=-5,-2,0,0
results=[(x1,x2)]
for i in range(steps):
if f_grad:
x1,x2,s1,s2=trainer(x1,x2,s1,s2,f_grad)
else:
x1,x2,s1,s2=trainer(x1,x2,s1,s2)
results.append((x1,x2))
print(f'epoch{i+1},x1:{float(x1):f},x2:{float(x2):f}')
return results
#显示优化过程中2D变量的轨迹
def show_trace_2d(f,results):
d2l.set_figsize()
d2l.plt.plot(*zip(*results),'-o',color='#ff7f0e')
x1,x2=torch.meshgrid(torch.arange(-5.5,1.0,0.1),
torch.arange(-3.0,1.0,0.1))
d2l.plt.contour(x1,x2,f(x1,x2),colors='#1f77b4')
d2l.plt.xlabel('x1')
d2l.plt.ylabel('x2')
接下来,使用学习率为时优化变量的轨迹。在经过20步时,的值接近其位于[0,0]的最小值。
#目标函数
def f_2d(x1,x2):
return x1**2+2*x2**2
#目标函数的梯度
def f_2d_grad(x1,x2):
return (2*x1,4*x2)
#SGD更新参数
def gd_2d(x1,x2,s1,s2,f_grad):
g1,g2=f_grad(x1,x2)
return (x1-eta*g1,x2-eta*g2,0,0)
eta=0.1
show_trace_2d(f_2d,train_2d(gd_2d,f_grad=f_2d_grad))
epoch20,x1:-0.057646,x2:-0.000073
针对传统梯度优化算法的缺点,许多优化算法从梯度方向和学习率两方面入手。有些从梯度方向入手,如动量更新策略;而有些从学习率入手,这涉及调参问题;还有从两方面同时入手,如自适应更新策略。
在pytorch中使用传统的梯度下降算法可以使用torch.optim.SGD
其格式为:
torch.optim.SGD(params, lr=<required parameter>, momentum=0, dampening=0, weight_decay=0, nesterov=False, *, maximize=False)
因为使用的是传统的梯度下降算法,则momentum
参数和nesterov
参数默认即可不需要设置。下面看一看它的用法。
import torch
#改代码不可运行
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
#梯度清零
optimizer.zero_grad()
loss_fn(model(input), target).backward()
#参数更新
optimizer.step()
3.2、动量(Momentum)
动量(Momentum)是模拟物理中动量的概念,具有物理上惯性的含义,一个物体在运动时具有惯性,把这个思想运用到梯度下降的计算中,可以增加算法的收敛速度和稳定性,具体实现如图所示:
动量算法每下降一步都是由前面下降方向的一个累积和当前点梯度方向组合而成。含动量的随机梯度下降算法,其更新方式如下:
其中为动量参数,为学习率。
为了更好的观察动量带来的好处,使用一个新函数上使用不带动量的传统梯度下降算法观察下降过程。与上节的函数一样,f的最低水平为(0,0)。该函数在方向上比较平坦,在此选择0.4的学习率。
import torch
from d2l import torch as d2l
%matplotlib inline
eta=0.4
#目标函数
def f_2d(x1,x2):
return 0.1*x1**2+2*x2**2
#sgd更新参数
def gd_2d(x1,x2,s1,s2):
return (x1-eta*0.2*x1,x2-eta*4*x2,0,0)
d2l.show_trace_2d(f_2d,d2l.train_2d(gd_2d))
epoch 20, x1: -0.943467, x2: -0.000073
从结果来看,方向的梯度比水平方向的渐变高得多,变化快得多。因此就陷入了两个不可取的选择:如果选择较小的准确率。可以确保不会朝方向发生偏离,但在反向收敛会缓慢。如果学习率较高,方向会收敛很快,但在方向就不会向最优点靠近。下面将学习率从0.4调整到0.6。可以看出在方向会有所改善,但是整体解决方案会很差。
eta=0.6
d2l.show_trace_2d(f_2d,d2l.train_2d(gd_2d))
epoch 20, x1: -0.387814, x2: -1673.365109
下面看一下使用动量算法在实践中的应用。
#动量法更新参数
def momentum_2d(x1,x2,v1,v2):
v1=beta*v1+0.2*x1
v2=beta*v2+4*x2
return x1-eta*v1,x2-eta*v2,v1,v2
eta,beta=0.6,0.5
d2l.show_trace_2d(f_2d,d2l.train_2d(momentum_2d))
epoch 20, x1: 0.007188, x2: 0.002553
可见使用和之前一样的学习率,也能够很好的收敛,下面看看当降低动量参数时会发生啥。虽然将其减半到会导致一条几乎没有收敛的轨迹。但是也要比没有动力好很多。
eta,beta=0.6,0.25
d2l.show_trace_2d(f_2d,d2l.train_2d(momentum_2d))
epoch 20, x1: -0.126340, x2: -0.186632
既然每一步都要将两个梯度方向(历史梯度、当前梯度)做一个合并再下降,因此可以按照前面一小步位置的“超前梯度”来做梯度合并。这样就可以先往前走一小步,在靠前一点的位置看到梯度,然后按照那个位置再来修正这一步的梯度方向,如下图所示。这样就得到动量算法的一种改进算法,称为Nesterov Accelerated Gradient,简称NAG算法。这种更新的算法能够防止大幅振荡,不会错过最小值,并会对参数更加敏感。
下面看看在pytorch中的使用:
torch.optim.SGD(params, lr=<required parameter>, momentum=0, dampening=0, weight_decay=0, nesterov=False, *, maximize=False)
因为使用了动量,因此参数momentum
就需要给定数值,nesterov
设置为True时,将会使用NAG算法,它是动量算法的一种优化。
3.3、AdaGrad算法
AdaGrad算法是通过参数来调整合适的学习率,是能独立自动调整模型参数的学习率,对稀疏参数进行大幅更新和对频繁参数进行小幅更新,因此,AdaGrad方法非常适合处理稀疏数据。AdaGrad算法在某些深度学习模型上效果不错。但还是有些不足,可能是因其累积梯度平方导致学习率过早或过量的减少所致。以下是AdaGrad算法的更新步骤:
其中为累积梯度变量,初始为0;为学习率;为小参数,避免分母为0。
通过上述更新步骤可以看出:
- 随着迭代时间越长,累积梯度越大,导致学习速率随着时间较小,在接近目标值时,不会因为学习率过大而越过极值点。
- 不同参数之间的学习速率不同,因此,与之前固定学习率相比,不容易卡在鞍点。
- 如果梯度累积参数比较小,则速率会比较大,所以参数迭代的步长就会比较大。相反,如果梯度累积参数比较大,则速率会比较小,所以参数迭代的步长就会比较小。
下面使用和以前相同的问题:
将使用与之前相同的学习率来实施AdaGrad,即。
import math
import torch
from d2l import torch as d2l
%matplotlib inline
#adagrad更新参数
def adagrad_2d(x1,x2,s1,s2):
eps=1e-6
g1,g2=0.2*x1,4*x2
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 f_2d(x1,x2):
return 0.1*x1**2+2*x2**2
eta=0.4
d2l.show_trace_2d(f_2d,d2l.train_2d(adagrad_2d))
epoch 20, x1: -2.382563, x2: -0.158591
由结果来看,参数更新的过程变得平稳,但是由于梯度累积的越来越大,学习率持续下降,因此参数在后期阶段移动的不会那么多。现在我们适当的提高学习率到2,看看结果怎么样。
eta=2
d2l.show_trace_2d(f_2d,d2l.train_2d(adagrad_2d))
epoch 20, x1: -0.002295, x2: -0.000000
可以看出,在越接近最优点附近,学习率越来越小,参数更新变得更慢,以至于不会错过最优点的位置。
下面看看在Pytorch中如何使用AdaGrad
优化算法,在Pytorch中的格式为:
torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0, initial_accumulator_value=0, eps=1e-10)
各个参数的功能为:
params
– 要优化的参数。lr
(float, optional) – 学习率 (默认: 1e-2)lr_decay
(float, optional) – 学习率衰减 (默认: 0)weight_decay
(float, optional) – 权重衰减 (L2 penalty) (默认: 0)eps
(float, optional) – 为提高数字稳定性,在分母上添加了该项 (默认: 1e-10)
3.4、RMSProp算法
RMSProp算法通过修改AdaGrad得来,其目的是在非凸背景下效果更好。针对梯度平方和累计越来越大的问题,RMSProp指数加权的移动平均代替梯度平方和。RMSProp为了使用移动平均,还引入了一个新的超参数,用来控制移动平均的长度范围。以下是RMSProp算法的更新步骤:
RMSProp算法在实践中已被证明是一种有效且实用的深度神经网络优化算法,因而在深度学习中得到了广泛应用。
和之前一样,使用二次函数来观察RMSProp的轨迹。在使用学习率为0.4的AdaGrad
的时候,参数在算法的后期阶段移动的越来越慢,因为学习率下降太快。由于是单独控制的,RMSProp
不会发生这种情况。
import math
from d2l import torch as d2l
#rmsprop更新参数
def rmsprop_2d(x1,x2,s1,s2):
g1,g2,eps=0.2*x1,4*x2,1e-6
s1=gamma*s1+(1-gamma)*g1**2
s2=gamma*s2+(1-gamma)*g2**2
x1-=eta/math.sqrt(s1+eps)*g1
x2-=eta/math.sqrt(s2+eps)*g2
return x1,x2,s1,s2
#目标函数
def f_2d(x1,x2):
return 0.1*x1**2+2*x2**2
eta,gamma=0.4,0.9
d2l.show_trace_2d(f_2d,d2l.train_2d(rmsprop_2d))
epoch 20, x1: -0.010599, x2: 0.000000
在pytorch中,RMSProp算法的格式为:
torch.optim.RMSprop(params, lr=0.01, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0, centered=False)
alpha
为平滑常数,momentum
为动量。
3.5、Adam算法
Adam本质上是带有动量项的RMSProp,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使参数比较稳定。
Adam是一种学习速率自适应的深度神经网络方法,他利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam算法的更新步骤如下:
下面看看每个步骤的含义是:
首先,计算梯度的指数移动平均数,初始化为0。类似于Momentum算法,综合考虑之前时间步的梯度动量。系数为指数衰减率,控制权重分配(动量与当前梯度),通常取接近于1的值。默认为0.9
其次,计算梯度平方的指数移动平均数,初始化为0。系数为指数衰减率,控制之前的梯度平方的影响情况。类似于RMSProp算法,对梯度平方进行加权均值。默认为0.999
第三,由于初始化为0,会导致偏向于0,尤其在训练初期阶段。所以,此处需要对梯度均值进行偏差纠正,降低偏差对训练初期的影响。
第四,与类似,因为初始化为0导致训练初始阶段偏向0,对其进行纠正。
最后,更新参数,初始的学习率乘以梯度均值与梯度方差的平方根之比。其中默认学习率,,避免除数变为0。由表达式可以看出,对更新的步长计算,能够从梯度均值及梯度平方两个角度进行自适应地调节,而不是直接由当前梯度决定。
Adam主要包含以下几个显著的优点:
- 实现简单,计算高效,对内存需求少
- 参数的更新不受梯度的伸缩变换影响
- 超参数具有很好的解释性,且通常无需调整或仅需很少的微调
- 更新的步长能够被限制在大致的范围内(初始学习率)
- 能自然地实现步长退火过程(自动调整学习率)
- 很适合应用于大规模的数据及参数的场景
- 适用于不稳定目标函数
- 适用于梯度稀疏或梯度存在很大噪声的问题
和之前一样,使用二次函数 𝑓(𝑥)=0.1𝑥21+2𝑥22 来观察Adam的轨迹。使用学习率为0.16的AdaGrad并迭代50次。
import math
from d2l import torch as d2l
%matplotlib inline
#针对Adam改造原来的优化目标函数
def train_2d_adam(trainer,steps=20,f_grad=None):
x1,x2,m1,m2,v1,v2=-5,-2,0,0,0,0
results=[(x1,x2)]
for i in range(steps):
x1,x2,m1,m2,v1,v2=trainer(x1,x2,m1,m2,v1,v2)
results.append((x1,x2))
print(f'epoch{i+1},x1:{float(x1):f},x2:{float(x2):f}')
return results
#Adam更新参数过程
def rmsprop_2d(x1,x2,m1,m2,v1,v2):
g1,g2,eps=0.2*x1,4*x2,1e-8
m1=beta1*m1+(1-beta1)*g1
m2=beta1*m2+(1-beta1)*g2
v1=beta2*v1+(1-beta2)*g1**2
v2=beta2*v2+(1-beta2)*g2**2
m_hat_1=m1/(1-beta1)
m_hat_2=m2/(1-beta1)
v_hat_1=v1/(1-beta2)
v_hat_2=v2/(1-beta2)
x1-=alpha*(m_hat_1/(eps+math.sqrt(v_hat_1)))
x2-=alpha*(m_hat_2/(eps+math.sqrt(v_hat_2)))
return x1,x2,m1,m2,v1,v2
#目标函数
def f_2d(x1,x2):
return 0.1*x1**2+2*x2**2
alpha,beta1,beta2=0.16,0.9,0.999
d2l.show_trace_2d(f_2d,train_2d_adam(rmsprop_2d))
epoch20,x1:0.131652,x2:0.447787
在pytorch中Adam的使用格式为
torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0, amsgrad=False, *, maximize=False)
参数betas
为和的集合,分别控制权重分配和之前的梯度平方的影响情况。
文章出处登录后可见!