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.Module类是所有神经网络模块(modules)的基类,它的实现在torch/nn/modules/module.py中。你的模型也应该继承这个类,主要重载__init__、forward和extra_repr函数。Modules还可以包含其它Modules,从而可以将它们嵌套在树结构中。
      只要在自己的类中定义了forward函数,backward函数就会利用Autograd被自动实现。只要实例化一个对象并传入对应的参数就可以自动调用forward函数。因为此时会调用对象的__call__方法,而nn.Module类中的__call__方法会调用forward函数。

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

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

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

class Model_MLP_L2_V2(torch.nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Model_MLP_L2_V4, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        w=torch.normal(0,0.1,size=(hidden_size,input_size),requires_grad=True)
        self.fc1.weight = nn.Parameter(w)

        self.fc2 = nn.Linear(hidden_size, output_size)
        w = torch.normal(0, 0.1, size=(output_size, hidden_size), requires_grad=True)
        self.fc2.weight = nn.Parameter(w)

        # 使用'torch.nn.functional.sigmoid'定义 Logistic 激活函数
        self.act_fn = torch.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')
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.46875
[Train] epoch: 0/2000, loss: 0.6927348971366882
[Train] epoch: 50/2000, loss: 0.6835539937019348
[Evaluate] best accuracy performence has been updated: 0.46875 --> 0.47500
[Evaluate] best accuracy performence has been updated: 0.47500 --> 0.48125
[Evaluate] best accuracy performence has been updated: 0.48125 --> 0.51250
[Evaluate] best accuracy performence has been updated: 0.51250 --> 0.55000
[Evaluate] best accuracy performence has been updated: 0.55000 --> 0.56250
[Evaluate] best accuracy performence has been updated: 0.56250 --> 0.58750
[Evaluate] best accuracy performence has been updated: 0.58750 --> 0.60000
[Evaluate] best accuracy performence has been updated: 0.60000 --> 0.62500
[Evaluate] best accuracy performence has been updated: 0.62500 --> 0.65625
[Evaluate] best accuracy performence has been updated: 0.65625 --> 0.66875
[Evaluate] best accuracy performence has been updated: 0.66875 --> 0.68750
[Evaluate] best accuracy performence has been updated: 0.68750 --> 0.69375
[Evaluate] best accuracy performence has been updated: 0.69375 --> 0.70000
[Evaluate] best accuracy performence has been updated: 0.70000 --> 0.70625
[Evaluate] best accuracy performence has been updated: 0.70625 --> 0.71875
[Evaluate] best accuracy performence has been updated: 0.71875 --> 0.72500
[Evaluate] best accuracy performence has been updated: 0.72500 --> 0.73125
[Evaluate] best accuracy performence has been updated: 0.73125 --> 0.73750
[Evaluate] best accuracy performence has been updated: 0.73750 --> 0.74375
[Evaluate] best accuracy performence has been updated: 0.74375 --> 0.75000
[Train] epoch: 100/2000, loss: 0.6547828912734985
[Evaluate] best accuracy performence has been updated: 0.75000 --> 0.76250
[Evaluate] best accuracy performence has been updated: 0.76250 --> 0.77500
[Evaluate] best accuracy performence has been updated: 0.77500 --> 0.78125
[Evaluate] best accuracy performence has been updated: 0.78125 --> 0.78750
[Evaluate] best accuracy performence has been updated: 0.78750 --> 0.79375
[Evaluate] best accuracy performence has been updated: 0.79375 --> 0.80000
[Evaluate] best accuracy performence has been updated: 0.80000 --> 0.80625
[Evaluate] best accuracy performence has been updated: 0.80625 --> 0.81250
[Train] epoch: 150/2000, loss: 0.5826915502548218
[Train] epoch: 200/2000, loss: 0.49096494913101196
[Evaluate] best accuracy performence has been updated: 0.81250 --> 0.81875
[Evaluate] best accuracy performence has been updated: 0.81875 --> 0.82500
[Evaluate] best accuracy performence has been updated: 0.82500 --> 0.83125
[Train] epoch: 250/2000, loss: 0.42369627952575684
[Evaluate] best accuracy performence has been updated: 0.83125 --> 0.83750
[Evaluate] best accuracy performence has been updated: 0.83750 --> 0.84375
[Evaluate] best accuracy performence has been updated: 0.84375 --> 0.85000
[Evaluate] best accuracy performence has been updated: 0.85000 --> 0.85625
[Train] epoch: 300/2000, loss: 0.3816145360469818
[Train] epoch: 350/2000, loss: 0.353882372379303
[Evaluate] best accuracy performence has been updated: 0.85625 --> 0.86250
[Evaluate] best accuracy performence has been updated: 0.86250 --> 0.86875
[Evaluate] best accuracy performence has been updated: 0.86875 --> 0.87500
[Train] epoch: 400/2000, loss: 0.3344830870628357
[Evaluate] best accuracy performence has been updated: 0.87500 --> 0.88125
[Train] epoch: 450/2000, loss: 0.32065773010253906
[Train] epoch: 500/2000, loss: 0.3108382821083069
[Evaluate] best accuracy performence has been updated: 0.88125 --> 0.88750
[Train] epoch: 550/2000, loss: 0.30390477180480957
[Evaluate] best accuracy performence has been updated: 0.88750 --> 0.89375
[Train] epoch: 600/2000, loss: 0.2990153729915619
[Evaluate] best accuracy performence has been updated: 0.89375 --> 0.90000
[Train] epoch: 650/2000, loss: 0.2955561578273773
[Evaluate] best accuracy performence has been updated: 0.90000 --> 0.90625
[Train] epoch: 700/2000, loss: 0.29309362173080444
[Train] epoch: 750/2000, loss: 0.2913265824317932
[Train] epoch: 800/2000, loss: 0.2900467813014984
[Train] epoch: 850/2000, loss: 0.28911006450653076
[Train] epoch: 900/2000, loss: 0.28841620683670044
[Train] epoch: 950/2000, loss: 0.2878951132297516
[Train] epoch: 1000/2000, loss: 0.28749755024909973
[Train] epoch: 1050/2000, loss: 0.2871887683868408
[Train] epoch: 1100/2000, loss: 0.28694406151771545
[Train] epoch: 1150/2000, loss: 0.2867458760738373
[Train] epoch: 1200/2000, loss: 0.2865816354751587
[Train] epoch: 1250/2000, loss: 0.28644222021102905
[Train] epoch: 1300/2000, loss: 0.28632116317749023
[Evaluate] best accuracy performence has been updated: 0.90625 --> 0.91250
[Train] epoch: 1350/2000, loss: 0.28621378540992737
[Train] epoch: 1400/2000, loss: 0.2861166298389435
[Train] epoch: 1450/2000, loss: 0.2860272526741028
[Train] epoch: 1500/2000, loss: 0.28594380617141724
[Train] epoch: 1550/2000, loss: 0.28586506843566895
[Train] epoch: 1600/2000, loss: 0.28579002618789673
[Train] epoch: 1650/2000, loss: 0.2857179343700409
[Train] epoch: 1700/2000, loss: 0.28564831614494324
[Train] epoch: 1750/2000, loss: 0.2855807840824127
[Train] epoch: 1800/2000, loss: 0.28551506996154785
[Train] epoch: 1850/2000, loss: 0.28545090556144714
[Train] epoch: 1900/2000, loss: 0.28538817167282104
[Train] epoch: 1950/2000, loss: 0.2853267788887024

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

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()

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


  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))

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

