【深度学习实验】网络优化与正则化(三):随机梯度下降的改进——Adam算法详解(Adam≈梯度方向优化Momentum+自适应学习率RMSprop)

2024-07-30 09:10:13 浏览数 (1)

一、实验介绍

  深度神经网络在机器学习中应用时面临两类主要问题:优化问题和泛化问题。

  • 优化问题:深度神经网络的优化具有挑战性。
    • 神经网络的损失函数通常是非凸函数,因此找到全局最优解往往困难。
    • 深度神经网络的参数通常非常多,而训练数据也很大,因此使用计算代价较高的二阶优化方法不太可行,而一阶优化方法的训练效率通常较低。
    • 深度神经网络存在梯度消失梯度爆炸问题,导致基于梯度的优化方法经常失效。
  • 泛化问题:由于深度神经网络的复杂度较高且具有强大的拟合能力,很容易在训练集上产生过拟合现象。因此,在训练深度神经网络时需要采用一定的正则化方法来提高网络的泛化能力。

  目前,研究人员通过大量实践总结了一些经验方法,以在神经网络的表示能力、复杂度、学习效率和泛化能力之间取得良好的平衡,从而得到良好的网络模型。本系列文章将从网络优化和网络正则化两个方面来介绍如下方法:

  • 在网络优化方面,常用的方法包括优化算法的选择参数初始化方法数据预处理方法逐层归一化方法超参数优化方法
  • 在网络正则化方面,一些提高网络泛化能力的方法包括ℓ1和ℓ2正则化权重衰减提前停止丢弃法数据增强标签平滑等。

  本文将介绍基于自适应学习率的优化算法:Adam算法详解(Adam≈梯度方向优化Momentum 自适应学习率RMSprop)

二、实验环境

  本系列实验使用了PyTorch深度学习框架,相关操作如下:

1. 配置虚拟环境

代码语言:javascript复制
conda create -n DL python=3.7 
代码语言:javascript复制
conda activate DL
代码语言:javascript复制
pip install torch==1.8.1 cu102 torchvision==0.9.1 cu102 torchaudio==0.8.1 -f https://download.pytorch.org/whl/torch_stable.html
代码语言:javascript复制
conda install matplotlib
代码语言:javascript复制
 conda install scikit-learn

2. 库版本介绍

软件包

本实验版本

目前最新版

matplotlib

3.5.3

3.8.0

numpy

1.21.6

1.26.0

python

3.7.16

scikit-learn

0.22.1

1.3.0

torch

1.8.1 cu102

2.0.1

torchaudio

0.8.1

2.0.2

torchvision

0.9.1 cu102

0.15.2

三、实验内容

0. 导入必要的库

代码语言:javascript复制
import torch
import torch.nn.functional as F
from d2l import torch as d2l
from sklearn.datasets import load_iris
from torch.utils.data import Dataset, DataLoader

1. 随机梯度下降SGD算法

  随机梯度下降(Stochastic Gradient Descent,SGD)是一种常用的优化算法,用于训练深度神经网络。在每次迭代中,SGD通过随机均匀采样一个数据样本的索引,并计算该样本的梯度来更新网络参数。具体而言,SGD的更新步骤如下:

  1. 从训练数据中随机选择一个样本的索引。
  2. 使用选择的样本计算损失函数对于网络参数的梯度。
  3. 根据计算得到的梯度更新网络参数。
  4. 重复以上步骤,直到达到停止条件(如达到固定的迭代次数或损失函数收敛)。
a. PyTorch中的SGD优化器

Pytorch官方教程

代码语言:javascript复制
optimizer = torch.optim.SGD(model.parameters(), lr=0.2)
b. 使用SGD优化器的前馈神经网络

【深度学习实验】前馈神经网络(final):自定义鸢尾花分类前馈神经网络模型并进行训练及评价

2.随机梯度下降的改进方法

  传统的SGD在某些情况下可能存在一些问题,例如学习率选择困难和梯度的不稳定性。为了改进这些问题,提出了一些随机梯度下降的改进方法,其中包括学习率的调整和梯度的优化。

a. 学习率调整
  • 学习率衰减(Learning Rate Decay):随着训练的进行,逐渐降低学习率。常见的学习率衰减方法有固定衰减、按照指数衰减、按照时间表衰减等。
  • Adagrad:自适应地调整学习率。Adagrad根据参数在训练过程中的历史梯度进行调整,对于稀疏梯度较大的参数,降低学习率;对于稀疏梯度较小的参数,增加学习率。这样可以在不同参数上采用不同的学习率,提高收敛速度。
  • Adadelta:与Adagrad类似,但进一步解决了Adagrad学习率递减过快的问题。Adadelta不仅考虑了历史梯度,还引入了一个累积的平方梯度的衰减平均,以动态调整学习率。
  • RMSprop:也是一种自适应学习率的方法,通过使用梯度的指数加权移动平均来调整学习率。RMSprop结合了Adagrad的思想,但使用了衰减平均来减缓学习率的累积效果,从而更加稳定。
