用python中的Momentum 可视化梯度下降
这篇文章是为了直观地展示当loss surface是ravenlike(即其中一个方向明显比其他方向陡峭)。让我们从创建一个二维raven loss surface开始。
L (x, y)= 1/16 x² + 9 y²
Python代码如下:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
class LossSurface:
"""A loss surface with L(x, y) = a * x ^2 + b * y ^2.
"""
def __init__(self, a, b):
self.a = a
self.b = b
N = 1000
x_list = np.linspace(-2.5, 2.5, N)
y_list = np.linspace(-0.5, 0.5, N)
self.X, self.Y = np.meshgrid(x_list, y_list)
self.Z = self.a * (self.X ** 2) + self.b * (self.Y ** 2)
def plot(self):
fig, ax = plt.subplots()
cmap = cm.get_cmap('Greens_r')
cp = ax.contour(self.X, self.Y, self.Z, 50, cmap=cmap)
cbar = fig.colorbar(cp)
cbar.set_label('loss')
ax.set_xlim(-2.5, 2.5)
ax.set_ylim(-0.5, 0.5)
ax.set_xlabel('x')
ax.set_ylabel('y')
return fig, ax
if __name__ == '__main__':
a = 1 / 16
b = 9
loss_surface = LossSurface(a, b)
fig, ax = loss_surface.plot()
fig_name = 'loss_surface.png'
fig.savefig(fig_name)
print('{} saved.'.format(fig_name))
下图是raven loss surface。对于该loss surface,在点(0,0)处存在全局最小值,其中损失等于零。
Loss surface for L (x, y)= 1/16 x² + 9 y²
梯度下降-Momentum
Momentum方法可以在梯度指向跨越迭代的相同方向的方向上累积速度。它通过将以前的权重更新的一部分添加到当前的权重更新来实现这一点。
下面的等式是具有Momentum更新的梯度下降。β是您要添加到当前范围[0,1]的先前权重更新的一部分。当β= 0时,它会降低到vanilla 梯度下降。
v = βv + ∇L(w)
w = w − v
让我们首先在前面创建的raven loss surface中初始化权重(-2.4,0.2)。Next,尝试vanilla 梯度下降(β= 0)学习速率= 0.1,运行50迭代,看看损失。
Vanilla gradient descent, β = 0
在50次迭代之后,损失大约是0.1,但是它仍然离损失为零的全局最小值(0,0)很远。我们可以看到梯度一直在改变方向,因为y方向的梯度在每次迭代中都在改变符号,这使得算法慢慢地走向最优。我们可以增加Momentum 来加快学习速度。让我们尝试β= 0.8并运行相同数量的迭代进行权重更新。
Gradient descent with momentum, β = 0.8
我们现在使用Momentum实现了2.8e-5的相同迭代次数的损失!因为x方向上的梯度总是指向正的x方向,所以速度可以累积。实际上,这意味着在x方向上有更大的学习率,因此在相同迭代次数下,它可以达到比vanilla 梯度下降更低的损失。这也是为什么Momentum能抑制振荡的原因。
我们可能想知道如果我们选择β= 0.9,它会不会更多地积累速度并更快地达到全局最小值?
Gradient descent with momentum, β = 0.9
答案是不。上图显示,过大的速度将通过大步长的全局最小值。我们还可以绘制速度 - 迭代关系,以分别表示速度如何在x和y方向上变化。
Velocity-iteration x不同β对体重的关系。与β> 0,x方向的速度积累
不同β下权重y的速度 - 迭代关系
y方向的速度不会累积,因为梯度的sign 在每次迭代中都会改变。
下面的python代码用于生成上面的图形,包括权重轨迹和速度迭代图,第一个脚本(loss_surface.py)需要作为模块导入。
import collections
import matplotlib.pyplot as plt
from loss_surface import LossSurface
class Momentum:
""" Momentum sgd update and plot weight trajectory.
"""
def __init__(self, lr, iteration, beta, loss_surface):
self.lr = lr
self.iteration = iteration
self.beta = beta
self.a = loss_surface.a
self.b = loss_surface.b
self.w_1, self.w_2, self.v_1, self.v_2 = self.init_parameters()
def run(self):
w_1_curve = list()
w_2_curve = list()
v_1_curve = list()
v_2_curve = list()
w_1_curve.append(self.w_1)
w_2_curve.append(self.w_2)
v_1_curve.append(self.v_1)
v_2_curve.append(self.v_2)
for step in range(self.iteration):
self.gd_update()
loss = self.calculate_loss()
w_1_curve.append(self.w_1)
w_2_curve.append(self.w_2)
v_1_curve.append(self.v_1)
v_2_curve.append(self.v_2)
print('iteration: {}, w1: {}, w2: {}, v1: {}, v2:{}, loss: {}'.format(
step+1, self.w_1, self.w_2, self.v_1, self.v_2, loss))
return w_1_curve, w_2_curve, v_1_curve, v_2_curve
def gd_update(self):
self.v_1 = self.beta * self.v_1 + 2 * self.a * self.w_1
self.w_1 -= self.lr * self.v_1
self.v_2 = self.beta * self.v_2 + 2 * self.b * self.w_2
self.w_2 -= self.lr * self.v_2
def calculate_loss(self):
loss = self.a * (self.w_1**2) + self.b * self.w_2**2
return loss
def save_plot(self, loss_surface_fig, loss_surface_ax, w_1_curve, w_2_curve):
loss_surface_ax.plot(w_1_curve, w_2_curve, color="black")
fig_name = 'sgd_beta_{}_lr_{}_iter_{}_with_contour.png'.format(self.beta, self.lr, self.iteration)
loss_surface_fig.savefig(fig_name)
print('{} saved.'.format(fig_name))
@staticmethod
def init_parameters():
w_1 = -2.4
w_2 = 0.2
v_1 = 0
v_2 = 0
return w_1, w_2, v_1, v_2
def save_velocity_plot(name, v_curves):
fig, ax = plt.subplots()
ax.plot(v_curves['beta_0.0'], label='beta=0.0')
ax.plot(v_curves['beta_0.8'], label='beta=0.8')
ax.plot(v_curves['beta_0.9'], label='beta=0.9')
ax.legend()
ax.set_xlabel('iterations')
ax.set_ylabel('velocity')
fig_name = 'velocity_{}.png'.format(name)
fig.savefig(fig_name)
print('{} saved.'.format(fig_name))
def main():
lr = 0.1
iteration = 50
v_1_curves = collections.defaultdict(list)
v_2_curves = collections.defaultdict(list)
for beta in [0.0, 0.8, 0.9]:
a = 1 / 16
b = 9
ls = LossSurface(a, b)
ls_fig, ls_ax = ls.plot()
demo = Momentum(lr=lr, iteration=iteration, beta=beta, loss_surface=ls)
w_1_curve, w_2_curve, v_1_curve, v_2_curve = demo.run()
demo.save_plot(ls_fig, ls_ax, w_1_curve, w_2_curve)
v_1_curves['beta_{}'.format(beta)] = v_1_curve
v_2_curves['beta_{}'.format(beta)] = v_2_curve
save_velocity_plot('v_1', v_1_curves)
save_velocity_plot('v_2', v_2_curves)
if __name__ == '__main__':
main()
结论
我们直观地表明,当 loss surface is raven-like时,Momentum 梯度下降比vanilla 梯度下降更快地收敛。我们还了解到β不能太大,因为它会积累太多的Momentum 并快速超过最佳点。