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


在这里插入图片描述

4.3 自动梯度计算

虽然我们能够通过模块化的方式比较好地对神经网络进行组装,但是每个模块的梯度计算过程仍然十分繁琐且容易出错。在深度学习框架中,已经封装了自动梯度计算的功能,我们只需要聚焦模型架构,不再需要耗费精力进行计算梯度。
飞桨提供了paddle.nn.Layer类,来方便快速的实现自己的层和模型。模型和层都可以基于paddle.nn.Layer扩充实现,模型只是一种特殊的层。继承了paddle.nn.Layer类的算子中,可以在内部直接调用其它继承paddle.nn.Layer类的算子,飞桨框架会自动识别算子中内嵌的paddle.nn.Layer类算子,并自动计算它们的梯度,并在优化时更新它们的参数。
pytorch中的相应内容是什么?请简要介绍。
torch提供了torch.nn.Module类,来方便快速的实现自己的层和模型。模型和层都可以基于nn扩充实现,模型只是一种特殊的层。它继承了torch.nn.Module类的算子中,可以在内部直接调用其它继承的算子,torch框架会自动识别算子中内嵌的torch.nn.Module类算子,并自动计算它们的梯度,并在优化时更新它们的参数。
在这里插入图片描述

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

paddle.nn.Linear(in_features, out_features, weight_attr=None, bias_attr=None, name=None)
在paddle.nn.Linear里可以直接设置w和b,但是在torch.nn.Linear里
在这里插入图片描述
手动设置一下w和b

import torch.nn as nn
import torch.nn.functional as F
import os
import torch
from abc import abstractmethod
import math
import numpy as np
from make_moon import make_moons
from metric import accuracy
import matplotlib.pyplot as plt
from torch.nn.init  import normal_,constant_,uniform_
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)
        normal_(self.fc1.weight, mean=0., std=1.)
        constant_(self.fc1.bias, val=0.0)
        self.fc2 = nn.Linear(hidden_size, output_size)
        normal_(self.fc2.weight, mean=0., std=1.)
        constant_(self.fc2.bias, val=0.0)
        self.act_fn = torch.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

4.3.1.1完善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)
            # 计算交叉熵损失
            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()
 
            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()}")
 
    # 模型评估阶段,使用'paddle.no_grad()'控制不计算和存储梯度
    @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
 
    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.1.2 模型训练

# 设置模型
input_size = 2
hidden_size = 5
output_size = 1
model = Model_MLP_L2_V2(input_size=input_size, hidden_size=hidden_size, output_size=output_size)
 
# 设置损失函数
loss_fn = F.binary_cross_entropy
 
# 设置优化器
learning_rate = 0.2
optimizer = torch.optim.SGD(lr=learning_rate, params=model.parameters())
 
# 设置评价指标
metric = accuracy
 
# 其他参数
epoch_num = 1000
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_num, log_epochs=50, save_path="best_model.pdparams")

运行结果:

[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.50625
[Train] epoch: 0/2000, loss: 0.6948854327201843
[Train] epoch: 50/2000, loss: 0.6906917095184326
[Evaluate] best accuracy performence has been updated: 0.50625 --> 0.51250
[Evaluate] best accuracy performence has been updated: 0.51250 --> 0.53125
[Evaluate] best accuracy performence has been updated: 0.53125 --> 0.55625
[Evaluate] best accuracy performence has been updated: 0.55625 --> 0.58750
[Evaluate] best accuracy performence has been updated: 0.58750 --> 0.60625
[Evaluate] best accuracy performence has been updated: 0.60625 --> 0.65000
[Evaluate] best accuracy performence has been updated: 0.65000 --> 0.66250
[Evaluate] best accuracy performence has been updated: 0.66250 --> 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.70625
[Evaluate] best accuracy performence has been updated: 0.70625 --> 0.71875
[Evaluate] best accuracy performence has been updated: 0.71875 --> 0.75000
[Evaluate] best accuracy performence has been updated: 0.75000 --> 0.76250
[Evaluate] best accuracy performence has been updated: 0.76250 --> 0.76875
[Evaluate] best accuracy performence has been updated: 0.76875 --> 0.77500
[Evaluate] best accuracy performence has been updated: 0.77500 --> 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
[Train] epoch: 100/2000, loss: 0.6803449392318726
[Evaluate] best accuracy performence has been updated: 0.80625 --> 0.81250
[Train] epoch: 150/2000, loss: 0.6429552435874939
[Train] epoch: 200/2000, loss: 0.5555383563041687
[Evaluate] best accuracy performence has been updated: 0.81250 --> 0.81875
[Evaluate] best accuracy performence has been updated: 0.81875 --> 0.82500
[Train] epoch: 250/2000, loss: 0.46066856384277344
[Evaluate] best accuracy performence has been updated: 0.82500 --> 0.83125
[Train] epoch: 300/2000, loss: 0.39852553606033325
[Train] epoch: 350/2000, loss: 0.3601153790950775
[Evaluate] best accuracy performence has been updated: 0.83125 --> 0.83750
[Evaluate] best accuracy performence has been updated: 0.83750 --> 0.84375
[Train] epoch: 400/2000, loss: 0.3340659737586975
[Evaluate] best accuracy performence has been updated: 0.84375 --> 0.85000
[Train] epoch: 450/2000, loss: 0.3152155578136444
[Evaluate] best accuracy performence has been updated: 0.85000 --> 0.85625
[Train] epoch: 500/2000, loss: 0.30130961537361145
[Train] epoch: 550/2000, loss: 0.2910915017127991
[Evaluate] best accuracy performence has been updated: 0.85625 --> 0.86250
[Train] epoch: 600/2000, loss: 0.28365302085876465
[Train] epoch: 650/2000, loss: 0.27827388048171997
[Evaluate] best accuracy performence has been updated: 0.86250 --> 0.86875
[Train] epoch: 700/2000, loss: 0.27439233660697937
[Evaluate] best accuracy performence has been updated: 0.86875 --> 0.87500
[Train] epoch: 750/2000, loss: 0.27158764004707336
[Train] epoch: 800/2000, loss: 0.26955336332321167
[Train] epoch: 850/2000, loss: 0.2680703401565552
[Train] epoch: 900/2000, loss: 0.2669827342033386
[Train] epoch: 950/2000, loss: 0.266179621219635
[Train] epoch: 1000/2000, loss: 0.2655821442604065
[Train] epoch: 1050/2000, loss: 0.26513391733169556
[Train] epoch: 1100/2000, loss: 0.26479440927505493
[Train] epoch: 1150/2000, loss: 0.2645344138145447
[Train] epoch: 1200/2000, loss: 0.26433277130126953
[Train] epoch: 1250/2000, loss: 0.2641741633415222
[Train] epoch: 1300/2000, loss: 0.26404738426208496
[Train] epoch: 1350/2000, loss: 0.26394417881965637
[Train] epoch: 1400/2000, loss: 0.2638585865497589
[Train] epoch: 1450/2000, loss: 0.2637861371040344
[Train] epoch: 1500/2000, loss: 0.26372361183166504
[Train] epoch: 1550/2000, loss: 0.26366859674453735
[Train] epoch: 1600/2000, loss: 0.2636193037033081
[Train] epoch: 1650/2000, loss: 0.26357436180114746
[Train] epoch: 1700/2000, loss: 0.263532817363739
[Train] epoch: 1750/2000, loss: 0.26349392533302307
[Train] epoch: 1800/2000, loss: 0.26345711946487427
[Train] epoch: 1850/2000, loss: 0.2634219825267792
[Train] epoch: 1900/2000, loss: 0.2633882462978363
[Train] epoch: 1950/2000, loss: 0.26335567235946655
[Test] score/loss: 0.8650/0.2953

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

# 可视化观察训练集与验证集的指标变化情况
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.1.3 性能评价

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

运行结果:

[Test] score/loss: 0.8850/0.3513

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

具体改动如下:

class Model_MLP_L2_V2(nn.Module):
    def __init__(self, input_size, output_size,mean_init=0.,std_init=1.,b_init=0.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, output_size)
        normal_(tensor=self.fc3.weight, mean=mean_init, std=std_init)
        constant_(tensor=self.fc3.bias, val=b_init)
        # 使用'torch.nn.functional.sigmoid'定义 Logistic 激活函数
        self.act = F.sigmoid
 
    # 前向计算
    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)
        return outputs
