深度学习基础之优化器(optimizer)的介绍

  • Post author:
  • Post category:其他




前言

神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化(optimization)。

遗憾的是,神经网络的最优化问题非常难。这是因为参数空间非常复杂,无法轻易找到最优解(无法使用那种通过解数学式一下子就求得最小值的方法)。而且,在深度神经网络中,参数的数量非常庞大,导致最优化问题更加复杂。

优化器的角色就是用来更新模型参数的方法,例如SGD、Adam等都能充当优化器的角色。

首先我们先看这么一个函数:

在这里插入图片描述

假设我们需要求解这个函数的最小值。

我们可以用代码先画出这个函数的图像:

import numpy as np
import matplotlib.pyplot as plt #绘图用的模块
from mpl_toolkits.mplot3d import Axes3D #绘制3D坐标的函数
#%matplotlib inline
def fun(x,y): ## 定义XYZ函数
    return np.power(x,2) / 20  +np.power(y,2)

fig1=plt.figure()    ## 展示图片的窗口
ax=Axes3D(fig1) ## 显示3D坐标
X=np.arange(-10,10,0.1) ## xy在底面
Y=np.arange(-10,10,0.1)   #创建了从-2到2,步长为0.1的arange对象
X,Y=np.meshgrid(X,Y)
Z=fun(X,Y)
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=plt.cm.coolwarm)#用取样点(x,y,z)去构建曲面
ax.contourf(X,Y,Z, zdir='z',offset=-2, cmap=plt.cm.coolwarm)## 等高线图,zdir沿着哪个轴压缩
ax.set_xlabel('x label', color='r')
ax.set_ylabel('y label', color='g')
ax.set_zlabel('z label', color='b')
plt.title('fig6-1 left')
plt.show()

结果如下:

在这里插入图片描述

这是一个三维的图,可以观察到函数值z的最小值就是0。

我们可以尝试从上往下看,这样就能直观的反应梯度了。

# 在二维平面中画出他的等高线 
# 计算x,y坐标对应的高度值
def f(x, y):
    return np.power(x,2) / 20  + np.power(y,2)

# 生成x,y的数据
X = np.arange(-10, 10, 0.1) ## X的取值
Y = np.arange(-10, 10, 0.1) ## Y的取值

# 把x,y数据生成mesh网格状的数据,因为等高线的显示是在网格的基础上添加上高度值
X, Y = np.meshgrid(X, Y)

# 填充等高线
plt.contourf(X, Y, f(X, Y), cmap=plt.cm.coolwarm)
plt.xlabel('x')
plt.ylabel('y')
# 显示图表
plt.title('fig6-1 right')
plt.show()

输出:

在这里插入图片描述

我们可以发现这个函数的特征是在y轴上的梯度变化大,在x轴上的梯度变化小。

下面将介绍一些优化器。



SGD(随机梯度下降法)

为了找到最优参数,我们将参数的梯度(导数)作为了线索。使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机梯度下降法(stochastic gradient descent),简称SGD。

SGD是一个简单的方法,不过比起胡乱地搜索参数空间,也算是“聪明”的方法。但是,根据不同的问题,也存在比SGD更加聪明的方法。

SGD的数学公式如下:

在这里插入图片描述

代码实现如下:

class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr
    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]

由于随机梯度下降法从当前梯度进行更新,我们可以画出各个点的负梯度。

#从pyplot导入MultipleLocator类,这个类用于设置刻度间隔
from matplotlib.pyplot import MultipleLocator

#画出负梯度特征图
X = np.arange(-10, 11, 1)
Y = np.arange(-10, 12, 2)
U, V = np.meshgrid(-X/10, -2*Y) ## x的偏导,y的偏导
Y1=np.zeros(X.shape[0])#21个0组成的一维数组
Y2=Y1+4
Y3=Y1-4
fig, ax = plt.subplots()
q = ax.quiver(X, Y, U, V)
#ax.quiverkey(q, X=0.3, Y=1.1, U=10,label='Quiver key, length = 10', labelpos='E')
#plt.plot(X, Y1,'b--')#把横线y=0画出来
#plt.plot(X, Y2,'b--')#把横线y=4画出来
#plt.plot(X, Y3,'b--')#把横线y=-4画出来
plt.xlabel('x')
plt.ylabel('y')
#把x轴的刻度间隔设置为2,并存在变量里
x_major_locator=MultipleLocator(2)
#把y轴的刻度间隔设置为2,并存在变量里
y_major_locator=MultipleLocator(2)
#ax为两条坐标轴的实例
ax=plt.gca()
#把x轴的主刻度设置为2的倍数
ax.xaxis.set_major_locator(x_major_locator)
#把y轴的主刻度设置为2的倍数
ax.yaxis.set_major_locator(y_major_locator)

