Pytorch Note16 优化算法2 动量法(Momentum)

Posted Real&Love

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Pytorch Note16 优化算法2 动量法(Momentum)相关的知识,希望对你有一定的参考价值。

Pytorch Note16 优化算法2 动量法(Momentum)


全部笔记的汇总贴: Pytorch Note 快乐星球

动量法(Momentum)

SGD 在 ravines 的情况下容易被困住, ravines 就是曲面的一个方向比另一个方向更陡,这时 SGD 会发生震荡而迟迟不能接近极小值:
在这里插入图片描述

Momentum

考虑一个二维输入, [ x 1 , x 2 ] [x_1, x_2] [x1,x2],输出的损失函数 L : R 2 → R L: R^2 \\rightarrow R L:R2R,下面是这个函数的等高线

可以想象成一个很扁的漏斗,这样在竖直方向上,梯度就非常大,在水平方向上,梯度就相对较小,所以我们在设置学习率的时候就不能设置太大,为了防止竖直方向上参数更新太过了,这样一个较小的学习率又导致了水平方向上参数在更新的时候太过于缓慢,所以就导致最终收敛起来非常慢。

动量法的提出就是为了应对这个问题,我们梯度下降法做一个修改如下,可以加速 SGD, 并且抑制震荡
v i = γ v i − 1 + η ∇ L ( θ ) v_i = \\gamma v_{i-1} + \\eta \\nabla L(\\theta) vi=γvi1+ηL(θ)

θ i = θ i − 1 − v i \\theta_i = \\theta_{i-1} - v_i θi=θi1vi

其中 v i v_i vi 是当前速度, γ \\gamma γ 是动量参数,是一个小于 1的正数, η \\eta η 是学习率

相当于每次在进行参数更新的时候,都会将之前的速度考虑进来,每个参数在各方向上的移动幅度不仅取决于当前的梯度,还取决于过去各个梯度在各个方向上是否一致,如果一个梯度一直沿着当前方向进行更新,那么每次更新的幅度就越来越大,如果一个梯度在一个方向上不断变化,那么其更新幅度就会被衰减,这样我们就可以使用一个较大的学习率,使得收敛更快,同时梯度比较大的方向就会因为动量的关系每次更新的幅度减少

比如我们的梯度每次都等于 g,而且方向都相同,那么动量法在该方向上使参数加速移动,有下面的公式:

v 0 = 0 v_0 = 0 v0=0

v 1 = γ v 0 + η g = η g v_1 = \\gamma v_0 + \\eta g = \\eta g v1=γv0+ηg=ηg

v 2 = γ v 1 + η g = ( 1 + γ ) η g v_2 = \\gamma v_1 + \\eta g = (1 + \\gamma) \\eta g v2=γv1+ηg=(1+γ)ηg

v 3 = γ v 2 + η g = ( 1 + γ + γ 2 ) η g v_3 = \\gamma v_2 + \\eta g = (1 + \\gamma + \\gamma^2) \\eta g v3=γv2+ηg=(1+γ+γ2)ηg

⋯ \\cdots

v + ∞ = ( 1 + γ + γ 2 + γ 3 + ⋯   ) η g = 1 1 − γ η g v_{+ \\infty} = (1 + \\gamma + \\gamma^2 + \\gamma^3 + \\cdots) \\eta g = \\frac{1}{1 - \\gamma} \\eta g v+=(1+γ+γ2+γ3+)ηg=1γ1ηg

如果我们把 γ \\gamma γ 定为 0.9,那么更新幅度的峰值就是原本梯度乘学习率的 10 倍。

一般来说, 一般 γ 取值 0.9 左右。

本质上说,动量法就仿佛我们从高坡上推一个球,小球在向下滚动的过程中积累了动量,在途中也会变得越来越快,最后会达到一个峰值,对应于我们的算法中就是,动量项会沿着梯度指向方向相同的方向不断增大,对于梯度方向改变的方向逐渐减小,得到了更快的收敛速度以及更小的震荡。

在这里插入图片描述

但是这种情况相当于小球从山上滚下来时是在盲目地沿着坡滚,如果它能具备一些先知,例如快要上坡时,就知道需要减速了的话,适应性会更好。所以我们就提出了Nesterov Accelerated Gradient。

Nesterov Accelerated Gradient

Nesterov Accelerated Gradient(NAG)是一种为我们的动量提供这种先见之明的方法。我们知道我们会用 γ v t − 1 γv{t-1} γvt1去更更新我们的参数 θ。因此,在我们计算 θ − γ v t − 1 θ - γv{t-1} θγvt1为我们提供了参数下一个位置的近似值,就可以通过计算梯度来有效的向前看。

简单来说在计算梯度时,不是在当前位置,而是未来的位置上

img

一般来说, 一般 γ 还是取值 0.9 左右。

img

蓝色是 Momentum 的过程,会先计算当前的梯度,然后在更新后的累积梯度后会有一个大的跳跃。
而 NAG 会先在前一步的累积梯度上(brown vector)有一个大的跳跃,然后衡量一下梯度做一下修正(red vector),这种预期的更新可以避免我们走的太快。

NAG 可以使 RNN 在很多任务上有更好的表现。

目前为止,我们可以做到,在更新梯度时顺应 loss function 的梯度来调整速度,并且对 SGD 进行加速

代码从0实现

我们还是利用minst的数据

