NNDL 实验五 前馈神经网络(2)自动梯度计算 & 优化问题

目录

4.3 自动梯度计算

1. 使用pytorch的预定义算子来重新实现二分类任务。

4.3.1 利用预定义算子重新实现前馈神经网络

4.3.2 完善Runner类

4.3.3 模型训练

​4.3.4 性能评价

2. 增加一个3个神经元的隐藏层,再次实现二分类,并与1做对比。

3. 自定义隐藏层层数和每个隐藏层中的神经元个数,尝试找到最优超参数完成二分类。可以适当修改数据集,便于探索超参数。(选做)

【思考题】

4.4 优化问题

4.4.1 参数初始化

4.4.2 梯度消失问题

4.4.2.1 模型构建

4.4.2.2 使用Sigmoid型函数进行训练

4.4.2.3 使用ReLU函数进行模型训练

4.4.3 死亡ReLU问题

4.4.3.1 使用ReLU进行模型训练

4.4.3.2 使用Leaky ReLU进行模型训练

记录一下:


 

4.3 自动梯度计算

虽然我们能够通过模块化的方式比较好地对神经网络进行组装,但是每个模块的梯度计算过程仍然十分繁琐且容易出错。在深度学习框架中,已经封装了自动梯度计算的功能,我们只需要聚焦模型架构,不再需要耗费精力进行计算梯度。

飞桨提供了paddle.nn.Layer类,来方便快速的实现自己的层和模型。模型和层都可以基于paddle.nn.Layer扩充实现,模型只是一种特殊的层。继承了paddle.nn.Layer类的算子中,可以在内部直接调用其它继承paddle.nn.Layer类的算子,飞桨框架会自动识别算子中内嵌的paddle.nn.Layer类算子,并自动计算它们的梯度,并在优化时更新它们的参数。

pytorch中的相应内容是什么?简要介绍:

torch.nn是包含了构筑神经网络结构基本元素的包,在这个包中,可以找到任意的神经网络层。这些神经网络层都是nn.Module这个大类的子类。

Containers  容器
Module  模块
class torch.nn.Module
    Base class for all neural network modules.
    这是所有神经网络模块的父类(基类).
    Your models should also subclass this class.
    你自己实现的模型也应该继承这个类.
    Modules can also contain other Modules, allowing to
    nest them in a tree structure. You can assign the
    submodules as regular attributes:
    我们可以让这种类型的模块包含这种类型的其他模块,以此将它们
    嵌套地形成一个树形结构.用户可以将子模块赋值成普通的类属性:


import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.conv1 = nn.Conv2d(1, 20, 5)
        self.conv2 = nn.Conv2d(20, 20, 5)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        return F.relu(self.conv2(x))

Submodules assigned in this way will be registered, and will
have their parameters converted too when you call to(), etc.
以这种方式赋值给属性的子模块将会被自动地登记注册,而且当你调用to()方法
的时候,这些子模块的参数也会被正确地相应转换.

参考:

torch.nn.Module所有方法总结及其使用举例_敲代码的小风的博客-CSDN博客

torch.nn.Linear(in_features, out_features, bias=True) 函数是一个线性变换函数:

y=xA^{T}+b

其中,in_features为输入样本的大小,out_features为输出样本的大小,bias默认为true。如果设置bias = false那么该层将不会学习一个加性偏差。Linear()函数通常用于设置网络中的全连接层。

其用法与形参说明如下:

代码很简单,但有许多细节需要声明:

1)nn.Linear是一个类,使用时进行类的实例化

2)实例化的时候,nn.Linear需要输入两个参数,in_features为上一层神经元的个数,out_features为这一层的神经元个数

3)不需要定义w和b。所有nn.Module的子类,形如nn.XXX的层,都会在实例化的同时随机生成w和b的初始值。所以实例化之后,我们就可以调用属性weight和bias来查看生成的w和b。其中w是必然会生成的,b是我们可以控制是否要生成的。在nn.Linear类中,有参数bias,默认 bias = True。如果我们希望不拟合常量b,在实例化时将参数bias设置为False即可。

4)由于w和b是随机生成的,所以同样的代码多次运行后的结果是不一致的。如果我们希望控制随机性,则可以使用torch中的random类。如:torch.random.manual_seed(420) #人为设置随机数种子

5)由于不需要定义常量b,因此在特征张量中,不需要留出与常数项相乘的那一列,只需要输入特征张量。

6)输入层只有一层,并且输入层的结构(神经元的个数)由输入的特征张量X决定,因此在PyTorch中构筑神经网络时,不需要定义输入层。

7)实例化之后,将特征张量输入到实例化后的类中。
参考:https://blog.csdn.net/qq_44289607/article/details/122750531

1. 使用pytorch的预定义算子来重新实现二分类任务。

4.3.1 利用预定义算子重新实现前馈神经网络

