NNDL 实验七 循环神经网络(2)梯度爆炸实验

目录

6.2 梯度爆炸实验

6.2.1 梯度打印函数

6.2.2 复现梯度爆炸现象

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

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

什么是范数

什么是L2范数

为什么要打印梯度范数?

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

 总结:

6.2 梯度爆炸实验

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

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

6.2.1 梯度打印函数

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

import torch
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()[0]
        if name == "rnn_model.U":
            U_grad_l2 = torch.norm(param.grad, p=2).numpy()[0]
        if name == "rnn_model.b":
            b_grad_l2 = torch.norm(param.grad, p=2).numpy()[0]
    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)


6.2.2 复现梯度爆炸现象

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

获取训练过程中关于W,U和b参数梯度L2范数,并将其绘制为图片以便展示。

因为Tanh为Sigmoid型函数,其饱和区的导数接近于0,

由于梯度的急剧变化,参数数值变的较大或较小,容易落入梯度饱和区,导致梯度为0,

模型很难继续训练.在这里插入图片描述

代码实现如下:

import os
import random
import torch
import numpy as np

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

# 训练轮次
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"./datasets/{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(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_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)

得到以下结果:

[Train] epoch: 49/50, step: 246/250, loss: 3264474.75000
[Training] W_grad_l2: 0.00000, U_grad_l2: 0.00000, b_grad_l2: 0.00000 
[Train] epoch: 49/50, step: 247/250, loss: 3699495.25000
[Training] W_grad_l2: 0.00000, U_grad_l2: 0.00000, b_grad_l2: 0.00000 
[Train] epoch: 49/50, step: 248/250, loss: 2951603.50000
[Training] W_grad_l2: 0.00000, U_grad_l2: 0.00000, b_grad_l2: 0.00000 
[Train] epoch: 49/50, step: 249/250, loss: 4115297.75000
[Training] W_grad_l2: 0.00000, U_grad_l2: 0.00000, b_grad_l2: 0.00000 
[Evaluate]  dev score: 0.10000, dev loss: 4538019.50000
[Evaluate] best accuracy performence has been updated: 0.06000 --> 0.10000
[Train] Training done!

进程已结束,退出代码为 0

接下来,可以获取训练过程中关于W,U和b参数梯度的L2范数,并将其绘制为图片以便展示,相应代码如下:

import matplotlib.pyplot as plt


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)
    plt.show()
    print("image has been saved to: ", save_path)


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

图6.8 展示了在训练过程中关于WW,UU和bb参数梯度的L2范数,可以看到经过学习率等方式的调整,梯度范数急剧变大,而后梯度范数几乎为0. 这是因为TanhTanh为SigmoidSigmoid型函数,其饱和区的导数接近于0,由于梯度的急剧变化,参数数值变的较大或较小,容易落入梯度饱和区,导致梯度为0,模型很难继续训练. 

NNDL 实验七 循环神经网络(2)梯度爆炸实验_第1张图片

 接下来,使用该模型在测试集上进行测试。

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}")
[SRN] length:20, Score:  0.07000

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

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

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

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

在引入梯度截断之后,将重新观察模型的训练情况。这里我们重新实例化一下:模型和优化器,然后组装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,并实例化优化器

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)
# 进行模型训练
model_save_path = os.path.join(save_dir, f"srn_explosion_model_{length}.pdparams")

得到以下结果:

Train] epoch: 49/50, step: 247/250, loss: 737.77722
[Training] W_grad_l2: 30058.17188, U_grad_l2: 85546.31250, b_grad_l2: 23573.27539 
[Train] epoch: 49/50, step: 248/250, loss: 935.16846
[Training] W_grad_l2: 2453.63599, U_grad_l2: 8865.46289, b_grad_l2: 2278.44800 
[Train] epoch: 49/50, step: 249/250, loss: 490.52774
[Training] W_grad_l2: 1128.79370, U_grad_l2: 3956.14282, b_grad_l2: 726.18701 
[Evaluate]  dev score: 0.03000, dev loss: 513.28258
[Train] Training done!

进程已结束,退出代码为 0

发现结果比刚才好多了。

在引入梯度截断后,获取训练过程中关于WW,UU和bb参数梯度的L2范数,并将其绘制为图片以便展示,相应代码如下:

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

NNDL 实验七 循环神经网络(2)梯度爆炸实验_第2张图片

 展示了引入按模截断的策略之后,模型训练时参数梯度的变化情况。可以看到,随着迭代步骤的进行,梯度始终保持在一个有值的状态,表明按模截断能够很好地解决梯度爆炸的问题.

接下来,使用梯度截断策略的模型在测试集上进行测试。

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

得到以下结果:

Evaluate SRN with data length 20.
[SRN] length:20, Score:  0.10000

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

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

什么是范数

范数(norm)是线性代数中的一个基本概念。在泛函分析中,它定义在赋范线性空间中,并满足一定的条件,即①非负性;②齐次性;③三角不等式,它常常被用来度量某个向量空间(或矩阵)中的每个向量的长度或大小。
划重点:
范数的三个特性(满足条件):非负性、齐次性、三角不等式
外文名:n o r m normnorm
作用:常常被用来度量某个向量空间(或矩阵)中的每个向量的长度或大小

记号:f ( x ) = ∥ x ∥

我们常用||x||_{symb}表示具体范数,其中下标s y m b symbsymb是区分范数的助记符号,如||x||_0||x||_1||x||_2||x||_{}\infty

范数是对向量x xx的长度的度量。我们可以用两个向量x xx和y yy的差异的长度度量它们之间的距离,即:dist(x,y)=∥x−y∥

我们用dist(x,y)来表示两个向量x和y之间用范数∥⋅∥表示的距离

在线性代数中,常用到范数这个概念。对于一维空间的实数集,标准的范数就是绝对值;那么将绝对值的概念推广到多维空间就叫做范数,即范数是绝对值函数的推广

什么是L2范数

要想知道什么是L2范数就要先知道什么是L-P范数。

L-p范数的定义如下:

L_p=||x||_p=\sqrt[P]{\sum_{i=1}^n|x_i|^p}=(\sum_{i=1}^n|x_i|^p)^{\frac{1}{p}}

L-p范数只是一个概念上宽泛的说法。取p=1,就得到了L1​范数;取p=2,就得到了L2​范数(L2​范数又称为Euclid范数,即欧几里得范数,欧几里得距离,欧式距离)。

L2范数是我们最常见的范数。比如欧氏距离(Euclid distance,欧几里得距离)就是一种L2范数。定义如下:

L_2=||x||_2=\sqrt{\sum_{i=1}^nx^2_i},i=1,2,3\cdots n

即:表示向量x的各元素平方和后再开方

像L1范数一样,L2也可以度量两个向量之间的差异,如平方差和:

SSD(x_1,x_2)=\sqrt{\sum^n_{i=1}(x_{1i}-x_{2i}^2)},i=1,2,\cdots ,n

差值平方和之后,再开方

为什么要打印梯度范数?

根据上面的实验我们可以看到首先设置一个clip_grad作为梯度阈值,然后按照往常一样求出各个梯度,不一样的是,我们没有立马进行更新,而是求出这些梯度的L2范数,如果L2范数大于设置好的clip_grad,则求clip_grad除以L2范数,然后把除好的结果乘上原来的梯度完成更新。当梯度很大的时候,作为分母的结果就会很小,那么乘上原来的梯度,整个值就会变小,从而可以有效地控制梯度的范围。

在这里我找到了一个可以让大家很好理解什么是范数的网站,也可以自由的调整P值,很清晰的可以看出来L1范数L2范数的区别

NNDL 实验七 循环神经网络(2)梯度爆炸实验_第3张图片NNDL 实验七 循环神经网络(2)梯度爆炸实验_第4张图片

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

为了能够在利用在线学习的同时产生稀疏解,最直接的想法是采用截断的方法,截断,即通过某个阈值来控制系数的大小,若系数小于某个阈值便将该系数设置为0,这便是简单截断的含义。在简单截断方法中,直接的截断太过于暴力,在截断梯度法中,将截断的步骤适当放缓,其具体的更新公式如下:

其中, 称为重力参数(gravity parameter),截断函数 的具体形式如下: 

 

与简单截断类似,每隔 次对参数 进行更新,其更新公式如下:

其中, 。可以通过调节参数 和参数 控制稀疏度,参数 和参数 越大,解越稀疏。

 总结:

这次实验用了梯度截断法解决梯度爆炸,我记得之前的实验中也提到过梯度爆炸的问题,当时是用的换激活函数来解决,这次用梯度截断法算是把解决梯度爆炸的两种方法都用过了一遍,这次试验内容也比较少,其中范数的部分我记得之前也提过,最后一句话总结梯度裁剪:设定一个阈值,然后求所有参数的L2范数,比较后如果大于阈值了就把他截断改为阈值。感觉应该还挺好理解吧

参考文献:(35条消息) 优化算法——截断梯度法(TG)_zhiyong_will的博客-CSDN博客_截断共轭梯度法

(35条消息) 什么是范数(norm)?以及L1,L2范数的简单介绍_小白的进阶之路的博客-CSDN博客_l1 normp-norm ball – GeoGebra

你可能感兴趣的:(rnn,深度学习)