【深度学习实验】网络优化与正则化(一):优化算法:使用动量优化的随机梯度下降算法(Stochastic Gradient Descent with Momentum)
文章目录
任何数学技巧都不能弥补信息的缺失。
——科尼利厄斯·兰佐斯(Cornelius Lanczos)匈牙利数学家、物理学家
一、实验介绍
深度神经网络在机器学习中应用时面临两类主要问题:优化问题和泛化问题。
-
优化问题:深度神经网络的优化具有挑战性。
- 神经网络的损失函数通常是非凸函数,因此找到全局最优解往往困难。
- 深度神经网络的参数通常非常多,而训练数据也很大,因此使用计算代价较高的二阶优化方法不太可行,而一阶优化方法的训练效率通常较低。
- 深度神经网络存在梯度消失或梯度爆炸问题,导致基于梯度的优化方法经常失效。
-
泛化问题:由于深度神经网络的复杂度较高且具有强大的拟合能力,很容易在训练集上产生过拟合现象。因此,在训练深度神经网络时需要采用一定的正则化方法来提高网络的泛化能力。
目前,研究人员通过大量实践总结了一些经验方法,以在神经网络的表示能力、复杂度、学习效率和泛化能力之间取得良好的平衡,从而得到良好的网络模型。本系列文章将从网络优化和网络正则化两个方面来介绍如下方法:
- 在网络优化方面,常用的方法包括优化算法的选择、参数初始化方法、数据预处理方法、逐层归一化方法和超参数优化方法。
- 在网络正则化方面,一些提高网络泛化能力的方法包括ℓ1和ℓ2正则化、权重衰减、提前停止、丢弃法、数据增强和标签平滑等。
本文将介绍使用动量优化的随机梯度下降算法(Stochastic Gradient Descent with Momentum)
二、实验环境
本系列实验使用了PyTorch深度学习框架,相关操作如下:
1. 配置虚拟环境
conda create -n DL python=3.7
conda activate DL
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
conda install matplotlib
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. 导入必要的库
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的更新步骤如下:
- 从训练数据中随机选择一个样本的索引。
- 使用选择的样本计算损失函数对于网络参数的梯度。
- 根据计算得到的梯度更新网络参数。
- 重复以上步骤,直到达到停止条件(如达到固定的迭代次数或损失函数收敛)。
a. PyTorch中的SGD优化器
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
动量(Momentum)是模拟物理中的概念.一个物体的动量指的是该物体在它运动方向上保持运动的趋势,是该物体的质量和速度的乘积.动量法(Momentum Method)是用之前积累动量来替代真正的梯度.每次迭代的梯度可以看作加速度。
在第𝑡 次迭代时,计算**负梯度的“加权移动平均”**作为参数的更新方向,
其中𝜌为动量因子,通常设为0.9,𝛼为学习率。
这样,每个参数的实际更新差值取决于最近一段时间内梯度的加权平均值。当某个参数在最近一段时间内的梯度方向不一致时,其真实的参数更新幅度变小;相反,当在最近一段时间内的梯度方向都一致时,其真实的参数更新幅度变大,起到加速作用。一般而言:
- 在迭代初期,梯度方向都比较一致,动量法会起到加速作用,可以更快地到达最优点。
- 在迭代后期,梯度方向会不一致,在收敛值附近振荡,动量法会起到减速作用,增加稳定性。
从某种角度来说,当前梯度叠加上部分的上次梯度,一定程度上可以近似看作二阶梯度,下面将介绍如何使用动量法训练模型:
a. init_momentum_states
def init_momentum_states(feature_dim):
v_w = torch.zeros((feature_dim, 3))
v_b = torch.zeros(3)
return (v_w, v_b)
init_momentum_states(feature_dim) 函数用于初始化动量状态:
- 接受一个特征维度
feature_dim作为输入,返回一个包含两个张量的元组(v_w, v_b)。v_w是一个形状为(feature_dim, 3)的全零张量,v_b是一个长度为 3 的全零张量。
b. sgd_momentum
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_()
sgd_momentum(params, states, hyperparams) 函数实现了使用动量优化的随机梯度下降算法:
- 它接受三个参数:
params是模型的参数张量列表,states是动量状态的元组(v_w, v_b),hyperparams是超参数字典,包含学习率和动量参数。 - 在函数内部,对于每个参数
p和对应的动量状态v,它执行以下操作:v[:] = hyperparams['momentum'] * v + p.grad:更新动量状态,将当前梯度p.grad乘以动量参数hyperparams['momentum']并加到动量状态v上。p[:] -= hyperparams['lr'] * v:更新参数p,将学习率hyperparams['lr']乘以动量v得到的梯度,从当前参数p中减去。p.grad.data.zero_():清零参数p的梯度。
c. evaluate_loss
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]
evaluate_loss函数用于在给定数据集上评估模型的损失。
- 接受一个神经网络模型
net、一个数据迭代器data_iter和一个损失函数loss作为输入。 - 在函数内部,它通过迭代数据迭代器中的样本,计算模型在每个样本上的损失,并累积总损失和样本数量。
- 最后,函数返回总损失除以样本数量的平均值,作为模型在给定数据集上的损失评估值。
d. train
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]
- 参数:训练器函数
trainer_fn、状态列表states、超参数字典hyperparams、数据迭代器data_iter、输入特征维度feature_dim和训练的总轮数num_epochs(默认为2)。 - 在函数内部,它首先初始化模型的参数
w和b,然后使用训练数据迭代器进行训练。- 训练过程中的每个迭代步骤中
- 首先将输入数据转换为浮点类型,
- 然后计算模型在当前批次数据上的损失
l。 - 接下来,通过反向传播计算损失关于模型参数的梯度,并使用
trainer_fn函数来更新参数。trainer_fn函数接受参数列表[w, b]、状态列表states和超参数字典hyperparams,用于更新模型的参数。
- 在每个训练迭代步骤结束后,记录已经处理的样本数量
n,并根据一定条件计算并绘制损失值的动画图。
- 训练过程中的每个迭代步骤中
- 最后,函数返回训练过程中的时间累计值和损失值列表。
e. 模型测试
batch_size = 24
train_dataset = IrisDataset(mode='train')
train_loader = DataLoader(train_dataset, batch_size=batch_size,shuffle=True)
lr = 0.02
momentum = 0.9
train(sgd_momentum, init_momentum_states(4), {'lr': lr, 'momentum': momentum}, train_loader, 4, num_epochs=100)
- 设置批量大小
batch_size; - 创建训练数据集
train_dataset; - 使用
DataLoader创建一个训练数据迭代器train_loader,用于按批次加载训练数据; - 定义学习率
lr和动量momentum超参数; - 调用
train函数进行模型训练。- 传递训练函数
sgd_momentum、初始化的动量状态init_momentum_states(4)、超参数字典{'lr': lr, 'momentum': momentum}、训练数据迭代器train_loader、特征维度4和训练的总轮数100。
- 传递训练函数

4. 代码整合
# 导入需要的工具包
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
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_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_()
# 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
momentum = 0.9
train(sgd_momentum, init_momentum_states(4), {'lr': lr, 'momentum': momentum}, train_loader, 4, num_epochs=100)
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)