【pytorch】 grad、grad_fn、requires_grad()、with torch.no_grad() 、net.train()、net.eval():记录一次奇怪的debug经历

刚开始接触pytorch框架时,最让我觉得神奇的就是它居然可以–自 动 求 导 !

于是我开始尝试理解内部的运行机制,但很快放弃了,直接当成黑盒使用……

最近又遇到一个奇怪的bug,让我不得不去学一下相关知识,预防忘记,故记录下来,共勉之。

1 grad、grad_fn、requires_grad()

一个Tensor中通常会有以下属性:

data: 即存储的数据信息

requires_grad: 设置为True则表示该Tensor需要求导

grad: 该Tensor的梯度值,每次在计算backward时都需要将前一时刻的梯度归零,否则梯度值会一直累加。

grad_fn: 叶子节点通常为None,只有结果节点的grad_fn才有效,用于指示梯度函数是哪种类型。例如 y.grad_fn = , z.grad_fn=

is_leaf: 用来指示该Tensor是否是叶子节点。

这里要区分一下两个概念,参数更新和梯度更新。在pytorch框架里,参数由变量data保存,梯度由专门的变量grad保存。参数更新是靠梯度来实现的。

grad以矩阵的形式来存储梯度值,还有一个grad_fn指向梯度函数(fn指的是function),可以指导梯度更新参数。

大佬总结的详细介绍
Pytorch autograd,backward详解
leaf variable & with torch.no_grad & -=
Pytorch的aotugrad大白话理解及属性使用解释

关于leaf tensor

并不是每个requires_grad()设为True的值都会在backward的时候得到相应的grad,它还必须为leaf。如果想得到当前自己创建的,requires_grad为True的tensor在反向传播时的grad, 可以用retain_grad()这个属性

详细介绍 leaf tensor

2 with torch.no_grad():

众所周知,代码训练一般分为train和eval两部分,train负责训练模型,让模型尽可能拟合train_set,而eval负责检验模型,看看模型是不是收敛了,是不是可以结束训练了。

往往一个epoch的train后,就进行一次eval。在这个过程里,train需要进行参数更新,而eval不需要更新参数。

那怎么让模型知道自己需不需要更新呢?

很简单,用一行代码就行。当你需要让模型训练,可以用

    with torch.no_grad():

举个例子:

	with torch.no_grad():
        for batch_index, data_set in enumerate(val_loader):
            ……
            logits = net(data)
            loss = criterion(logits, true_label)
            loss.backward()
            optimizer.step()
			……

with torch.no_grad():的原理是将grad和grad_fn设为None,强制不参与梯度更新。
【pytorch】 grad、grad_fn、requires_grad()、with torch.no_grad() 、net.train()、net.eval():记录一次奇怪的debug经历_第1张图片
【pytorch】 grad、grad_fn、requires_grad()、with torch.no_grad() 、net.train()、net.eval():记录一次奇怪的debug经历_第2张图片
图片来自with torch.no_grad() 详解

在此时,所有计算都不会导致梯度的累积参数的更新。通过debug,发现代码运行到with torch.no_grad() 代码块下时,参数权重的requires_grad()并不会改变。

3 net.train()和net.eval()的机制

我们常常见到两个函数

	net.train()
	net.eval()

举个例子:

		net.train()
		for batch_index, data_set in enumerate(val_loader):
            ……
            logits = net(data)
            loss = criterion(logits, true_label)
            loss.backward()
            optimizer.step()
			……

经过研究,我发现net.train()和net.eval()会改变模型中的一个变量:
【pytorch】 grad、grad_fn、requires_grad()、with torch.no_grad() 、net.train()、net.eval():记录一次奇怪的debug经历_第3张图片
通过维护这个变量=True或者=False,模型就知道自己处于哪个阶段。

training=false时,不能调用loss.backward(),否则会报错。以此达到区分train和eval的效果。

