赞
踩
造成简单循环网络较难建模长程依赖问题的原因有两个:梯度爆炸和梯度消失。
梯度爆炸问题:比较容易解决,一般通过权重衰减或梯度截断可以较好地来避免;
梯度消失问题:更加有效的方式是改变模型,比如通过长短期记忆网络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范数,这里为什么要打印梯度范数?
为了更好地复现梯度爆炸问题,使用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)
结果:
获取训练过程中关于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.show() plt.savefig(save_path) 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)
结果:
接下来,使用梯度截断策略的模型在测试集上进行测试。
# 加载训练过程中效果最好的模型
model_path = os.path.join(save_dir, "srn_explosion_model_20.pdparams")
runner.load_model(model_path)
# 使用测试集评价模型,获取测试集上的预测准确率
score, _ = runner.evaluate(test_loader)
print(f"[SRN] length:{length}, Score: {score: .5f}")
结果:
梯度截断是一种可以有效解决梯度爆炸问题的启发式方法,
当梯度的模大于一定阈值时,就将它截断成为一个较小的数。
一般有两种截断方式:按值截断和按模截断.
本实验使用按模截断的方式解决梯度爆炸问题。
将RunnerV3修改如下:
class RunnerV3(object): def __init__(self, model, optimizer, loss_fn, metric, **kwargs): self.model = model self.optimizer = optimizer self.loss_fn = loss_fn self.metric = metric # 只用于计算评价指标 # 记录训练过程中的评价指标变化情况 self.dev_scores = [] # 记录训练过程中的损失函数变化情况 self.train_epoch_losses = [] # 一个epoch记录一次loss self.train_step_losses = [] # 一个step记录一次loss self.dev_losses = [] # 记录全局最优指标 self.best_score = 0 def train(self, train_loader, dev_loader=None, **kwargs): # 将模型切换为训练模式 self.model.train() # 传入训练轮数,如果没有传入值则默认为0 num_epochs = kwargs.get("num_epochs", 0) # 传入log打印频率,如果没有传入值则默认为100 log_steps = kwargs.get("log_steps", 100) # 评价频率 eval_steps = kwargs.get("eval_steps", 0) # 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams" save_path = kwargs.get("save_path", "best_model.pdparams") custom_print_log = kwargs.get("custom_print_log", None) # 训练总的步数 num_training_steps = num_epochs * len(train_loader) if eval_steps: if self.metric is None: raise RuntimeError('Error: Metric can not be None!') if dev_loader is None: raise RuntimeError('Error: dev_loader can not be None!') # 运行的step数目 global_step = 0 # 进行num_epochs轮训练 for epoch in range(num_epochs): # 用于统计训练集的损失 total_loss = 0 for step, data in enumerate(train_loader): X, y = data # 获取模型预测 logits = self.model(X) loss = self.loss_fn(logits, y.long()) # 默认求mean total_loss += loss # 训练过程中,每个step的loss进行保存 self.train_step_losses.append((global_step, loss.item())) if log_steps and global_step % log_steps == 0: print( f"[Train] epoch: {epoch}/{num_epochs}, step: {global_step}/{num_training_steps}, loss: {loss.item():.5f}") # 梯度反向传播,计算每个参数的梯度值 loss.backward() if custom_print_log: custom_print_log(self) # 小批量梯度下降进行参数更新 nn.utils.clip_grad_norm_(parameters=model.parameters(), max_norm=20, norm_type=2) self.optimizer.step() # 梯度归零 self.optimizer.zero_grad() # 判断是否需要评价 if eval_steps > 0 and global_step > 0 and \ (global_step % eval_steps == 0 or global_step == (num_training_steps - 1)): dev_score, dev_loss = self.evaluate(dev_loader, global_step=global_step) print(f"[Evaluate] dev score: {dev_score:.5f}, dev loss: {dev_loss:.5f}") # 将模型切换为训练模式 self.model.train() # 如果当前指标为最优指标,保存该模型 if dev_score > self.best_score: self.save_model(save_path) print( f"[Evaluate] best accuracy performence has been updated: {self.best_score:.5f} --> {dev_score:.5f}") self.best_score = dev_score global_step += 1 # 当前epoch 训练loss累计值 trn_loss = (total_loss / len(train_loader)).item() # epoch粒度的训练loss保存 self.train_epoch_losses.append(trn_loss) print("[Train] Training done!") # 模型评估阶段,使用'torch.no_grad()'控制不计算和存储梯度 @torch.no_grad() def evaluate(self, dev_loader, **kwargs): assert self.metric is not None # 将模型设置为评估模式 self.model.eval() global_step = kwargs.get("global_step", -1) # 用于统计训练集的损失 total_loss = 0 # 重置评价 self.metric.reset() # 遍历验证集每个批次 for batch_id, data in enumerate(dev_loader): X, y = data # 计算模型输出 logits = self.model(X) # 计算损失函数 loss = self.loss_fn(logits, y.long()).item() # 累积损失 total_loss += loss # 累积评价 self.metric.update(logits, y) dev_loss = (total_loss / len(dev_loader)) dev_score = self.metric.accumulate() # 记录验证集loss if global_step != -1: self.dev_losses.append((global_step, dev_loss)) self.dev_scores.append(dev_score) return dev_score, dev_loss # 模型评估阶段,使用'torch.no_grad()'控制不计算和存储梯度 @torch.no_grad() def predict(self, x, **kwargs): # 将模型设置为评估模式 self.model.eval() # 运行模型前向计算,得到预测值 logits = self.model(x) return logits def save_model(self, save_path): torch.save(self.model.state_dict(), save_path) def load_model(self, model_path): state_dict = torch.load(model_path) self.model.load_state_dict(state_dict) from torch.utils.data import Dataset,DataLoader class DigitSumDataset(Dataset): def __init__(self, data): self.data = data def __getitem__(self, idx): example = self.data[idx] seq = torch.tensor(example[0], dtype=torch.int64) label = torch.tensor(example[1], dtype=torch.int64) return seq, label def __len__(self): return len(self.data)
结果:
引入按模截断的策略之后,模型训练时参数梯度的变化情况。可以看到,随着迭代步骤的进行,梯度始终保持在一个有值的状态,表明按模截断能够很好地解决梯度爆炸的问题。
接下来,使用梯度截断策略的模型在测试集上进行测试:
print(f"Evaluate SRN with data length {length}.")
# 加载训练过程中效果最好的模型
model_path = os.path.join(save_dir, "srn_explosion_model_20.pdparams")
runner.load_model(model_path)
# 使用测试集评价模型,获取测试集上的预测准确率
score, _ = runner.evaluate(test_loader)
print(f"[SRN] length:{length}, Score: {score: .5f}")
结果:
由于为复现梯度爆炸现象,改变了学习率,优化器等,因此准确率相对比较低。但由于采用梯度截断策略后,在后续训练过程中,模型参数能够被更新优化,因此准确率有一定的提升。
【思考题】梯度截断解决梯度爆炸问题的原理是什么?
梯度截断,也就是设定阈值,当预更新的梯度小于阈值时,那么将预更新的梯度设置为阈值。梯度截断通常发送在,损失函数反向传播计算完之后,优化器梯度更新之前。在 pytorch 中通过 clip_grad_norm 方法来实现。
函数相关参数:
①按值截断
按值截断是比较简单粗暴的方法,由于梯度太大会产生梯度爆炸的现象,太小会产生梯度消失的现象(参数不更新),所以为梯度提供一个范围[a,b]:
②按模截断
为梯度g设置一个最大阈值threshold,用梯度的L2范数与该阈值做比较;
这次实验觉得新名词比较多,先是了解了一下梯度范数(这个可能也不算新名词,只是给了一个专业的名词来解释)。然后动手实践一个梯度爆炸问题通过梯度截断来解决它,最后思考题查资料的时候,还了解到了梯度剪裁,两者对参数的处理方法有所不同,但它们的作用都是用来处理梯度爆炸问题。
神经网络训练过程中,梯度范数为什么大多数情况会增加?
8.5. 循环神经网络的从零开始实现
【深度学习】梯度截断(grad_clip)
神经网络优化(1)之梯度截断
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。