HBU_神经网络与深度学习 实验14 网络优化与正则化:不同优化算法的比较分析


不同优化算法的比较分析

除了批大小对模型收敛速度的影响外,学习率和梯度估计也是影响神经网络优化的重要因素。神经网络优化中常用的优化方法也主要是如下两方面的改进,包括:

  • 学习率调整:主要通过自适应地调整学习率使得优化更稳定。这类算法主要有AdaGrad、RMSprop、AdaDelta算法等。
  • 梯度估计修正:主要通过修正每次迭代时估计的梯度方向来加快收敛速度。这类算法主要有动量法、Nesterov加速梯度方法等。

除上述方法外,本节还会介绍综合学习率调整和梯度估计修正的优化算法,如Adam算法。


1. 优化算法的实验设定

为了更好地对比不同的优化算法,我们准备两个实验:第一个是2D可视化实验。第二个是简单拟合实验。

首先介绍下这两个实验的任务设定。

(1) 2D可视化实验

为了更好地展示不同优化算法的能力对比,我们选择一个二维空间中的凸函数,然后用不同的优化算法来寻找最优解,并可视化梯度下降过程的轨迹。

被优化函数 选择Sphere函数作为被优化函数,并对比它们的优化效果。Sphere函数的定义为
s p h e r e ( x ) = ∑ d = 1 D x d 2 = x 2 , \mathrm{sphere}(\bm x) = \sum_{d=1}^{D} x_d^2 = \bm x^2, sphere(x)=d=1Dxd2=x2,其中 x ∈ R D \bm x \in \mathbb{R}^D xRD x 2 \bm x^2 x2表示逐元素平方。Sphere函数有全局的最优点 x ∗ = 0 \bm x^*=0 x=0

这里为了展示方便,我们使用二维的输入并略微修改Sphere函数,定义 s p h e r e ( x ) = w ⊤ x 2 \mathrm{sphere}(\bm x) = \bm w^\top \bm x^2 sphere(x)=wx2,并根据梯度下降公式计算对 x \bm x x的偏导
∂ s p h e r e ( x ) ∂ x = 2 w ⊙ x , \frac{\partial \mathrm{sphere}(\bm x)}{\partial \bm x} = 2 \bm w \odot \bm x, xsphere(x)=2wx,其中 ⊙ \odot 表示逐元素积。

将被优化函数实现为OptimizedFunction算子,其forward方法是Sphere函数的前向计算,backward方法则计算被优化函数对 x \bm x x的偏导。代码实现如下:

import torch
from op import Op

class OptimizedFunction(Op):
    def __init__(self, w):
        super(OptimizedFunction, self).__init__()
        self.w = torch.as_tensor(w, dtype=torch.float32)
        self.params = {'x': torch.as_tensor(0, dtype=torch.float32)}
        self.grads = {'x': torch.as_tensor(0, dtype=torch.float32)}

    def forward(self, x):
        self.params['x'] = x
        return torch.matmul(self.w.T, torch.tensor(torch.square(self.params['x']), dtype=torch.float32))

    def backward(self):
        self.grads['x'] = 2 * torch.multiply(self.w.T, self.params['x'])

小批量梯度下降优化器 复用3.1.4.3节定义的梯度下降优化器SimpleBatchGD。按照梯度下降的梯度更新公式 θ t ← θ t − 1 − α g t \theta_t \leftarrow \theta_{t-1} - \alpha \mathbf g_t θtθt1αgt进行梯度更新。

训练函数 定义一个简易的训练函数,记录梯度下降过程中每轮的参数 x \bm x x和损失。代码实现如下:

import copy

def train_f(model, optimizer, x_init, epoch):
    """
    训练函数
    输入:
        - model:被优化函数
        - optimizer:优化器
        - x_init:x初始值
        - epoch:训练回合数
    """
    x = x_init
    all_x = []
    losses = []
    for i in range(epoch):
        all_x.append(copy.copy(x.numpy()))
        loss = model(x)
        losses.append(loss)
        model.backward()
        optimizer.step()
        x = model.params['x']
    return torch.as_tensor(all_x), losses

可视化函数 定义一个Visualization类,用于绘制 x \bm x x的更新轨迹。代码实现如下:

import numpy as np
import matplotlib.pyplot as plt

class Visualization(object):
    def __init__(self):
        """
        初始化可视化类
        """
        # 只画出参数x1和x2在区间[-5, 5]的曲线部分
        x1 = np.arange(-5, 5, 0.1)
        x2 = np.arange(-5, 5, 0.1)
        x1, x2 = np.meshgrid(x1, x2)
        self.init_x = torch.as_tensor([x1, x2])

    def plot_2d(self, model, x, fig_name):
        """
        可视化参数更新轨迹
        """
        fig, ax = plt.subplots(figsize=(10, 6))
        cp = ax.contourf(self.init_x[0], self.init_x[1], model(self.init_x.transpose(1, 0)),
                         colors=['#e4007f', '#f19ec2', '#e86096', '#eb7aaa', '#f6c8dc', '#f5f5f5', '#000000'])
        c = ax.contour(self.init_x[0], self.init_x[1], model(self.init_x.transpose(1, 0)), colors='black')
        cbar = fig.colorbar(cp)
        ax.plot(x[:, 0], x[:, 1], '-o', color='#000000')
        ax.plot(0, 'r*', markersize=18, color='#fefefe')

        ax.set_xlabel('$x1$')
        ax.set_ylabel('$x2$')

        ax.set_xlim((-2, 5))
        ax.set_ylim((-2, 5))
        plt.savefig(fig_name)