class Model_MLP_L2_V4(torch.nn.Module):
    def __init__(self, input_size, hidden_size, hidden_size2, output_size):
        super(Model_MLP_L2_V4, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        w1=torch.normal(0,0.1,size=(hidden_size,input_size),requires_grad=True)
        self.fc1.weight = nn.Parameter(w1)

        self.fc2 = nn.Linear(hidden_size, hidden_size2)
        w2 = torch.normal(0, 0.1, size=(hidden_size2, hidden_size), requires_grad=True)
        self.fc2.weight = nn.Parameter(w2)

        self.fc3 = nn.Linear(hidden_size2, output_size)
        w3 = torch.normal(0, 0.1, size=(output_size, hidden_size2), requires_grad=True)
        self.fc3.weight = nn.Parameter(w3)

        # 使用'torch.nn.functional.sigmoid'定义 Logistic 激活函数
        self.act_fn = torch.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)
        z3 = self.fc3(a2)
        a3 = self.act_fn(z3)
        return a3

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

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

 

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

 效果不稳定,将学习率由0.2改为2

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

 

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

 

 将学习率调整完之后,效果变好,结果也稳定了

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

相关知识转载于知乎文章如何确定神经网络的层数和隐藏层神经元数量

 BP神经网络主要由输入层隐藏层输出层构成,输入和输出层的节点数是固定的,不论是回归还是分类任务,选择合适的层数以及隐藏层节点数,在很大程度上都会影响神经网络的性能。

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

 输入层和输出层的节点数量很容易得到。输入层的神经元数量等于待处理数据中输入变量的数量,输出层的神经元的数量等于与每个输入关联的输出的数量。但是真正的困难之处在于确定合适的隐藏层及其神经元的数量

隐藏层的层数

如何确定隐藏层的层数是一个至关重要的问题。首先需要注意一点:

在神经网络中,当且仅当数据非线性分离时才需要隐藏层!

因此,对于一般简单的数据集,一两层隐藏层通常就足够了。但对于涉及时间序列或计算机视觉的复杂数据集,则需要额外增加层数。单层神经网络只能用于表示线性分离函数,也就是非常简单的问题,比如分类问题中的两个类可以用一条直线整齐地分开。

 概括来说就是多个隐藏层可以用于拟合非线性函数。

简要概括一下——

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

 层数越深,理论上拟合函数的能力增强,效果按理说会更好,但是实际上更深的层数可能会带来过拟合的问题,同时也会增加训练难度,使模型难以收敛。因此我的经验是,在使用BP神经网络时,最好可以参照已有的表现优异的模型,如果实在没有,则根据上面的表格,从一两层开始尝试,尽量不要使用太多的层数。在CV、NLP等特殊领域,可以使用CNN、RNN、attention等特殊模型,不能不考虑实际而直接无脑堆砌多层神经网络。尝试迁移和微调已有的预训练模型,能取得事半功倍的效果

确定隐藏的神经元层的数量只是问题的一小部分。还需要确定这些隐藏层中的每一层包含多少个神经元。

隐藏层中的神经元数量

在隐藏层中使用太少的神经元将导致欠拟合(underfitting)。相反,使用过多的神经元同样会导致一些问题。首先,隐藏层中的神经元过多可能会导致过拟合(overfitting)。当神经网络具有过多的节点(过多的信息处理能力)时,训练集中包含的有限信息量不足以训练隐藏层中的所有神经元,因此就会导致过拟合。即使训练数据包含的信息量足够,隐藏层中过多的神经元会增加训练时间,从而难以达到预期的效果。显然,选择一个合适的隐藏层神经元数量是至关重要的。

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

通常,对所有隐藏层使用相同数量的神经元就足够了。对于某些数据集,拥有较大的第一层并在其后跟随较小的层将导致更好的性能,因为第一层可以学习很多低阶的特征,这些较低层的特征可以馈入后续层中,提取出较高阶特征。

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

 如何确定神经元数量

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

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

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

总而言之,隐藏层神经元是最佳数量需要自己通过不断试验获得,建议从一个较小数值比如1到5层和1到100个神经元开始,如果欠拟合然后慢慢添加更多的层和神经元,如果过拟合就减小层数和神经元。此外,在实际过程中还可以考虑引入Batch Normalization, Dropout, 正则化等降低过拟合的方法。

一个隐藏层5个神经元

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

 

 一个隐藏层4个神经元

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

 

 神经元变为4效果变差了

一个隐藏层六个神经元

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

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

 改为六个神经元,效果变好

 一个隐藏层五个神经元 另一个隐藏层三个神经元

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

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

  一个隐藏层五个神经元 另一个隐藏层六个神经元

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

 效果变差

 通过对隐藏层层数和神经元数量的调整发现,在 一个隐藏层五个神经元 另一个隐藏层三个神经元的情况下效果最好


【思考题】

自定义梯度计算自动梯度计算:

从计算性能、计算结果等多方面比较,谈谈自己的看法。

计算性能:自动梯度计算采用记录历史操作的方法从根到叶子结点追踪图,利用链式法则进行计算。自定义梯度计算是手工推导公式,再代入数值进行计算。自定义梯度计算比自动 梯度计算快

计算结果:自定义梯度计算的话过程十分复杂并且容易出错,导致神经网络的实现效率低下;而自动梯度计算可以由框架中的函数自动计算计算准确率和效率都大幅提高。


4.4 优化问题


  4.4.1 参数初始化


实现一个神经网络前,需要先初始化模型参数。

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

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

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.init import constant_, normal_
 
class Model_MLP_L2_V4(torch.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为权重参数属性,bias为偏置参数属性,这里使用'torch.nn.init.constant_'进行常量初始化
        self.fc1 = nn.Linear(input_size, hidden_size)
        constant_(tensor=self.fc1.weight,val=0.0)
        constant_(tensor=self.fc1.bias,val=0.0)
        self.fc2 = nn.Linear(hidden_size, output_size)
        constant_(tensor=self.fc2.weight, val=0.0)
        constant_(tensor=self.fc2.bias, val=0.0)
        # 使用'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 _, param in enumerate(runner.model.named_parameters()):
        print(param)

利用Runner类训练模型:

from metric import accuracy
from dataset import make_moons
n_samples = 1000
X, y = make_moons(n_samples=n_samples, shuffle=True, noise=0.15)
 
num_train = 640
num_dev = 160
num_test = 200
 
X_train, y_train = X[:num_train], y[:num_train]
X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]
 
