7 - 网络优化与正则化 - 书籍1

本文最后更新于:3 个月前

通过实验对比不同的优化策略对模型的影响

[√] 第7章 - 网络优化与正则化


alec收获/总结:

  • 神经网络的损失函数是一个非凸函数,找到全局最优解通常比较困难。
  • 深度神经网络的参数非常多,训练数据也比较大,因此也无法使用计算代价很高的二阶优化方法,而一阶优化方法的训练效率通常比较低。
  • 深度神经网络存在梯度消失或爆炸问题,导致基于梯度的优化方法经常失效。
  • 目前,神经网络变得流行除了本身模型能力强之外,还有一个重要的原因是研究者从大量的实践中总结了一些经验方法,在神经网络的表示能力、复杂度、学习效率和泛化能力之间找到了比较好的平衡。

本章主要介绍神经网络的参数学习中常用的优化和正则化方法。

image-20221223165014564

本章内容主要包含两部分:

  • 网络优化:通过案例和可视化对优化算法、参数初始化、逐层规范化等网络优化算法进行分析和对比,展示它们的效果,通过代码详细展示这些算法的实现过程。
  • 网络正则化:通过案例和可视化对$\ell_{1}$和$\ell_{2}$正则化、权重衰减、暂退法等网络正则化方法进行分析和对比,展示它们的效果。

提醒

在本书中,对《神经网络与深度学习》中一些术语的翻译进行修正。Normalization翻译为规范化、Dropout翻译为暂退法。

[√] 7.1 小批量梯度下降法


alec收获/总结:

  • 目前,深度神经网络的优化方法主要是通过梯度下降法来寻找一组可以最小化结构风险的参数。
  • 在具体实现中,梯度下降法可以分为批量梯度下降、随机梯度下降和小批量梯度下降(Mini-Batch Gradient Descent)三种方式。
  • 它们的区别在于批大小(Batch Size)不同,这三种梯度下降法分别针对全部样本、单个随机样本和小批量随机样本进行梯度计算。
  • 根据不同的数据量和参数量,可以选择不同的实现形式。
  • (随机梯度下降指的是单个随机样本,小批量梯度下降指的是小批量随机样本)
  • ((批量梯度下降是在整个数据集上进行一次反向传播,随机梯度下降是每次在一张图像是反传传播,小批量梯度下降是每次在一个min-batch的图像上进行传播传播梯度下降))

下面我们以小批量梯度下降法为主进行介绍。

令$f(x; \theta)$表示一个神经网络模型,$\theta$为模型参数,$\mathcal{L}(\cdot)$为可微分的损失函数,$\nabla_\theta \mathcal{L}(y, f(x; \theta))=\frac{\partial \mathcal{L}(y, f(x; \theta))}{\partial \theta}$为损失函数关于参数$\theta$的偏导数。在使用小批量梯度下降法进行优化时,每次选取$K$个训练样本$\mathcal{S}t = {(x^{(k)}, y^{(k)})}^K{k=1}$。第$t$次迭代时参数$\theta$的梯度为

$$
\mathbf g_t = \frac{1}{K}\sum_{(x, y) \in \mathcal{S}t} \nabla{\theta} \mathcal{L}(y, f(x; \theta_{t-1})),
$$

其中$\mathcal{L}(\cdot)$为可微分的损失函数,$K$为批大小。

使用梯度下降来更新参数,
$$
\theta_t \leftarrow \theta_{t-1} - \alpha \mathbf g_t,
$$

其中$\alpha > 0$为学习率。

从上面公式可以看出,影响神经网络优化的主要超参有三个:

  1. 批大小$K$
  2. 学习率$\alpha$
  3. 梯度计算$\mathbf g_t$

不同优化算法主要从这三个方面进行改进。下面我们通过动手实践来更好地理解不同的网络优化方法。

alec收获/总结:

  • 影响神经网络优化的超参数主要有三个,分别是:批量大小、学习率、梯度
  • 不同的优化算法主要从这三个方面进行改进