plt.xlim(-10,10)
plt.ylim(-6,6)
plt.grid()
plt.title('fig6-2')
plt.show()

输出:

在这里插入图片描述

通过图可以发现,(0,0)是最小值的位置,但是很多点的梯度并没有指向(0,0),这说明SGD在面对函数的形状非均向的时候,效率并不高,因为下降的方向并不是最小的方向。

下面用代码实现SGD对此函数的最小值求解:

#假设我们按照SGD的思路,从(-7.0, 2.0)起,步长为0.9,前进
X_SGD = [-7.0]#保存点的横坐标坐标
Y_SGD = [2.0]#保存点的纵坐标坐标
x = - 7.0
y = 2.0
N = 40
lr = 0.95
for i in range (N):

    x -= x/10*lr#更新x的值
    y -= 2*y*lr#更新y的值
    X_SGD.append(x)
    Y_SGD.append(y)

#我们在等高线的基础上,画出点的移动
def f(x, y):
    return np.power(x,2) / 20  + np.power(y,2)

X = np.arange(-10, 10, 0.01) 
Y = np.arange(-5, 5, 0.01) 

X, Y = np.meshgrid(X, Y)

plt.plot(X_SGD, Y_SGD, color='red', marker='o', linestyle='solid')#把每一步梯度下降的点用折线连接起来画出

plt.contourf(X, Y, f(X, Y), cmap=plt.cm.coolwarm)
plt.xlabel('x')
plt.ylabel('y')
plt.xlim(-10, 10)
plt.ylim(-10, 10)
    
plt.title('fig6-3')
plt.show()

输出结果:



SGD呈“之”字形向使函数取得最小值的(0,0)处移动。SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。

为了改正SGD的缺点,我们将继续介绍其他的一些方法。



Momentum

Momentum是“动量”的意思,和物理有关。用数学式表示Momentum方法,如下所示:

在这里插入图片描述

在这里插入图片描述

因为初始速度为0,所以第一次更新时其实和SGD是一样的,当第二次更新时,Momentum也便发挥作用,因为第一次更新留下一个“速度”,会影响第二次更新的幅度。

class Momentum:

    """Momentum SGD"""

    def __init__(self, lr=0.01, momentum=0.9):
        self.lr = lr
        self.momentum = momentum
        self.v = None
        
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            for key, val in params.items():                                
                self.v[key] = np.zeros_like(val)#存放权重、偏置的数组,数组的形状和对应的权重、偏置形状一致,数组元素值都为0
                
        for key in params.keys():
            self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] 
            params[key] += self.v[key]

用这个方法求解一下函数的最小值:

#假设我们按照momentum的思路,从(-7.0, 2.0)起,前进
X_momentum = [-7.0]#保存点的横坐标坐标
Y_momentum = [2.0]#保存点的纵坐标坐标
x = - 7.0
y = 2.0
N = 40
lr = 0.1
momentum=0.9
vx=0 # 初始速度为0
vy=0
for i in range (N):#对x和y分别更新值
    vx=momentum*vx-x/10*lr
    x += vx
    vy=momentum*vy-2*y*lr
    y += vy
    X_momentum.append(x)
    Y_momentum.append(y)

#我们在等高线的基础上,画出点的移动
def f(x, y):
    return np.power(x,2) / 20  + np.power(y,2)

X = np.arange(-10, 10, 0.01) 
Y = np.arange(-5, 5, 0.01) 

X, Y = np.meshgrid(X, Y)

plt.plot(X_momentum, Y_momentum, color='red', marker='o', linestyle='solid')#把每一步梯度下降的点用折线连接起来画出

plt.contourf(X, Y, f(X, Y), cmap=plt.cm.coolwarm)
plt.xlabel('x')
plt.ylabel('y')
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.title('fig6-5')
plt.show()

输出结果:

在这里插入图片描述