# 设置模型
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)

运行结果:

运行了几次,loss和score都特别不稳定。其中任意两次结果:

[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.48750
[Train] epoch: 0/2000, loss: 0.6951087117195129
[Evaluate] best accuracy performence has been updated: 0.48750 --> 0.52500
[Train] epoch: 50/2000, loss: 0.6928004026412964
[Train] epoch: 100/2000, loss: 0.6927953362464905
[Train] epoch: 150/2000, loss: 0.6927904486656189
[Train] epoch: 200/2000, loss: 0.6927850842475891
[Train] epoch: 250/2000, loss: 0.6927791237831116
[Train] epoch: 300/2000, loss: 0.6927725672721863
[Train] epoch: 350/2000, loss: 0.6927651166915894
[Train] epoch: 400/2000, loss: 0.6927568316459656
[Train] epoch: 450/2000, loss: 0.6927474737167358
[Train] epoch: 500/2000, loss: 0.6927368640899658
[Train] epoch: 550/2000, loss: 0.6927248239517212
[Train] epoch: 600/2000, loss: 0.6927108764648438
[Train] epoch: 650/2000, loss: 0.6926950216293335
[Train] epoch: 700/2000, loss: 0.6926764845848083
[Train] epoch: 750/2000, loss: 0.6926550269126892
[Train] epoch: 800/2000, loss: 0.6926299333572388
[Train] epoch: 850/2000, loss: 0.6926003694534302
[Train] epoch: 900/2000, loss: 0.6925650835037231
[Train] epoch: 950/2000, loss: 0.6925231218338013
[Train] epoch: 1000/2000, loss: 0.6924725770950317
[Train] epoch: 1050/2000, loss: 0.6924110054969788
[Train] epoch: 1100/2000, loss: 0.6923354864120483
[Train] epoch: 1150/2000, loss: 0.6922417879104614
[Train] epoch: 1200/2000, loss: 0.6921240091323853
[Train] epoch: 1250/2000, loss: 0.6919741034507751
[Train] epoch: 1300/2000, loss: 0.6917803287506104
[Train] epoch: 1350/2000, loss: 0.6915253400802612
[Train] epoch: 1400/2000, loss: 0.6911832094192505
[Train] epoch: 1450/2000, loss: 0.6907137632369995
[Train] epoch: 1500/2000, loss: 0.6900527477264404
[Train] epoch: 1550/2000, loss: 0.6890929341316223
[Train] epoch: 1600/2000, loss: 0.6876490712165833
[Train] epoch: 1650/2000, loss: 0.6853843927383423
[Evaluate] best accuracy performence has been updated: 0.52500 --> 0.53125
[Evaluate] best accuracy performence has been updated: 0.53125 --> 0.53750
[Evaluate] best accuracy performence has been updated: 0.53750 --> 0.54375
[Evaluate] best accuracy performence has been updated: 0.54375 --> 0.55000
[Evaluate] best accuracy performence has been updated: 0.55000 --> 0.56250
[Evaluate] best accuracy performence has been updated: 0.56250 --> 0.58125
[Evaluate] best accuracy performence has been updated: 0.58125 --> 0.58750
[Evaluate] best accuracy performence has been updated: 0.58750 --> 0.60000
[Evaluate] best accuracy performence has been updated: 0.60000 --> 0.61250
[Evaluate] best accuracy performence has been updated: 0.61250 --> 0.61875
[Evaluate] best accuracy performence has been updated: 0.61875 --> 0.63125
[Evaluate] best accuracy performence has been updated: 0.63125 --> 0.63750
[Evaluate] best accuracy performence has been updated: 0.63750 --> 0.65625
[Train] epoch: 1700/2000, loss: 0.6816560626029968
[Evaluate] best accuracy performence has been updated: 0.65625 --> 0.66250
[Evaluate] best accuracy performence has been updated: 0.66250 --> 0.66875
[Evaluate] best accuracy performence has been updated: 0.66875 --> 0.67500
[Evaluate] best accuracy performence has been updated: 0.67500 --> 0.68125
[Evaluate] best accuracy performence has been updated: 0.68125 --> 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.71250
[Evaluate] best accuracy performence has been updated: 0.71250 --> 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
[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
[Train] epoch: 1750/2000, loss: 0.675173282623291
[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
[Evaluate] best accuracy performence has been updated: 0.81250 --> 0.81875
[Train] epoch: 1800/2000, loss: 0.6632643938064575
[Train] epoch: 1850/2000, loss: 0.6406115889549255
[Train] epoch: 1900/2000, loss: 0.5991426706314087
[Train] epoch: 1950/2000, loss: 0.5365291833877563
[Test] score/loss: 0.7700/0.6734

在这里插入图片描述

[Test] score/loss: 0.4850/0.6932

在这里插入图片描述
调大学习率,Ir=1:

[Test] score/loss: 0.8700/0.3023

在这里插入图片描述
Ir=3:

[Test] score/loss: 0.9800/0.0626

在这里插入图片描述
Ir=5:

[Test] score/loss: 0.9900/0.0527

在这里插入图片描述
通过运行结果看出,增加了一层含有神经元的隐藏层后,误差和学习率均往好的方面走了!通过调试学习率,使结果也稳定了下来。

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

首先我们先了解一下如何确定隐藏层的层数以及隐藏层中的神经元数量:

1.确定隐藏层的层数
对于一些很简单的数据集,一层甚至两层隐藏元都已经够了,隐藏层的层数不一定设置的越好,过多的隐藏层可能会导致数据过拟合。对于自然语言处理以及CV领域,则建议增加网络层数。
隐藏层的层数与神经网络的结果如下表所示:
在这里插入图片描述
层数越深,理论上来说模型拟合函数的能力增强,效果会更好,但是实际上更深的层数可能会带来过拟合的问题,同时也会增加训练难度,使模型难以收敛。
2.确定隐藏层中的神经元数量
在隐藏层中使用太少的神经元将导致欠拟合(underfitting)。
相反,使用过多的神经元同样会导致一些问题。首先,隐藏层中的神经元过多可能会导致过拟合(overfitting)。
当神经网络具有过多的节点时,训练集中包含的有限信息量不足以训练隐藏层中的所有神经元,因此就会导致过拟合。即使训练数据包含的信息量足够,隐藏层中过多的神经元会增加训练时间,从而难以达到预期的效果。显然,选择一个合适的隐藏层神经元数量是至关重要的。
在这里插入图片描述
通常对于某些数据集,拥有较大的第一层并在其后跟随较小的层将导致更好的性能,因为第一层可以学习很多低阶的特征,这些较低层的特征可以馈入后续层中,提取出较高阶特征。

需要注意的是,与在每一层中添加更多的神经元相比,添加层层数将获得更大的性能提升。因此,不要在一个隐藏层中加入过多的神经元。
按照经验来说,神经元数量可以由以下规则来确定:
还有另一种方法可供参考,神经元数量通常可以由一下几个原则大致确定:
(1)隐藏神经元的数量应在输入层的大小和输出层的大小之间。
(2)隐藏神经元的数量应为输入层大小的2/3加上输出层大小的2/3。
(3)隐藏神经元的数量应小于输入层大小的两倍。

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

同时神经元的数量也可以参考以下公式来确定:
在这里插入图片描述
其中:
Nh是输入层神经元个数;
No是输出层神经元个数;
Ns是训练集的样本数;
α是任意值变量,通常取值范围为2-10。

这是一个二分类问题,所以我认为隐藏层有一个或者两个就可以了,然后我们进行神经元的设置。边的都以学习率为5做实验!
隐藏层5个神经元:

[Test] score/loss: 0.8600/0.3191

在这里插入图片描述
隐藏层4个神经元:

[Test] score/loss: 0.9750/0.0888

在这里插入图片描述
这个结果很好,然后我决定试试两个隐藏层都是3个神经元再试试:
隐藏层3个神经元:

[Test] score/loss: 0.9600/0.1506

在这里插入图片描述
学习率下降了一些。前边一直以学习率为3,现在调一下学习率变为5:

[Test] score/loss: 0.9750/0.0771

在这里插入图片描述
通过以上结果来看,我设置的两个隐层,两个神经元个数都为4,学习率为5的情况下,模型性能相对最好。
【思考题】
自定义梯度计算和自动梯度计算:
从计算性能、计算结果等多方面比较,谈谈自己的看法。

在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。

上次实验,手动计算梯度,得到的模型评价:

[Test] score/loss: 0.7750/0.4362

本次实验自动梯度计算模型评价:

[Test] score/loss: 0.8600/0.3191

自动梯度计算得到的模型效果更好,后向传播的计算部分变成loss.backward()方法,和之前的代码相比更加简洁。并且加入requires_grad=True之后,意味着所有后续跟params相关的调用和操作记录都会被保留下来,任何一个经过params变换得到的新的tensor都可以追踪它的变换记录,如果它的变换函数是可微的,导数的值会被自动放进params的grad属性中。

4.4 优化问题

4.4.1 参数初始化

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

class Model_MLP_L2_V2(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Model_MLP_L2_V4, self).__init__()
        # 使用'paddle.nn.Linear'定义线性层。
        # 其中in_features为线性层输入维度;out_features为线性层输出维度
        # weight_attr为权重参数属性
        # bias_attr为偏置参数属性
 
        self.fc1 = nn.Linear(input_size, hidden_size)
        constant_(self.fc1.weight, val=0.0)
        constant_(self.fc1.bias, val=0.0)
        self.fc2 = nn.Linear(hidden_size, output_size)
        constant_(self.fc2.weight, val=0.0)
        constant_(self.fc2.bias, val=0.0)
        self.act_fn = torch.sigmoid
 
        # 使用'paddle.nn.functional.sigmoid'定义 Logistic 激活函数
        self.act_fn = torch.sigmoid
 
    # 前向计算
    def forward(self, inputs):
        z1 = self.fc1(inputs.float())
        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)

利用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(lr=learning_rate, params=model.parameters())
 
# 设置评价指标
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)

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

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

运行结果:

The weights of the Layers:
('fc1.weight', Parameter containing:
tensor([[0., 0.],
        [0., 0.],
        [0., 0.],
        [0., 0.],
        [0., 0.]], requires_grad=True))
('fc1.bias', Parameter containing:
tensor([0., 0., 0., 0., 0.], requires_grad=True))
('fc2.weight', Parameter containing:
tensor([[0., 0., 0., 0., 0.]], requires_grad=True))
('fc2.bias', Parameter containing:
tensor([0.], requires_grad=True))
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.48750
[Train] epoch: 0/5, loss: 0.6931473016738892
The weights of the Layers:
('fc1.weight', Parameter containing:
tensor([[0., 0.],
        [0., 0.],
        [0., 0.],
        [0., 0.],
        [0., 0.]], requires_grad=True))
('fc1.bias', Parameter containing:
tensor([0., 0., 0., 0., 0.], requires_grad=True))
('fc2.weight', Parameter containing:
tensor([[-0.0020, -0.0020, -0.0020, -0.0020, -0.0020]], requires_grad=True))
('fc2.bias', Parameter containing:
tensor([-0.0041], requires_grad=True))
The weights of the Layers:
('fc1.weight', Parameter containing:
tensor([[-2.2723e-05,  1.9955e-05],
        [-2.2723e-05,  1.9955e-05],
        [-2.2723e-05,  1.9955e-05],
        [-2.2723e-05,  1.9955e-05],
        [-2.2723e-05,  1.9955e-05]], requires_grad=True))
('fc1.bias', Parameter containing:
tensor([1.8309e-06, 1.8309e-06, 1.8309e-06, 1.8309e-06, 1.8309e-06],
       requires_grad=True))
('fc2.weight', Parameter containing:
tensor([[-0.0038, -0.0038, -0.0038, -0.0038, -0.0038]], requires_grad=True))
('fc2.bias', Parameter containing:
tensor([-0.0077], requires_grad=True))
The weights of the Layers:
('fc1.weight', Parameter containing:
tensor([[-6.5808e-05,  5.7519e-05],
        [-6.5808e-05,  5.7519e-05],
        [-6.5808e-05,  5.7519e-05],
        [-6.5808e-05,  5.7519e-05],
        [-6.5808e-05,  5.7519e-05]], requires_grad=True))
('fc1.bias', Parameter containing:
tensor([4.8980e-06, 4.8980e-06, 4.8980e-06, 4.8980e-06, 4.8980e-06],
       requires_grad=True))
('fc2.weight', Parameter containing:
tensor([[-0.0054, -0.0054, -0.0054, -0.0054, -0.0054]], requires_grad=True))
('fc2.bias', Parameter containing:
tensor([-0.0109], requires_grad=True))
The weights of the Layers:
('fc1.weight', Parameter containing:
tensor([[-0.0001,  0.0001],
        [-0.0001,  0.0001],
        [-0.0001,  0.0001],
        [-0.0001,  0.0001],
        [-0.0001,  0.0001]], requires_grad=True))
('fc1.bias', Parameter containing:
tensor([8.7562e-06, 8.7562e-06, 8.7562e-06, 8.7562e-06, 8.7562e-06],
       requires_grad=True))
('fc2.weight', Parameter containing:
tensor([[-0.0069, -0.0069, -0.0069, -0.0069, -0.0069]], requires_grad=True))
('fc2.bias', Parameter containing:
tensor([-0.0137], requires_grad=True))

在这里插入图片描述
从输出结果看,二分类准确率为50%左右,说明模型没有学到任何内容。训练和验证loss几乎没有怎么下降。
为了避免对称权重现象,可以使用高斯分布或均匀分布初始化神经网络的参数。

4.4.2 梯度消失问题

在神经网络的构建过程中,随着网络层数的增加,理论上网络的拟合能力也应该是越来越好的。但是随着网络变深,参数学习更加困难,容易出现梯度消失问题。
由于Sigmoid型函数的饱和性,饱和区的导数更接近于0,误差经过每一层传递都会不断衰减。当网络层数很深时,梯度就会不停衰减,甚至消失,使得整个网络很难训练,这就是所谓的梯度消失问题。
在深度神经网络中,减轻梯度消失问题的方法有很多种,一种简单有效的方式就是使用导数比较大的激活函数,如:ReLU。

4.4.2.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型函数作为激活函数,为了便于观察梯度消失现象,只进行一轮网络优化。代码实现如下:

# 学习率大小
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

实例化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=None, 
            save_path="best_model.pdparams", 
            custom_print_log=custom_print_log)

