5.1 梯度下降与最优化

梯度下降及最优化是机器学习、深度学习中关键技术之一,也是核心内容之一。梯度下降法是基本、经典方法,现在很多深度学习仍然使用,当然不是简单使用,而是做了很多优化。
本节就是从基础梯度下降方法定义开始,由浅入深、由简单到复杂,从训练样本数、学习率、梯度等多方面对算法进行优化。为便于大家更好理解,为此我们提供了很多实例代码、主要公式推导、图形等。

5.1.1梯度下降法简介

梯度下降法是一种致力于找到函数极值点的算法,在机器学习中,我们一般通过这种方法获取模型参数,从而求得目标函数或损失函数的极值。
问题描述:


随着迭代步数的增加,逐渐逼近极值点,可参考下图:

5.1.2训练集数据大小对优化的影响

根据每一次迭代所使用的数据集范围的不同,可以把梯度下降算法区分为批量梯度下降(Batch Gradient Descent, BGD)、随机梯度下降(Stochastic Gradient Descent, SGD)和小批量梯度下降(Mini-Batch Gradient Descent, MBGD)。
1)、BGD 是梯度下降算法最原始的形式, 其特点是每次更新参数 时, 都使用整个训练集的数据。当数据集比较大时,速度将非常慢,同时,它不能以在线的方式更新模型,当有新元素加入时,需要对全量数据进行更新,效率较低,因此,当前的梯度下降法一般不采用这种方法。
2)、SGD是对BGD的改进,SGD每次更新,只考虑一个样本数据,因此,它的速度比较快,一般要远远BGD,尤其是它能在线更新参数。不过由于单个样本会出现相似或重复的情况,因此,数据更新会出现冗余;另外,因单个样本之间的数据差异会比较大,因此,训练时可能造成每次迭代的损失函数会出现较大的波动。
示例代码:

for i in range(epochs):
np.random.shuffle(data)
for example in data:
params_grad = evaluate_gradient(loss_function,example,params)
params = params - learning_rate * params_grad

随机梯度下降最大的缺点在于每次更新可能并不会按照正确的方向进行,因此可以带来优化波动(扰动),如下图

3)、MBGD每次参数更新时,由m个样本数据构成,这种优化方法结合了BGD与SGD两者的优点,同时克服了两者的不足。m的取值一般较小,一般在[10,500]之间。
示例代码:

for i in range(epochs):
np.random.shuffle(data)
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function,batch,params)
params = params - learning_rate * params_grad

5.1.3步长对优化的影响

在利用梯度下降法求目标函数极值时,学习速率(即探索步长)这个参数非常重要,太小可能导致迭代慢,太大了有可能跳过极值点。如何调整搜索的步长、如何加快收敛速度以及如何防止搜索时发生震荡却是一门值得深究的学问。接下来本文将分析第一个问题:学习速率的大小对搜索过程的影响。以下通过一个实例来具体说明,为简便起见,这里目标函数为一个一元二次函数:

1)、定义函数:

import numpy as np
import matplotlib.flot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

2)、编写梯度下降法函数

def GD(x_start, df, epochs, lr):
"""
梯度下降法。给定起始点与目标函数的一阶导函数,求在epochs次迭代中x的更新值
:param x_start: x的起始点
:param df: 目标函数的一阶导函数
:param epochs: 迭代周期
:param lr: 学习速率
:return: x在每次迭代后的位置(包括起始点),长度为epochs+1
"""

xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
for i in range(epochs):
dx = df(x)
# v表示x要改变的幅度
v = - dx * lr
x += v
xs[i+1] = x
return xs

3)、根据以上梯度的定义,假设起始搜索点(即:x_0)为-5,迭代周期(或迭代次数)为5,学习速率为0.3,测试代码如下:

def demo_GD():
x_start = -5
epochs = 5
lr = 0.3
x = GD(x_start, dfunc, epochs, lr=lr)
print x

运行demo0_GD()的结果如下:
[-5. -2. -0.8 -0.32 -0.128 -0.0512]

4)、继续修改一下demo_GD()函数,可视化梯度下降法的搜索的整个过程。

ef demo0_GD():
"""演示如何使用梯度下降法GD()"""
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)

x_start = -5
epochs = 5

lr = 0.3
x = GD(x_start, dfunc, epochs, lr=lr)

color = 'r'
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color, label='lr={}'.format(lr))
plt.scatter(x, func(x), c=color, )
plt.legend()
plt.show()

运行函数demo0_GD(),得到下图:

5)、看了lr=0.3时,效果不错,假设选用其它学习速率,情况如何呢?以下我们分别设置学习速率为0.1、0.3与0.9,比较一下各个学习速率的迭代过程,并把整个迭代过程进行可视化。

def demo1_GD_lr():
# 函数图像
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Learning Rate')

x_start = -5
epochs = 5

lr = [0.1, 0.3, 0.9]

color = ['r', 'g', 'y']
size = np.ones(epochs+1) * 10
size[-1] = 70
for i in range(len(lr)):
x = GD(x_start, dfunc, epochs, lr=lr[i])
plt.subplot(1, 3, i+1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}'.format(lr[i]))
plt.scatter(x, func(x), c=color[i])
plt.legend()
plt.show()

运行函数demo1_GD_lr()的结果如下:

由此可以看出,学习速率大小对梯度下降法的搜索过程起着非常大的影响,太小,搜索速度比较慢;太大又可能跳过极值点。要选中一个恰当的学习速率往往要花费不少时间。是否有方法能避免或解决这个问题呢?使我们不必花费太多时间在选择学习速率这个参数上,又能获取同样、甚至更好的效果呢?
有的,我们可以通过衰减因子、引入动量、自动调整学习速率的方法(又称为自适应梯度策略)等方法就可。
(3)衰减因子
当学习速率较大时,容易在搜索过程中发生震荡,而发生震荡的根本原因无非就是搜索的步长迈的太大了。
如果能够让 lr 随着迭代周期不断衰减变小,那么搜索时迈的步长就能不断减少以减缓震荡。学习速率衰减因子由此诞生。如何使lr 随着迭代周期不断衰减变小?我们只要把循环次数加入参数中即可:
lr_i = lr_start * 1.0 / (1.0 + decay * i)
上面的公式即为学习速率衰减公式,其中 lr_i 为第 i 次迭代时的学习速率, lr_start 为原始学习速率, decay 为 一个介于[0.0, 1.0]的小数。
decay 越小,学习速率衰减地越慢,当 decay = 0 时,学习速率保持不变。
decay 越大,学习速率衰减地越快,当 decay = 1 时,学习速率衰减最快。
以下我们通过一个实例来说明,如果通过衰减因子避免因学习速率过大,导致搜索震荡问题。
还是以函数为例,这次引入衰减因子decay 来改变学习速率。
(1)、先定义函数及导数,确定衰减因子与学习速率lr的关系

lr = [0.1, 0.3, 0.9, 0.99]
decay = [0.0, 0.01, 0.5, 0.9]

import numpy as np
import matplotlib.pyplot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

def GD_decay(x_start, df, epochs, lr, decay):
xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
v = 0
for i in range(epochs):
dx = df(x)
# 学习速率衰减
lr_i = lr * 1.0 / (1.0 + decay * i)
# v表示x要改变的幅度
v = - dx * lr_i
x += v
xs[i+1] = x
return xs

(2)、利用新的学习速率来进行优化

def demo3_GD_decay():
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Decay')

x_start = -5
epochs = 10

lr = [0.1, 0.3, 0.9, 0.99]
decay = [0.0, 0.01, 0.5, 0.9]

color = ['k', 'r', 'g', 'y']

plt.figure(figsize=(14,10))
row = len(lr)
col = len(decay)
size = np.ones(epochs + 1) * 10
size[-1] = 70
for i in range(row):
for j in range(col):
x = GD_decay(x_start, dfunc, epochs, lr=lr[i], decay=decay[j])
plt.subplot(row, col, i * col + j + 1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}, de={}'.format(lr[i], decay[j]))
plt.scatter(x, func(x), c=color[i], s=size)
plt.legend(loc=0)
plt.show()

demo3_GD_decay()

运行结果如下:

在所有行中均可以看出,decay越大,学习速率衰减地越快。在第三行与第四行可看到,decay确实能够对震荡起到减缓的作用。
(3)、比较多种衰减因子,对学习速率的影响。
假设起始学习速率为1.0,decay为[0.0, 0.001, 0.1, 0.5, 0.9, 0.99],迭代周期为300

ef demo4_how_to_chose_decay():
lr = 1.0
iterations = np.arange(300)

decay = [0.0, 0.001, 0.1, 0.5, 0.9, 0.99]
for i in range(len(decay)):
decay_lr = lr * (1.0 / (1.0 + decay[i] * iterations))
plt.plot(iterations, decay_lr, label='decay={}'.format(decay[i]))

plt.ylim([0, 1.1])
plt.legend(loc='best')
plt.show()

demo4_how_to_chose_decay()

运行结果如下:

可以看到,当decay为0.1时,50次迭代后学习速率已从1.0急剧降低到了0.2。如果decay设置得太大,则可能会收敛到一个不是极值的位置。
由上可知,小批量梯度下降法选择合适的learning rate比较困难,也很难保证良好地收敛,此外,对神经网络最优化非凸的罚函数时,另一个通常面临的挑战,是如何避免目标函数被困在局部最小值中。Dauphin 及其他人认为,这个困难并不来自于局部最小值,而是来自于「鞍点」,也就是在一个方向上斜率是正的、在一个方向上斜率是负的点。这些鞍点通常由一些函数值相同的面环绕,它们在各个方向的梯度值都为 0,所以 SGD 很难从这些鞍点中脱开。是否有更好的方法能解决这些问题或挑战?下面我们介绍引入动量(momentum)的优化方法。

5.1.4 动量对梯度下降法的影响

通过前面学习速率的实例,我们知道学习速率较小时,收敛到极值的速度较慢。
学习速率较大时,容易在搜索过程中发生震荡,如下图。

在这种情况下,SGD 在陡谷的周围震荡,向局部极值处缓慢地前进。优化方法除与学习率有关外,还与梯度方法有关,这里我们可以通过引入一个动量方法,来避免这个比较费时问题。动量(momentum)是模拟物理里动量的概念,积累之前的动量来替代真正的梯度。动量在物理学上定义为质量乘以速度,这里我们不妨假设单位质量,因此速度向量就可以看成为动量。一个物体在运动时具有惯性,把这个思想运用到梯度下降计算中,增加算法的收敛速度和稳定性,如下图:

从这个图形可以看出:
当本次梯度下降- lr*dx 的方向与上次更新量的方向相同时,上次的更新量能够对本次的搜索起到一个正向加速的作用。
当本次梯度下降- lr*dx的方向与上次更新量的方向相反时,上次的更新量能够对本次的搜索起到一个减速的作用。
使用动量的梯度下降法的Python代码如下:

import numpy as np
import matplotlib.pyplot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

def GD_momentum(x_start, df, epochs, lr, mu):
"""
带有冲量的梯度下降法。
:param x_start: x的起始点
:param df: 目标函数的一阶导函数
:param epochs: 迭代周期
:param lr: 学习速率
:param mu: 动量
:return: x在每次迭代后的位置(包括起始点),长度为epochs+1
"""

xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
v = 0
for i in range(epochs):
dx = df(x)
# v表示x要改变的幅度
v = - dx * lr + mu * v
x += v
xs[i+1] = x
return xs

为了查看mu大小对不同学习速率的影响,此处设置学习速率为lr = [0.01, 0.1, 0.6, 0.9],冲量依次为mu = [0.0, 0.1, 0.5, 0.9],起始位置为x_start = -5,迭代周期为6。测试以及绘图代码如下:

def demo2_GD_momentum():
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Learning Rate, Momentum')

x_start = -5
epochs = 6

lr = [0.01, 0.1, 0.6, 0.9]
mu = [0.0, 0.1, 0.5, 0.9]

color = ['k', 'r', 'g', 'y']

row = len(lr)
col = len(mu)
size = np.ones(epochs+1) * 10
size[-1] = 70
for i in range(row):
for j in range(col):
x = GD_momentum(x_start, dfunc, epochs, lr=lr[i], mu=mu[j])
plt.subplot(row, col, i * col + j + 1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}, mu={}'.format(lr[i], mu[j]))
plt.scatter(x, func(x), c=color[i], s=size)
plt.legend(loc=0)
plt.show()

运行结果如下:

从上图不难以下几点:
(1)从第一行可看出:在学习率较小的时候,适当的momentum能够起到一个加速收敛速度的作用。
(2)从第四行可看出:在学习率较大的时候,适当的momentum能够起到一个减小收敛时震荡幅度的作用。
从上述两点来看,momentum确实能够解决收敛慢或震荡的两个问题。

然而在第二行与第三行的最后一列图片中也发现了一个问题,当momentum较大时,原本能够正确收敛的时候却因为刹不住车跑过头了。那么怎么继续解决这个新出现的问题呢?下面我们介绍一种改进动量方法。

5.1.5 改进的动量更新策略

 

NAG算法相对于Momentum多了一个本次梯度相对上次梯度的变化量,这个变化量本质上是对目标函数二阶导的近似。由于利用了二阶导的信息,NAG算法才会比Momentum具有更快的收敛速度.
NAG的python代码具体实现:

import numpy as np
import matplotlib.pyplot as plt
def f(x):
return x[0] * x[0] + 50 * x[1] * x[1]
def g(x):
return np.array([2 * x[0], 100 * x[1]])
xi = np.linspace(-200,200,1000)
yi = np.linspace(-100,100,1000)
X,Y = np.meshgrid(xi, yi)
Z = X * X + 50 * Y * Y

%matplotlib inline
def contour(X,Y,Z, arr = None):
plt.figure(figsize=(15,7))
xx = X.flatten()
yy = Y.flatten()
zz = Z.flatten()
plt.contour(X, Y, Z, colors='black')
plt.plot(0,0,marker='*')
if arr is not None:
arr = np.array(arr)
for i in range(len(arr) - 1):
plt.plot(arr[i:i+2,0],arr[i:i+2,1])

contour(X,Y,Z)

def nesterov(x_start, step, g, discount = 0.7):
x = np.array(x_start, dtype='float64')
passing_dot = [x.copy()]
pre_grad = np.zeros_like(x)
for i in range(50):
x_future = x - step * discount * pre_grad
grad = g(x_future)
pre_grad = pre_grad * discount + grad
x -= pre_grad * step
passing_dot.append(x.copy())
#print '[ Epoch {0} ] grad = {1}, x = {2}'.format(i, grad, x)
if abs(sum(grad)) < 1e-6:
break;
return x, passing_dot

start_point = [150,75]
step = 0.012
discount = 0.9
res2, x_arr2 = nesterov(start_point, step, g, discount)
contour(X,Y,Z, x_arr2)


参考了:
http://www.jianshu.com/p/58b3fe300ecb
https://github.com/WarBean/zhihuzhuanlan/blob/master/Momentum_Nesterov.ipynb

5.1.6 自适应梯度策略

通过以上实例的分析我们了解到,虽然梯度下降算法效果很好,并且广泛使用,但同时其也存在一些挑战与问题需要解决:
1)选择一个合理的学习速率很难。
如果学习速率过小,则会导致收敛速度很慢。如果学习速率过大,那么其会阻碍收敛,即在极值点附近会振荡。
2)学习速率调整也不易
学习速率调整(又称学习速率调度),试图在每次更新过程中,改变学习速率,一般使用某种事先设定的策略或者在每次迭代中衰减一个较小的阈值。无论哪种调整方法,都需要事先进行固定设置,这边便无法自适应每次学习的数据集特点。
3)固定学习速率易导致算法卡在鞍点。
前面的策略都是针对迭代方向进行优化,学习速率为固定值,即所有参数共享相同的学习速率,学习速率在每一个方向上的大小固定,很容易造成算法被卡在鞍点的位置。

图 5.3 鞍点
这是实现上图的python代码

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
plot_args = {'rstride': 1, 'cstride': 1, 'cmap':"Blues_r",
'linewidth': 0.4, 'antialiased': True,
'vmin': -1, 'vmax': 1}

x, y = np.mgrid[-1:1:31j, -1:1:31j]
z = x**2 - y**2