定义train_and_plot_f函数,调用train_f和Visualization,训练模型并可视化参数更新轨迹。代码实现如下:

def train_and_plot_f(model, optimizer, epoch, fig_name):
    """
    训练模型并可视化参数更新轨迹
    """
    # 设置x的初始值
    x_init = torch.as_tensor([3, 4], dtype=torch.float32)
    print('x1 initiate: {}, x2 initiate: {}'.format(x_init[0].numpy(), x_init[1].numpy()))
    x, losses = train_f(model, optimizer, x_init, epoch)
    losses = np.array(losses)

    # 展示x1、x2的更新轨迹
    vis = Visualization()
    vis.plot_2d(model, x, fig_name)

模型训练与可视化 指定Sphere函数中 w \bm w w的值,实例化被优化函数,通过小批量梯度下降法更新参数,并可视化 x \bm x x的更新轨迹。

from op import SimpleBatchGD

# 固定随机种子
torch.manual_seed(0)
w = torch.as_tensor([0.2, 2])
model = OptimizedFunction(w)
opt = SimpleBatchGD(init_lr=0.2, model=model)
train_and_plot_f(model, opt, epoch=20, fig_name='opti-vis-para.pdf')
x1 initiate: 3.0, x2 initiate: 4.0


输出图中不同颜色代表 f ( x 1 , x 2 ) f(x_1, x_2) f(x1,x2)的值,具体数值可以参考图右侧的对应表,比如深粉色区域代表 f ( x 1 , x 2 ) f(x_1, x_2) f(x1,x2)在0~8之间,不同颜色间黑色的曲线是等值线,代表落在该线上的点对应的 f ( x 1 , x 2 ) f(x_1, x_2) f(x1,x2)的值都相同。

(2) 简单拟合实验

除了2D可视化实验外,我们还设计一个简单的拟合任务,然后对比不同的优化算法。

这里我们随机生成一组数据作为数据样本,再构建一个简单的单层前馈神经网络,用于前向计算。

数据集构建 通过paddle.randn随机生成一些训练数据 X \bm X X,并根据一个预定义函数 y = 0.5 × x 1 + 0.8 × x 2 + 0.01 × n o i s e y = 0.5\times x_{1}+ 0.8\times x_{2} + 0.01\times noise y=0.5×x1+0.8×x2+0.01×noise 计算得到 y \bm y y,再将 X \bm X X y \bm y y拼接起来得到训练样本。代码实现如下:

# 固定随机种子
torch.manual_seed(0)
# 随机生成shape为(1000,2)的训练数据
X = torch.randn([1000, 2])
w = torch.as_tensor([0.5, 0.8])
w = torch.unsqueeze(w, dim=1)
noise = 0.01 * torch.rand([1000])
noise = torch.unsqueeze(noise, dim=1)
# 计算y
y = torch.matmul(X, w) + noise
# 打印X, y样本
print('X: ', X[0].numpy())
print('y: ', y[0].numpy())

# X,y组成训练样本数据
data = torch.cat((X, y), dim=1)
print('input data shape: ', data.shape)
print('data: ', data[0].numpy())
X:  [-1.1258398 -1.1523602]
y:  [-1.4770346]
input data shape:  torch.Size([1000, 3])
data:  [-1.1258398 -1.1523602 -1.4770346]

模型构建 定义单层前馈神经网络, X ∈ R N × D \bm X \in \mathbb{R}^{N \times D} XRN×D为网络输入, w ∈ R D \bm w \in \mathbb{R}^{D} wRD是网络的权重矩阵, b ∈ R \bm b \in \mathbb{R} bR为偏置。
y = X w + b ∈ R K × 1 , \bm y =\bm X \bm w + b \in \mathbb{R}^{K\times 1}, y=Xw+bRK×1,其中 K K K代表一个批次中的样本数量, D D D为单层网络的输入特征维度。

损失函数 使用均方误差作为训练时的损失函数,计算损失函数关于参数 w \bm w w b b b的偏导数。定义均方误差损失函数的计算方法为
L = 1 2 K ∑ k = 1 K ( y ( k ) − z ( k ) ) 2 , \mathcal{L} = \frac{1}{2K}\sum_{k=1}^K(\bm y^{(k)} - \bm z^{(k)})^2, L=2K1k=1K(y(k)z(k))2,其中 z ( k ) \bm z^{(k)} z(k)是网络对第 k k k个样本的预测值。根据损失函数关于参数的偏导公式,得到 L ( ⋅ ) \mathcal{L}(\cdot) L()对于参数 w \bm w w b b b的偏导数,
∂ L ∂ w = 1 K ∑ k = 1 K x ( k ) ( z ( k ) − y ( k ) ) = 1 K X ⊤ ( z − y ) , ∂ L ∂ b = 1 K ∑ k = 1 K ( z ( k ) − y ( k ) ) = 1 K 1 ⊤ ( z − y ) . \frac{\partial \mathcal{L}}{\partial \bm w} = \frac{1}{K}\sum_{k=1}^K\bm x^{(k)}(\bm z^{(k)} - \bm y^{(k)}) = \frac{1}{K}\bm X^\top(\bm z - \bm y), \\ \frac{\partial \mathcal{L}}{\partial b} = \frac{1}{K}\sum_{k=1}^K(\bm z^{(k)} - \bm y^{(k)}) = \frac{1}{K}\mathbf{1}^\top(\bm z - \bm y). wL=K1k=1Kx(k)(z(k)y(k))=K1X(zy),bL=K1k=1K(z(k)y(k))=K11(zy).定义Linear算子,实现一个线性层的前向和反向计算。代码实现如下:

class Linear(Op):
    def __init__(self, input_size, weight_init=np.random.standard_normal, bias_init=torch.zeros):
        self.params = {}
        self.params['W'] = weight_init([input_size, 1])
        self.params['W'] = torch.as_tensor(self.params['W'], dtype=torch.float32)
        self.params['b'] = bias_init([1])

        self.inputs = None
        self.grads = {}

    def forward(self, inputs):
        self.inputs = inputs
        self.outputs = torch.matmul(self.inputs, self.params['W']) + self.params['b']
        return self.outputs

    def backward(self, labels):
        K = self.inputs.shape[0]
        self.grads['W'] = 1. / K * torch.matmul(self.inputs.T, (self.outputs - labels))
        self.grads['b'] = 1. / K * torch.sum(self.outputs - labels, dim=0)

笔记
这里backward函数中实现的梯度并不是forward函数对应的梯度,而是最终损失关于参数的梯度.由于这里的梯度是手动计算的,所以直接给出了最终的梯度。

训练函数 在准备好样本数据和网络以后,复用优化器SimpleBatchGD类,使用小批量梯度下降来进行简单的拟合实验。

这里我们重新定义模型训练train函数。主要以下两点原因:

  • 在一般的随机梯度下降中要在每回合迭代开始之前随机打乱训练数据的顺序,再按批大小进行分组。这里为了保证每次运行结果一致以便更好地对比不同的优化算法,这里不再随机打乱数据。
  • 与RunnerV2中的训练函数相比,这里使用小批量梯度下降。而与RunnerV3中的训练函数相比,又通过继承优化器基类Optimizer实现不同的优化器。

模型训练train函数的代码实现如下:

def train(data, num_epochs, batch_size, model, calculate_loss, optimizer, verbose=False):
    """
    训练神经网络
    输入:
        - data:训练样本
        - num_epochs:训练回合数
        - batch_size:批大小
        - model:实例化的模型
        - calculate_loss:损失函数
        - optimizer:优化器
        - verbose:日志显示,默认为False
    输出:
        - iter_loss:每一次迭代的损失值
        - epoch_loss:每个回合的平均损失值
    """
    # 记录每个回合损失的变化
    epoch_loss = []
    # 记录每次迭代损失的变化
    iter_loss = []
    N = len(data)
    for epoch_id in range(num_epochs):
        # np.random.shuffle(data) #不再随机打乱数据
        # 将训练数据进行拆分,每个mini_batch包含batch_size条的数据
        mini_batches = [data[i:i + batch_size] for i in range(0, N, batch_size)]
        for iter_id, mini_batch in enumerate(mini_batches):
            # data中前两个分量为X
            inputs = mini_batch[:, :-1]
            # data中最后一个分量为y
            labels = mini_batch[:, -1:]
            # 前向计算
            outputs = model(inputs)
            # 计算损失
            loss = calculate_loss(outputs, labels).numpy()
            # 计算梯度
            model.backward(labels)
            # 梯度更新
            optimizer.step()
            iter_loss.append(loss)
        # verbose = True 则打印当前回合的损失
        if verbose:
            print('Epoch {:3d}, loss = {:.4f}'.format(epoch_id, np.mean(iter_loss)))
        epoch_loss.append(np.mean(iter_loss))
    return iter_loss, epoch_loss

优化过程可视化 定义plot_loss函数,用于绘制损失函数变化趋势。代码实现如下:

def plot_loss(iter_loss, epoch_loss, fig_name):
    """
    可视化损失函数的变化趋势
    """
    plt.figure(figsize=(10, 4))
    ax1 = plt.subplot(121)
    ax1.plot(iter_loss, color='#e4007f')
    plt.title('iteration loss')
    ax2 = plt.subplot(122)
    ax2.plot(epoch_loss, color='#f19ec2')
    plt.title('epoch loss')
    plt.savefig(fig_name)
    plt.show()

对于使用不同优化器的模型训练,保存每一个回合损失的更新情况,并绘制出损失函数的变化趋势,以此验证模型是否收敛。定义train_and_plot函数,调用train和plot_loss函数,训练并展示每个回合和每次迭代(Iteration)的损失变化情况。在模型训练时,使用paddle.nn.MSELoss()计算均方误差。代码实现如下:

import torch.nn as nn