运行结果:

The gradient of the Layers:
Parameter containing:
tensor([[ 0.3185,  0.2314],
        [ 0.7356, -0.08762],
        [ 0.6159, -0.7905]], requires_grad=True)
fc1.weight tensor(1.1255, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[-0.6321, -0.3518, -0.4033],
        [ 0.4413,  0.3763, -0.1825],
        [ 0.3609, -0.1537,  0.2926]], requires_grad=True)
fc2.weight tensor(1.0455, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[ 0.3768,  0.4889,  0.1055],
        [-0.4200,  0.3725, -0.5390],
        [-0.4808,  0.2739, -0.4394]], requires_grad=True)
fc3.weight tensor(1.1501, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[ 0.5159,  0.3937, -0.2794],
        [-0.4812,  0.2626, -0.5522],
        [ 0.4008, -0.2584,  0.1896]], requires_grad=True)
fc4.weight tensor(1.1696, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[-0.4302, -0.4532, -0.0690]], requires_grad=True)
fc5.weight tensor(0.6286, grad_fn=<NormBackward1>)
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.35800

观察得出梯度经过每一个神经层的传递都会不断衰减,最终传递到第一个神经层时,梯度几乎完全消失。

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

torch.random.manual_seed(102)
# 学习率大小
lr = 0.01
 
