IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 人工智能 -> NNDL 实验五 前馈神经网络(2) -> 正文阅读

[人工智能]NNDL 实验五 前馈神经网络(2)

前言

附上这次实验的kaggle连接,前馈神经网络实验2,上面有全套运行的python代码,这次是学习自动梯度计算和优化问题,主要是确定模型后,寻找让模型最优的办法。

4.3 自动梯度计算和预定义算子

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

torch提供了torch.nn.Module类,来方便快速的实现自己的层和模型。模型和层都可以基于`nn扩充实现,模型只是一种特殊的层。

继承了torch.nn.Module类的算子中,可以在内部直接调用其它继承torch.nn.Module`类的算子,torch框架会自动识别算子中内嵌的torch.nn.Module类算子,并自动计算它们的梯度,并在优化时更新它们的参数。

4.3.1 使用torch的预定义算子来重新实现二分类任务。

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

PYTORCH中的相应内容是什么?请简要介绍。
简单说说我认为各个框架中大致内容都是一样的,只是名称不同,参数不同。
pytorch和paddlepaddle的联系

PyTorchPaddlePaddle说明
torch.nnpaddle.nn包括了神经网络相关的大部分函数
nn.Modulenn.Layer搭建网络时集成的父类,包含了初始化等基本功能
torch.optimpaddle.optimizer训练优化器
torchvision.transformspaddle.vision.transforms数据预处理、图片处理
torchvision.datasetspaddle.vision.datasets数据集的加载与处理

官方文档
在这里插入图片描述
在这里插入图片描述

torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)

in_features——表示输入的形状
out_features——表示输出的形状
bias——表示是否添加

nn.Module

Module是所有神经网络模型的基块,也可以包含其他基块,说白了,就是你在定义Model的时候加上个这就行了。


代码实现如下:

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

建立两个带偏移量的线性层,初始化权重参数为以0为均值,以1为方差的正太分布的随机产生的值。

基于上一节实现的 RunnerV2_1 类,本节的 RunnerV2_2 类在训练过程中使用自动梯度计算;模型保存时,使用state_dict方法获取模型参数;模型加载时,使用set_state_dict方法加载模型参数.

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.set_state_dict(state_dict)

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

# 设置模型
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(model.parameters(), learning_rate)

# 设置评价指标
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")


在这里插入图片描述
将训练过程中训练集与验证集的准确率变化情况进行可视化。

# 可视化观察训练集与验证集的指标变化情况
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')

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

在这里插入图片描述

使用测试数据对训练完成后的最优模型进行评价,观察模型在测试集上的准确率以及loss情况。代码如下:


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

从结果来看,模型在测试集上取得了较高的准确率。

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

更改模型定义代码如下

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, hidden_size)
        normal_(self.fc2.weight, mean=0., std=1.)
        constant_(self.fc2.bias, val=0.0)
        self.act_fn = torch.sigmoid
        self.fc3 = nn.Linear(hidden_size, output_size)
        normal_(self.fc3.weight, mean=0., std=1.)
        constant_(self.fc3.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)
        z3 = self.fc3(a2)
        a3 = self.act_fn(z3)
        return a3

更改参数定义代码如下

# 设置模型
input_size = 2
hidden_size = 5
add_hidden_size = 3 
output_size = 1
model = Model_MLP_L2_V2(input_size=input_size, hidden_size=hidden_size, output_size=output_size,add_size = add_hidden_size)

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

# 设置优化器
learning_rate = 0.2
optimizer = torch.optim.SGD(model.parameters(), learning_rate)

# 设置评价指标
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")

在这里插入图片描述
可视化结果:
在这里插入图片描述
在这里插入图片描述

与4.3.1增加隐藏层神经元做对比
通过结果我们可以显著的发现,增加了 一层含有三个神经元的隐藏层后,误差和学习率均往好的方面呈现,但是我们发现在达到最优的时候,增加了一层隐藏元后,达到最优解的学习次数明显增多,即模型越复杂训练事件越长,而误差和得分往好的方面可能是出现了过拟合。

在这里插入图片描述