def train_and_plot(optimizer, fig_name):
    """
    训练网络并画出损失函数的变化趋势
    输入:
        - optimizer:优化器
    """
    # 定义均方差损失
    mse = nn.MSELoss()
    iter_loss, epoch_loss = train(data, num_epochs=30, batch_size=64, model=model, calculate_loss=mse,
                                  optimizer=optimizer)
    plot_loss(iter_loss, epoch_loss, fig_name)

训练网络并可视化损失函数的变化趋势。代码实现如下:

# 固定随机种子
torch.manual_seed(0)
# 定义网络结构
model = Linear(2)
# 定义优化器
opt = SimpleBatchGD(init_lr=0.01, model=model)
train_and_plot(opt, 'opti-loss.pdf')


从输出结果看,loss在不断减小,模型逐渐收敛。

提醒
在本小节中,我们定义了两个实验:2D可视化实验和简单拟合实验。这两个实验会在本节介绍的所有优化算法中反复使用,以便进行对比。

与Paddle API对比,验证正确性

分别实例化自定义SimpleBatchGD优化器和调用paddle.optimizer.SGD API, 验证自定义优化器的正确性。

torch.manual_seed(0)
model = Linear(2)
opt = SimpleBatchGD(init_lr=0.01, model=model)

x = data[0, :-1].unsqueeze(0)
y = data[0, -1].unsqueeze(0)

model1 = Linear(2)
print('model1 parameter W: ', model1.params['W'].numpy())
opt1 = SimpleBatchGD(init_lr=0.01, model=model1)
output1 = model1(x)

model2 = nn.Linear(2, 1)
model2.weight = torch.nn.Parameter(model1.params['W'])
print('model2 parameter W: ', model2.state_dict()['weight'].numpy())
output2 = model2(x.T)

model1.backward(y)
opt1.step()
print('model1 parameter W after train step: ', model1.params['W'].numpy())

opt2 = torch.optim.SGD(lr=0.01, params=model2.parameters())
loss = torch.nn.functional.mse_loss(output2, y) / 2
loss.backward()
opt2.step()
opt2.zero_grad()
print('model2 parameter W after train step: ', model2.state_dict()['weight'].numpy())
model1 parameter W:  [[-0.76494044]
 [ 0.8000999 ]]
model2 parameter W:  [[-0.76494044]
 [ 0.8000999 ]]
model1 parameter W after train step:  [[-0.74899596]
 [ 0.81642   ]]
model2 parameter W after train step:  [[-0.75487924]
 [ 0.80000615]]

从输出结果看,在一次梯度更新后,两个模型的参数值保持一致,证明优化器实现正确。


2. 学习率调整

学习率是神经网络优化时的重要超参数。在梯度下降法中,学习率 α \alpha α的取值非常关键,如果取值过大就不会收敛,如果过小则收敛速度太慢。

常用的学习率调整方法包括如下几种方法:

  • 学习率衰减:如分段常数衰减(Piecewise Constant Decay)、余弦衰减(Cosine Decay)等;
  • 学习率预热:如逐渐预热(Gradual Warmup) 等;
  • 周期性学习率调整:如循环学习率等;
  • 自适应调整学习率的方法:如AdaGrad、RMSprop、AdaDelta等。自适应学习率方法可以针对每个参数设置不同的学习率。

下面我们来详细介绍AdaGrad和RMSprop算法。

(1) AdaGrad算法

AdaGrad算法(Adaptive Gradient Algorithm,自适应梯度算法)是借鉴 ℓ 2 \ell_2 2 正则化的思想,每次迭代时自适应地调整每个参数的学习率。在第 t t t次迭代时,先计算每个参数梯度平方的累计值。
G t = ∑ τ = 1 t g τ ⊙ g τ , G_t = \sum^t_{\tau=1} \mathbf g_{\tau} \odot \mathbf g_{\tau}, Gt=τ=1tgτgτ,其中 ⊙ \odot 为按元素乘积, g τ ∈ R ∣ θ ∣ \mathbf g_{\tau} \in \mathbb R^{\mid \theta \mid} gτRθ是第 τ \tau τ次迭代时的梯度。
Δ θ t = − α G t + ϵ ⊙ g t , \Delta \theta_t = - \frac{\alpha}{\sqrt{G_t + \epsilon}} \odot \mathbf g_{t}, Δθt=Gt+ϵ αgt,其中 α \alpha α是初始的学习率, ϵ \epsilon ϵ是为了保持数值稳定性而设置的非常小的常数,一般取值 e − 7 e^{−7} e7 e − 10 e^{−10} e10。此外,这里的开平方、除、加运算都是按元素进行的操作。

构建优化器 定义Adagrad类,继承Optimizer类。定义step函数调用adagrad进行参数更新。代码实现如下:

from op import Optimizer