# 定义网络,激活函数使用sigmoid
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
 
from metric import accuracy
 
# 定义评价指标
metric = accuracy
 
# 指定梯度打印函数
custom_print_log=print_grads
 
# 实例化Runner类
runner = RunnerV2_2(model, optimizer, metric, loss_fn)
 
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:
Parameter containing:
tensor([[-0.0723,  0.3647],
        [ 0.1154, -0.6956],
        [-0.6200,  0.4321]], requires_grad=True)
fc1.weight tensor(1.0712, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[ 0.3968,  0.2156, -0.0731],
        [-0.1264,  0.5368,  0.1555],
        [-0.5234, -0.3148, -0.2681]], requires_grad=True)
fc2.weight tensor(1.0270, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[ 0.0585,  0.1545,  0.3562],
        [ 0.0751,  0.1382, -0.3609],
        [ 0.4400, -0.4026,  0.2186]], requires_grad=True)
fc3.weight tensor(0.8442, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[-0.3096, -0.4293,  0.2616],
        [ 0.5773,  0.3067,  0.1469],
        [ 0.2019,  0.4589,  0.5674]], requires_grad=True)
fc4.weight tensor(1.1708, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[-0.3251, -0.2534,  0.4465]], requires_grad=True)
fc5.weight tensor(0.6077, grad_fn=<NormBackward1>)
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.51250