由图我们可以发现,输入层和输出层的数目是确定,我们在训练的时候确定只是隐藏层的数目和其神经元的数目的确定
对于神经元层数的选取
1、隐层节点数必须小于N-1
?? 学过数学多项式的人都知道,如果含有n个数,那么该多项式的阶数达到n-1的时候,即可通过所有的点,即百分百过拟合,无论你给的数据是怎样的,只要是训练集上的点那么就可以达到百分百准确率,训练就没有用。通俗解释:给了一套公式,肯定满足你所有的训练集,但是世间万物各异,没有相同的两片的叶子,所以不可能存在和数据集一样的类型的东西,所以是不可取的。
2、训练样本数必须多于网络模型的连接权数,一般为2~10倍,否则,样本必须分成几部分并采用“轮流训练”的方法才可能得到可靠的神经网络模型。
3、Since a single sufficiently large hidden layer is adequate for approximation of most functions, why would anyone ever use more? One reason hangs on the words “sufficiently large”. Although a single hidden layer is optimal for some functions, there are others for which a single-hidden-layer-solution is very inefficient compared to solutions with more layers.
大概意思:只有线性不可分的时候才需要隐藏层,线性可分就不需要隐藏层了。因此,对于一般简单的数据集,一两层隐藏层通常就足够了。但对于涉及时间序列或计算机视觉的复杂数据集,则需要额外增加层数。单层神经网络只能用于表示线性分离函数,也就是非常简单的问题,比如分类问题中的两个类可以用一条直线整齐地分开。
4、层数越深,理论上拟合函数的能力增强,效果按理说会更好,但是实际上更深的层数可能会带来过拟合的问题,同时也会增加训练难度,使模型难以收敛。因此我的经验是,在使用BP神经网络时,最好可以参照已有的表现优异的模型,如果实在没有,则根据上面的表格,从一两层开始尝试,尽量不要使用太多的层数。在CV、NLP等特殊领域,可以使用CNN、RNN、attention等特殊模型,不能不考虑实际而直接无脑堆砌多层神经网络。尝试迁移和微调已有的预训练模型,能取得事半功倍的效果。
隐藏层神经元层数的选取——Heaton Research: The Number of Hidden Layers
对于隐藏层神经元层中神经元数目的选取
?对于隐藏层神经元数目的选取,如果我们选取过少,那么会造成欠拟合,如果过多,则会造成过拟合,这可不是多多益善的,也不要被其所展现出来较高的准确率和较低的误差迷惑,就跟量子力学是的,总是和人直观相悖。那么我们如何选取呢?
在这里插入图片描述
通常,对所有隐藏层使用相同数量的神经元就足够了。对于某些数据集,拥有较大的第一层并在其后跟随较小的层将导致更好的性能,因为第一层可以学习很多低阶的特征,这些较低层的特征可以馈入后续层中,提取出较高阶特征。
需要注意的是,与在每一层中添加更多的神经元相比,添加层层数将获得更大的性能提升。因此,不要在一个隐藏层中加入过多的神经元。
在这里插入图片描述
对于如何确定神经元数量,有很多经验之谈。
第一种参考方式是:
在这里插入图片描述
在这里插入图片描述
第二种参考方式:
隐藏神经元的数量应在输入层的大小和输出层的大小之间。
隐藏神经元的数量应为输入层大小的2/3加上输出层大小的2/3。
隐藏神经元的数量应小于输入层大小的两倍。

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

4.2.2中我们已经说过了探索神经元参数的方法,那么这里我们就来实现一下。采用宁可杀错,不可放过的原则,我们多多更新学习率,神经元参数,训练次数等,由于所给的数据集,并不是很复杂,这里我们就不再更新神经元层数以免造成过拟合,这里我们依此更新各个参数。

更改学习率为0.1、0.5、1、2、5,结果如下

lr = 0.1
在这里插入图片描述
lr = 0.2
在这里插入图片描述
lr = 0.5
在这里插入图片描述
lr = 1
在这里插入图片描述
lr=2
在这里插入图片描述
lr = 5
在这里插入图片描述
在这里插入图片描述

通过观察图像可知,训练结果出现了震荡的现象,即训练结果不稳定,这次我们选取训练最快,且相对来说较稳定学习率参数lr = 1.当然实际应用中还要尝试很多。

更改神经元的参数

神经元数目=5
在这里插入图片描述
神经元数目=3
在这里插入图片描述
神经元数目=10
在这里插入图片描述
在这里插入图片描述

通过结果我们可以发现,当超参数学习率等于1,神经元数目=3,隐藏层层数为2时取得最佳效果。
留个疑问:怎么才能对各个超参数同时更新来寻得最优参数呢?

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

答:
先简单解释下自定义梯度计算和自动梯度计算。然后从计算性能和计算结果进行比较。
自定义梯度计算即我们自己定义的的梯度计算。即我们定义的backward函数。
自动梯度计算使用的是pytorch框架的backward。
举个例子:
自定义梯度计算:
Model_SRm模型

def backward(self, labels):
        """
        输入:
            - labels:真实标签,shape=[N, 1],其中N为样本数量
        """
        # 计算偏导数
        N = labels.shape[0]
        labels = torch.nn.functional.one_hot(labels.to(torch.int64), self.output_dim)
        self.grads['W'] = -1 / N * torch.matmul(self.X.t(), (labels - self.outputs))
        self.grads['b'] = -1 / N * torch.matmul(torch.ones(size=[N]), (labels - self.outputs))

自动梯度计算:

	trn_loss.backward();