[√] 7.2 批大小的调整实验


alec收获/总结:

  • 在训练深度神经网络时,训练数据的规模通常都比较大。如果在梯度下降时每次迭代都要计算整个训练数据上的梯度,这就需要比较多的计算资源。另外,大规模训练集中的数据通常会非常冗余,也没有必要在整个训练集上计算梯度。因此,在训练深度神经网络时,经常使用小批量梯度下降法。

为了观察不同批大小对模型收敛速度的影响,我们使用经典的LeNet网络进行图像分类,调用paddle.vision.datasets.MNIST函数读取MNIST数据集,并将数据进行规范化预处理。代码实现如下:

alec收获/总结:

  • paddle.unsqueeze(x, axis, name=None),方法讲解

    • 扩充输入数据的维度:向输入 Tensor 的 Shape 中一个或多个位置(axis)插入尺寸为 1 的维度。

    • 代码示例:

    •   import paddle
        
        x = paddle.rand([5, 10])
        print(x.shape)  # [5, 10]
        
        out1 = paddle.unsqueeze(x, axis=0)
        print(out1.shape)  # [1, 5, 10]
        
        out2 = paddle.unsqueeze(x, axis=[0, 2])
        print(out2.shape)  # [1, 5, 1, 10]
        
        axis = paddle.to_tensor([0, 1, 2])
        out3 = paddle.unsqueeze(x, axis=axis)
        print(out3.shape)  # [1, 1, 1, 5, 10]
        
      1
      2
      3
      4
      5
      6
      7
      8
      9

      ```python
      import paddle

      # 将图像值规范化到0~1之间
      def transform(image):
      image = paddle.to_tensor(image / 255, dtype='float32')
      image = paddle.unsqueeze(image, axis=0)#数据扩充一维,便于多张图像组装成batch
      return image
1
2
运行时长: 2187毫秒
结束时间: 2022-12-23 17:12:27

方便起见,本节使用第4.5.4节构建的RunnerV3类进行模型训练,并使用paddle.vision.models.LeNet快速构建LeNet网络,使用paddle.io.DataLoader根据批大小对数据进行划分,使用交叉熵损失函数及标准的随机梯度下降优化器paddle.optimizer.SGD。RunnerV3类会保存每轮迭代和每个回合的损失值,可以方便地观察批大小对模型收敛速度的影响。

alec收获/总结:

  • 本实验要观察批大小对模型收敛速度的影响。
  • 通常情况下,批大小与学习率大小成正比。选择批大小为16、32、64、128、256的情况进行训练。相应地,学习率大小被设置为0.01、0.02、0.04、0.08、0.16。代码实现如下:(批翻倍,那么学习率也翻倍)
  • 批很小,比如一次只学习一张图像,那么这个时候学习率肯定要设置非常小,因此如果在这一张图像上,学习的很快,那么这个数据是不可靠的,学习曲线会震荡的非常严重,频繁的跳过最优点,无法收敛。如果批量大的话,那么这个学习到的梯度是相对可靠的,这个时候学习率要大一点;如果学习率很小的话,那么就会学习的很慢,可能导致无法走出局部最优点。所以,批的大小和学习率的大小要成正比。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import paddle.io as io
import paddle.optimizer as optimizer
import paddle.nn.functional as F

from nndl import RunnerV3
from paddle.vision.models import LeNet
from paddle.vision.datasets import MNIST

# 固定随机种子
paddle.seed(0)

# 准备数据
# 确保从paddle.vision.datasets.MNIST中加载的图像数据是np.ndarray类型
paddle.vision.image.set_image_backend('cv2')
train_dataset = MNIST(mode='train', transform=transform)
# 迭代器加载数据集
# 为保证每次输出结果相同,没有设置shuffle=True,真实模型训练场景需要开启
train_loader1 = io.DataLoader(train_dataset, batch_size=16)

# 定义网络
model1 = LeNet()
# 定义优化器,使用随机梯度下降(SGD)优化器
opt1 = optimizer.SGD(learning_rate=0.01, parameters=model1.parameters())
# 定义损失函数
loss_fn = F.cross_entropy
# 定义runner类
runner1 = RunnerV3(model1, opt1, loss_fn, None)
runner1.train(train_loader1, num_epochs=30, log_steps=0)

model2 = LeNet()
train_loader2 = io.DataLoader(train_dataset, batch_size=32)
opt2 = optimizer.SGD(learning_rate=0.02, parameters=model2.parameters())
runner2 = RunnerV3(model2, opt2, loss_fn, None)
runner2.train(train_loader2, num_epochs=30, log_steps=0)

model3 = LeNet()
train_loader3 = io.DataLoader(train_dataset, batch_size=64)
opt3 = optimizer.SGD(learning_rate=0.04, parameters=model3.parameters())
runner3 = RunnerV3(model3, opt3, loss_fn, None)
runner3.train(train_loader3, num_epochs=30, log_steps=0)

model4 = LeNet()
train_loader4 = io.DataLoader(train_dataset, batch_size=128)
opt4 = optimizer.SGD(learning_rate=0.08, parameters=model4.parameters())
runner4 = RunnerV3(model4, opt4, loss_fn, None)
runner4.train(train_loader4, num_epochs=30, log_steps=0)

model5 = LeNet()
train_loader5 = io.DataLoader(train_dataset, batch_size=256)
opt5 = optimizer.SGD(learning_rate=0.16, parameters=model5.parameters())
runner5 = RunnerV3(model5, opt5, loss_fn, None)
runner5.train(train_loader5, num_epochs=30, log_steps=0)
1
2
运行时长: 25分钟47673毫秒
结束时间: 2022-12-23 17:54:39

可视化损失函数的变化趋势。代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import matplotlib.pyplot as plt
%matplotlib inline

# 绘制每个回合的损失
plt.plot(runner1.train_epoch_losses, label='batch size: 16, lr: 0.01', c='#9c9d9f')
plt.plot(runner2.train_epoch_losses, label='batch size: 32, lr: 0.02', c='#f7d2e2')
plt.plot(runner3.train_epoch_losses, label='batch size: 64, lr: 0.04', c='#f19ec2')
plt.plot(runner4.train_epoch_losses, label='batch size: 128, lr: 0.08', c='#e86096', linestyle='-.')
plt.plot(runner5.train_epoch_losses, label='batch size: 256, lr: 0.16', c='#000000', linestyle='--')
plt.legend(fontsize='x-large')
plt.title('epoch loss with different bs and lr')
plt.savefig('opt-mnist-loss.pdf')
plt.show()

image-20221223180753106

从输出结果看,如果按每个回合的损失来看,每批次样本数越小,下降效果越明显。适当小的批大小可以导致更快的收敛。

alec收获/总结:

  • 适当小的批大小,收敛的更快

[√] 7.3 不同优化算法的比较分析


alec收获/总结:

  • 除了批大小对模型收敛速度的影响外,学习率和梯度估计也是影响神经网络优化的重要因素。
  • 神经网络优化中常用的优化方法也主要是如下两方面的改进,包括:
    • 学习率调整:主要通过自适应地调整学习率使得优化更稳定。这类算法主要有AdaGrad、RMSprop、AdaDelta算法等。(自适应的调整学习率)
    • 梯度估计修正:主要通过修正每次迭代时估计的梯度方向来加快收敛速度。这类算法主要有动量法、Nesterov加速梯度方法等。(修正梯度方法加快收敛速度)
  • 除上述方法外,本节还会介绍综合学习率调整和梯度估计修正的优化算法,如Adam算法。
  • Adaptive,自适应,适应的,适合的

[√] 7.3.1 优化算法的实验设定


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

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

[√] 7.3.1.1 2D可视化实验

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

被优化函数

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from nndl.op import Op

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

def forward(self, x):
self.params['x'] = x
return paddle.matmul(self.w.T, paddle.square(self.params['x']))

def backward(self):
self.grads['x'] = 2 * paddle.multiply(self.w.T, self.params['x'])
1
2
运行时长: 87毫秒
结束时间: 2022-12-23 19:40:05

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def train_f(model, optimizer, x_init, epoch):
"""
训练函数
输入:
- model:被优化函数 # model - 模型,其实就是一个待被优化的函数
- optimizer:优化器
- x_init:x初始值
- epoch:训练回合数
"""
x = x_init
all_x = []
losses = []
for i in range(epoch):
all_x.append(x.numpy())
loss = model(x)
losses.append(loss)
model.backward() # 反向传播计算梯度
optimizer.step() # 通过梯度优化更新参数
x = model.params['x']
return paddle.to_tensor(all_x), losses
1
2
运行时长: 5毫秒
结束时间: 2022-12-23 19:45:27

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 = paddle.to_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, 2])), 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, 2])), 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)
1
2
运行时长: 8毫秒
结束时间: 2022-12-23 19:55:54

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np

def train_and_plot_f(model, optimizer, epoch, fig_name):
"""
训练模型并可视化参数更新轨迹
"""
# 设置x的初始值
x_init = paddle.to_tensor([3, 4], dtype='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)
1
2
运行时长: 4毫秒
结束时间: 2022-12-23 19:57:51

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

1
2
3
4
5
6
7
8
from nndl.op import SimpleBatchGD # 小批量梯度下降优化算法

# 固定随机种子
paddle.seed(0)
w = paddle.to_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')
1
x1 initiate: [3.], x2 initiate: [4.]

image-20221223200147935

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

alec收获/总结:

  • 梯度下降法,梯度下降的意思是,我们想要误差即损失最小,在损失函数的曲线中,假如损失函数是凸函数,那么最小的点就是梯度为0的点。因此我们想要最快的达到梯度为0的点,那么就需要沿着梯度的方向,不断的减去计算得来的梯度,沿着梯度减,让梯度尽快的下降为0。当梯度下降为0的时候,这个时候损失为0,那么此时的参数就是能够使得模型正确的代表数据的映射的关系的模型。
[√] 7.3.1.2 简单拟合实验

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

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

[√] 数据集构建

通过paddle.randn随机生成一些训练数据$X$,并根据一个预定义函数$y = 0.5\times x_{1}+ 0.8\times x_{2} + 0.01\times noise$ 计算得到$ y$,再将$ X$和$ y$拼接起来得到训练样本。代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 固定随机种子
paddle.seed(0)
# 随机生成shape为(1000,2)的训练数据
X = paddle.randn([1000, 2])
w = paddle.to_tensor([0.5, 0.8]) # 一维数据,只有一个中括号
w = paddle.unsqueeze(w, axis=1) # 二维数据
noise = 0.01 * paddle.rand([1000]) # 一维数据
noise = paddle.unsqueeze(noise, axis=1) # 二维数据
# 计算y
y = paddle.matmul(X, w) + noise
# 打印X, y样本
print('X: ', X[0].numpy())
print('y: ', y[0].numpy())

# X,y组成训练样本数据
data = paddle.concat((X, y), axis=1)
print('input data shape: ', data.shape)
print('data: ', data[0].numpy())
1
2
3
4
X:  [-4.080414  -1.3719953]
y: [-3.136211]
input data shape: [1000, 3]
data: [-4.080414 -1.3719953 -3.136211 ]
[√] 模型构建

定义单层前馈神经网络,$ X\in\mathbb{R}^{N \times D}$为网络输入, $ w \in \mathbb{R}^{D}$是网络的权重矩阵,$ b \in \mathbb{R}$为偏置。
$$
y =X w + b \in \mathbb{R}^{K\times 1},
$$

其中$K$代表一个批次中的样本数量,$D$为单层网络的输入特征维度。

[√] 损失函数

使用均方误差作为训练时的损失函数,计算损失函数关于参数$ w$和$b$的偏导数。定义均方误差损失函数的计算方法为
$$
\mathcal{L} = \frac{1}{2K}\sum_{k=1}^K(y^{(k)} - z^{(k)})^2,
$$

其中$ z^{(k)}$是网络对第$k$个样本的预测值。根据损失函数关于参数的偏导公式,得到$\mathcal{L}(\cdot)$对于参数$ w$和$b$的偏导数,
$$
\frac{\partial \mathcal{L}}{\partial w} = \frac{1}{K}\sum_{k=1}^Kx^{(k)}(z^{(k)} - y^{(k)}) = \frac{1}{K}X^\top(z - y), \
\frac{\partial \mathcal{L}}{\partial b} = \frac{1}{K}\sum_{k=1}^K(z^{(k)} - y^{(k)}) = \frac{1}{K}\mathbf{1}^\top(z - y).
$$

定义Linear算子,实现一个线性层的前向和反向计算。代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Linear(Op):
def __init__(self, input_size, weight_init=paddle.standard_normal, bias_init=paddle.zeros):
super(Linear, self).__init__()
self.params = {}
self.params['W'] = weight_init(shape=[input_size, 1])
self.params['b'] = bias_init(shape=[1])
self.inputs = None
self.grads = {}

def forward(self, inputs):
self.inputs = inputs
self.outputs = paddle.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 * paddle.matmul(self.inputs.T, (self.outputs - labels))
self.grads['b'] = 1. /K * paddle.sum(self.outputs - labels, axis=0)
1
2
运行时长: 7毫秒
结束时间: 2022-12-23 20:32:32

笔记

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

[√] 训练函数

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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()[0]
# 计算梯度 # 此处计算的梯度,优化器能看到
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
1
2
运行时长: 7毫秒
结束时间: 2022-12-23 20:39:36
[√] 优化过程可视化

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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()
1
2
运行时长: 5毫秒
结束时间: 2022-12-23 20:42:06

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

1
2
3
4
5
6
7
8
9
10
11
import paddle.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)
1
2
运行时长: 4毫秒
结束时间: 2022-12-23 20:43:04

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

1
2
3
4
5
6
7
# 固定随机种子
paddle.seed(0)
# 定义网络结构
model = Linear(2)
# 定义优化器
opt = SimpleBatchGD(init_lr=0.01, model=model) # 将model传给优化器,是因为模型反向传播计算完梯度后,优化器需要给模型优化更新参数
train_and_plot(opt, 'opti-loss.pdf')

image-20221223204530573

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

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

[√] 与Paddle API对比,验证正确性

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
paddle.seed(0)

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, paddle.nn.initializer.Assign(model1.params['W']))
print('model2 parameter W: ', model2.state_dict()['weight'].numpy())
output2 = model2(x)

model1.backward(y) # 计算梯度
opt1.step() # 更新参数
print('model1 parameter W after train step: ', model1.params['W'].numpy())

opt2 = optimizer.SGD(learning_rate=0.01, parameters=model2.parameters())
loss = paddle.nn.functional.mse_loss(output2, y) / 2
loss.backward()
opt2.step()
opt2.clear_grad()
print('model2 parameter W after train step: ', model2.state_dict()['weight'].numpy())
1
2
3
4
5
6
7
8
model1 parameter W:  [[-4.080414 ]
[-1.3719953]]
model2 parameter W: [[-4.080414 ]
[-1.3719953]]
model1 parameter W after train step: [[-3.196255 ]
[-1.0747064]]
model2 parameter W after train step: [[-3.196255 ]
[-1.0747064]]

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

[√] 7.3.2 学习率调整(AdaGrad && RMSprop)


alec收获/总结:

  • 学习率是神经网络优化时的重要超参数。在梯度下降法中,学习率α的取值非常关键,如果取值过大就不会收敛,如果过小则收敛速度太慢。
  • 取值过大,反复震荡,不会收敛;取值过小,收敛的很慢,还可能局部最优。

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

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

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

alec:

  • AdaGrad算法(Adaptive Gradient Algorithm,自适应梯度算法)是借鉴 ℓ2 正则化的思想

AdaGrad算法(Adaptive Gradient Algorithm,自适应梯度算法)是借鉴 $\ell_2$ 正则化的思想,每次迭代时自适应地调整每个参数的学习率。在第$t$次迭代时,先计算每个参数梯度平方的累计值。
$$
G_t = \sum^t_{\tau=1} \mathbf g_{\tau} \odot \mathbf g_{\tau},
$$
其中$\odot$为按元素乘积,$\mathbf g_{\tau} \in \mathbb R^{\mid \theta \mid}$是第$\tau$次迭代时的梯度。

在AdaGrad梯度优化算法中,计算的梯度为:
$$
\Delta \theta_t = - \frac{\alpha}{\sqrt{G_t + \epsilon}} \odot \mathbf g_{t},
$$

其中$\alpha$是初始的学习率,$\epsilon$是为了保持数值稳定性而设置的非常小的常数,一般取值$e^{−7}$到$e^{−10}$。此外,这里的开平方、除、加运算都是按元素进行的操作。

alec:

在AdaGrad梯度优化算法中,计算的梯度为:
$$
\Delta \theta_t = - \frac{\alpha}{\sqrt{G_t + \epsilon}} \odot \mathbf g_{t},
$$

即在基本的随机梯度下降算法的基础上,为学习率α添加了分母根号下(G_t + e),其中e是为了稳定设置的数值,防止分母为0产生梯度爆炸;G_t则是该梯度对应的参数的梯度平方的累计值,随着梯度的累计,分母不断变大,则对应的,学习率总体不断变小,因此称为自适应的学习率调整算法。通过对应的梯度自身来调整对应学习率的大小。

[√] 构建优化器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from nndl.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 # 参数的梯度的平方的累计值
# alec: 在初试学习率的基础上,随着G的累加,自适应的调整总体学习率的大小,学习率越来越小。
x -= init_lr / paddle.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算法的参数更新轨迹。代码实现如下:

1
2
3
4
5
6
# 固定随机种子
paddle.seed(0)
w = paddle.to_tensor([0.2, 2])
model = OptimizedFunction(w)
opt = Adagrad(init_lr=0.5, model=model, epsilon=1e-7)
train_and_plot_f(model, opt, epoch=50, fig_name='opti-vis-para2.pdf')

image-20221223220005109

alec:

  • 能够看出,AdaGrad算法,前几个回合的梯度更新幅度较大,随着回合的增加,参数的更新幅度逐渐减小。

  • AdaGrad算法的优点是,能够自适应的调整学习率,刚开始快一点,后面细致一点。
  • 缺点是,随着训练次数的迭代,梯度的平方的累计值非常大了,这个时候学习率很小,如果没有找到最优点,那么就很难找到最优点了。

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

[√] 简单拟合实验

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

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

image-20221223220354205

alec:

  • 刚开始损失的波动是比较大的,后来损失的波动逐渐减小
[√] 7.3.2.2 RMSprop算法

alec:

  • RMSprop算法是一种自适应学习率的方法,可以在有些情况下避免AdaGrad算法中学习率不断单调下降以至于过早衰减的缺点。
  • AdamGrad算法,学习率分母中根号下的内容是G+e,其中G是每个参数梯度的平方和
  • RMS prop算法,学习率分母中根号下的内容是G+e,其中G是每个参数梯度de(平方的加权移动平均)
    • image-20221223223416992
    • 其中 β 为衰减率,一般取值为0.9。
  • RMSprop算法和AdaGrad算法的区别在于RMSprop算法中G_t的计算由累积方式变成了加权移动平均,在迭代过程中,每个参数的学习率并不是呈衰减趋势,既可以变小也可以变大。

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

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

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

[√] 构建优化器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 / paddle.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算法的参数更新轨迹。代码实现如下:

1
2
3
4
5
6
# 固定随机种子
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')

image-20221223224148615

[√] 简单拟合实验

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

1
2
3
4
5
6
7
# 固定随机种子
paddle.seed(0)
# 定义网络结构
model = Linear(2)
# 定义优化器
opt = RMSprop(init_lr=0.1, model=model, beta=0.9, epsilon=1e-7)
train_and_plot(opt, 'opti-loss3.pdf')

image-20221224102145578

[√] 7.3.3 梯度估计修正


alec:

  • 除了调整学习率之外,还可以进行梯度估计修正。
  • 在小批量梯度下降法中,由于每次迭代的样本具有一定的随机性,因此每次迭代的梯度估计和整个训练集上的最优梯度并不一致。
  • 如果每次选取样本数量比较小,损失会呈振荡的方式下降。
  • 每次选取的样本数量少,则不确定性和代表性差,因此损失曲线会呈现震荡的方式下降。
  • 一种有效地缓解梯度估计随机性的方式是通过使用最近一段时间内的平均梯度来代替当前时刻的随机梯度来作为参数更新的方向,从而提高优化速度。(缓解思路是使用一段时间内的平均梯度代替当前梯度,作为参数更新的方向。)
[√] 7.3.3.1 动量法(Momentum算法)

alec:

  • 动量法(Momentum Method)是用之前积累动量来替代真正的梯度。
  • 每次迭代的梯度可以看作加速度。
  • 通过调整学习率优化的思路是通过梯度的平方和和梯度平方和的加权移动平均作为分母来调整学习率;通过梯度优化的思路是将连续时间内梯度的加权移动平均值作为梯度来调整参数

  • 动量法(Momentum Method)是用之前积累动量来替代真正的梯度。每次迭代的梯度可以看作加速度。
  • 在第 t 次迭代时,计算负梯度的“加权移动平均”作为参数的更新方向

$$
\Delta \theta_t = \rho \Delta \theta_{t-1} - \alpha \mathbf g_t = - \alpha \sum_{\tau=1}^t\rho^{t - \tau} \mathbf g_{\tau},
$$
其中$\rho$为动量因子,通常设为0.9,$\alpha$为学习率。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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算法的参数更新轨迹。

1
2
3
4
5
6
# 固定随机种子
paddle.seed(0)
w = paddle.to_tensor([0.2, 2])
model = OptimizedFunction(w)
opt = Momentum(init_lr=0.01, model=model, rho=0.9)
train_and_plot_f(model, opt, epoch=50, fig_name='opti-vis-para4.pdf')

image-20221224111636737

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

[√] 简单拟合实验

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

1
2
3
4
5
6
7
8
# 固定随机种子
paddle.seed(0)

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

image-20221224111752771

[√] 7.3.3.2 Adam算法

alec:

  • Adam算法(Adaptive Moment Estimation Algorithm,自适应矩估计算法)。

  • 其中ada指的是Adaptive,即调整学习率的角度;m指的是moentum,动量,即调整梯度的角度。

  • 可以看作动量法和RMSprop算法的结合,不但使用动量作为参数更新方向,而且可以自适应调整学习率。

  • adam算法对两部分做了修改,分别是学习率和梯度。

  • Adam算法一方面计算梯度平方$\mathbf g_t^2$的加权移动平均(和RMSprop算法类似),另一方面计算梯度$\mathbf g_t$的加权移动平均(和动量法类似)。

  • (1)计算梯度平方的加权移动平均用于学习率控制:
    $$
    G_t = \beta_2 G_{t-1} + (1 - \beta_2)\mathbf g_t \odot \mathbf g_t,
    $$

  • (2)计算梯度的加权移动平均,用于梯度方向稳定:
    $$
    M_t = \beta_1 M_{t-1} + (1 - \beta_1)\mathbf g_t, \
    $$

  • 其中$\beta_1$和$\beta_2$分别为两个移动平均的衰减率,通常取值为$\beta_1 = 0.9, \beta_2 = 0.99$。我们可以把$M_t$和$G_t$分别看作梯度的均值(一阶矩)和未减去均值的方差(二阶矩)。


假设$M_0 = 0, G_0 = 0$,那么在迭代初期$M_t$和$G_t$的值会比真实的均值和方差要小。特别是当$\beta_1$和$\beta_2$都接近于1时,偏差会很大。因此,需要对偏差进行修正。
$$
\hat M_t = \frac{M_t}{1 - \beta^t_1}, \
\hat G_t = \frac{G_t}{1 - \beta^t_2}。
$$

Adam算法的参数更新差值为
$$
\Delta \theta_t = - \frac{\alpha}{\sqrt{\hat G_t + \epsilon}}\hat M_t,
$$
其中学习率$\alpha$通常设为0.001,并且也可以进行衰减,比如$a_t = \frac{a_0}{\sqrt{t}}$。

其中学习率和梯度部分都是可以变化的,学习率部分分母中的G是当前梯度的平方的加权移动平均,梯度部分中的M是当前梯度的加权移动平均。

其中α通常设为0.001,梯度部分的当前梯度的加权移动平均的衰减率β1通常为0.9,学习率部分的当前梯度的平方的加权移动平均的衰减率β2通常为0.99

为了修正一开始的时候的偏差,需要将M和G变为M_hat和G_hat,随着T的增加,M_hat和G_hat慢慢地就变成M和G了

[√] 构建优化器

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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

# alec:gradient_x是当前批次新计算的梯度,x是要被更新的参数,t是迭代次数
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 / paddle.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算法的参数更新轨迹。代码实现如下:

1
2
3
4
5
6
# 固定随机种子
paddle.seed(0)
w = paddle.to_tensor([0.2, 2])
model = OptimizedFunction(w)
opt = Adam(init_lr=0.2, model=model, beta1=0.9, beta2=0.99, epsilon=1e-7)
train_and_plot_f(model, opt, epoch=20, fig_name='opti-vis-para5.pdf')

image-20221224120121140

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

[√] 简单拟合实验

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

1
2
3
4
5
6
7
# 固定随机种子
paddle.seed(0)
# 定义网络结构
model = Linear(2)
# 定义优化器
opt = Adam(init_lr=0.1, model=model, beta1=0.9, beta2=0.99, epsilon=1e-7)
train_and_plot(opt, 'opti-loss5.pdf')

image-20221224120200596

[√] 7.3.4 不同优化器的3D可视化对比


[√] 7.3.4.1 构建一个三维空间中的被优化函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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]
self.grads['x'] = paddle.concat([gradient1, gradient2])

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 构建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函数,用于可视化三维的参数更新轨迹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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=60, 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([])
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from mpl_toolkits.mplot3d import Axes3D

# 使用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 = paddle.to_tensor([x1, x2])
model = OptimizedFunction3D()

# 绘制f_3d函数的三维图像
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.plot_surface(init_x[0], init_x[1], model(init_x), color='#f19ec2')
ax.set_xlabel('x1')
ax.set_ylabel('x2')
ax.set_zlabel('f(x1,x2)')
plt.savefig('opti-f-3d.pdf')

image-20221224121914517

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

1
2
3
4
5
6
7
8
from IPython.display import HTML

labels = ['SGD', 'AdaGrad', 'RMSprop', 'Momentum', 'Adam']
colors = ['#9c9d9f', '#f7d2e2', '#f19ec2', '#e86096', '#000000']

anim = Visualization3D(*x_all_opts, z_values=z_all_opts, labels=labels, colors=colors, fig=fig, ax=ax)
ax.legend(loc='upper left')
HTML(anim.to_html5_video())
image-20221224122650985

alec:

  • 希望训练过程中,优化算法可以使参数离开鞍点,向模型最优解收敛。
  • 鞍点就是一个局部最优点,但是不是最小的点。

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


7 - 网络优化与正则化 - 书籍1
https://alec-97.github.io/posts/867345559/
作者
Shuai Zhao
发布于
2022年12月25日
许可协议