y_train = y_train.reshape([-1,1])
y_dev = y_dev.reshape([-1,1])
y_test = y_test.reshape([-1,1])
 
# 设置模型
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=5, log_epochs=50, save_path="best_model.pdparams",custom_print_log=print_weights)

The weights of the Layers:
('fc1.weight', Parameter containing:
tensor([[-4.1772e-05,  3.4384e-05],
        [-4.1772e-05,  3.4384e-05],
        [-4.1772e-05,  3.4384e-05],
        [-4.1772e-05,  3.4384e-05],
        [-4.1772e-05,  3.4384e-05]], requires_grad=True))
('fc1.bias', Parameter containing:
tensor([8.2898e-07, 8.2898e-07, 8.2898e-07, 8.2898e-07, 8.2898e-07],
       requires_grad=True))
('fc2.weight', Parameter containing:
tensor([[-0.0021, -0.0021, -0.0021, -0.0021, -0.0021]], requires_grad=True))
('fc2.bias', Parameter containing:
tensor([-0.0042], requires_grad=True))

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

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

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

 由结果可以看出二分类准确率为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()

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

  4.4.2 梯度消失问题


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

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

4.4.2.1 模型构建

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

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.init import constant_, normal_
 
# 定义多层前馈神经网络
class Model_MLP_L5(torch.nn.Module):
    def __init__(self, input_size, output_size, act='relu',mean_init=0.,std_init=0.01,b_init=1.0):
        super(Model_MLP_L5, self).__init__()
        self.fc1 = torch.nn.Linear(input_size, 3)
        normal_(tensor=self.fc1.weight, mean=mean_init, std=std_init)
        constant_(tensor=self.fc1.bias, val=b_init)
        self.fc2 = torch.nn.Linear(3, 3)
        normal_(tensor=self.fc2.weight, mean=mean_init, std=std_init)
        constant_(tensor=self.fc2.bias, val=b_init)
        self.fc3 = torch.nn.Linear(3, 3)
        normal_(tensor=self.fc3.weight, mean=mean_init, std=std_init)
        constant_(tensor=self.fc3.bias, val=b_init)
        self.fc4 = torch.nn.Linear(3, 3)
        normal_(tensor=self.fc4.weight, mean=mean_init, std=std_init)
        constant_(tensor=self.fc4.bias, val=b_init)
        self.fc5 = torch.nn.Linear(3, output_size)
        normal_(tensor=self.fc5.weight, mean=mean_init, std=std_init)
        constant_(tensor=self.fc5.bias, val=b_init)
        # 定义网络使用的激活函数
        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!")
 
 
    def forward(self, inputs):
        outputs = self.fc1(inputs.to(torch.float32))
        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 grad of the Layers:')
 
    for name, parms in runner.model.named_parameters():
        print('-->name:', name, ' -->grad_value:', parms.grad)
torch.random.manual_seed(102)
# 学习率大小
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
 
from metric import accuracy
 
# 定义评价指标
metric = accuracy
 
# 指定梯度打印函数
custom_print_log=print_grads

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

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

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

# 启动训练
runner.train([X_train, y_train], [X_dev, y_dev],
            num_epochs=1, log_epochs=None,
            save_path="best_model.pdparams",
            custom_print_log=custom_print_log)
The gradient of the Layers:
linear_0 3.4578118e-11
linear_1 1.828572e-08
linear_2 3.8579387e-06
linear_3 0.0026030989
linear_4 0.29877562
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.49250

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

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)

The gradient of the Layers:
linear_0 2.230126e-08
linear_1 3.3545493e-06
linear_2 0.0001365277
linear_3 0.0084260989
linear_4 0.38877562
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.52350

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

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

  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)
The gradient of the Layers:
linear_0 0.0
linear_1 0.0
linear_2 0.0
linear_3 0.0
linear_4 0.0
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.52931

从输出结果可以发现,使用 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)
The gradient of the Layers:
linear_0 4.0927596e-16
linear_1 6.1566283e-13
linear_2 2.4625623e-09
linear_3 1.5207311e-05
linear_4 0.070108235
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.49750
[Train] epoch: 0/1, loss: 4.251660133278516

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

ref:



如何确定神经网络的层数和隐藏层神经元数量

http://【PyTorch中nn.Module类简介】https://mbd.baidu.com/ma/s/QnH9zdwh

https://blog.csdn.net/qq_38975453/article/details/126772521

个人总结: 

这次实验让我印象最深的是选做题中对隐藏层神经元个数不断修改调试的过程,可以修改的量有很多 虽然不能做到探究的十分全面 但在通过自己动手一步步对比找到 性能较佳的模型还是令我颇有收获

你可能感兴趣的:(神经网络,人工智能,深度学习)