net.train()和net.eval()到底在什么时候使用?如果一个模型有Dropout与BatchNormalization,那么它在训练时要以一定概率进行Dropout或者更新BatchNormalization参数,而在测试时不在需要Dropout或更新BatchNormalization参数。此时,要用net.train()和net.eval()进行区分。

在没有涉及到BN与Dropout的模型,这两个函数没什么用。

4 require_grad以冻结部分参数

当你想让模型的某些层不参与参数更新,可以手动将该层的requires_grad设为false,比如使用bert时,如果你不想调bert的参数,可以

      for p in self.bert_layer.parameters():
          p.requires_grad = False

5 debug

复现别人的代码做实验的时候,突然发现模型跑了好多个epoch后,accuracy一直很低没怎么变化。观察后发现,loss也是一直在20左右徘徊。

先是以为代码出错了,毕竟改了很多地方,仔细排查后还是没解决,搞到后来甚至怀疑是net.eval()、with torch.no_grad在作怪。

接着怀疑是 梯度消失 ,想做排查,无从下手。

研究了半天,最终我确定了还真是梯度消失。

排查过程如下:

我们都知道,loss.backward()时,会计算好梯度,保存到grad属性中,但不会更新参数。只有到了optimizer.step()后,才会根据grad更新参数。

所以第一步,在每一次loss.backward()前后,将grad打印出来;在每一次optimizer.step()前后将网络参数打印出来,观察是否发生变化。代码如下。

for named, parameters in net.named_parameters():
	print(named, parameters, parameters.grad)

对于一些非叶子tensor,你想查看梯度,可以使用 .retain_grad() 查看。

如果你会debug,将参数加入监视,通过打断点观察会更加方便。添加监视的其中一种方法如下图所示:
【pytorch】 grad、grad_fn、requires_grad()、with torch.no_grad() 、net.train()、net.eval():记录一次奇怪的debug经历_第4张图片

接着分析结果,分几种情况:

① 梯度变化,参数也变化

这时候,你得多让模型跑几个epoch(一个epoch内可能会更新几百次参数),很可能在几个epoch后,梯度就越来越小,从1e-2一直减到1e-8、1e-22,梯度矩阵也越来越稀疏,最后干脆等于0了!!我遇到的就属于这种情况,才1个epoch没结束,梯度就一直等于0了,参数也不更新了。

经过艰难的排查,最终发现是learning_rate设置太大了,虽然设置了学习率衰减,让学习率慢慢下降,但学习率还来不及下降呢,最开始的一个epoch内就把模型搞崩了。(我的模型用了BERT,学习率一般设为1e-5,否则会发生灾难性遗忘。)

附上参考方法:梯度消失、爆炸排查和解决方法

  • 预训练加微调
  • 梯度剪切、权重正则(针对梯度爆炸)
  • 使用不同的激活函数
  • 使用batchnorm
  • 使用残差结构
  • 使用LSTM网络
  • 学习率decay
  • 初始化参数,如用nn.init.xavier_normal_()

② 梯度变化、参数不变

如果连第一次optimizer.step()权重矩阵都没变,那可能是你的代码有问题,比如你的参数所在的类,没有加入optimal的优化参数序列中等等。

③ 梯度不变,参数不变

如果在第一次backward()的时候,梯度都一直是None或者0,说明梯度没有传到该变量,顺着代码往下一直输出变量的梯度,直到梯度出现为止,然后检查为啥梯度消失了。再检查一下学习率是不是太小了,导致超出范围?

④梯度不变,参数变

这基本不可能,因为每轮训练前一般都要加上optimizer.zero_grad()这个代码,把梯度清零,然后再backward()计算梯度,防止梯度累加。在梯度一直为0或者None的情况下,自动求导不会改变参数,只有可能是你手动的用代码改动了参数。


这次debug的经历很曲折,我一直以为只有学习率太小会导致梯度消失,参数不更新。今天算是长见识了,学习率太大,最终达到的效果居然也是参数不更新。深层次的原因还没弄懂,有大佬讲解一下嘛?

你可能感兴趣的:(NLP那些事儿,pytorch,深度学习,机器学习)