#使用Paddle的预定义算子来重新实现二分类任务。主要使用到paddle.nn.Linear。

import paddle.nn as nn
import paddle.nn.functional as F
from paddle.nn.initializer import Constant, Normal, Uniform

class Model_MLP_L2_V2(paddle.nn.Layer):
    def __init__(self, input_size, hidden_size, output_size):
        super(Model_MLP_L2_V2, self).__init__()
        # 使用'paddle.nn.Linear'定义线性层。
        # 其中第一个参数(in_features)为线性层输入维度;第二个参数(out_features)为线性层输出维度
        # weight_attr为权重参数属性,这里使用'paddle.nn.initializer.Normal'进行随机高斯分布初始化
        # bias_attr为偏置参数属性,这里使用'paddle.nn.initializer.Constant'进行常量初始化

        self.fc1 = nn.Linear(input_size, hidden_size,
                                weight_attr=paddle.ParamAttr(initializer=Normal(mean=0., std=1.)),
                                bias_attr=paddle.ParamAttr(initializer=Constant(value=0.0)))
        self.fc2 = nn.Linear(hidden_size, output_size,
                                weight_attr=paddle.ParamAttr(initializer=Normal(mean=0., std=1.)),
                                bias_attr=paddle.ParamAttr(initializer=Constant(value=0.0)))
        # 使用'paddle.nn.functional.sigmoid'定义 Logistic 激活函数
        self.act_fn = F.sigmoid
        
    # 前向计算
    def forward(self, inputs):
        z1 = self.fc1(inputs)
        a1 = self.act_fn(z1)
        z2 = self.fc2(a1)
        a2 = self.act_fn(z2)
        return a2

使用pytorch的预定义算子来重新实现


class Model_MLP_L2_V2(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Model_MLP_L2_V2, self).__init__()
       
        self.fc1 = nn.Linear(input_size, hidden_size)
        w1 = torch.empty(hidden_size,input_size)
        b1=  torch.empty(hidden_size)
        self.fc1.weight=torch.nn.Parameter(normal_(w1,mean=0.0, std=1.0))
        self.fc1.bias=torch.nn.Parameter(constant_(b1,val=0.0))

        self.fc2 = nn.Linear(hidden_size, output_size)
        w2 = torch.empty(output_size,hidden_size)
        b2 = torch.empty(output_size)
        self.fc2.weight=torch.nn.Parameter(normal_(w2,mean=0.0, std=1.0))
        self.fc2.bias=torch.nn.Parameter(constant_(b2,val=0.0))
        
        self.act_fn = F.sigmoid
 
    # 前向计算
    def forward(self, inputs):
        z1 = self.fc1(inputs.to(torch.float32))  #修改后
        a1 = self.act_fn(z1)
        z2 = self.fc2(a1)
        a2 = self.act_fn(z2)
        return a2

4.3.2 完善Runner类

class RunnerV2_2(object):
    def __init__(self, model, optimizer, metric, loss_fn, **kwargs):
        self.model = model
        self.optimizer = optimizer
        self.loss_fn = loss_fn
        self.metric = metric

        # 记录训练过程中的评估指标变化情况
        self.train_scores = []
        self.dev_scores = []

        # 记录训练过程中的评价指标变化情况
        self.train_loss = []
        self.dev_loss = []

    def train(self, train_set, dev_set, **kwargs):
        # 将模型切换为训练模式
        self.model.train()

        # 传入训练轮数,如果没有传入值则默认为0
        num_epochs = kwargs.get("num_epochs", 0)
        # 传入log打印频率,如果没有传入值则默认为100
        log_epochs = kwargs.get("log_epochs", 100)
        # 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams"
        save_path = kwargs.get("save_path", "best_model.pdparams")

        # log打印函数,如果没有传入则默认为"None"
        custom_print_log = kwargs.get("custom_print_log", None)

        # 记录全局最优指标
        best_score = 0
        # 进行num_epochs轮训练
        for epoch in range(num_epochs):
            X, y = train_set

            # 获取模型预测
            logits = self.model(X.to(torch.float32))
            # 计算交叉熵损失
            trn_loss = self.loss_fn(logits, y)
            self.train_loss.append(trn_loss.item())
            # 计算评估指标
            trn_score = self.metric(logits, y).item()
            self.train_scores.append(trn_score)

            # 自动计算参数梯度
            trn_loss.backward()
            if custom_print_log is not None:
                # 打印每一层的梯度
                custom_print_log(self)

            # 参数更新
            self.optimizer.step()
            # 清空梯度
            self.optimizer.zero_grad()   # reset gradient

            dev_score, dev_loss = self.evaluate(dev_set)
            # 如果当前指标为最优指标,保存该模型
            if dev_score > best_score:
                self.save_model(save_path)
                print(f"[Evaluate] best accuracy performence has been updated: {best_score:.5f} --> {dev_score:.5f}")
                best_score = dev_score

            if log_epochs and epoch % log_epochs == 0:
                print(f"[Train] epoch: {epoch}/{num_epochs}, loss: {trn_loss.item()}")
    @torch.no_grad()
    def evaluate(self, data_set):
        # 将模型切换为评估模式
        self.model.eval()

        X, y = data_set
        # 计算模型输出
        logits = self.model(X)
        # 计算损失函数
        loss = self.loss_fn(logits, y).item()
        self.dev_loss.append(loss)
        # 计算评估指标
        score = self.metric(logits, y).item()
        self.dev_scores.append(score)
        return score, loss

    # 模型测试阶段,使用'torch.no_grad()'控制不计算和存储梯度
    @torch.no_grad()
    def predict(self, X):
        # 将模型切换为评估模式
        self.model.eval()
        return self.model(X)

    # 使用'model.state_dict()'获取模型参数,并进行保存
    def save_model(self, saved_path):
        torch.save(self.model.state_dict(), saved_path)

    # 使用'model.set_state_dict'加载模型参数
    def load_model(self, model_path):
        state_dict = torch.load(model_path)
        self.model.load_state_dict(state_dict)