计算原理:
自定义梯度计算:顾名思义就是根据所需的计算梯度的方式。即根据所给激活函数,根据其导数的性质来 进行编写梯度计算函数。
而自动梯度计算就大有来头了,采用的是分而治之的思想,将梯度求解分开成更多更简单的梯度求解。
分为前向模式和后向模式两种
在这里插入图片描述
数学方面:
自定义梯度求导:根据函数特定的计算方式来计算导数
自动梯度求导:根据链式法则求导,即复合函数求导。一步一步求。
计算性能方面:我盲猜自动梯度计算是没有自定义梯度计算快的。毕竟自定义梯度计算是一步到位。
下面开始对相同模型采用不用的求导方式进行实践,实践是检验真理的唯一标准。
自动梯度求导
在这里插入图片描述
自定义梯度求导
在这里插入图片描述
经过多次实验。我们发现自动梯度求导相对于自定义梯度求导还是无论在时间上还有准确率上相对于自定义求导还是有一定差距的。
这正好验证了我们的猜想。所以以后用一些求导方式,在时间充裕爱好数学的方式下,尽量采用自定义求导。毕竟效率和运算时间摆在这。

4.4 优化问题

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

4.4.1 参数初始化

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

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

class Model_MLP_L2_V4(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)
        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")

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

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

# 使用'paddle.normal'实现高斯分布采样,其中'mean'为高斯分布的均值,'std'为高斯分布的标准差,'shape'为输出形状
gausian_weights = torch.normal(mean=0.0, std=1.0, size=[10000])
# 使用'paddle.uniform'实现在[min,max)范围内的均匀分布采样,其中'shape'为输出形状
uniform_weights = torch.Tensor(10000)
uniform_weights.uniform_(-1,1)
print(uniform_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(item)
             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(lr=lr, params=model.parameters())
# 定义损失函数,使用交叉熵损失函数
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 \ell_2 ?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)

在这里插入图片描述

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

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(lr=lr, params=model.parameters())

# 定义损失函数
# 定义损失函数,这里使用交叉熵损失函数
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=1, log_epochs=None, 
            save_path="best_model.pdparams", 
            custom_print_log=custom_print_log)

在这里插入图片描述

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


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

4.4.3 死亡 ReLU 问题

ReLU激活函数可以一定程度上改善梯度消失问题,但是ReLU函数在某些情况下容易出现死亡 ReLU问题,使得网络难以训练。这是由于当 x < 0 x<0 x<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 \ell_2 ?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.pdparams", 
            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=1, log_epochps=None, 
            save_path="best_model.pdparams", 
            custom_print_log=custom_print_log)

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

梯度消失和梯度爆炸产生的原因即解决办法

首先解释一下什么是梯度消失和梯度爆炸,
梯度消失:梯度趋近于零,网络权重无法更新或更新的很微小,网络训练再久也不会有效果;
梯度爆炸:梯度呈指数级增长,变的非常大,然后导致网络权重的大幅更新,使网络变得不稳定。
其实不管是梯度消失还是梯度爆炸本质上都是由于——深度神经网络的反向传播造成的。
可以通过两个角度去解释:一是在深层网络中,二是采用的激活函数导致的。
梯度消失:(1)隐藏层的层数过多;(2)采用了不合适的激活函数(更容易产生梯度消失,但是也有可能产生梯度爆炸)
梯度爆炸:(1)隐藏层的层数过多;(2)权重的初始化值过大
(1)隐藏层层数多
在这里插入图片描述
明显可以看出第四层神经元明显比第一个神经元更新慢
(2)采用不同的激活函数
拿sigmoid函数举例。下面是sigmoid函数的导数,不难发现,sigmoid函数的导数是一个双饱和函数。
在这里插入图片描述
我们发现sigmoid函数的导数当x取值为[-6 6]之外时,sigmoid函数的导数即小,所以如果我们的更新权重不够大,那么就会出现梯度消失问题。
在这里插入图片描述
而如果取值在[-6 6]之内时,如果我们此时的更新权重较大,则会出现梯度爆炸现象。
好了,我们已经说了产生原因呢,那么怎么改正呢?
1、给梯度计算增加正则化
2、更改激活函数

总结:

1、加固了paddlepaddle和pytorch之间一些函数的转换。同时通过官网学习到了torch一些函数的底层代码。例如torch.nn.Linear
2、在实验总结了梯度爆炸,梯度消失产生的原因及其解决办法。并自己亲身进行实践。
3、进行超参数的寻优,虽然麻烦,但是也经历了寻优的过程熟能生巧。

不足:

在参数寻优的时候是一个一个参数进行尝试,极易容易局部最优。

马虎点:在模型参数b中设置的是常量,但是我由于马虎,直接使用的是torch.tensor,并未使用torch.nn.init.constant设置常量。所以导致b的常量变了,最后梯度也变了,蝴蝶效应

参考

出现梯度消失与梯度爆炸的原因以及解决方案

pytorch自定义函数实现自动梯度

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

神经网络中隐层数和隐层节点数问题的讨论

  人工智能 最新文章
2022吴恩达机器学习课程——第二课(神经网
第十五章 规则学习
FixMatch: Simplifying Semi-Supervised Le
数据挖掘Java——Kmeans算法的实现
大脑皮层的分割方法
【翻译】GPT-3是如何工作的
论文笔记:TEACHTEXT: CrossModal Generaliz
python从零学(六)
详解Python 3.x 导入(import)
【答读者问27】backtrader不支持最新版本的
上一篇文章      下一篇文章      查看所有文章
加:2022-10-08 20:42:06  更:2022-10-08 20:43:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 20:49:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码