函数的梯度方向表示了函数值增长速度最快的方向,那么和它相反的方向就可以看作函数值减少速度最快的方向。就机器学习模型优化的问题而言,当目标设定为求解目标函数最小值时,只要朝着梯度下降的方向前进就能不断逼近最优值。
最简单的梯度下降算法—固定学习率的方法,这种梯度下降算法由两个函数和三个变量组成。
函数1:待优化的函数f(x),它可以根据给定的输入返回函数值
函数2:待优化函数的导数g(x),它可以根据给定的输入返回函数的导数值
变量x:保存当前优化过程的参数值,优化开始时该变量将被初始化为某个数值,优化过程中这个变量会被不断变化,直到它找到最小值。
变量grad:保存变量x点处的梯度值
变量step:表示沿着梯度下降方向行进的步长,也被称为学习率。
(备注:代码在本文最后有完整代码,此处只给出其中函数部分。所以实现的图和结果想看的直接先到文末把代码copy下来,然后直接执行即可)
python函数举例:
def gd(x_start, step, g):
x = np.array(x_start,'float64')
for i in range(30):
grad = g(x)
x -= grad*step
print("[Epoch{0}] grad={1}, x={2}".format(i, grad, x))
if isinstance(grad, float):
if abs(grad) < 1e-6:
break
else:
if abs(sum(grad)) < 1e-6:
break
return x
假设函数f(x)=x^2-2x+1,则很容易看出最小值是x=1,f(x)=0。其图像就是一个简单的抛物线。
执行gd(5,0.55,g):
如果步长设置过大我们可以看到,参数的梯度不但没有收敛,反而越来越大:
执行gd(5,2.55,g):
优化的本意是让目标值朝着梯度下降的方向前进,结果它却走向了另外一个方向。实际上,函数在某一点的梯度指的是它在当前变量处的梯度,对于这一点来说,它的梯度方向指向了函数上升的方向,可以利用泰勒公式证明在一定范围内,沿着负梯度方向前进,函数值是会下降的。但是,公式只能保证在一定范围内是成立的,从函数的实际图像中也可以看出,如果优化的步长太大,就有可能跳出函数值下降的范围,那么函数值是否下降就不好说了 。 当然有可能越变越大,造成优化的悲剧。
既然小步长会使目标值的梯度下降,大步长会使梯度发散,那么有没有一个步长会让优化问题原地打转呢?在这个问题中,这样的步长是存在且容易找到的。如果step= 1时,求解会原地打转,梯度下降法就失效了。
执行gd(5,1,g):
通过上面的实验可以发现,对于初始值为5这个点,当步长大于1时,梯度下降法会出现求解目标值发散的现象;而步长小于1时,则不会发散,参数会逐渐收敛。所以1就是步长的临界点。那么问题又来了,对于别的初始值,这个规律还适用吗?接下来就把初始值换成4,再进行一次实验:
实验发现,步长等于1时,初始值设置成(最优值除外)任何数字,参数都不会收敛到最优值。这个实验揭示了一个道理:对于这个二次函数,如果采用固定步长的梯度下降法进行优化,步长要小于1,否则不论初始值等于多少,问题都会发散或者原地打转!
若换个函数:fx=4x2-4x+1,它的安全步长就不再是1了,而是0.25,这里就不再重复了,感兴趣的可以自己改下函数表达式f和导数表达式g,然后执行gd(5,0.25,g)即可.
动量(Momentum)算法:动量代表了前面优化过程积累的“能量”,它将在后面的优化过程中持续发戚,推动目标值前进。拥有了动量,一个已经结束的更新量不会立刻消失,只会以一定的形式衰减,剩下的能量将继续在优化中发挥作用。
python函数举例:
def momentum(x_start, step, g, discount=0.7):
x = np.array(x_start, dtype='float64')
pre_grad = np.zeros_like(x) # 存储积累的动量
for i in range(60):
grad = g(x)
pre_grad = pre_grad*discount + grad*step
x -= pre_grad
print("[Epoch {0}] grad={1},x={2}".format(i, grad, x))
if abs(sum(grad)) < 1e-6:
break
return x
代码中多出了一个新变量pre_grad。这个变量就是用于存储历史积累的动量,每一轮迭代动量都会乘以一个打折量(discount)做能量衰减但是它依然会被用于更新参数。动量算法和前面介绍的梯度下降法相比有什么优点呢?用形象的话来说,它可以帮助目标值穿越“狭窄山谷”形状的优化曲面,从而到达最终的最优点。
假设函数,函数在等高线图上的样子如图(三维图和平面图):
则容易看出其中中心的点表示最优值。把等高线上的图像想象成地形图,从等高线的疏密程度可以看出,这个函数在y轴方向十分陡峭,在x轴方向则相对平缓。也就是说,函数在y轴的方向导数比较大,在x轴的方向导数比较小
将50轮迭代过程中参数的优化过程图画出来,如图:
gd([150,75],0.016,g)
可以看出目标值从某个点出发,整体趋势向着最优点前进,它和最优值的距离不断靠近,说明优化过程在收敛,步长设置是没有问题的,但是前进的速度似乎有点乏力,50轮迭代井没有到达最优值,有可能是步长设置得偏小。有了前面的经验,这次设置步长时很谨慎。我们只将步长稍变大一点,结果如图:
虽然优化效果有了一定的提高,但成效依然不明显,而且优化的过程图中出现了参数值左右抖动的现象,这是怎么回事呢?看上去参数在优化过程中产生了某种“打转儿”的现象,做了很多无用功。如果继续增加步长,优化曲线会变成:
从结果来看,这个步长已经是能设置的最大步长,如果步长再设大些,就要从U形赛道飞出去,优化参数的梯度也将发散出去。在这个问题中,由于两个坐标轴方向的函数的陡峭性质不同,两个方向对最大步伏的限制不同,显然y方向对步长的限制更严格。但满足y方向的更新,x方向就无法获得充分的更新,这样梯度下降法将无法获得很好的效果。
我们会发现每一次的行动只会在以下三个方向进行:沿-x方向滑行;沿+y方向滑行。沿-y方向滑行。
这样看来,要是能把行动的力量集中在往-x方向走而不是沿y方向打转就好了。这个想法可以被动量算法实现。
我们可以想象,使用了动量后,历史的更新量会以衰减的形式不断作用在这些方向上,那么沿-y和+y两个方向的动量就可以相互抵消,而-x方向的力则会一直加强,这样虽然还会沿y方向打转,但是他在-x方向的速度会因为之前的累积变得越来越快。
momentum ( [150, 75],0.016, g)
优化曲线果然没有令人失望,尽管还是有打转现象,但是在50轮迭代后,他进入了最优点的邻域,完成了优化任务,和前面的梯度下降法相比有了很大的进步。
当然,还是暴露了动量优化存在的一点问题,前面几轮迭代过程中目标值在ν轴上的震荡比过去还要大。在发明动量算法后,又有科研人员发明了基于动量算法的改进算法,解决了动量算法没有解决的问题一一更强烈的抖动,干脆停止玩耍,专心赶路。这就是牛顿动量(Nesterov)法
python函数代码:
def nesterov(x_start, step, g, discount=0.7):
x = np.array(x_start, dtype='float64')
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
print('[Epoch {0} grad={1}, x={2}'.format(i, grad, x))
if abs(sum(grad)) < 1e-6:
break
return x
算法不再贪玩,放弃在U形赛道上摩擦。那么,Nesterov算法和动量算法相比有什么区别呢?动量算法计算的梯度是在当前的目标点的;而 Nesterov算法计算的梯度是在动量更新后的优化点的。这其中关键的区别在于计算梯度的点。
可以想象一个场景,当优化点已经积累了某个抖动方向的梯度后,对动量算法来说,虽然当前点的梯度指向积累梯度的相反方向,但是量不够大,所以最终的优化方向还会在积累的方向上前进一段,这就是上图所示的效果。对Nesterov方法来说,如果按照积累方法再向前多走一段,则梯度中指向积累梯度相反方向的量会变大许多,最终两个方向的梯度抵消,反而使抖动方向的量迅速减少。Nesterov的衰减速度确实比动量方法的快不少。
最后来讲述动量在数值上的事情。科研人员已经给出了动量打折率的建议配置一一0.9 (刚才的例子全部是0.7)那么0.9的动量打折率能使历史更新量发挥多大作用呢?
如果用G表示每一轮迭代的动量,g表示当前一轮迭代的更新量(方向×步长),t表示迭代轮数,γ表示动量的打折率,那么对于时刻t的梯度更新量就有公式:
这样可以计算出对于第一轮迭代的更新g0来说,从G0到GT,它的总贡献量为。它的贡献和是一个等比数列的和,比值为γ。如果γ=0.9,那么根据公比小于1的等比数列的极限公式,可以知道更新量在极限状态下的贡献值:
那么当γ=0.9时,它一共贡献了相当于自身10倍的能量。如果γ=0.99,那就是100倍能量。在实际应用中,打折率的设置需要分析具体任务中更新量需要多“持久”的动力。
python完整代码:
import numpy as np
import sklearn.datasets as d
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def plot1():
reg_data = d.make_regression(100, 1, 1, 1, 1.0)
plt.plot(reg_data[0], reg_data[1])
plt.show()
cls_data = d.make_classification(100, 2, 2, 0, 0, 2)
plt.plot(cls_data[0], 'o')
plt.show()
fig = plt.figure()
ax = Axes3D(fig)
X = np.linspace(0.01, 0.99, 101)
Y = np.linspace(0.01, 0.99, 101)
X, Y = np.meshgrid(X, Y)
Z = -X * np.log2(Y) - (1 - X) * np.log2(1 - Y)
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='rainbow')
plt.show()
xi = np.linspace(-200,200,800)
yi = np.linspace(-100,100,800)
X, Y = np.meshgrid(xi, yi)
Z = X*X+50*Y*Y
plt.contour(X, Y, Z, 10)
plt.plot(0,0,"o")
plt.show()
fig = plt.figure()
ax = Axes3D(fig)
ax.plot_surface(Y, X, Z, rstride=1, cstride=1, cmap='rainbow')
plt.show()
def gd(x_start, step, g):
"""
梯度下降
:param x_start:
:param step:
:param g:
:return:
"""
result = [[], []]
x = np.array(x_start,'float64')
for i in range(30):
grad = g(x)
x -= grad*step
print("[Epoch{0}] grad={1}, x={2}".format(i, grad, x))
if isinstance(grad, float):
if abs(grad) < 1e-6:
break
else:
if abs(sum(grad)) < 1e-6:
break
result[0].append(x[0])
result[1].append(x[1])
return result
def momentum(x_start, step, g, discount=0.7):
"""
动量算法
:param x_start:
:param step:
:param g:
:param discount:
:return:
"""
result = [[], []]
x = np.array(x_start, dtype='float64')
pre_grad = np.zeros_like(x) # 存储积累的动量
for i in range(60):
grad = g(x)
pre_grad = pre_grad*discount + grad*step
x -= pre_grad
print("[Epoch {0}] grad={1},pre_grad={2},x={3}".format(i, grad, pre_grad, x))
if abs(sum(grad)) < 1e-6:
break
result[0].append(x[0])
result[1].append(x[1])
return result
def nesterov(x_start, step, g, discount=0.7):
"""
Nesterov算法
:param x_start:
:param step:
:param g:
:param discount:
:return:
"""
result = [[], []]
x = np.array(x_start, dtype='float64')
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
print('[Epoch {0} grad={1}, x={2}'.format(i, grad, x))
if abs(sum(grad)) < 1e-6:
break
result[0].append(x[0])
result[1].append(x[1])
return result
def test1():
def f(x):
return x ** 2 - 2 * x + 1
def g(x):
return 2 * x - 2
x = np.linspace(-5,7,100)
y = f(x)
plt.plot(x,y)
plt.show()
gd(5, 0.55, g)
gd(5,2.55,g)
gd(5,1,g)
def test2():
xi = np.linspace(-200, 200, 800)
yi = np.linspace(-100, 100, 800)
X, Y = np.meshgrid(xi, yi)
Z = X * X + 50 * Y * Y
def f(x):
return x[0]**2+50*x[1]**2
def g(x):
return np.array([2*x[0], 100*x[1]])
result = gd([150,75],0.016,g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
result = gd([150,75],0.019,g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
result = gd([150, 75], 0.02, g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
result = momentum([150,75],0.016,g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
result = nesterov([150,75],0.012,g)
plt.contour(X, Y, Z, 10)
plt.plot(0, 0, "o")
plt.plot(result[0], result[1])
plt.show()
if __name__ == '__main__':
plot1()
test1()
test2()
参考书籍:《强化学习精要 核心算法与TensorFlow实现》冯超著,电子工业出版社