class Adagrad(Optimizer):
    def __init__(self, init_lr, model, epsilon):
        """
        Adagrad 优化器初始化
        输入:
            - init_lr: 初始学习率
            - model:模型,model.params存储模型参数值
            - epsilon:保持数值稳定性而设置的非常小的常数
        """
        super(Adagrad, self).__init__(init_lr=init_lr, model=model)
        self.G = {}
        for key in self.model.params.keys():
            self.G[key] = 0
        self.epsilon = epsilon

    def adagrad(self, x, gradient_x, G, init_lr):
        """
        adagrad算法更新参数,G为参数梯度平方的累计值。
        """
        G += gradient_x ** 2
        x -= init_lr / torch.sqrt(G + self.epsilon) * gradient_x
        return x, G

    def step(self):
        """
        参数更新
        """
        for key in self.model.params.keys():
            self.model.params[key], self.G[key] = self.adagrad(self.model.params[key],
                                                               self.model.grads[key],
                                                               self.G[key],
                                                               self.init_lr)

2D可视化实验 使用被优化函数展示Adagrad算法的参数更新轨迹。代码实现如下:

# 固定随机种子
torch.manual_seed(0)
w = torch.as_tensor([0.2, 2])
model2 = OptimizedFunction(w)
opt2 = Adagrad(init_lr=0.5, model=model2, epsilon=1e-7)
train_and_plot_f(model2, opt2, epoch=50, fig_name='opti-vis-para2.pdf')
x1 initiate: 3.0, x2 initiate: 4.0


从输出结果看,AdaGrad算法在前几个回合更新时参数更新幅度较大,随着回合数增加,学习率逐渐缩小,参数更新幅度逐渐缩小。在AdaGrad算法中,如果某个参数的偏导数累积比较大,其学习率相对较小。相反,如果其偏导数累积较小,其学习率相对较大。但整体随着迭代次数的增加,学习率逐渐缩小。该算法的缺点是在经过一定次数的迭代依然没有找到最优点时,由于这时的学习率已经非常小,很难再继续找到最优点。

简单拟合实验 训练单层线性网络,验证损失是否收敛。代码实现如下:

# 固定随机种子
torch.manual_seed(0)
# 定义网络结构
model = Linear(2)
# 定义优化器
opt = Adagrad(init_lr=0.1, model=model, epsilon=1e-7)
train_and_plot(opt, 'opti-loss2.pdf')

(2) RMSprop算法

RMSprop算法是一种自适应学习率的方法,可以在有些情况下避免AdaGrad算法中学习率不断单调下降以至于过早衰减的缺点。

RMSprop算法首先计算每次迭代梯度平方 g t 2 \mathbf g_{t}^{2} gt2的加权移动平均
G t = β G t − 1 + ( 1 − β ) g t ⊙ g t , G_t = \beta G_{t-1} + (1 - \beta) \mathbf g_t \odot \mathbf g_t, Gt=βGt1+(1β)gtgt,其中 β \beta β为衰减率,一般取值为0.9。

RMSprop算法的参数更新差值为:
Δ θ t = − α G t + ϵ ⊙ g t , \Delta \theta_t = - \frac{\alpha}{\sqrt{G_t + \epsilon}} \odot \mathbf g_t, Δθt=Gt+ϵ αgt,其中 α \alpha α是初始的学习率,比如0.001。RMSprop算法和AdaGrad算法的区别在于RMSprop算法中 G t G_t Gt的计算由累积方式变成了加权移动平均。在迭代过程中,每个参数的学习率并不是呈衰减趋势,既可以变小也可以变大。

构建优化器 定义RMSprop类,继承Optimizer类。定义step函数调用rmsprop更新参数。代码实现如下:

class RMSprop(Optimizer):
    def __init__(self, init_lr, model, beta, epsilon):
        """
        RMSprop优化器初始化
        输入:
            - init_lr:初始学习率
            - model:模型,model.params存储模型参数值
            - beta:衰减率
            - epsilon:保持数值稳定性而设置的常数
        """
        super(RMSprop, self).__init__(init_lr=init_lr, model=model)
        self.G = {}
        for key in self.model.params.keys():
            self.G[key] = 0
        self.beta = beta
        self.epsilon = epsilon

    def rmsprop(self, x, gradient_x, G, init_lr):
        """
        rmsprop算法更新参数,G为迭代梯度平方的加权移动平均
        """
        G = self.beta * G + (1 - self.beta) * gradient_x ** 2
        x -= init_lr / torch.sqrt(G + self.epsilon) * gradient_x
        return x, G

    def step(self):
        """参数更新"""
        for key in self.model.params.keys():
            self.model.params[key], self.G[key] = self.rmsprop(self.model.params[key],
                                                               self.model.grads[key],
                                                               self.G[key],
                                                               self.init_lr)

2D可视化实验 使用被优化函数展示RMSprop算法的参数更新轨迹。代码实现如下:

# 固定随机种子
paddle.seed(0)
w = paddle.to_tensor([0.2, 2])
model = OptimizedFunction(w)
opt = RMSprop(init_lr=0.1, model=model, beta=0.9, epsilon=1e-7)
train_and_plot_f(model, opt, epoch=50, fig_name='opti-vis-para3.pdf')
x1 initiate: 3.0, x2 initiate: 4.0


简单拟合实验 训练单层线性网络,进行简单的拟合实验。代码实现如下:

# 固定随机种子
torch.manual_seed(0)
# 定义网络结构
model = Linear(2)
# 定义优化器
opt3 = RMSprop(init_lr=0.1, model=model, beta=0.9, epsilon=1e-7)
train_and_plot(opt3, 'opti-loss3.pdf')


