目录
6.2 梯度爆炸实验
6.2.1 梯度打印函数
6.2.2 复现梯度爆炸现象
6.2.3 使用梯度截断解决梯度爆炸问题
总结
造成简单循环网络较难建模长程依赖问题的原因有两个:梯度爆炸和梯度消失。梯度爆炸问题:比较容易解决,一般通过权重衰减或梯度截断可以较好地来避免;梯度消失问题:更加有效的方式是改变模型,比如通过长短期记忆网络LSTM来进行缓解。本节将首先进行复现简单循环网络中的梯度爆炸问题,然后尝试使用梯度截断的方式进行解决。
采用长度为20的数据集进行实验,训练过程中将进行输出W,U,b的梯度向量的范数,以此来衡量梯度的变化情况。
在训练过程中打印梯度,分别定义W_list, U_list和b_list,用于分别存储训练过程中参数W,U和b的梯度范数。
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范数是指向量各元素的平方和然后求平方根,L2范数可以防止过拟合,提升模型的泛化能力。打印梯度范数是为了观察梯度变化趋势,来更好地进行模型训练
为了更好地复现梯度爆炸问题,使用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"D:/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(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,U和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"D:/datasets/images/6.8.pdf"
plot_grad(W_list, U_list, b_list, save_path)
经过学习率等方式的调整,梯度范数急剧变大,而后梯度范数几乎为0,因为Tanh为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}")
Evaluate SRN with data length 20.
[SRN] length: 20,Score: 0.08000.
梯度截断是一种可以有效解决梯度爆炸问题的启发式方法,当梯度的模大于一定阈值时,就将它截断成为一个较小的数。一般有两种截断方式:按值截断和按模截断。本实验使用按模截断的方式解决梯度爆炸问题。
在飞桨中,可以使用paddle.nn.ClipGradByNorm进行按模截断.--- pytorch中用什么?
答: pytorch中使用torch.nn.utils.clip_grad_norm_()进行按模截断
在引入梯度截断之后,将重新观察模型的训练情况。这里我们重新实例化一下:模型和优化器,然后组装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 = nn.ClipGradByNorm(clip_norm=5.0)
optimizer = paddle.optimizer.SGD(learning_rate=lr, parameters=model.parameters(), grad_clip=clip)
# 定义评价指标
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,U和b参数梯度的L2范数并将其绘制为图片展示
save_path = f"./images/6.9.pdf"
plot_grad(W_list, U_list, b_list, save_path, keep_steps=100)
引入按模截断的策略之后,随着迭代步骤的进行,梯度始终保持在一个有值的状态,表明按模截断能够很好地解决梯度爆炸的问题
接下来,使用梯度截断策略的模型在测试集上进行测试
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.18000.
【思考题】梯度截断解决梯度爆炸问题的原理是什么?
答:我认为梯度截断解决梯度爆炸问题的原理就是在计算梯度的过程中出现梯度爆炸的情况(梯度越来越大),梯度截断就可以把超过设定的某个阈值或范围的梯度重置为某个范围内的值。比如设定阈值为100,现在某个梯度已经超过了100,我们就把这个值重置为1~10内的一个数。
梯度爆炸一般出现在深层网络和权值初始化值太大的情况下,梯度爆炸会引起网络不稳定,最好的结果是无法从训练数据中学习,而最坏的结果是出现无法再更新的NaN权重值。想要避免这个问题,就需要调小初始化的权值或者是进行梯度截断,一个是从开始上入手减缓梯度的增长速度,一个是从过程中入手让走向“邪路”的梯度回归正轨。
参考
Pytorch梯度截断:torch.nn.utils.clip_grad_norm_
NNDL 实验6(上) - HBU_DAVID - 博客园 (cnblogs.com)
8. 循环神经网络 — 动手学深度学习 2.0.0-beta1 documentation (d2l.ai)