前言
优化器是神经网络训练过程中非常重要的结构,正是因为优化器的存在帮助初始化参数的网络一步步学习到了符合训练集数据特征的最优参数。
本文主要通过分析无优化器、SGD优化器、Momentum优化器、Adam优化器模型原理,编写优化器函数,求其极值点和分类任务下的结果,得到不同优化器的作用。然后,通过初始化一个全连接网络,通过对比网络训练前后网络的预测能力来体会优化器的重要性。
接下来我们将通过以下两部分进行介绍:
-
利用不同优化器求解函数极值点;
-
鸢尾花数据在不同优化器下的分类表现。
利用不同优化器求解函数极值点
这一部分我们重点在于理解各个优化器的原理,自己编写优化器,体会优化器的作用,主要设计思路如下:
构建Beale 公式及其导数:Beale 公式是一个经典的二元函数,它在三维空间中有一个复杂的曲面,方程最低点在(3,0.5)处。该部分首先要定义这个函数并定义其偏导数。这部分重点掌握如何将数学公式在python中实例化。
编写动量优化器并优化Beale 公式:通过编写动量优化器函数来优化上一步定义好的beale函数。该步骤重点掌握优化器函数是如何实现的和动量优化器的原理。
编写Adagrad优化器并优化Beale公式: 这步与上一步类似,重点掌握Adagrad优化器的原理。
目标函数Beale 公式:
f(x_1,x_2)=(1.5−x_1+x_1x_2)^2+(2.25−x_1+x_1x_2^2)^2+(2.625−x_1+x_1x_2^3)^2
方程极值点 (x_1, x_2) = (3, 0.5)
我们需要通过各种不同的优化器来优化目标函数。优化器的主要目的是通过迭代找到目标函数的极小值或者极大值。常用的优化器有:SGD、Momentum、NAG、Adagrad、Adam等。
构建目标函数画出极值点
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as plt_cl
# ------------------定义目标函数beale、目标函数的偏导函数dbeale_dx,并画出目标函数---------------------
#定义beale公式
def beale(x1,x2):
return (1.5-x1+x1*x2)**2+(2.25-x1+x1*x2**2)**2+(2.625-x1+x1*x2**3)**2
#定义beale公式的偏导函数
def dbeale_dx(x1, x2):
dfdx1 = 2*(1.5-x1+x1*x2)*(x2-1)+2*(2.25-x1+x1*x2**2)*(x2**2-1)+2*(2.625-x1+x1*x2**3)*(x2**3-1)
dfdx2 = 2*(1.5-x1+x1*x2)*x1+2*(2.25-x1+x1*x2**2)*(2*x1*x2)+2*(2.625-x1+x1*x2**3)*(3*x1*x2**2)
return dfdx1, dfdx2
# 定义画图函数
def gd_plot(x_traj):
plt.rcParams['figure.figsize'] = [6, 6]
plt.contour(X1, X2, Y, levels=np.logspace(0, 6, 30),
norm=plt_cl.LogNorm(), cmap=plt.cm.jet)
plt.title('2D Contour Plot of Beale function(Momentum)')
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.axis('equal')
plt.plot(3, 0.5, 'k*', markersize=10)
if x_traj is not None:
x_traj = np.array(x_traj)
plt.plot(x_traj[:, 0], x_traj[:, 1], 'k-')
plt.show()
step_x1, step_x2 = 0.2, 0.2
X1, X2 = np.meshgrid(np.arange(-5, 5 + step_x1, step_x1),
np.arange(-5, 5 + step_x2, step_x2))
Y = beale(X1, X2)
print("目标结果 (x_1, x_2) = (3, 0.5)")
gd_plot(None)
不使用优化器优化Beale 公式
无优化器训练不更新参数,不管训练多少次,模型的参数没有发生变化。模型效果与模型初始化参数关系较大
下面是不使用优化器求解Beale 公式极值点实现。
# ------------------------------------------------------------无优化器-------------------------------------------
#定义无优化器函数
def gd_no(df_dx, x0, conf_para=None):
if conf_para is None:
conf_para = {}
conf_para.setdefault('n_iter', 1000) # 迭代次数
conf_para.setdefault('learning_rate', 0.001) # 设置学习率
x_traj = []
x_traj.append(x0)
v = np.zeros_like(x0)
#没有迭代更新的操作,所以,坐标没有变化
for iter in range(1, conf_para['n_iter'] + 1):
x_traj.append(x_traj[-1])
return x_traj
#初始化坐标
x0 = np.array([1.0, 1.5])
conf_para_no = {'n_iter': 2000, 'learning_rate': 0.005}
#调用函数进行更新
x_traj_no = gd_no(dbeale_dx, x0, conf_para_no)
print("无优化器求得极值点 (x_1, x_2) = (%s, %s)" % (x_traj_no[-1][0], x_traj_no[-1][1]))
gd_plot(x_traj_no)
使用SGD优化器并优化Beale 公式
梯度下降法:梯度下降(gradient descent)在机器学习中应用十分的广泛,是求解无约束优化问题最简单和最古老的方法之一。通过迭代,参数向梯度的反方向更新,直到收敛。
W_{new} = W - \eta\frac{\partial J(W)}{\partial W}
$$
其中\frac{\partial J(W)}{\partial W}表示损失函数 J 关于参数W的梯度;\eta表示学习率;
缺点:
-
有可能会陷入局部最小值;
-
不会收敛,最终会一直在最小值附近波动,并不会达到最小值并停留在此;
-
下降速度慢;
-
选择合适的学习率比较困难;
-
在所有方向上统一的缩放梯度,不适用于稀疏数据;
下面是使用SGD优化器求解Beale 公式极值点实现。
# ------------------------------------------------------------SGD-------------------------------------------
def gd_sgd(df_dx, x0, conf_para=None):
if conf_para is None:
conf_para = {}
conf_para.setdefault('n_iter', 1000) # 迭代次数
conf_para.setdefault('learning_rate', 0.001) # 设置学习率
x_traj = []
x_traj.append(x0)
v = np.zeros_like(x0)
#利用梯度值对坐标进行更新
for iter in range(1, conf_para['n_iter'] + 1):
dfdx = np.array(df_dx(x_traj[-1][0], x_traj[-1][1]))
v = - conf_para['learning_rate'] * dfdx
x_traj.append(x_traj[-1] + v)
return x_traj
x0 = np.array([1.0, 1.5])
conf_para_sgd = {'n_iter': 2000, 'learning_rate': 0.005}
x_traj_sgd = gd_sgd(dbeale_dx, x0, conf_para_sgd)
print("SGD求得极值点 (x_1, x_2) = (%s, %s)" % (x_traj_sgd[-1][0], x_traj_sgd[-1][1]))
gd_plot(x_traj_sgd)
使用动量优化器优化Beale 公式
Momentum:是动量优化法中的一种(Momentum、NAG),即使用动量(Momentum)的随机梯度下降法(SGD),主要思想是引入一个积攒历史梯度信息的动量来加速SGD。其参数优化公式如下所示:
v_{new} = \gamma v - \eta\frac{\partial J(W)}{\partial W} \\ W_{new} = W + v_{new} \\
$$
其中 \frac{\partial J(W)}{\partial W} 表示损失函数 J 关于参数W的梯度; \eta 表示学习率; \gamma 表示动量的大小,一般取值为0.9。
这个算法和之前的梯度下降法(SGD)相比,唯一不同的就是多了一个 \gamma v 。这一改动使Momentum会观察历史梯度,若当前梯度的方向与历史梯度一致(表明当前样本不太可能为异常点),则会增强这个方向的梯度;若当前梯度与历史梯方向不一致,则梯度会衰减。一种形象的解释是:我们把一个球推下山,球在下坡时积聚动量,在途中变得越来越快,γ可视为空气阻力,若球的方向发生变化,则动量会衰减。
优点:
-
参考了历史梯度,增加了稳定性;
-
由于引入加速动量,加快收敛速度。下降初期时,使用上一次参数更新,下降方向一致,乘上较大的 \gamma 能够进行很好的加速;
-
还有一定摆脱局部最优的能力。下降中后期时,在局部最小值来回震荡的时候,梯度趋近于0, \gamma 使得更新幅度增大,跳出陷阱(局部最优);
下面是使用Momentum优化器求解Beale 公式极值点实现。
def gd_momentum(df_dx, x0, conf_para=None):
if conf_para is None:
conf_para = {}
conf_para.setdefault('n_iter', 1000) # 迭代次数
conf_para.setdefault('learning_rate', 0.001) # 设置学习率
conf_para.setdefault('momentum', 0.9) # 设置动量参数
x_traj = []
x_traj.append(x0)
v = np.zeros_like(x0)
#套用动量优化器公式,对坐标值进行更新
for iter in range(1, conf_para['n_iter'] + 1):
dfdx = np.array(df_dx(x_traj[-1][0], x_traj[-1][1]))
v = conf_para['momentum'] * v - conf_para['learning_rate'] * dfdx
x_traj.append(x_traj[-1] + v)
return x_traj
x0 = np.array([1.0, 1.5])
conf_para_momentum = {'n_iter': 500, 'learning_rate': 0.005}
x_traj_momentum = gd_momentum(dbeale_dx, x0, conf_para_momentum)
print("Momentum求得极值点 (x_1, x_2) = (%s, %s)" % (x_traj_momentum[-1][0], x_traj_momentum[-1][1]))
gd_plot(x_traj_momentum)
使用自适应优化器优化Beale 公式
自适应学习率优化算法主要有:AdaGrad算法,RMSProp算法,Adam算法以及AdaDelta算法。
AdaGrad
AdaGrad的基本思想是对每个变量用不同的学习率。这个学习率在一开始比较大,用于快速梯度下降。随着优化过程的进行,对于已经下降很多的变量,则减缓学习率,对于还没怎么下降的变量,则保持一个较大的学习率。其参数优化公式如下所示:
G_{new} = G + (\frac{\partial J(W)}{\partial W})^2 \\ W_{new} = W - \frac{\eta}{(\sqrt{G_{new}} + \varepsilon)}\cdot\frac{\partial J(W)}{\partial W}
$$
其中 \frac{\partial J(W)}{\partial W} 表示损失函数 J 关于参数W的梯度; \eta 表示学习率,一般取值0.01; \varepsilon 是一个很小的数,防止分母为0;G_{new}表示了前t 步参数W梯度的平方累加。把沿路的Gradient的平方根,作为Regularizer。分母作为Regularizer项的工作机制如下:
-
训练前期,梯度较小,使得Regularizer项很大,放大梯度。[激励阶段]
-
训练后期,梯度较大,使得Regularizer项很小,缩小梯度。[惩罚阶段]
优点:
-
在数据分布稀疏的场景,能更好利用稀疏梯度的信息,比标准的SGD算法更有效地收敛;
-
对每个变量用不同的学习率,对输入参数学习率的依赖小,容易调节参数;
缺点:
-
主要缺陷来自分母项的对梯度平方不断累积,随之时间步地增加,分母项越来越大,最终导致学习率收缩到太小无法进行有效更新;
RMSProp
为了解决 Adagrad 学习率急剧下降问题,RMSProp保留过去梯度的微分平方数项,旨在消除梯度下降中的摆动。与Momentum的效果一样,某一维度的导数比较大,则指数加权平均就大,某一维度的导数比较小,则其指数加权平均就小,这样就保证了各维度导数都在一个量级,进而减少了摆动。允许使用一个更大的学习率η。其参数优化公式如下所示:
v_{new} = \gamma\cdot v + (1 - \gamma)\cdot{(\frac{\partial J(W)}{\partial W})}^2 \\ W_{new} = W - \frac{\eta}{(\sqrt{v_{new}} + \varepsilon)}(\frac{\partial J(W)}{\partial W})
$$
其中 \frac{\partial J (W)}{\partial W} 表示损失函数 J 关于参数W的梯度; \eta 表示学习率,一般取值0.001; \varepsilon 是一个很小的数,防止分母为0; \gamma 表示动量的大小,一般取值为0.9。
Adam
Adam算法是另一种计算每个参数的自适应学习率的方法。相当于 RMSprop + Momentum。除了像RMSprop存储了过去梯度的平方 v_t 的指数衰减平均值 ,也像 momentum 一样保持了过去梯度 m_t 的指数衰减平均值。其参数优化公式如下所示:
m_{new} = \beta _1 m + (1 - \beta _1)(\frac{\partial J(W)}{\partial W})
$$
v_{new} = \beta _2 v + (1 - \beta _2)(\frac{\partial J(W)}{\partial W})^2
$$
由于\frac{m_0}{v_0}初始化为0,会导致\frac{m_{new}}{v_{new}}偏向于0,尤其在训练初期阶段,所以,此处需要对梯度均值\frac{m_{new}}{v_{new}}进行偏差纠正,降低偏差对训练初期的影响。
\hat{m_{new}} = m_{new} / (1 - \beta _1)
$$
\hat{v_{new}} = v_{new} / (1 - \beta _2)
$$
W_{new} = W - \eta\frac{1}{\sqrt{\hat{v_{new}}} + \varepsilon}\hat{m_{new}}
$$
其中 \frac{\partial J (W)}{\partial W} 表示损失函数 J 关于参数W的梯度; \eta 表示学习率,一般取值0.001; \varepsilon 是一个很小的数,一般取值10e−8,防止分母为0; \beta _1 \beta _2 分别表示一阶和二阶动量的大小,一般取值为 \beta _1 = 0.9 \beta _2 = 0.99 。
优点
-
能够克服AdaGrad梯度急剧减小的问题,在很多应用中都展示出优秀的学习率自适应能力;
-
实现简单,计算高效,对内存需求少;
-
参数的更新不受梯度的伸缩变换影响;
-
超参数具有很好的解释性,且通常无需调整或仅需很少的微调;
-
更新的步长能够被限制在大致的范围内(初始学习率);
-
能自然地实现步长退火过程(自动调整学习率);
-
很适合应用于大规模的数据及参数的场景;
-
适用于不稳定目标函数;
-
适用于梯度稀疏或梯度存在很大噪声的问题;
下面是使用自适应优化器求解Beale 公式极值点实现。
# ----------------------------------------------------adagrad-----------------------------
def gd_adagrad(df_dx, x0, conf_para=None):
if conf_para is None:
conf_para = {}
conf_para.setdefault('n_iter', 1000) # 迭代次数
conf_para.setdefault('learning_rate', 0.001) # 学习率
conf_para.setdefault('epsilon', 1e-7)
x_traj = []
x_traj.append(x0)
r = np.zeros_like(x0)
#套用adagrad优化器公式,对参数进行更新
for iter in range(1, conf_para['n_iter'] + 1):
dfdx = np.array(df_dx(x_traj[-1][0], x_traj[-1][1]))
r += dfdx ** 2
x_traj.append(x_traj[-1] - conf_para['learning_rate'] / (np.sqrt(r) + conf_para['epsilon']) * dfdx)
return x_traj
x0 = np.array([1.0, 1.5])
conf_para_adag = {'n_iter': 500, 'learning_rate': 2}
x_traj_adag = gd_adagrad(dbeale_dx, x0, conf_para_adag)
print("Adagrad求得极值点 (x_1, x_2) = (%s, %s)" % (x_traj_adag[-1][0], x_traj_adag[-1][1]))
gd_plot(x_traj_adag)
总结
从以上结果可以看出:无优化器参数不更新,求函数极值点无效。SGD、Momentum、自适应优化器求得的极值点与目标点(3.0, 0.5)较近。SGD、Momentum、自适应优化器求解极值点方法有效。其中SGD优化器实验需要的迭代次数2000多,相比与后边Momentum优化器多迭代1500次,证明了SGD优化器收敛速度慢。从图像可以看出自适应优化器对收敛方向把握比较好。
下一节我们将具体介绍使用鸢尾花数据集在不同优化器下的分类表现。