[2022-12-11]神经网络与深度学习第6章 - 网络优化与正则化

网络优化与正则化 - 不同优化算法比较

写在开头

我们通过学习,已经知道了在深度学习中各式各样的优化算法了。在本次实验中,我们将对不同的优化算法进行比较分析。
除了批大小对于模型的收敛速度有影响外,学习率和梯度估计也是影响神经网络优化的重要因素。
神经网络优化中常用的方法主要为以下两个方面的改进:

  • 学习率 :通过自适应调整学习率使得优化更稳定,如AdaGrad、RMSProp、AdaDelta等;
  • 梯度估计修正 :通过修正每次迭代时估计的梯度方向来加快收敛速度,如Momentum、Nesterov等。
    本次实验还会介绍将两种方法综合的优化算法,如Adam。

不同优化算法的比较分析

优化算法的实验设定

2D可视化实验

优化结果好坏仅凭一个个数据看起来很麻烦,因此我们将优化算法进行二维可视化。我们选择一个二维空间中的凸函数,然后用不同的优化算法来寻找最优解,可视化梯度下降过程的轨迹。
本次选用的待优化函数为sphere函数,其计算公式如下:
s ( x ) = ∑ d = 1 D x d 2 = x 2 s(\textbf{x})=\sum_{d=1}^{D}x^2_d=\textbf{x}^2 s(x)=d=1Dxd2=x2
其梯度如下:
∂ s ( x ) ∂ x = 2 ω ⊙ x \frac{\partial s(\textbf{x})}{\partial \textbf{x}} = 2 \textbf{ω} \odot \textbf{x} xs(x)=2ωx
我们能够非常容易地得到这个函数的构建代码:

class Sphere(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.square(self.params['x']))
 
    def backward(self):
        self.grads['x'] = 2 * torch.multiply(self.w.T, self.params['x'])

由于最低点是输入全为0时,因此损失函数编写如下:

class CLoss(torch.nn.modules.loss._Loss):
    def __init__(self, size_average=None, reduce=None, reduction: str = 'mean'):
        super(CLoss, self).__init__(size_average, reduce, reduction)

    def forward(self, x):
        return x

我们再定义一个简易的训练函数,用于记录梯度下降时的参数和损失,代码如下:

def train(model, optimizer, x_init, 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

然后我们定义可视化部分的代码:

import numpy as np
import matplotlib.pyplot as plt
class Visualization(object):
    def __init__(self):
        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, 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))

训练和绘制一体的代码如下:

def train_and_plot(model, optimizer, epoch, fig_name):
    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(model, optimizer, x_init, epoch)
    losses = np.array(losses)
    vis = Visualization()
    vis.plot_2d(model, x, fig_name)

执行代码如下:

from op import SimpleBatchGD

torch.seed()
w = torch.as_tensor([0.2, 2])
model = Sphere(w)
opt = SimpleBatchGD(init_lr=0.2, model=model)
train_and_plot_f(model, opt, epoch=20)

结果如下:
在这里插入图片描述

简单拟合实验

在通过这边我们随机生成一组数据作为数据样本,再构建一个简单的单层前馈神经网络,用于前向计算。
这个代码我们已经在很早的时候就进行过简单的实验。其数据集构建如下:

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

# X,y组成训练样本数据
data = torch.concat((X, y), axis=1)
print('input data shape: ', data.shape)
print('data: ', data[0].numpy())

在这里插入图片描述
然后是自定义Linear层算子,代码如下:

class Linear(Op):
    def __init__(self, input_size, weight_init=torch.randn, bias_init=torch.zeros):
        super(Linear, self).__init__()
        self.params = {}
        self.params['W'] = weight_init(size=[input_size, 1])
        self.params['b'] = bias_init(size=[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, axis=0)

特别地,这边的反向函数经过特殊改动,用于计算最终损失关于参数的梯度。方便对后续的优化进行实验。
接下来是训练函数,代码如下:

def train(data, num_epochs, batch_size, model, calculate_loss, optimizer, verbose=False):
    # 记录每个回合损失的变化
    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

关于损失绘制的函数代码如下:

def plot_loss(iter_loss, epoch_loss):
    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.show()

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

def train_and_plot(optimizer, fig_name):
    mse = torch.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.seed()
# 定义网络结构
model = Linear(2)
# 定义优化器
opt = SimpleBatchGD(init_lr=0.01, model=model)
train_and_plot(opt)

结果如下:
在这里插入图片描述
模型的权重为:
在这里插入图片描述
对应torch模型的权重为:
在这里插入图片描述

学习率调整

神经网络研究员早就意识到学习率肯定是难以设置的超参数之一,因为它对模型的性能有显著的影响。损失通常高度敏感于参数空间中的某些方向,而不敏感于其他。动量算法可以在一定程度缓解这些问题,但这样做的代价是引入了另一个超参数。如果我们相信方向敏感度在某种程度是轴对齐的,那么每个参数设置不同的学习率,在整个学习过程中自动适应这些学习率是有道理的。
这些算法的实例,我们都在前面的作业中进行了验证,这边给出其详细解释和算法伪代码。

AdaGrad

AdaGrad 算法,独立地适应所有模型参数的学习率,缩放每个参数反比于其所有梯度历史平方值总和的平方根 (Duchi et al., 2011)。具有损失最大偏导的参数相应地有一个快速下降的学习率,而具有小偏导的参数在学习率上
有相对较小的下降。净效果是在参数空间中更为平缓的倾斜方向会取得更大的进步。在凸优化背景中,AdaGrad 算法具有一些令人满意的理论性质。然而,经验上已经发现,对于训练深度神经网络模型而言,从训练开始时积累梯度平方会导致有效学习率过早和过量的减小。AdaGrad 在某些深度学习模型上效果不错,但不是全部。
在这里插入图片描述

RMSProp

RMSProp 算法 (Hinton, 2012) 修改 AdaGrad 以在非凸设定下效果更好,改
变梯度积累为指数加权的移动平均。AdaGrad 旨在应用于凸问题时快速收敛。当应用于非凸函数训练神经网络时,学习轨迹可能穿过了很多不同的结构,最终到达一个局部是凸碗的区域。AdaGrad 根据平方梯度的整个历史收缩学习率,可能使得学习率在达到这样的凸结构前就变得太小了。RMSProp 使用指数衰减平均以丢弃遥远过去的历史,使其能够在找到凸碗状结构后快速收敛,它就像一个初始化于该碗状结构的 AdaGrad 算法实例。
RMSProp 的标准形式如下图所示,相比于 AdaGrad,使用移动平均引入了一个新的超参数ρ,用来控制移动平均的长度范围。
经验上,RMSProp 已被证明是一种有效且实用的深度神经网络优化算法。目前它是深度学习从业者经常采用的优化方法之一。
在这里插入图片描述

梯度估计修正

动量法

动量算法引入了变量 v 充当速度角色——它代表参数在参数空间移动的方向和速率。速度被设为负梯度的指数衰减平均。名称 动量(momentum)来自物理类比,根据牛顿运动定律,负梯度是移动参数空间中粒子的力。动量在物理学上定义为质量乘以速度。在动量学习算法中,我们假设是单位质量,因此速度向量 v 也可以看作是粒子的动量。超参数α ∈ [0, 1) 决定了之前梯度的贡献衰减得有多快。更新规则如下:
在这里插入图片描述
速度 v 累积了梯度元素 ∇ θ ( 1 / m ∑ i = 1 m L ( f ( x ( i ) ; θ ) , y ( i ) ) ) ∇θ(1/m∑^m_{i=1} L(f(x(i); θ), y(i))) θ(1/mi=1mL(f(x(i);θ),y(i)))。相对于 ϵ,α 越大,之前梯度对现在方向的影响也越大。其伪代码如下:
在这里插入图片描述

Adam算法

Adam (Kingma and Ba, 2014) 是另一种学习率自适应的优化算法,如下图所示。“Adam’’ 这个名字派生自短语 “adaptive moments’’。早期算法背景下,它也许最好被看作结合 RMSProp 和具有一些重要区别的动量的变种。首先,在 Adam 中,动量直接并入了梯度一阶矩(指数加权)的估计。将动量加入 RMSProp 最直观的方法是将动量应用于缩放后的梯度。结合缩放的动量使用没有明确的理论动机。其次,Adam 包括偏置修正,修正从原点初始化的一阶矩(动量项)和(非中心的)二阶矩的估计(算法8.7 )。RMSProp 也采用了(非中心的)二阶矩估计,然而缺失了修正因子。因此,不像 Adam,RMSProp 二阶矩估计可能在训练初期有很高的偏置。Adam 通常被认为对超参数的选择相当鲁棒,尽管学习率有时需要从建议的默认修改。
在这里插入图片描述

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

相较于二维,三维的多出了一个参数,因此修改还是比较容易的,代码如下:

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 = []
 # 使用不同优化器训练
 
for model, opt in zip(models, opts):
    x_init = torch.FloatTensor([2, 3])
    x_one_opt, z_one_opt = train_f(model, opt, x_init, 150)  # epoch
    # 保存参数值
    x_all_opts.append(x_one_opt.numpy())
    z_all_opts.append(np.squeeze(z_one_opt))

class Visualization3D(animation.FuncAnimation):
    """    绘制动态图像,可视化参数更新轨迹    """
 
    def __init__(self, *xy_values, z_values, labels=[], colors=[], fig, ax, interval=600, blit=True, **kwargs):

        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()  # 改为 model(init_x).numpy() David 2022.12.4
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()

输出结果如下:
在这里插入图片描述

关于制作动画图像

matplotlib支持动画绘制,只需要plt.ion()即可进行动态化的绘制,上面的输出图像如果要实现动态效果,只需要执行此函数,并在每次有新数据时进行绘制即可有动态效果。但是,我没发现如何导出动画图像,希望以后能够解决此问题。

写在最后

本次实验是对上次作业的一个展开和验证。通过对数据的处理和优化,我们了解了这些不同的优化算法,结合上次作业,我们能够了解这些算法的内在机理、熟悉他们的特点和缺陷,为实际项目的算法选择提供指导意义。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值