ax.plot_surface(x, y, z, **plot_args)
ax.plot([0], [0], [0], 'ro')
ax.view_init(azim=-60, elev=30)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
plt.xticks([-1, -0.5, 0, 0.5, 1],
[r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
plt.yticks([-1, -0.5, 0, 0.5, 1],
[r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
ax.set_zticks([-1, -0.5, 0, 0.5, 1])
ax.set_zticklabels([r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
plt.savefig("Saddle_point.svg", bbox_inches="tight", transparent=True)
plt.show()

4)对于非凸目标函数,容易陷入那些次优的局部极值点中。
如何解决这些问题?这里我们介绍几种自适应方法来解决此类问题。

5.1.6.1 AdaGrad自适应梯度策略

如图5.3 所示,假设鞍点为a,由于学习速率不变,容易在鞍点处来回震荡,无法找到最优点。下面我们介绍学习速率随迭代数的变化而变化的自适应梯度策略,简称为AdaGrad。AdaGrad是一种自适应的调整学习速率的优化方法。
假设每个模型参数使用相同的学习速率η,而Adagrad在每一个更新步骤中对于每一个模型参数使用不同的学习速率lr,设第t次更新步骤中,目标函数的参数梯度为
AdaGrad算法主要步骤如下:
lr为全局学习速率参数
初始参数为θ
小参数δ
初始化梯度累积变量 γ=0

由上面算法的伪代码可知,
1)、随着迭代时间越长,累积梯度r越大,从而学习速率随着时间就减小,在接近目标值时,不会因为学习速率过大而越过极值点;
2)、不同参数之间学习速率是不同,因此,与前面固定学习速率相比,不容易在鞍点卡住;
3)、如果梯度累积参数r比较小,则学习速率会比较大,所以参数迭代的步长就会比较大;相反,如果梯度累积参数比较大,则学习速率会比较小,所以迭代的步长会比较小。

5.1.6.2RMSprop自适应梯度策略

经验上,RMSProp已被证明是一种有效而且实用的深度学习网络优化算法,这也是深度学习中经常采用的方法之一。

5.1.6.3 Adam自适应梯度策略

Adam(Adaptive Moment Estimation)本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
Adam是另一种学习速率自适应的深度神经网络方法,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习速率。Adam的优点主要在于经过偏置校正后,每一次迭代学习速率都有个确定范围,使得参数比较平稳。公式如下:


特点:
结合了Adagrad善于处理稀疏梯度和RMSprop善于处理非平稳目标的优点
对内存需求较小
为不同的参数计算不同的自适应学习速率
也适用于大多非凸优化 - 适用于大数据集和高维空间

5.1.7 各种算法可视化

如下的两个动画(图像版权:Alec Radford)给了我们关于各种优化算法在优化过程中行为的直观说明。

contours_evaluation_optimizers
图5.4
在图 5.4 中,我们可以看到,在罚函数的等高线图中,优化器的位置随时间的变化情况。注意到,Adagrad、 Adadelta 及 RMSprop 法几乎立刻就找到了正确前进方向并以相似的速度很快收敛。而动量法和 NAG 法,则找错了方向,如图所示,让小球沿着梯度下降的方向前进。但 NAG 法能够很快改正它的方向向最小指出前进,因为他能够往前看并对前面的情况做出响应。
saddle_point_evaluation_optimizers

图5.5

图 5.5 展现了各算法在鞍点附近的表现。如上面所说,这对对于 SGD 法、动量法及 NAG 法制造了一个难题。他们很难打破」对称性「带来的壁垒,尽管最后两者设法逃脱了鞍点。而 Adagrad 法、RMSprop 法及 Adadelta 法都能快速的沿着负斜率的方向前进。

5.1.8 如何选择优化方法

在一般机器学习中很多会用 SGD,SGD 虽然能达到极小值,但是比其它算法用的时间长,而且可能会被困在鞍点。
如果需要更快的收敛,或者是训练更深更复杂的神经网络,需要用一种自适应的算法。
最后两张动图从直观上展现了算法的优化过程。第一张图为不同算法在损失平面等高线上随时间的变化情况,第二张图为不同算法在鞍点处的行为比较。
我们该如何选择优化器呢?如果数据为稀疏的,而且采用深度学习,一般需要考虑使用自适用方法,如 Adagrad, RMSprop, Adam等。当然RMSprop, Adam 在很多情况下的效果是相似的。Adam在 RMSprop 的基础上加了bias-correction 和 momentum,随着梯度变的稀疏,Adam 比 RMSprop 效果会好。整体来讲,Adam 是最好的选择。
总的来说,RMSprop 法是一种基于 Adagrad 法的拓展,他从根本上解决学习率骤缩的问题。而 Adam 法,则基于 RMSprop 法添加了偏差修正项和动量项。在我们地讨论范围中,RMSprop、Adam 法都是非常相似地算法,在相似地情况下都能做的很好。Kingma 及其他人展示了他们的偏差修正项帮助 Adam 法,在最优化过程快要结束、梯度变得越发稀疏的时候,表现略微优于 RMSprop 法。总的来说,Adam 也许是总体来说最好的选择。
有趣的是,很多最新的论文,都直接使用了(不带动量项的)Vanilla SGD 法,配合一个简单的学习率(退火)列表。如论文所示,这些 SGD 最终都能帮助他们找到一个最小值,但会花费远多于上述方法的时间。并且这些方法非常依赖于鲁棒的初始化值及退火列表。因此,如果你非常在你的模型能快速收敛,或是你需要训练一个深度或复杂模型,你可能需要选择上述的适应性模型。
就一般经验而言:
对于稀疏数据,尽量使用学习率可自适应的优化方法,不用手动调节,而且最好采用默认值
SGD通常训练时间更长,但是在好的初始化和学习率调度方案的情况下,结果更可靠
如果在意更快的收敛,并且需要训练较深较复杂的网络时,推荐使用学习率自适应的优化方法。
RMSprop,Adam是比较相近的算法,在相似的情况下表现差不多。
在想使用带动量的RMSprop,或者Adam的地方,大多可以使用Nadam取得更好的效果。

零基础学习-深度学习核心-梯度下降与最优化》有1个想法

  1. Pingback引用通告: Python与人工智能 – 飞谷云人工智能

评论已关闭。