4.4.3 死亡ReLU问题

ReLU激活函数可以一定程度上改善梯度消失问题,但是在某些情况下容易出现死亡ReLU问题,使得网络难以训练。
这是由于当x<0x<0时,ReLU函数的输出恒为0。在训练过程中,如果参数在一次不恰当的更新后,某个ReLU神经元在所有训练数据上都不能被激活(即输出为0),那么这个神经元自身参数的梯度永远都会是0,在以后的训练过程中永远都不能被激活。
一种简单有效的优化方式就是将激活函数更换为Leaky ReLU、ELU等ReLU的变种。

4.4.3.1 使用ReLU进行模型训练

# 定义多层前馈神经网络
class Model_MLP_L5(torch.nn.Module):
    def __init__(self, input_size, output_size, act='relu'):
        super(Model_MLP_L5, self).__init__()
        self.fc1 = torch.nn.Linear(input_size, 3)
        w_ = torch.normal(0, 0.01, size=(3, input_size), requires_grad=True)
        self.fc1.weight = nn.Parameter(w_)
        # self.fc1.bias = nn.init.constant_(self.fc1.bias, val=1.0)
        self.fc1.bias = nn.init.constant_(self.fc1.bias, val=-8.0)
        w= torch.normal(0, 0.01, size=(3, 3), requires_grad=True)

        self.fc2 = torch.nn.Linear(3, 3)
        self.fc2.weight = nn.Parameter(w)
        # self.fc2.bias = nn.init.constant_(self.fc2.bias, val=1.0)
        self.fc1.bias = nn.init.constant_(self.fc1.bias, val=-8.0)
        self.fc3 = torch.nn.Linear(3, 3)
        self.fc3.weight = nn.Parameter(w)
        # self.fc3.bias = nn.init.constant_(self.fc2.bias, val=1.0)
        self.fc3.bias = nn.init.constant_(self.fc3.bias, val=-8.0)
        self.fc4 = torch.nn.Linear(3, 3)
        self.fc4.weight = nn.Parameter(w)
        # self.fc4.bias = nn.init.constant_(self.fc2.bias, val=1.0)
        self.fc4.bias = nn.init.constant_(self.fc4.bias, val=-8.0)
        self.fc5 = torch.nn.Linear(3, output_size)
        w1 = torch.normal(0, 0.01, size=(output_size, 3), requires_grad=True)
        self.fc5.weight = nn.Parameter(w1)
        # self.fc5.bias = nn.init.constant_(self.fc2.bias, val=1.0)
        self.fc5.bias = nn.init.constant_(self.fc5.bias, val=-8.0)
        # 定义网络使用的激活函数
        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