b. 梯度估计修正
  • Momentum:使用梯度的“加权移动平均”作为参数的更新方向。Momentum方法引入了一个动量项,用于加速梯度下降的过程。通过积累之前的梯度信息,可以在更新参数时保持一定的惯性,有助于跳出局部最优解、加快收敛速度。
  • Nesterov accelerated gradient:Nesterov加速梯度(NAG)是Momentum的一种变体。与Momentum不同的是,NAG会先根据当前的梯度估计出一个未来位置,然后在该位置计算梯度。这样可以更准确地估计当前位置的梯度,并且在参数更新时更加稳定。
  • 梯度截断(Gradient Clipping):为了应对梯度爆炸或梯度消失的问题,梯度截断的方法被提出。梯度截断通过限制梯度的范围,将梯度控制在一个合理的范围内。常见的梯度截断方法有阈值截断和梯度缩放。

3. 梯度估计修正:动量法Momentum

【深度学习实验】网络优化与正则化(一):优化算法:使用动量优化的随机梯度下降算法(Stochastic Gradient Descent with Momentum)

代码语言:javascript复制
def init_momentum_states(feature_dim):
    v_w = torch.zeros((feature_dim, 3))
    v_b = torch.zeros(3)
    return (v_w, v_b)


def sgd_momentum(params, states, hyperparams):
    for p, v in zip(params, states):
        with torch.no_grad():
            v[:] = hyperparams['momentum'] * v   p.grad
            p[:] -= hyperparams['lr'] * v
            p.grad.data.zero_()

4. 自适应学习率

【深度学习实验】网络优化与正则化(二):基于自适应学习率的优化算法详解:Adagrad、Adadelta、RMSprop

RMSprop算法

   RMSprop(Root Mean Square Propagation)算法是一种针对Adagrad算法的改进方法,通过引入衰减系数来平衡历史梯度和当前梯度的贡献。它能够更好地适应不同参数的变化情况,对于非稀疏数据集表现较好。

代码语言:javascript复制
def init_rmsprop_states(feature_dim):
    s_w = torch.zeros((feature_dim, 3))
    s_b = torch.zeros(3)
    return (s_w, s_b)


def rmsprop(params, states, hyperparams):
    gamma, eps = hyperparams['gamma'], 1e-6
    for p, s in zip(params, states):
        with torch.no_grad():
            s[:] = gamma * s   (1 - gamma) * torch.square(p.grad)
            p[:] -= hyperparams['lr'] * p.grad / torch.sqrt(s   eps)
        p.grad.data.zero_()

5. Adam算法

Adam算法(Adaptive Moment Estimation Algorithm)[Kingma et al., 2015]可以看作动量法和 RMSprop 算法的结合,不但使用动量作为参数更新方向,而且可以自适应调整学习率

更新公式
算法实现
代码语言:javascript复制
def init_adam_states(feature_dim):
    v_w, v_b = torch.zeros((feature_dim, 3)), torch.zeros(3)
    s_w, s_b = torch.zeros((feature_dim, 3)), torch.zeros(3)
    return ((v_w, s_w), (v_b, s_b))

