如何理解detach从计算图分离
class TestDetach(nn.Module):
def __init__(self, InDim, HiddenDim, OutDim):
super().__init__()
self.layer1 = nn.Linear(InDim, HiddenDim, False)
self.layer2 = nn.Linear(HiddenDim, OutDim, False)
def forward(self, x, DetachLayer1):
x = torch.relu(self.layer1(x))
x = x.detach()
x = self.layer2(x)
return x
x = x.detach()
的意思是:detach()
方法会创建一个新的张量,它与原始张量的计算历史没有关联,也不会计算梯度。x从此刻开始,后面对x的操作才被记录梯度,i.e. 反向传播更新参数的时候,从后往前更新,更新到这就停了。
在这个模型中,第一层的参数(self.layer1)不会被更新,只有第二层的参数(self.layer2)会被更新。
这种思想可以解决下面的RNN的问题:
第一次跑到backward没事,第二次报错。因为第二次在跑backward的时候用到了第一次的数据,但是在跑第一次和第二次之间先用了loss.backward()
清空梯度,这样第二次就找不到数据了。
找到问题解决方法:
torch.autograd.set_detect_anomaly(True)
#待检测的代码区域
torch.autograd.set_detect_anomaly(False)
!没问题的时候不要开!巨慢!原本5分钟跑完开了以后2-3倍时间才能跑完!而且:一般情况下,反向传播中有个别Nan值,并不会引起训练发生报错,只有在打开自动微分异常监测时。才会出现任意Nan都会引起模型报错
参照这一篇文章:https://blog.csdn.net/qq_41682740/article/details/126304613
检测出来我的问题在这里:
def forward(self, X_t):
out = torch.mm(X_t, self.W_ih) + torch.mm(self.hx, self.W_hh) + self.b_x + self.b_h
#self.hx = out
self.hx = out.detach()
return out
这里self.hx = out
,第一次backward没问题,第二次的时候,追踪self.hx
的梯度的时候,追踪到了上一次保存的out
,再由out
往前,已经被清空了。
for t in range(in_maps.shape[0]):
out_maps = getattr(snn, 'layer' + str(layer_id))(in_maps[t])
in_maps_t = in_maps[t].sum(1).div(duration)
out_maps = out_maps.sum(1).div(duration)
# in_maps[t].requires_grad_()
output = tuner(in_maps_t)
output.data = out_maps.data
# 其实你这个地方想怎么赋值怎么赋值,不过grad为true只能通过.data进行赋值。
loss = criterion(output, target_map[t])
这人写的很深:
https://zhuanlan.zhihu.com/p/344916574
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.
1.什么是 leaf variable?
一句话:grad_fn为None的就是leaf variable。
grad_fn
全称 gradient function,即你是通过什么function 得到的 y, i.e. y = fn(x)
所以,对于所有自己创建的 tensor,grad_fn = None
叶子节点是通过用户创建的张量,而不是通过任何操作生成的张量。
注意:叶子节点和require_grad
无关,叶子节点可以为True也可以为False。所以这个RE是:a leaf Variable that requires grad is being used in an in-place operation.
也就是说,一个不需要记录gradient的叶子结点,是可以进行in-place操作的(浅拷贝);而自己创建的需要记录梯度的tensor,不可以进行in-place操作。如下。
import torch
# 创建一个需要梯度计算的张量
x = torch.tensor([1.0], requires_grad=False)
print(x.is_leaf) # True 因为是自己造的
print(x.grad_fn) # None
y = x # 可以,因为不需要梯度
y += 1 # 可以,就是普通的数值运算罢了
x = torch.tensor([1.0], requires_grad=True)
print(x.is_leaf) # True 因为是自己造的
print(x.grad_fn) # None
y = x.clone() #深拷贝,长出来一个新的节点
y += 1 # 可以作为左值
y = x #默认浅拷贝,相当于y就是x
z = y * y #这种没什么问题,因为y没有被改
y += 1 #不可以作为左值,报错
- 为什么要求必须深拷贝呢?
很简单,最终梯度求完的目的是更新参数,只有叶子节点会被更新,只有叶子节点会计算梯度,其他节点只是传递自己这个操作的梯度,不计算。那么问题来了,如果对叶子节点进行了更新,也就是原来存的数据发生了更改old_w–>new_w,那么更新的时候,old_w是哪个?变成了我是谁的问题了。所以说叶子节点是不可以in-place操作,就会造成“我是谁?”的问题。
如果你想要不跟踪梯度的作为左值被修改:用.data
属性即可。
import torch
# 创建一个需要梯度计算的叶子节点张量
x = torch.tensor([1.0], requires_grad=True)
x.haha = torch.tensor([2.0])
print(x.haha) # tensor([2.])
x.detach = 2.0 # .detach和上面没有区别,自定义属性。因为不是关键字。
print(x.detach) # 2.0
# x.data = 2 # 报错:Variable data has to be a tensor, but got int
x.data = torch.tensor([2.0])
y = x * 2 # y 作为新的子节点,纳入x的梯度计算范围
# 也就是说x的梯度计算是跟他有关系的
y.backward()
print(x.grad) # 2.0,虽然改了x的值,但是和梯度无关,是两套体系。
# 梯度在乎的是节点和节点间的关系,哪些需要考虑进来。
# .data作为属性,就是访问地址原本的值,可以直接改。
print(x) # tensor([2.], requires_grad=True) 也确实赋值为2了
最后总结一下:
一、不被允许的inplace场景:
- require_grad = True的叶子结点不能原址操作
- 在求梯度阶段用到的张量不能使用原址操作
二、inplace操作有哪些: https://blog.csdn.net/qq_61888524/article/details/125919872
- 数值运算,如
x+=1
属于inplace操作,会直接对x的值进行操作;而x=x+5
则不属于inplace操作。
import torch
x = torch.tensor([1.0], requires_grad=True)
print(x, id(x))
x.data = torch.tensor([2.0]) # 地址不变,还是原来的x
print(x, id(x))
x = x + 3 # 地址发生了改变,其实是创建新的变量
print(x, id(x))
x = 666 # 新地址,新变量
print(x, id(x))
''''
tensor([1.], requires_grad=True) 139665138485904
tensor([2.], requires_grad=True) 139665138485904
tensor([5.], grad_fn=<AddBackward0>) 139665138826832
666 139665139174064
''''
-
pytorch提供的一些inplace选项,如nn.ReLU(inplace=True)、nn.LeakyReLU(inplace=True),这些选项的安全性要高一些,但也需要注意中间变量后续是否需要,如果后面还需要使用中间变量,就应当设置inplace=False
-
具有 _ 后缀的方法,如x.copy_(y),x.t_(),将直接改变x。同时,一些常见的操作如x.add_(y)、x.mul_(y)也会直接改变x,除非有特定需求,否则不建议使用这类inplace操作,隐患比前两种情况高很多。
改法example:
fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.size()))
# 改为
fusedconv.weight.data = torch.mm(w_bn, w_conv).view(fusedconv.weight.size()).clone()
UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed.
当你想输出一个非叶子节点的.grad属性的时候会给warning,返回None。
因为backward()
最终的目的是为了找到叶子结点(父节点),从而计算梯度后存到叶子结点中的.grad
属性里。还有,非叶子节点也不会去计算梯度的,因为没有意义,只是中间量,不是待优化的参数。而且backward之后就会把中间的loss清空,optimizer的时候就只有叶子节点有梯度。
但是我们可以有以下的方式去看哪些是待优化的参数,然后也可以在optimizer_step()
前输出出来这一次backward()
更新的梯度。
for name, param in tuner.named_parameters():
if param.grad is not None:
print(f'Parameter {name} has gradients:{param.grad}')
梯度累加操作:
https://www.zhihu.com/question/303070254
24.4.9补充:
置位操作的理解
Pytorch中in-place操作相关错误解析及detach()方法说明
import torch
x = torch.tensor(3.0, requires_grad=True)
z = x.detach() # 没有grad,从grad的计算图中抽离出来。但是共享同一个数值,不同的内存。
z -= 1
print(z, x, id(z), id(x), z.is_leaf) # z 如果改变 x 也会改,他们共享同一个数值。很离谱。
zz = x.clone() # 内存不同,也不共享数值了。
zz -= 1
print(zz, x, id(zz), id(x))
# x -= 1 不行,因为只要有grad的其实就不能进行in-palce操作
y = x # 置位操作,所以x和y实际上是一个地址,一个数值
print(id(y))
# y -= 1 #
print(id(x.data)) # x.data和 x.detach()一样,都是内存不同,但是和x的数值是共享的。
# 所以如果想改grad为true的tensor的数值,只能通过这种间接的方式改。
"""
tensor(2.) tensor(2., requires_grad=True) 140476611967904 140476587904112 True
tensor(1., grad_fn=<SubBackward0>) tensor(2., requires_grad=True) 140476587767216 140476587904112
140476587904112
140476588887152
"""