运行结果:

The gradient of the Layers:
Parameter containing:
tensor([[-0.1353,  0.4477],
        [-0.1761,  0.7017],
        [ 0.1922, -0.6636]], requires_grad=True)
fc1.weight tensor(1.1043, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[ 0.3453, -0.3862, -0.5430],
        [ 0.2807,  0.5112, -0.0911],
        [-0.3687,  0.4393,  0.3405]], requires_grad=True)
fc2.weight tensor(1.1618, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[ 0.5757,  0.5254, -0.4195],
        [ 0.1654, -0.3798, -0.3237],
        [ 0.3662,  0.3267,  0.0957]], requires_grad=True)
fc3.weight tensor(1.1445, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[ 0.3627, -0.4328, -0.0668],
        [ 0.1782, -0.0804, -0.4991],
        [-0.3512, -0.1673, -0.1121]], requires_grad=True)
fc4.weight tensor(0.8800, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[ 0.1301,  0.5472, -0.5523]], requires_grad=True)
fc5.weight tensor(0.7883, grad_fn=<NormBackward1>)
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.49000

从输出结果可以发现,使用 ReLU 作为激活函数,当满足条件时,会发生死亡ReLU问题,网络训练过程中 ReLU 神经元的梯度始终为0,参数无法更新。针对死亡ReLU问题,一种简单有效的优化方式就是将激活函数更换为Leaky ReLU、ELU等ReLU 的变种。接下来,观察将激活函数更换为 Leaky ReLU时的梯度情况。