3. 梯度估计修正

除了调整学习率之外,还可以进行梯度估计修正。在小批量梯度下降法中,由于每次迭代的样本具有一定的随机性,因此每次迭代的梯度估计和整个训练集上的最优梯度并不一致。如果每次选取样本数量比较小,损失会呈振荡的方式下降。

一种有效地缓解梯度估计随机性的方式是通过使用最近一段时间内的平均梯度来代替当前时刻的随机梯度来作为参数更新的方向,从而提高优化速度。

(1) 动量法

动量法(Momentum Method)是用之前积累动量来替代真正的梯度。每次迭代的梯度可以看作加速度。

在第 t t t次迭代时,计算负梯度的“加权移动平均”作为参数的更新方向,
Δ θ t = ρ Δ θ t − 1 − α g t = − α ∑ τ = 1 t ρ t − τ g τ , \Delta \theta_t = \rho \Delta \theta_{t-1} - \alpha \mathbf g_t = - \alpha \sum_{\tau=1}^t\rho^{t - \tau} \mathbf g_{\tau}, Δθt=ρΔθt1αgt=ατ=1tρtτgτ,其中 ρ \rho ρ为动量因子,通常设为0.9, α \alpha α为学习率。

这样,每个参数的实际更新差值取决于最近一段时间内梯度的加权平均值。当某个参数在最近一段时间内的梯度方向不一致时,其真实的参数更新幅度变小。相反,当某个参数在最近一段时间内的梯度方向都一致时,其真实的参数更新幅度变大,起到加速作用。一般而言,在迭代初期,梯度方向都比较一致,动量法会起到加速作用,可以更快地到达最优点。在迭代后期,梯度方向会不一致,在收敛值附近振荡,动量法会起到减速作用,增加稳定性。从某种角度来说,当前梯度叠加上部分的上次梯度,一定程度上可以近似看作二阶梯度。

构建优化器 定义Momentum类,继承Optimizer类。定义step函数调用momentum进行参数更新。代码实现如下:

class Momentum(Optimizer):
    def __init__(self, init_lr, model, rho):
        """
        Momentum优化器初始化
        输入:
            - init_lr:初始学习率
            - model:模型,model.params存储模型参数值
            - rho:动量因子
        """
        super(Momentum, self).__init__(init_lr=init_lr, model=model)
        self.delta_x = {}
        for key in self.model.params.keys():
            self.delta_x[key] = 0
        self.rho = rho

    def momentum(self, x, gradient_x, delta_x, init_lr):
        """
        momentum算法更新参数,delta_x为梯度的加权移动平均
        """
        delta_x = self.rho * delta_x - init_lr * gradient_x
        x += delta_x
        return x, delta_x

    def step(self):
        """参数更新"""
        for key in self.model.params.keys():
            self.model.params[key], self.delta_x[key] = self.momentum(self.model.params[key],
                                                                      self.model.grads[key],
                                                                      self.delta_x[key],
                                                                      self.init_lr)

2D可视化实验 使用被优化函数展示Momentum算法的参数更新轨迹。

# 固定随机种子
torch.manual_seed(0)
w = torch.as_tensor([0.2, 2])
model4 = OptimizedFunction(w)
opt4 = Momentum(init_lr=0.01, model=model4, rho=0.9)
train_and_plot_f(model4, opt4, epoch=50, fig_name='opti-vis-para4.pdf')
x1 initiate: 3.0, x2 initiate: 4.0


从输出结果看,在模型训练初期,梯度方向比较一致,参数更新幅度逐渐增大,起加速作用;在迭代后期,参数更新幅度减小,在收敛值附近振荡。

简单拟合实验 训练单层线性网络,进行简单的拟合实验。代码实现如下:

# 固定随机种子
torch.manual_seed(0)

# 定义网络结构
model = Linear(2)
# 定义优化器
opt = Momentum(init_lr=0.01, model=model, rho=0.9)
train_and_plot(opt, 'opti-loss4.pdf')

(2) Adam算法

Adam算法(Adaptive Moment Estimation Algorithm,自适应矩估计算法)可以看作动量法和RMSprop算法的结合,不但使用动量作为参数更新方向,而且可以自适应调整学习率。

Adam算法一方面计算梯度平方 g t 2 \mathbf g_t^2 gt2的加权移动平均(和RMSprop算法类似),另一方面计算梯度 g t \mathbf g_t gt的加权移动平均(和动量法类似)。
M t = β 1 M t − 1 + ( 1 − β 1 ) g t , G t = β 2 G t − 1 + ( 1 − β 2 ) g t ⊙ g t , M_t = \beta_1 M_{t-1} + (1 - \beta_1)\mathbf g_t, \\ G_t = \beta_2 G_{t-1} + (1 - \beta_2)\mathbf g_t \odot \mathbf g_t, Mt=β1Mt1+(1β1)gt,Gt=β2Gt1+(1β2)gtgt,其中 β 1 \beta_1 β1 β 2 \beta_2 β2分别为两个移动平均的衰减率,通常取值为 β 1 = 0.9 , β 2 = 0.99 \beta_1 = 0.9, \beta_2 = 0.99 β1=0.9,β2=0.99。我们可以把 M t M_t Mt G t G_t Gt分别看作梯度的均值(一阶矩)和未减去均值的方差(二阶矩)。