4.3.3 模型训练

# 设置模型
input_size = 2
hidden_size = 5
output_size = 1
model = Model_MLP_L2_V4(input_size=input_size, hidden_size=hidden_size, output_size=output_size)

# 设置损失函数
loss_fn = F.binary_cross_entropy

# 设置优化器
learning_rate = 0.2 #5e-2
optimizer = torch.optim.SGD(model.parameters(),lr=learning_rate)

# 设置评价指标
metric = accuracy

# 其他参数
epoch = 2000
saved_path = 'best_model.pdparams'

# 实例化RunnerV2类,并传入训练配置
runner = RunnerV2_2(model, optimizer, metric, loss_fn)

runner.train([X_train, y_train], [X_dev, y_dev], num_epochs = epoch, log_epochs=50, save_path="best_model.pdparams")

plot(runner, 'fw-acc.pdf')

将训练过程中训练集与验证集的准确率变化情况进行可视化。

import matplotlib.pyplot as plt
 
# 可视化观察训练集与验证集的指标变化情况
def plot(runner, fig_name):
    plt.figure(figsize=(10, 5))
    epochs = [i for i in range(len(runner.train_scores))]
 
    plt.subplot(1, 2, 1)
    plt.plot(epochs, runner.train_loss, color='#e4007f', label="Train loss")
    plt.plot(epochs, runner.dev_loss, color='#f19ec2', linestyle='--', label="Dev loss")
    # 绘制坐标轴和图例
    plt.ylabel("loss", fontsize='large')
    plt.xlabel("epoch", fontsize='large')
    plt.legend(loc='upper right', fontsize='x-large')
 
    plt.subplot(1, 2, 2)
    plt.plot(epochs, runner.train_scores, color='#e4007f', label="Train accuracy")
    plt.plot(epochs, runner.dev_scores, color='#f19ec2', linestyle='--', label="Dev accuracy")
    # 绘制坐标轴和图例
    plt.ylabel("score", fontsize='large')
    plt.xlabel("epoch", fontsize='large')
    plt.legend(loc='lower right', fontsize='x-large')
 
    plt.savefig(fig_name)
    plt.show()
 
plot(runner, 'fw-acc.pdf')


4.3.4 性能评价

#模型评价
runner.load_model("best_model.pdparams")
score, loss = runner.evaluate([X_test, y_test])
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))

可视化图像:

# 均匀生成40000个数据点
x1, x2 = torch.meshgrid(torch.linspace(-math.pi, math.pi, 200), torch.linspace(-math.pi, math.pi, 200))
x = torch.stack([torch.flatten(x1), torch.flatten(x2)], dim=1)
 
# 预测对应类别
y = runner.predict(x)
y = torch.squeeze(torch.tensor((y>=0.5),dtype=torch.float32),dim=-1)
 
# 绘制类别区域
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(x[:,0].tolist(), x[:,1].tolist(), c=y.tolist(), cmap=plt.cm.Spectral)
 
plt.scatter(X_train[:, 0].tolist(), X_train[:, 1].tolist(), marker='*', c=torch.squeeze(y_train,dim=-1).tolist())
plt.scatter(X_dev[:, 0].tolist(), X_dev[:, 1].tolist(), marker='*', c=torch.squeeze(y_dev,dim=-1).tolist())
plt.scatter(X_test[:, 0].tolist(), X_test[:, 1].tolist(), marker='*', c=torch.squeeze(y_test,dim=-1).tolist())
 
plt.show()

2. 增加一个3个神经元的隐藏层,再次实现二分类,并与1做对比。

需要修改的代码:

class Model_MLP_L2_V2(nn.Module):
    def __init__(self, input_size, hidden_size1,hidden_size2,output_size):
        super(Model_MLP_L2_V2, self).__init__()
        
        self.fc1 = nn.Linear(input_size, hidden_size1)
        w1 = torch.empty(hidden_size1,input_size)
        b1=  torch.empty(hidden_size1)
        self.fc1.weight=torch.nn.Parameter(normal_(w1,mean=0.0, std=1.0))
        self.fc1.bias=torch.nn.Parameter(constant_(b1,val=0.0))
 
        self.fc2 = nn.Linear(hidden_size1, hidden_size2)
        w2 = torch.empty(hidden_size2,hidden_size1)
        b2 = torch.empty(hidden_size2)
        self.fc2.weight=torch.nn.Parameter(normal_(w2,mean=0.0, std=1.0))
        self.fc2.bias=torch.nn.Parameter(constant_(b2,val=0.0))
 
        self.fc3 = nn.Linear(hidden_size2, output_size)
        w3 = torch.empty(output_size, hidden_size2)
        b3 = torch.empty(output_size)
        self.fc3.weight = torch.nn.Parameter(normal_(w3, mean=0.0, std=1.0))
        self.fc3.bias = torch.nn.Parameter(constant_(b3, val=0.0))
 
        self.act_fn = F.sigmoid
 
    # 前向计算
    def forward(self, inputs):
        z1 = self.fc1(inputs)
        a1 = self.act_fn(z1)
        z2 = self.fc2(a1)
        a2 = self.act_fn(z2)
        z3=self.fc3(a2)
        a3=self.act_fn(z3)
        return a3
# 设置模型
input_size = 2
hidden_size1 = 5
hidden_size2=3
output_size = 1
model = Model_MLP_L2_V2(input_size=input_size, hidden_size1=hidden_size1,hidden_size2=hidden_size2, output_size=output_size)

 

增加了一个3个神经元的隐藏层后发现,训练结果score变高,loss变低,效果更好了。

试着将学习率由0.2调整为0.1看看:

变化比较明显,但效果不好。

所以试着将学习率变大,由0.2调整为1看看:

效果变得更好了。

3. 自定义隐藏层层数和每个隐藏层中的神经元个数,尝试找到最优超参数完成二分类。可以适当修改数据集,便于探索超参数。(选做)

  • 理论知识
  • 动手实践

理论知识:

一、关于神经网络中隐藏层和神经元的理解

1、隐藏层的意义:

要说明隐藏层的意义,需要从两个方面理解,一个是单个隐藏层的意义,一个是多层隐藏层的意义

(1)单个隐藏层的意义:

隐藏层的意义就是把输入数据的特征,抽象到另一个维度空间,来展现其更抽象化的特征,这些特征能更好的进行线性划分。

(2)多个隐藏层的意义:

多个隐藏层其实是对输入特征多层次的抽象,最终的目的就是为了更好的线性划分不同类型的数据(隐藏层的作用)

那么,是不是隐藏层约多就越好呢,可以特征划分的更清楚啊?
理论上是这样的,但实际这样会带来两个问题

  1.  层数越多参数会爆炸式增多
  2. 到了一定层数,再往深了加隐藏层,分类效果的增强会越来越不明显。上面那个例子,两层足以划分人是有多帅,再加几层是要得到什么特征呢?这些特征对划分没有什么提升了。

2、神经元的意义:

联系到为什么西瓜书讲神经网络之前要讲logistic regression。就是所谓的感知器的意义。

可以把一个神经元(感知器)的作用理解成为一种线性划分方式。
一个决策边界,一个神经元,就是线性划分,多个神经元,就是多个线性划分,而多个线性划分就是不断在逼近决策边界。可以把这个过程想象成积分过程。

一个决策边界就是又多个线性划分组成的。

那么如果神经元数量很多很多,这就导致会有很多个线性划分,决策边界就越扭曲,基本上就是过拟合了。

换一个角度来说神经元,把它理解为学习到一种东西的物体,如果这种物体越多,学到的东西就越来越像样本,也就是过拟合。
二、关于神经网络中隐藏层层数以及神经元个数的确定

1.确定隐藏层的层数

神经网络主要由输入层,隐藏层以及输出层构成,合理的选择神经网络的层数以及隐藏层神经元的个数,会在很大程度上影响模型的性能(不论是进行分类还是回归任务)。

输入层的节点数量以及输出层的节点数量是最容易获得的。输入层的神经元数量等于数据的特征数量(feature个数)。若为回归,则输出层的神经元数量等于1;若为分类,则输出层的神经元数量为分类的类别个数(如区分猫狗,则为2;区分手写数字0-9,则为10)。

对于一些很简单的数据集,一层甚至两层隐藏元都已经够了,隐藏层的层数不一定设置的越好,过多的隐藏层可能会导致数据过拟合。对于自然语言处理以及CV领域,则建议增加网络层数。

隐藏层的层数与神经网络的结果如下表所示:

如何确定隐藏层的层数
隐藏层层数 用途
无  仅能够表示线性可分函数或决策
可以拟合任何“从一个有限空间到另一个有限空间的连续映射”的函数
搭配适当的激活函数(比如Relu)可以表示任意精度的任意决策边界,并且可以拟合任何精度的任何平滑映射
>2多出来的隐藏层可以学习复杂的描述(某种自动特征工程)

层数越深,理论上来说模型拟合函数的能力增强,效果会更好,但是实际上更深的层数可能会带来过拟合的问题,同时也会增加训练难度,使模型难以收敛。

2.确定隐藏层中的神经元数量

在隐藏层中使用太少的神经元将导致欠拟合(underfitting)。

相反,使用过多的神经元同样会导致一些问题。首先,隐藏层中的神经元过多可能会导致过拟合(overfitting)。

当神经网络具有过多的节点时,训练集中包含的有限信息量不足以训练隐藏层中的所有神经元,因此就会导致过拟合。即使训练数据包含的信息量足够,隐藏层中过多的神经元会增加训练时间,从而难以达到预期的效果。显然,选择一个合适的隐藏层神经元数量是至关重要的。

通常对于某些数据集,拥有较大的第一层并在其后跟随较小的层将导致更好的性能,因为第一层可以学习很多低阶的特征,这些较低层的特征可以馈入后续层中,提取出较高阶特征。

需要注意的是,与在每一层中添加更多的神经元相比,添加层层数将获得更大的性能提升。因此,不要在一个隐藏层中加入过多的神经元。

按照经验来说,神经元数量可以由以下规则来确定:

还有另一种方法可供参考,神经元数量通常可以由一下几个原则大致确定:

  •     隐藏神经元的数量应在输入层的大小和输出层的大小之间。
  •     隐藏神经元的数量应为输入层大小的2/3加上输出层大小的2/3。
  •     隐藏神经元的数量应小于输入层大小的两倍。

总而言之,隐藏层神经元是最佳数量需要自己通过不断试验来进行微调,建议从一个较小数值比如1到3层和1到100个神经元开始。

如果欠拟合然后慢慢添加更多的层和神经元,如果过拟合就减小层数和神经元。此外,在实际过程中还可以考虑引入Batch Normalization, Dropout, 正则化等降低过拟合的方法。

同时神经元的数量也可以参考以下公式来确定:

其中:

Nh是输入层神经元个数;

No是输出层神经元个数;

Ns是训练集的样本数;

α是任意值变量,通常取值范围为2-10。

在前馈神经网络中,隐藏层的数量和层数的确定尚无依据,一般是由经验决定。

这里需要明确的一点是,这些只是根据经验提出的一些参考的方法,具体的层数和大小还是要在实际实验中进行验证。

参考链接:https://blog.csdn.net/sinat_38079265/article/details/121519632
https://blog.csdn.net/chinwuforwork/article/details/84141078

http://t.csdn.cn/2YF3e

动手实践:

可以调整的参数很多:

  • 隐藏层层数(二分类问题比较简单,没必要那么多层隐藏层)
  • 每个隐藏层中的神经元个数
  • 学习率(由第2个任务可以看出学习率对运行效果也有明显影响)
  • epoch_num

在这个任务里,将学习率设置为0.2,epoch_num为1000不变,只调整隐藏层层数和每个隐藏层中的神经元个数。

1、隐藏层层数为1

(1)神经元个数为5

(2)变小,神经元个数为3

效果差不多。

(3)变大,神经元个数为7

效果变好了一些。

(4)变大,神经元个数为8

 效果不如个数为7。

2、隐藏层层数为2(将学习率调整为1)

(1)hidden_size1 = 5 ,hidden_size2 = 3

效果很好。

(2)hidden_size1 = 5 ,hidden_size2 = 4

效果变得更好了。

(3)hidden_size1 = 5 ,hidden_size2 = 5 

调整后效果不理想。

(4)hidden_size1 = 3 ,hidden_size2 = 3

效果并不好。

【思考题】

自定义梯度计算和自动梯度计算:从计算性能、计算结果等多方面比较,谈谈自己的看法。

在PyTorch中,torch.Tensor类是存储和变换数据的重要工具,相比于Numpy,Tensor提供GPU计算和自动求梯度等更多功能,在深度学习中,我们经常需要对函数求梯度(gradient)。PyTorch提供的autograd包能够根据输入和前向传播过程自动构建计算图,并执行反向传播。

Tensor是这个pytorch的自动求导部分的核心类,如果将其属性.requires_grad=True,它将开始追踪(track) 在该tensor上的所有操作,从而实现利用链式法则进行的梯度传播。完成计算后,可以调用.backward()来完成所有梯度计算。此Tensor的梯度将累积到.grad属性中。
如果不想要被继续对tensor进行追踪,可以调用.detach()将其从追踪记录中分离出来,接下来的梯度就传不过去了。此外,还可以用with torch.no_grad()将不想被追踪的操作代码块包裹起来,这种方法在评估模型的时候很常用,因为此时并不需要继续对梯度进行计算。

