我是靠谱客的博主 聪慧小蘑菇,最近开发中收集的这篇文章主要介绍NNDL 实验七 循环神经网络(2)梯度爆炸实验,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

    • 6.2 梯度爆炸实验
      • 6.2.1 梯度打印函数
        • 【思考】什么是范数,什么是L2范数,这里为什么要打印梯度范数?
      • 6.2.2 复现梯度爆炸现象
      • 6.2.3 使用梯度截断解决梯度爆炸问题
        • 在飞桨中,可以使用paddle.nn.ClipGradByNorm进行按模截断.--- pytorch中用什么?
      • 【思考题】梯度截断解决梯度爆炸问题的原理是什么?
      • 总结
      • 参考

6.2 梯度爆炸实验

造成简单循环网络较难建模长程依赖问题的原因有两个:梯度爆炸和梯度消失。一般来讲,循环网络的梯度爆炸问题比较容易解决,一般通过权重衰减或梯度截断可以较好地来避免;对于梯度消失问题,更加有效的方式是改变模型,比如通过长短期记忆网络LSTM来进行缓解。

本节将首先进行复现简单循环网络中的梯度爆炸问题,然后尝试使用梯度截断的方式进行解决。这里采用长度为20的数据集进行实验,训练过程中将进行输出 W W W, U U U, b b b的梯度向量的范数,以此来衡量梯度的变化情况。

6.2.1 梯度打印函数

使用custom_print_log实现了在训练过程中打印梯度的功能,custom_print_log需要接收runner的实例,并通过model.named_parameters()获取该模型中的参数名和参数值. 这里我们分别定义W_list, U_listb_list,用于分别存储训练过程中参数 W , U 和 b W, U 和 b W,Ub的梯度范数。

import torch
import os
import random
import torch
import numpy as np
from torch.utils.data import DataLoader
W_list = []
U_list = []
b_list = []
# 计算梯度范数
def custom_print_log(runner):
    model = runner.model
    W_grad_l2, U_grad_l2, b_grad_l2 = 0, 0, 0
    for name, param in model.named_parameters():
        if name == "rnn_model.W":
            W_grad_l2 = torch.norm(param.grad, p=2).numpy()
        if name == "rnn_model.U":
            U_grad_l2 = torch.norm(param.grad, p=2).numpy()
        if name == "rnn_model.b":
            b_grad_l2 = torch.norm(param.grad, p=2).numpy()
    print(f"[Training] W_grad_l2: {W_grad_l2:.5f}, U_grad_l2: {U_grad_l2:.5f}, b_grad_l2: {b_grad_l2:.5f} ")
    W_list.append(W_grad_l2)
    U_list.append(U_grad_l2)
    b_list.append(b_grad_l2)

【思考】什么是范数,什么是L2范数,这里为什么要打印梯度范数?

L2范数是各个元素的平方和然后开根号,范数是一种抽象的距离概念。看看随着训练次数增加,求导经历tanh激活函数的次数增多,看看梯度是否一直在减小。

6.2.2 复现梯度爆炸现象

为了更好地复现梯度爆炸问题,使用SGD优化器将批大小和学习率调大,学习率为0.2,同时在计算交叉熵损失时,将reduction设置为sum,表示将损失进行累加。 代码实现如下:

np.random.seed(0)
random.seed(0)
torch.manual_seed(0)

# 训练轮次
num_epochs = 50
# 学习率
lr = 0.2
# 输入数字的类别数
num_digits = 10
# 将数字映射为向量的维度
input_size = 32
# 隐状态向量的维度
hidden_size = 32
# 预测数字的类别数
num_classes = 19
# 批大小
batch_size = 64
# 模型保存目录
save_dir = "./checkpoints"


# 可以设置不同的length进行不同长度数据的预测实验
length = 20
print(f"n====> Training SRN with data of length {length}.")

# 加载长度为length的数据
data_path = f"./{length}"
train_examples, dev_examples, test_examples = load_data(data_path)
train_set, dev_set, test_set = DigitSumDataset(train_examples), DigitSumDataset(dev_examples),DigitSumDataset(test_examples)
train_loader = DataLoader(train_set, batch_size=batch_size)
dev_loader = DataLoader(dev_set, batch_size=batch_size)
test_loader = DataLoader(test_set, batch_size=batch_size)
# 实例化模型
base_model = SRN(input_size, hidden_size)
model = Model_RNN4SeqClass(base_model, num_digits, input_size, hidden_size, num_classes)
# 指定优化器
optimizer = torch.optim.SGD(model.parameters(),lr)
# 定义评价指标
metric = Accuracy()
# 定义损失函数
loss_fn = nn.CrossEntropyLoss(reduction="sum")