假设 M 0 = 0 , G 0 = 0 M_0 = 0, G_0 = 0 M0=0,G0=0,那么在迭代初期 M t M_t Mt G t G_t Gt的值会比真实的均值和方差要小。特别是当 β 1 \beta_1 β1 β 2 \beta_2 β2都接近于1时,偏差会很大。因此,需要对偏差进行修正。
M ^ t = M t 1 − β 1 t , G ^ t = G t 1 − β 2 t 。 \hat M_t = \frac{M_t}{1 - \beta^t_1}, \\ \hat G_t = \frac{G_t}{1 - \beta^t_2}。 M^t=1β1tMt,G^t=1β2tGtAdam算法的参数更新差值为
Δ θ t = − α G ^ t + ϵ M ^ t , \Delta \theta_t = - \frac{\alpha}{\sqrt{\hat G_t + \epsilon}}\hat M_t, Δθt=G^t+ϵ αM^t,其中学习率 α \alpha α通常设为0.001,并且也可以进行衰减,比如 a t = a 0 t a_t = \frac{a_0}{\sqrt{t}} at=t a0

构建优化器 定义Adam类,继承Optimizer类。定义step函数调用adam函数更新参数。代码实现如下:

class Adam(Optimizer):
    def __init__(self, init_lr, model, beta1, beta2, epsilon):
        """
        Adam优化器初始化
        输入:
            - init_lr:初始学习率
            - model:模型,model.params存储模型参数值
            - beta1, beta2:移动平均的衰减率
            - epsilon:保持数值稳定性而设置的常数
        """
        super(Adam, self).__init__(init_lr=init_lr, model=model)
        self.beta1 = beta1
        self.beta2 = beta2
        self.epsilon = epsilon
        self.M, self.G = {}, {}
        for key in self.model.params.keys():
            self.M[key] = 0
            self.G[key] = 0
        self.t = 1

    def adam(self, x, gradient_x, G, M, t, init_lr):
        """
        adam算法更新参数
        输入:
            - x:参数
            - G:梯度平方的加权移动平均
            - M:梯度的加权移动平均
            - t:迭代次数
            - init_lr:初始学习率
        """
        M = self.beta1 * M + (1 - self.beta1) * gradient_x
        G = self.beta2 * G + (1 - self.beta2) * gradient_x ** 2
        M_hat = M / (1 - self.beta1 ** t)
        G_hat = G / (1 - self.beta2 ** t)
        t += 1
        x -= init_lr / torch.sqrt(G_hat + self.epsilon) * M_hat
        return x, G, M, t

    def step(self):
        """参数更新"""
        for key in self.model.params.keys():
            self.model.params[key], self.G[key], self.M[key], self.t = self.adam(self.model.params[key],
                                                                                 self.model.grads[key],
                                                                                 self.G[key],
                                                                                 self.M[key],
                                                                                 self.t,
                                                                                 self.init_lr)

2D可视化实验 使用被优化函数展示Adam算法的参数更新轨迹。代码实现如下:

# 固定随机种子
torch.manual_seed(0)
w = torch.as_tensor([0.2, 2])
model5 = OptimizedFunction(w)
opt5 = Adam(init_lr=0.2, model=model5, beta1=0.9, beta2=0.99, epsilon=1e-7)
train_and_plot_f(model5, opt5, epoch=20, fig_name='opti-vis-para5.pdf')
x1 initiate: 3.0, x2 initiate: 4.0


从输出结果看,Adam算法可以自适应调整学习率,参数更新更加平稳。

简单拟合实验 训练单层线性网络,进行简单的拟合实验。代码实现如下:

# 固定随机种子
torch.manual_seed(0)
# 定义网络结构
model = Linear(2)
# 定义优化器
opt5 = Adam(init_lr=0.1, model=model5, beta1=0.9, beta2=0.99, epsilon=1e-7)
train_and_plot(opt5, 'opti-loss5.pdf')


4. 不同优化器的3D可视化对比

(1) 构建一个三维空间中的被优化函数

定义OptimizedFunction3D算子,表示被优化函数 f ( x ) = x [ 0 ] 2 + x [ 1 ] 2 + x [ 1 ] 3 + x [ 0 ] ∗ x [ 1 ] f(\bm x) = \bm x[0]^2 + \bm x[1]^2 + \bm x[1]^3 + \bm x[0]*\bm x[1] f(x)=x[0]2+x[1]2+x[1]3+x[0]x[1],其中 x [ 0 ] \bm x[0] x[0], x [ 1 ] \bm x[1] x[1]代表两个参数。该函数在(0,0)处存在鞍点,即一个既不是极大值点也不是极小值点的临界点。希望训练过程中,优化算法可以使参数离开鞍点,向模型最优解收敛。代码实现如下:

class OptimizedFunction3D(Op):
    def __init__(self):
        super(OptimizedFunction3D, self).__init__()
        self.params = {'x': 0}
        self.grads = {'x': 0}

    def forward(self, x):
        self.params['x'] = x
        return x[0] ** 2 + x[1] ** 2 + x[1] ** 3 + x[0] * x[1]

    def backward(self):
        x = self.params['x']
        gradient1 = 2 * x[0] + x[1]
        gradient2 = 2 * x[1] + 3 * x[1] ** 2 + x[0]
        grad1 = torch.Tensor([gradient1])
        grad2 = torch.Tensor([gradient2])
        self.grads['x'] = torch.cat([grad1, grad2])

对于相同的被优化函数,分别使用不同的优化器进行参数更新,并保存不同优化器下参数更新的值,用于可视化。代码实现如下:

# 构建5个模型,分别配备不同的优化器
model1 = OptimizedFunction3D()
opt_gd = SimpleBatchGD(init_lr=0.01, model=model1)

model2 = OptimizedFunction3D()
opt_adagrad = Adagrad(init_lr=0.5, model=model2, epsilon=1e-7)

model3 = OptimizedFunction3D()
opt_rmsprop = RMSprop(init_lr=0.1, model=model3, beta=0.9, epsilon=1e-7)

model4 = OptimizedFunction3D()
opt_momentum = Momentum(init_lr=0.01, model=model4, rho=0.9)

model5 = OptimizedFunction3D()
opt_adam = Adam(init_lr=0.1, model=model5, beta1=0.9, beta2=0.99, epsilon=1e-7)

models = [model1, model2, model3, model4, model5]
opts = [opt_gd, opt_adagrad, opt_rmsprop, opt_momentum, opt_adam]

x_all_opts = []
z_all_opts = []
x_init = paddle.to_tensor([2, 3], dtype='float32')
# 使用不同优化器训练
for model, opt in zip(models, opts):
    x_one_opt, z_one_opt = train_f(model, opt, x_init, 150)
    # 保存参数值
    x_all_opts.append(x_one_opt.numpy())
    z_all_opts.append(np.squeeze(z_one_opt))

定义Visualization3D函数,用于可视化三维的参数更新轨迹。

from matplotlib import animation
from itertools import zip_longest

class Visualization3D(animation.FuncAnimation):
    """
    绘制动态图像,可视化参数更新轨迹
    """
    def __init__(self, *xy_values, z_values, labels=[], colors=[], fig, ax, interval=600, blit=True, **kwargs):
        """
        初始化3d可视化类
        输入:
            xy_values:三维中x,y维度的值
            z_values:三维中z维度的值
            labels:每个参数更新轨迹的标签
            colors:每个轨迹的颜色
            interval:帧之间的延迟(以毫秒为单位)
            blit:是否优化绘图
        """
        self.fig = fig
        self.ax = ax
        self.xy_values = xy_values
        self.z_values = z_values
        frames = max(xy_value.shape[0] for xy_value in xy_values)
        self.lines = [ax.plot([], [], [], label=label, color=color, lw=2)[0]
                      for _, label, color in zip_longest(xy_values, labels, colors)]
        super(Visualization3D, self).__init__(fig, self.animate, init_func=self.init_animation, frames=frames,
                                              interval=interval, blit=blit, **kwargs)

    def init_animation(self):
        # 数值初始化
        for line in self.lines:
            line.set_data([], [])
            # line.set_3d_properties(np.asarray([]))  # 源程序中有这一行,加上会报错。 Edit by David 2022.12.4
        return self.lines

    def animate(self, i):
        # 将x,y,z三个数据传入,绘制三维图像
        for line, xy_value, z_value in zip(self.lines, self.xy_values, self.z_values):
            line.set_data(xy_value[:i, 0], xy_value[:i, 1])
            line.set_3d_properties(z_value[:i])
        return self.lines

绘制出被优化函数的三维图像。代码实现如下:

# 使用numpy.meshgrid生成x1,x2矩阵,矩阵的每一行为[-3, 3],以0.1为间隔的数值
x1 = np.arange(-3, 3, 0.1)
x2 = np.arange(-3, 3, 0.1)
x1, x2 = np.meshgrid(x1, x2)
init_x = torch.Tensor(np.array([x1, x2]))
model = OptimizedFunction3D()

# 绘制 f_3d函数 的 三维图像
fig = plt.figure()
ax = plt.axes(projection='3d')
X = init_x[0].numpy()
Y = init_x[1].numpy()
Z = model(init_x).numpy()
ax.plot_surface(X, Y, Z, cmap='rainbow')
ax.set_xlabel('x1')
ax.set_ylabel('x2')
ax.set_zlabel('f(x1,x2)')

可视化不同优化器情况下参数变化轨迹。

labels = ['SGD', 'AdaGrad', 'RMSprop', 'Momentum', 'Adam']
colors = ['#f6373c', '#f6f237', '#45f637', '#37f0f6', '#000000']

animator = Visualization3D(*x_all_opts, z_values=z_all_opts, labels=labels, colors=colors, fig=fig, ax=ax)
ax.legend(loc='upper left')
plt.show()


从输出结果看,对于我们构建的函数,有些优化器如Momentum在参数更新时成功逃离鞍点,其他优化器在本次实验中收敛到鞍点处没有成功逃离。但这并不证明Momentum优化器是最好的优化器,在模型训练时使用哪种优化器,还要结合具体的场景和数据具体分析。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值