Function是另外一个很重要的类。Tensor和Function互相结合就可以构建一个记录有整个计算过程的有向无环图(DAG)。每个Tensor都有一个.grad_fn属性,该属性即创建该Tensor的Function, 就是说该Tensor是不是通过某些运算得到的,若是,则grad_fn返回一个与这些运算相关的对象,否则是None。

(参考链接:https://blog.csdn.net/weixin_43199584/article/details/106876811)

上次实验用的是自定义梯度计算,这次实验用的是自动梯度计算,将这两次的结果比较来看:

自定义梯度计算:

[Test] score/loss: 0.7674/0.4386

自动梯度计算:

[Test] score/loss: 0.9100/0.2619

比较后发现自动梯度计算的效果更好。
 

4.4 优化问题

在本节中,我们通过实践来发现神经网络模型的优化问题,并思考如何改进。

4.4.1 参数初始化

实现一个神经网络前,需要先初始化模型参数。如果对每一层的权重和偏置都用0初始化,那么通过第一遍前向计算,所有隐藏层神经元的激活值都相同;在反向传播时,所有权重的更新也都相同,这样会导致隐藏层神经元没有差异性,出现对称权重现象

接下来,将模型参数全都初始化为0,看实验结果。这里重新定义了一个类TwoLayerNet_Zeros,两个线性层的参数全都初始化为0。

import torch.nn as nn
import torch.nn.functional as F

class Model_MLP_L2_V4(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Model_MLP_L2_V4, self).__init__()
        # 使用'torch.nn.Linear'定义线性层。
        # 其中in_features为线性层输入维度;out_features为线性层输出维度
        # weight_attr为权重参数属性
        # bias_attr为偏置参数属性
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, output_size)
        # 使用'torch.nn.functional.sigmoid'定义 Logistic 激活函数
        self.act_fn = F.sigmoid
        
    # 前向计算
    def forward(self, inputs):
        z1 = self.fc1(inputs)
        a1 = self.act_fn(z1)
        z2 = self.fc2(a1)
        a2 = self.act_fn(z2)
        return a2
def print_weights(runner):
    print('The weights of the Layers:')
    for item in runner.model.named_parameters():
        print(item)
    for _,param in enumerate(runner.model.named_parameters()):
        print(param)

利用Runner类训练模型:

# 设置模型
input_size = 2
hidden_size = 5
output_size = 1
model = Model_MLP_L2_V4(input_size=input_size, hidden_size=hidden_size, output_size=output_size)

# 设置损失函数
loss_fn = F.binary_cross_entropy

# 设置优化器
learning_rate = 0.2 #5e-2
optimizer = torch.optim.SGD(model.parameters(),lr=learning_rate)

# 设置评价指标
metric = accuracy

# 其他参数
epoch = 2000
saved_path = 'best_model.pt'

# 实例化RunnerV2类,并传入训练配置
runner = RunnerV2_2(model, optimizer, metric, loss_fn)

runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=5, log_epochs=50, save_path="best_model.pt",custom_print_log=print_weights)

可视化训练和验证集上的主准确率和loss变化:

plot(runner, "fw-zero.pdf")

 

从输出结果看,二分类准确率为50%左右,说明模型没有学到任何内容。训练和验证loss几乎没有怎么下降。

为了避免对称权重现象,可以使用高斯分布或均匀分布初始化神经网络的参数。

高斯分布和均匀分布采样的实现和可视化代码如下:

# 使用'torch.normal'实现高斯分布采样,其中'mean'为高斯分布的均值,'std'为高斯分布的标准差,'shape'为输出形状
gausian_weights = torch.normal(mean=0.0, std=1.0, size=[10000])# 使用'torch.uniform'实现在[min,max)范围内的均匀分布采样,其中'shape'为输出形状
uniform_weights = torch.Tensor(10000)
uniform_weights.uniform_(-1,1)
gausian_weights=gausian_weights.numpy()
uniform_weights=uniform_weights.numpy()
print(uniform_weights)# 绘制两种参数分布
print(gausian_weights)
plt.figure()
plt.subplot(1,2,1)
plt.title('Gausian Distribution')
plt.hist(gausian_weights, bins=200, density=True, color='#f19ec2')
plt.subplot(1,2,2)
plt.title('Uniform Distribution')
plt.hist(uniform_weights, bins=200, density=True, color='#e4007f')
plt.savefig('fw-gausian-uniform.pdf')
plt.show()

4.4.2 梯度消失问题

在神经网络的构建过程中,随着网络层数的增加,理论上网络的拟合能力也应该是越来越好的。但是随着网络变深,参数学习更加困难,容易出现梯度消失问题。