def adam(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-6
    for p, (v, s) in zip(params, states):
        with torch.no_grad():
            v[:] = beta1 * v   (1 - beta1) * p.grad
            s[:] = beta2 * s   (1 - beta2) * torch.square(p.grad)
            v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
            s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
            p[:] -= hyperparams['lr'] * v_bias_corr / (torch.sqrt(s_bias_corr)   eps)
        p.grad.data.zero_()
    hyperparams['t']  = 1

init_adam_states函数用于初始化Adam优化算法的状态。它接受一个特征维度feature_dim作为输入,并返回包含权重和偏置项的状态变量((v_w, s_w), (v_b, s_b))。这些状态变量用于存储权重和偏置项的一阶矩估计(动量)和二阶矩估计(RMSProp)。

adam函数是Adam优化算法的主要实现部分,它接受三个参数:params(待优化的参数),states(状态变量),和hyperparams(超参数)。

  • 在函数内部,使用一个循环来遍历待优化的参数params和对应的状态变量states,然后根据Adam算法的更新规则,对每个参数进行更新:
    • 在更新过程中,使用torch.no_grad()上下文管理器,表示在更新过程中不会计算梯度。
      • 根据Adam算法的公式,计算动量和二阶矩估计的更新值,并将其累加到对应的状态变量中。
      • 根据偏差修正公式,计算修正后的动量和二阶矩估计。
      • 根据修正后的动量和二阶矩估计,计算参数的更新量,并将其应用到参数上。
    • 使用p.grad.data.zero_()将参数的梯度清零,以便下一次迭代时重新计算梯度。
  • 在代码的最后,hyperparams['t'] = 1用于更新迭代次数t的计数器。
算法测试
代码语言:javascript复制
# batch_size = 1
batch_size = 24
# batch_size = 120

# 分别构建训练集、验证集和测试集
train_dataset = IrisDataset(mode='train')

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

lr = 0.02
train(adam, init_adam_states(4), {'lr': lr, 't': 1}, train_loader, 4)
  • IrisDataset类:
    • 参照前文:【深度学习实验】前馈神经网络(七):批量加载数据(直接加载数据→定义类封装数据)
  • train函数:
    • 参照前文:【深度学习实验】网络优化与正则化(一):优化算法:使用动量优化的随机梯度下降算法(Stochastic Gradient Descent with Momentum)

6. 代码整合

代码语言:javascript复制
import torch
from torch import nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
from d2l import torch as d2l
from sklearn.datasets import load_iris
from torchvision.io import read_image
from torch.utils.data import Dataset, DataLoader


class FeedForward(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(FeedForward, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, output_size)
        self.act = nn.Sigmoid()

    def forward(self, inputs):
        outputs = self.fc1(inputs)
        outputs = self.act(outputs)
        outputs = self.fc2(outputs)
        return outputs


def evaluate_loss(net, data_iter, loss):
    """评估给定数据集上模型的损失

    Defined in :numref:`sec_model_selection`"""
    metric = d2l.Accumulator(2)  # 损失的总和,样本数量
    for X, y in data_iter:
        X = X.to(torch.float32)
        out = net(X)
        #         y = d2l.reshape(y, out.shape)
        l = loss(out, y.long())
        metric.add(d2l.reduce_sum(l), d2l.size(l))
    return metric[0] / metric[1]


def train(trainer_fn, states, hyperparams, data_iter, feature_dim, num_epochs=2):
    """Defined in :numref:`sec_minibatches`"""
    # 初始化模型
    w = torch.normal(mean=0.0, std=0.01, size=(feature_dim, 3),
                     requires_grad=True)
    b = torch.zeros((3), requires_grad=True)
    # 训练模型
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[0, num_epochs], ylim=[0.9, 1.1])
    n, timer = 0, d2l.Timer()

    # 这是一个单层线性层
    net = lambda X: d2l.linreg(X, w, b)
    loss = F.cross_entropy
    for _ in range(num_epochs):
        for X, y in data_iter:
            X = X.to(torch.float32)
            l = loss(net(X), y.long()).mean()
            l.backward()
            trainer_fn([w, b], states, hyperparams)
            n  = X.shape[0]
            if n % 48 == 0:
                timer.stop()
                animator.add(n / X.shape[0] / len(data_iter),
                             (evaluate_loss(net, data_iter, loss),))
                timer.start()
    print(f'loss: {animator.Y[0][-1]:.3f}, {timer.avg():.3f} sec/epoch')

    return timer.cumsum(), animator.Y[0]


def load_data(shuffle=True):
    x = torch.tensor(load_iris().data)
    y = torch.tensor(load_iris().target)

    # 数据归一化
    x_min = torch.min(x, dim=0).values
    x_max = torch.max(x, dim=0).values
    x = (x - x_min) / (x_max - x_min)

    if shuffle:
        idx = torch.randperm(x.shape[0])
        x = x[idx]
        y = y[idx]
    return x, y


class IrisDataset(Dataset):
    def __init__(self, mode='train', num_train=120, num_dev=15):
        super(IrisDataset, self).__init__()
        x, y = load_data(shuffle=True)
        if mode == 'train':
            self.x, self.y = x[:num_train], y[:num_train]
        elif mode == 'dev':
            self.x, self.y = x[num_train:num_train   num_dev], y[num_train:num_train   num_dev]
        else:
            self.x, self.y = x[num_train   num_dev:], y[num_train   num_dev:]

    def __getitem__(self, idx):
        return self.x[idx], self.y[idx]

    def __len__(self):
        return len(self.x)


def init_adam_states(feature_dim):
    v_w, v_b = torch.zeros((feature_dim, 3)), torch.zeros(3)
    s_w, s_b = torch.zeros((feature_dim, 3)), torch.zeros(3)
    return ((v_w, s_w), (v_b, s_b))


def adam(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-6
    for p, (v, s) in zip(params, states):
        with torch.no_grad():
            v[:] = beta1 * v   (1 - beta1) * p.grad
            s[:] = beta2 * s   (1 - beta2) * torch.square(p.grad)
            # 偏差修正,请在下方实现偏差修正的计算公式
            v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
            s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
            p[:] -= hyperparams['lr'] * v_bias_corr / (torch.sqrt(s_bias_corr)   eps)
        p.grad.data.zero_()
    hyperparams['t']  = 1


# batch_size = 1
batch_size = 24
# batch_size = 120

# 分别构建训练集、验证集和测试集
train_dataset = IrisDataset(mode='train')

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

lr = 0.02
train(adam, init_adam_states(4), {'lr':lr, 't':1}, train_loader, 4, num_epochs=200)

0 人点赞