# 基于以上组件,实例化Runner
runner = RunnerV3(model, optimizer, loss_fn, metric)

# 进行模型训练
model_save_path = os.path.join(save_dir, f"srn_explosion_model_{length}.pdparams")
runner.train(train_loader, dev_loader, num_epochs=num_epochs, eval_steps=100, log_steps=1,
             save_path=model_save_path, custom_print_log=custom_print_log)

在这里插入图片描述
在这里插入图片描述
接下来,可以获取训练过程中关于 W boldsymbol{W} W U boldsymbol{U} U b boldsymbol{b} b参数梯度的L2范数,并将其绘制为图片以便展示,相应代码如下:

def plot_grad(W_list, U_list, b_list, save_path, keep_steps=40):
    # 开始绘制图片
    plt.figure()
    # 默认保留前40步的结果
    steps = list(range(keep_steps))
    plt.plot(steps, W_list[:keep_steps], "r-", color="#e4007f", label="W_grad_l2")
    plt.plot(steps, U_list[:keep_steps], "-.", color="#f19ec2", label="U_grad_l2")
    plt.plot(steps, b_list[:keep_steps], "--", color="#000000", label="b_grad_l2")

    plt.xlabel("step")
    plt.ylabel("L2 Norm")
    plt.legend(loc="upper right")
    plt.savefig(save_path)
    print("image has been saved to: ", save_path)


save_path = f"./6.8.pdf"
plot_grad(W_list, U_list, b_list, save_path)

在这里插入图片描述
图6.8 展示了在训练过程中关于 W boldsymbol{W} W U boldsymbol{U} U b boldsymbol{b} b参数梯度的L2范数,可以看到经过学习率等方式的调整,梯度范数急剧变大,而后梯度范数几乎为0. 这是因为 Tanh text{Tanh} Tanh Sigmoid text{Sigmoid} Sigmoid型函数,其饱和区的导数接近于0,由于梯度的急剧变化,参数数值变的较大或较小,容易落入梯度饱和区,导致梯度为0,模型很难继续训练.
接下来,使用该模型在测试集上进行测试。


print(f"Evaluate SRN with data length {length}.")
# 加载训练过程中效果最好的模型
model_path = os.path.join(save_dir, f"srn_explosion_model_{length}.pdparams")
runner.load_model(model_path)

# 使用测试集评价模型,获取测试集上的预测准确率
score, _ = runner.evaluate(test_loader)
print(f"[SRN] length:{length}, Score: {score: .5f}")

在这里插入图片描述

6.2.3 使用梯度截断解决梯度爆炸问题

梯度截断是一种可以有效解决梯度爆炸问题的启发式方法,当梯度的模大于一定阈值时,就将它截断成为一个较小的数。一般有两种截断方式:按值截断和按模截断.本实验使用按模截断的方式解决梯度爆炸问题。

按模截断是按照梯度向量 g boldsymbol{g} g的模进行截断,保证梯度向量的模值不大于阈值 b b b,裁剪后的梯度为:

g = { g , ∣ ∣ g ∣ ∣ ≤ b b ∣ ∣ g ∣ ∣ ∗ g , ∣ ∣ g ∣ ∣ > b . boldsymbol{g} = left{begin{matrix} boldsymbol{g}, & ||boldsymbol{g}||leq b \ frac{b}{||boldsymbol{g}||} * boldsymbol{g}, & ||boldsymbol{g}||gt b end{matrix} right.. g={g,gbg,gbg>b.

当梯度向量 g boldsymbol{g} g的模不大于阈值 b b b时, g boldsymbol{g} g数值不变,否则对 g boldsymbol{g} g进行数值缩放。

在飞桨中,可以使用paddle.nn.ClipGradByNorm进行按模截断.— pytorch中用什么?

pytorch 用 torch.nn.utils.clip_grad_norm_ 进行梯度截断
在这里插入图片描述

在飞桨中,可以使用paddle.nn.ClipGradByNorm进行按模截断. 在代码实现时,将ClipGradByNorm传入优化器,优化器在反向迭代过程中,每次梯度更新时默认可以对所有梯度裁剪。

在引入梯度截断之后,将重新观察模型的训练情况。这里我们重新实例化一下:模型和优化器,然后组装runner,进行训练。代码实现如下:

# 清空梯度列表
W_list.clear()
U_list.clear()
b_list.clear()
# 实例化模型
base_model = SRN(input_size, hidden_size)
model = Model_RNN4SeqClass(base_model, num_digits, input_size, hidden_size, num_classes) 

# 定义clip,并实例化优化器
clip = torch.nn.utils.clip_grad_norm_(max_norm=5.0,parameters=model.parameters())
optimizer = torch.optim.SGD(lr=lr,params=model.parameters())
# 定义评价指标
metric = Accuracy()
# 定义损失函数
loss_fn = nn.CrossEntropyLoss(reduction="sum")

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

# 训练模型
model_save_path = os.path.join(save_dir, f"srn_fix_explosion_model_{length}.pdparams")
runner.train(train_loader, dev_loader, num_epochs=num_epochs, eval_steps=100, log_steps=1, save_path=model_save_path, custom_print_log=custom_print_log)

在这里插入图片描述
在引入梯度截断后,获取训练过程中关于 W boldsymbol{W} W U boldsymbol{U} U b boldsymbol{b} b参数梯度的L2范数,并将其绘制为图片以便展示,相应代码如下:

save_path =  f"./6.9.pdf"
plot_grad(W_list, U_list, b_list, save_path, keep_steps=100)

在这里插入图片描述
图6.9 展示了引入按模截断的策略之后,模型训练时参数梯度的变化情况。可以看到,随着迭代步骤的进行,梯度始终保持在一个有值的状态,表明按模截断能够很好地解决梯度爆炸的问题.
接下来,使用梯度截断策略的模型在测试集上进行测试。

print(f"Evaluate SRN with data length {length}.")

# 加载训练过程中效果最好的模型
model_path = os.path.join(save_dir, f"srn_fix_explosion_model_{length}.pdparams")
runner.load_model(model_path)

# 使用测试集评价模型,获取测试集上的预测准确率
score, _ = runner.evaluate(test_loader)
print(f"[SRN] length:{length}, Score: {score: .5f}")

在这里插入图片描述
由于为复现梯度爆炸现象,改变了学习率,优化器等,因此准确率相对比较低。但由于采用梯度截断策略后,在后续训练过程中,模型参数能够被更新优化,因此准确率有一定的提升。

【思考题】梯度截断解决梯度爆炸问题的原理是什么?

pytorch中使用的梯度截断是该torch.nn.utils.clip_grad_norm_ 函数,其作用是将超过指定梯度的梯度进行缩放,而梯度爆炸,故名思义就是梯度太大了,梯度太大产生的原因是因为其初始化权重过大,再加上连乘累积,所以最后的梯度非常非常大,所以这里使用按模截断的梯度截断将模大于指定数大小的梯度进行截断,使梯度变小,同时也有按数截断的梯度截断方式。

总结

梯度爆炸和梯度消失均是由于求导中各个阶段累乘造成的,例如初始化权重W较大,那么通过多次累乘最后造成的梯度非常大,同理如果初始化权重w较小,那么多次累乘后最后的梯度是很小的。在进行梯度实验的时候我们发现,当没有梯度截断的时候,梯度仅仅出现了一个高峰。然后就接近于0,而当增加梯度截断后,我们发现梯度在一定范围内增加或者减少。但是我有一个疑问,梯度消失近似于梯度为0,不能进行更新。但是梯度爆炸为什么不能更新呢,梯度爆炸也是有数的啊,我思索了半天,以为是由于python中数据类型是有限的。会溢出造成NaN,同时再进行测试的时候也的确是这样/】。

参考

梯度爆炸,梯度消失
深度学习BTTP算法

最后

以上就是聪慧小蘑菇为你收集整理的NNDL 实验七 循环神经网络(2)梯度爆炸实验的全部内容,希望文章能够帮你解决NNDL 实验七 循环神经网络(2)梯度爆炸实验所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(56)

评论列表共有 0 条评论

立即
投稿
返回
顶部