4.4.3.2 使用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=1, log_epochps=None, 
            save_path="best_model.pdparams", 
            custom_print_log=custom_print_log)

运行结果:

The gradient of the Layers:
Parameter containing:
tensor([[ 0.2212, -0.2221],
        [ 0.1319, -0.0810],
        [ 0.3792,  0.4328]], requires_grad=True)
fc1.weight tensor(0.6733, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[-0.2204, -0.3716,  0.4878],
        [ 0.1812,  0.3695, -0.2415],
        [-0.3263, -0.3294,  0.0499]], requires_grad=True)
fc2.weight tensor(0.9326, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[-0.1988, -0.0070,  0.2591],
        [-0.4716,  0.2334, -0.1754],
        [-0.4411, -0.2383, -0.0437]], requires_grad=True)
fc3.weight tensor(0.8171, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[-0.4036,  0.5367, -0.3690],
        [ 0.0459,  0.5360, -0.0508],
        [ 0.0682,  0.1038,  0.5499]], requires_grad=True)
fc4.weight tensor(1.0940, grad_fn=<NormBackward1>)
Parameter containing:
tensor([[-0.1032, -0.5637, -0.4058]], requires_grad=True)
fc5.weight tensor(0.7022, grad_fn=<NormBackward1>)
[Evaluate] best accuracy performence has been updated: 0.00000 --> 0.28576

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

总结

本次实验我学习了paddle和pytorch之间一些函数的转换,以及自定义梯度计算和自动梯度计算之间的区别。学习了神经网络模型的优化问题以及死亡regu问题。在训练模型的时候,通过调整学习率,自定义隐藏层和对应神经元的数量,了解了参数不同对模型性能的影响。

参考文章:

https://blog.csdn.net/sinat_38079265/article/details/121519632
https://www.cnblogs.com/hbuwyg/p/16617657.html
https://www.cnblogs.com/hbuwyg/p/16617662.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值