由于Sigmoid型函数的饱和性,饱和区的导数更接近于0,误差经过每一层传递都会不断衰减。当网络层数很深时,梯度就会不停衰减,甚至消失,使得整个网络很难训练,这就是所谓的梯度消失问题。
在深度神经网络中,减轻梯度消失问题的方法有很多种,一种简单有效的方式就是使用导数比较大的激活函数,如:ReLU。

4.4.2.1 模型构建

定义一个前馈神经网络,包含4个隐藏层和1个输出层,通过传入的参数指定激活函数。代码实现如下: 

# 定义多层前馈神经网络
class Model_MLP_L5(nn.Module):
    def __init__(self, input_size, output_size, act='sigmoid', w_init=torch.normal(mean=torch.tensor(0.0), std=torch.tensor(0.01)), b_init=torch.tensor(1.0)):
        super(Model_MLP_L5, self).__init__()
        self.fc1 = torch.nn.Linear(input_size, 3)
        self.fc2 = torch.nn.Linear(3, 3)
        self.fc3 = torch.nn.Linear(3, 3)
        self.fc4 = torch.nn.Linear(3, 3)
        self.fc5 = torch.nn.Linear(3, output_size)
        # 定义网络使用的激活函数
        if act == 'sigmoid':
            self.act = F.sigmoid
        elif act == 'relu':
            self.act = F.relu
        elif act == 'lrelu':
            self.act = F.leaky_relu
        else:
            raise ValueError("Please enter sigmoid relu or lrelu!")
        # 初始化线性层权重和偏置参数
        self.init_weights(w_init, b_init)

    # 初始化线性层权重和偏置参数
    def init_weights(self, w_init, b_init):
        # 使用'named_sublayers'遍历所有网络层
        for n, m in self.named_parameters():
            # 如果是线性层,则使用指定方式进行参数初始化
            if isinstance(m, nn.Linear):
                w_init(m.weight)
                b_init(m.bias)

    def forward(self, inputs):
        outputs = self.fc1(inputs)
        outputs = self.act(outputs)
        outputs = self.fc2(outputs)
        outputs = self.act(outputs)
        outputs = self.fc3(outputs)
        outputs = self.act(outputs)
        outputs = self.fc4(outputs)
        outputs = self.act(outputs)
        outputs = self.fc5(outputs)
        outputs = F.sigmoid(outputs)
        return outputs

4.4.2.2 使用Sigmoid型函数进行训练

使用Sigmoid型函数作为激活函数,为了便于观察梯度消失现象,只进行一轮网络优化。代码实现如下:

定义梯度打印函数

def print_grads(runner):
    # 打印每一层的权重的模
    print('The gradient of the Layers:')
    for name, item in runner.model.named_parameters():
        if(len(item.size())==2):
             print(name, torch.norm(input=item, p=2))
# 学习率大小
lr = 0.01

# 定义网络,激活函数使用sigmoid
model =  Model_MLP_L5(input_size=2, output_size=1, act='sigmoid')

# 定义优化器
optimizer = torch.optim.SGD(model.parameters(),lr=lr)

# 定义损失函数,使用交叉熵损失函数
loss_fn = F.binary_cross_entropy

# 定义评价指标
metric = accuracy

# 指定梯度打印函数
custom_print_log=print_grads

 实例化RunnerV2_2类,并传入训练配置。代码实现如下:

# 实例化Runner类
runner = RunnerV2_2(model, optimizer, metric, loss_fn)

模型训练,打印网络每层梯度值的ℓ2范数。代码实现如下:

# 启动训练
runner.train([X_train, y_train], [X_dev, y_dev], 
            num_epochs=10, log_epochs=None, 
            save_path="best_model.pdparams", 
            custom_print_log=custom_print_log)

观察实验结果可以发现,梯度经过每一个神经层的传递都会不断衰减,最终传递到第一个神经层时,梯度几乎完全消失。 

4.4.2.3 使用ReLU函数进行模型训练

lr = 0.01  # 学习率大小

# 定义网络,激活函数使用relu
model =  Model_MLP_L5(input_size=2, output_size=1, act='relu')

# 定义优化器
optimizer = torch.optim.SGD(model.parameters(),lr=lr)

# 定义损失函数
# 定义损失函数,这里使用交叉熵损失函数
loss_fn = F.binary_cross_entropy

# 定义评估指标
metric = accuracy

# 实例化Runner
runner = RunnerV2_2(model, optimizer, metric, loss_fn)

# 启动训练
runner.train([X_train, y_train], [X_dev, y_dev], 
            num_epochs=10, log_epochs=None, 
            save_path="best_model.pdparams", 
            custom_print_log=custom_print_log)

