【反向传播、计算图、梯度】.data .detach() .clone inplace操作等等

如何理解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

  1. 数值运算,如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
''''
  1. pytorch提供的一些inplace选项,如nn.ReLU(inplace=True)、nn.LeakyReLU(inplace=True),这些选项的安全性要高一些,但也需要注意中间变量后续是否需要,如果后面还需要使用中间变量,就应当设置inplace=False

  2. 具有 _ 后缀的方法,如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
"""
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值