import numpy as np
import torch
from torchvision.datasets import MNIST # 导入 pytorch 内置的 mnist 数据
from torch.utils.data import DataLoader
from torch import nn
from torch.autograd import Variable
import time
import matplotlib.pyplot as plt
%matplotlib inline

def data_tf(x):
    x = np.array(x, dtype='float32') / 255
    x = (x - 0.5) / 0.5 # 标准化,这个技巧之后会讲到
    x = x.reshape((-1,)) # 拉平
    x = torch.from_numpy(x)
    return x

train_set = MNIST('./data', train=True, transform=data_tf, download=True) # 载入数据集,申明定义的数据变换
test_set = MNIST('./data', train=False, transform=data_tf, download=True)

# 定义 loss 函数
criterion = nn.CrossEntropyLoss()

公式上面已经给了,现在手动实现一下动量法

def sgd_momentum(parameters, vs, lr, gamma):
    for param, v in zip(parameters, vs):
        v[:] = gamma * v + lr * param.grad.data
        param.data = param.data - v
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
# 使用 Sequential 定义 3 层神经网络
net = nn.Sequential(
    nn.Linear(784, 200),
    nn.ReLU(),
    nn.Linear(200, 10),
)

# 将速度初始化为和参数形状相同的零张量
vs = []
for param in net.parameters():
    vs.append(torch.zeros_like(param.data))
    
# 开始训练
losses = []
idx = 0
start = time.time() # 记时开始
for e in range(5):
    train_loss = 0
    for im, label in train_data:
        im = Variable(im)
        label = Variable(label)
        # 前向传播
        out = net(im)
        loss = criterion(out, label)
        # 反向传播
        net.zero_grad()
        loss.backward()
        sgd_momentum(net.parameters(), vs, 1e-2, 0.9) # 使用的动量参数为 0.9,学习率 0.01
        
        train_loss += loss.data
        # 记录误差
        if idx % 30 == 0: # 30 步记录一次
            losses.append(loss.data)
        idx += 1
    print('epoch: {}, Train Loss: {:.6f}'
          .format(e, train_loss / len(train_data)))
end = time.time() # 计时结束
print('使用时间: {:.5f} s'.format(end - start))
epoch: 0, Train Loss: 0.369268
epoch: 1, Train Loss: 0.179249
epoch: 2, Train Loss: 0.127735
epoch: 3, Train Loss: 0.102495
epoch: 4, Train Loss: 0.086471
使用时间: 30.78685 s
x_axis = np.linspace(0, 5, len(losses), endpoint=True)
plt.semilogy(x_axis, losses, label='momentum: 0.9')
plt.legend(loc='best')

在这里插入图片描述

可以看到,加完动量之后 loss 能下降非常快,但是一定要小心学习率和动量参数,这两个值会直接影响到参数每次更新的幅度,所以可以多试几个值

pytorch 内置优化器

当然,pytorch 内置了动量法的实现,非常简单,直接在 torch.optim.SGD(momentum=0.9) 即可,下面实现一下

train_data = DataLoader(train_set, batch_size=64, shuffle=True)
# 使用 Sequential 定义 3 层神经网络
net = nn.Sequential(
    nn.Linear(784, 200),
    nn.ReLU(),
    nn.Linear(200, 10),
)

optimizer = torch.optim.SGD(net.parameters(), lr=1e-2, momentum=0.9) # 加动量
# 开始训练
losses = []
idx = 0
start = time.time() # 记时开始
for e in range(5):
    train_loss = 0
    for im, label in train_data:
        im = Variable(im)
        label = Variable(label)
        # 前向传播
        out = net(im)
        loss = criterion(out, label)
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        # 记录误差
        train_loss += loss.data
        if idx % 30 == 0: # 30 步记录一次
            losses.append(loss.data)
        idx += 1
    print('epoch: {}, Train Loss: {:.6f}'
          .format(e, train_loss / len(train_data)))
end = time.time() # 计时结束
print('使用时间: {:.5f} s'.format(end - start))
epoch: 0, Train Loss: 0.368348
epoch: 1, Train Loss: 0.172883
epoch: 2, Train Loss: 0.126296
epoch: 3, Train Loss: 0.100889
epoch: 4, Train Loss: 0.085021
使用时间: 27.03662 s
x_axis = np.linspace(0, 5, len(losses), endpoint=True)
plt.semilogy(x_axis, losses, label='momentum: 0.9')
plt.legend(loc='best')

在这里插入图片描述

对比 动量 + 不加动量 的 SGD

我们可以对比一下不加动量的随机梯度下降法

# 使用 Sequential 定义 3 层神经网络
net = nn.Sequential(
    nn.Linear(784, 200),
    nn.ReLU(),
    nn.Linear(200, 10),
)

optimizer = torch.optim.SGD(net.parameters(), lr=1e-2) # 不加动量
# 开始训练
losses1 = []
idx = 0
start = time.time() # 记时开始
for e in range(5):
    train_loss = 0
    for im, label in train_data:
        im = Variable(im)
        label = Variable(label)
        # 前向传播
        out pytorch常用优化器总结(包括warmup介绍及代码实现)

pytorch常用优化器总结(包括warmup介绍及代码实现)

pytorch常用优化器总结(包括warmup介绍及代码实现)

Pytorch中常见的优化算法介绍

优化算法之间的关系及各自特点的简单分析

Pytorch优化器全总结SGDASGDRpropAdagrad