图6-5中,更新路径就像小球在碗中滚动一样。和SGD相比,我们发现“之”字形的“程度”减轻了。这是因为虽然x轴方向上受到的力非常小,但是一直在同一方向上受力,所以朝同一个方向会有一定的加速。反过来,虽然y轴方向上受到的力很大,但是因为交互地受到正方向和反方向的力,它们会互相抵消,所以y轴方向上的速度不稳定。因此,和SGD时的情形相比,可以更快地朝x轴方向靠近,减弱“之”字形的变动程度。



AdaGrad

在神经网络的学习中,学习率(数学式中记为η)的值很重要。学习率过小,会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能正确进行。

在关于学习率的有效技巧中,有一种被称为学习率衰减(learning rate decay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。

逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。而AdaGrad 进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。

AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习(AdaGrad的Ada来自英文单词Adaptive,即“适当的”的意思)。下面,让我们用数学式表示AdaGrad的更新方法。

在这里插入图片描述

在这里插入图片描述

首先我们先实现一下AdaGrad:

class AdaGrad:

    """AdaGrad"""

    def __init__(self, lr=0.01):
        self.lr = lr
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

这里需要注意的是,最后一行加上了微小值1e-7。这是为了防止当self.h[key]中有0时,将0用作除数的情况。在很多深度学习的框架中,这个微小值也可以设定为参数,但这里我们用的是1e-7这个固定值。现在,让我们试着使用AdaGrad解决函数的最优化问题。

#假设我们按照AdaGrad的思路,从(-7.0, 2.0)起,前进
X_AdaGrad = [-7.0]#保存点的横坐标坐标
Y_AdaGrad = [2.0]#保存点的纵坐标坐标
x = - 7.0
y = 2.0
N = 40
lr = 1.5
hx=0
hy=0
for i in range (N):
    hx+=x/10*x/10#保存了以前的所有梯度值x/10的平方和
    x-=x/10*lr/ (np.sqrt(hx) + 1e-7)
    hy+=2*y*2*y##保存了以前的所有梯度值2*y的平方和
    y-=2*y*lr/(np.sqrt(hy) + 1e-7)
    X_AdaGrad.append(x)
    Y_AdaGrad.append(y)
    
#我们在等高线的基础上,画出点的移动
def f(x, y):
    return np.power(x,2) / 20  + np.power(y,2)

X = np.arange(-10, 10, 0.01) 
Y = np.arange(-5, 5, 0.01) 

X, Y = np.meshgrid(X, Y)

plt.plot(X_AdaGrad, Y_AdaGrad, color='red', marker='o', linestyle='solid')#把每一步梯度下降的点用折线连接起来画出

plt.contourf(X, Y, f(X, Y), cmap=plt.cm.coolwarm)
plt.xlabel('x')
plt.ylabel('y')
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.title('fig6-6')
plt.show()

输出如下:

在这里插入图片描述
由图6-6由此可知,函数的取值高效地向着最小值移动。由于y轴方向上的梯度较大,因此刚开始变动较大,但是后面会根据这个较大的变动按比例进行调整,减小更新的步伐。因此,y轴方向上的更新程度被减弱,“之”字形的变动程度有所衰减。



RMSprop

AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,更新的幅度就越小。实际上,如果无止境地学习,更新量就会变为 0,完全不再更新。为了改善这个问题,可以使用 RMSProp方法。RMSProp方法并不是将过去所有的梯度一视同仁地相加,而是逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。这种操作从专业上讲,称为“指数移动平均”,呈指数函数式地减小过去的梯度的尺度。

更新公式如下:

在这里插入图片描述

类实现:

class RMSprop:

    """RMSprop"""

    def __init__(self, lr=0.01, decay_rate = 0.99):
        self.lr = lr
        self.decay_rate = decay_rate
        self.h = None
        
    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
            
        for key in params.keys():
            self.h[key] *= self.decay_rate
            self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

对上述函数最优化求解:

#假设我们按照RMSprop的思路,从(-7.0, 2.0)起,步长为0.9,前进
X_RMSprop = [-7.0]#保存点的横坐标坐标
Y_RMSprop = [2.0]#保存点的纵坐标坐标
x = - 7.0
y = 2.0
N = 40
lr = 0.1
decay_rate = 0.99
hx=0
hy=0
for i in range (N):
    hx*=decay_rate
    hx+=(1 - decay_rate)*x/10*x/10#保存了以前的所有梯度值x/10的平方和的衰减值
    #print(hx)
    x-=lr * x/10 / (np.sqrt(hx) + 1e-7)
    hy*=decay_rate
    hy+=(1 - decay_rate)*2*y*2*y##保存了以前的所有梯度值2*y的平方和的衰减值
    y-=2*y*lr/(np.sqrt(hy) + 1e-7)
    X_RMSprop.append(x)
    Y_RMSprop.append(y)

#我们在等高线的基础上,画出点的移动
def f(x, y):
    return np.power(x,2) / 20  + np.power(y,2)

X = np.arange(-10, 10, 0.01) 
Y = np.arange(-5, 5, 0.01) 

X, Y = np.meshgrid(X, Y)

plt.plot(X_RMSprop, Y_RMSprop, color='red', marker='o', linestyle='solid')#把每一步梯度下降的点用折线连接起来画出

plt.contourf(X, Y, f(X, Y), cmap=plt.cm.coolwarm)
plt.xlabel('x')
plt.ylabel('y')
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.title('fig6-6-')
plt.show()

输出结果:

在这里插入图片描述



Adam

Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起会怎么样呢?这就是Adam[8]方法的基本思路。Adam是2015年提出的新方法。它的理论有些复杂,直观地讲,就是融合了Momentum和AdaGrad的方法。通过组合前面两个方法的优点,有望实现参数空间的高效搜索。此外,进行超参数的“偏置校正”也是Adam的特征。

其更新公式如下:

在这里插入图片描述

代码实现Adam:

class Adam:

    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            #self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
            #self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
            
            #unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
            #unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
            #params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)

使用Adam求解函数最优化问题:

#假设我们按照Adam的思路,从(-7.0, 2.0)起,步长为0.9,前进
X_Adam = [-7.0]#保存点的横坐标坐标
Y_Adam = [2.0]#保存点的纵坐标坐标
x = - 7.0
y = 2.0
N = 40
iterr = 0
lr = 0.3
beta1 = 0.9
beta2 = 0.999

mx=0
vx=0
my=0
vy=0
for i in range (N):
    iterr += 1
    lr_t  = lr * np.sqrt(1.0 - beta2**iterr) / (1.0 - beta1**iterr)     
    mx+= (1 - beta1) * (x/10 - mx)
    vx+= (1 - beta2) * ((x/10)**2 - vx)
    x-= lr_t * mx / (np.sqrt(vx) + 1e-7)
    
    my+= (1 - beta1) * (2*y - mx)
    vy+= (1 - beta2) * ((2*y)**2 - vx)
    y-= lr_t * my / (np.sqrt(vy) + 1e-7)
    X_Adam.append(x)
    Y_Adam.append(y)

#我们在等高线的基础上,画出点的移动
def f(x, y):
    return np.power(x,2) / 20  + np.power(y,2)

X = np.arange(-10, 10, 0.01) 
Y = np.arange(-5, 5, 0.01) 

X, Y = np.meshgrid(X, Y)

plt.plot(X_Adam, Y_Adam, color='red', marker='o', linestyle='solid')#把每一步梯度下降的点用折线连接起来画出

plt.contourf(X, Y, f(X, Y), cmap=plt.cm.coolwarm)
plt.xlabel('x')
plt.ylabel('y')
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.title('fig6-7')
plt.show()

输出结果:

在这里插入图片描述

在图6-7中,基于Adam的更新过程就像小球在碗中滚动一样。虽然Momentun也有类似的移动,但是相比之下,Adam的小球左右摇晃的程度有所减轻。这得益于学习的更新程度被适当地调整了。Adam会设置 3个超参数。一个是学习率(论文中以α出现),另外两个是一次momentum系数β1和二次momentum系数β2。根据论文,标准的设定值是β1为 0.9,β2 为 0.999。设置了这些值后,大多数情况下都能顺利运行。



MNIST手写数据集四种方法对比

这个实验以一个5层神经网络为对象,其中每层有100个神经元。激活

函数使用的是ReLU,直接上结果。

在这里插入图片描述

实验结果表明,在这个实验当中除了SGD,另外三种都蛮不错的。



总结

上面我们介绍了SGD、Momentum、AdaGrad、Adam这4种方法,那么用哪种方法好呢?非常遗憾,(目前)并不存在能在所有问题中都表现良好的方法。这4种方法各有各的特点,都有各自擅长解决的问题和不擅长解决的问题。很多研究中至今仍在使用SGD。Momentum和AdaGrad也是值得一试

的方法。最近,很多研究人员和技术人员都喜欢用Adam。

确实,Adam用的比较多。



参考

上课深度学习的课件



版权声明:本文为qq_52785473原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。