图4.4 展示了使用不同激活函数时,网络每层梯度值的ℓ2范数情况。从结果可以看到,5层的全连接前馈神经网络使用Sigmoid型函数作为激活函数时,梯度经过每一个神经层的传递都会不断衰减,最终传递到第一个神经层时,梯度几乎完全消失。改为ReLU激活函数后,梯度消失现象得到了缓解,每一层的参数都具有梯度值。

     图4.4:网络每层梯度的L2范数变化趋势

4.4.3 死亡ReLU问题

ReLU激活函数可以一定程度上改善梯度消失问题,但是在某些情况下容易出现死亡ReLU问题,使得网络难以训练。

这是由于当x<0x<0时,ReLU函数的输出恒为0。在训练过程中,如果参数在一次不恰当的更新后,某个ReLU神经元在所有训练数据上都不能被激活(即输出为0),那么这个神经元自身参数的梯度永远都会是0,在以后的训练过程中永远都不能被激活。

一种简单有效的优化方式就是将激活函数更换为Leaky ReLU、ELU等ReLU的变种。

4.4.3.1 使用ReLU进行模型训练

使用第4.4.2节中定义的多层全连接前馈网络进行实验,使用ReLU作为激活函数,观察死亡ReLU现象和优化方法。当神经层的偏置被初始化为一个相对于权重较大的负值时,可以想像,输入经过神经层的处理,最终的输出会为负值,从而导致死亡ReLU现象。

# 定义网络,并使用较大的负值来初始化偏置
model =  Model_MLP_L5(input_size=2, output_size=1, act='relu', b_init=torch.tensor(-8.0))

实例化RunnerV2类,启动模型训练,打印网络每层梯度值的ℓ2范数。代码实现如下:

# 实例化Runner类
runner = RunnerV2_2(model, optimizer, metric, loss_fn)

# 启动训练
runner.train([X_train, y_train], [X_dev, y_dev], 
            num_epochs=1, log_epochs=0, 
            save_path="best_model.pt", 
            custom_print_log=custom_print_log)

从输出结果可以发现,使用 ReLU 作为激活函数,当满足条件时,会发生死亡ReLU问题,网络训练过程中 ReLU 神经元的梯度始终为0,参数无法更新。

针对死亡ReLU问题,一种简单有效的优化方式就是将激活函数更换为Leaky ReLU、ELU等ReLU 的变种。接下来,观察将激活函数更换为 Leaky ReLU时的梯度情况。

4.4.3.2 使用Leaky ReLU进行模型训练

将激活函数更换为Leaky ReLU进行模型训练,观察梯度情况。代码实现如下:

# 重新定义网络,使用Leaky ReLU激活函数
model =  Model_MLP_L5(input_size=2, output_size=1, act='lrelu', b_init=torch.tensor(-8.0))

# 实例化Runner类
runner = RunnerV2_2(model, optimizer, metric, loss_fn)

# 启动训练
runner.train([X_train, y_train], [X_dev, y_dev], 
            num_epochs=10, log_epochps=None, 
            save_path="best_model.pdparams", 
            custom_print_log=custom_print_log)

从输出结果可以看到,将激活函数更换为Leaky ReLU后,死亡ReLU问题得到了改善,梯度恢复正常,参数也可以正常更新。但是由于 Leaky ReLU 中,x<0 时的斜率默认只有0.01,所以反向传播时,随着网络层数的加深,梯度值越来越小。如果想要改善这一现象,将 Leaky ReLU 中,x<0 时的斜率调大即可。 

记录一下:

1、本次实验学习到了torch.nn.Module包,以及自定义梯度计算和自动梯度计算两者的区别,还有paddle与pytorch之间的转换,这些对自己来说还是有难度的,还是需要课下花更多的时间去理解,动手去练习。

在做实验的过程中,在网上查询时,看到了很多很好的资料,借鉴引用到了本篇文章中,但感觉还是没有完全吃透那些内容(真的好多,自己太菜,看到稍微有用的触及自己盲区的,都收藏了起来,已经很多篇了),这些内容还是要课下花时间去学习透彻,去动手练习,真正学到自己的脑子里面。

2、本次实验很大的收获就是自己动手尝试调参,花了很多时间,试了很多,并没有全部写到博客内,但还是在电脑因为运行偶尔发出的嗡嗡声中加深了对模型的印象和理解。

3、这篇博客其实是删除以后第二次发布的了,增加了4.3中的 3 选做部分。刚开始做的时候,没有做选做的内容,做完以后第一次发布的时候是在晚上,当时已经挺晚的了,但是发布以后,脑子里面还一直都是实验的内容,感觉越做实验越兴奋了,大脑都一直活跃着,也不困,就继续做了一下选做的内容,感觉很有意思,就把已经发布的博客删除了,补充了选做内容后重新发布。

参考链接:NNDL 实验五 前馈神经网络(2)自动梯度计算 & 优化问题_HBU_David的博客-CSDN博客

NNDL 实验五 前馈神经网络(2)自动梯度计算 & 优化问题_别被打脸的博客-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值