当前位置 博文首页 > 浅谈Pytorch中autograd的若干(踩坑)总结

    浅谈Pytorch中autograd的若干(踩坑)总结

    作者:叫我西瓜超人 时间:2021-06-08 18:16

    关于Variable和Tensor

    旧版本的Pytorch中,Variable是对Tensor的一个封装;在Pytorch大于v0.4的版本后,Varible和Tensor合并了,意味着Tensor可以像旧版本的Variable那样运行,当然新版本中Variable封装仍旧可以用,但是对Varieble操作返回的将是一个Tensor。

    import torch as t
    from torch.autograd import Variable
     
    a = t.ones(3,requires_grad=True)
    print(type(a))
    #输出:<class 'torch.Tensor'>
     
    a=Variable(a)
    print(type(a))
    #输出仍旧是:<class 'torch.Tensor'>
     
    print(a.volatile)
    #输出:__main__:1: UserWarning: volatile was removed (Variable.volatile is always False)
    a.volatile=True
    print(a.volatile)
    #输出:__main__:1: UserWarning: volatile was removed (Variable.volatile is always False)
    #现版本pytorch中移除了volatile这个属性,即volatile总是false

    叶子节点leaf

    对于那些不是任何函数(Function)的输出,由用户创建的节点称为叶子节点,叶子节点的grad_fn为None。

    import torch as t
    a = t.ones(3,requires_grad=True)
    b = t.rand(3,requires_grad=True)
    a,a.is_leaf
    #输出:(tensor([1., 1., 1.], requires_grad=True), True)
    b
    #输出:(tensor([0.4254, 0.8763, 0.5901], requires_grad=True), True)
     
    c = a*b
    c.is_leaf
    #输出:False.说明c不是叶子节点
    a.grad_fn
    #输出:None.叶子节点的grad_fn为None.
    c.grad_fn
    #输出:<MulBackward0 object at 0x7fa45c406278> 

    autograd操作

    首先Tensor是默认不需要求导的,即requires_grad默认为False。

    import torch as t
    a = t.ones(3)
    a.requires_grad
    #输出:False.Tensor默认不需要求导

    如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都为True。

    import torch as t
     
    a = t.ones(3)
    b = t.ones(3,requires_grad=True)
    b.requires_grad
    #输出:True
    c = a + b
    c.requires_grad
    #输出:True.虽然c没有指定需要求导,然是c依赖于b,而b需要求导,所以c.requires_grad=True

    只有scalar才能进行反向backward()操作,并且backward对于叶节点的grad的是累加的。当只进行计算操作不做backward,叶节点的grad不发生变化。

    更正一下,并不是只有scaler才能进行backward操作,矩阵和向量也可以,只不过backward()中要添加对应维度的参数。

    import torch as t
     
    a = t.ones(3,requires_grad=True)
    b = t.rand(3,requires_grad=True)
    a,b
    #输出:(tensor([1., 1., 1.], requires_grad=True), 
    #tensor([0.9373, 0.0556, 0.6426], requires_grad=True))
    c = a*b
    c
    #输出:tensor([0.9373, 0.0556, 0.6426], grad_fn=<MulBackward0>)
    c.backward(retain_graph=True)
    #输出:RuntimeError: grad can be implicitly created only for scalar outputs
    #只有数值scalar才能进行backward操作
    d = c.sum()
    d.backward(retain_graph=True)
    #retain_graph=True是为了保存中间缓存,否则再次backward的时候会报错
    a.grad
    #输出:tensor([0.9373, 0.0556, 0.6426])
    b.grad
    #输出:tensor([1., 1., 1.])
    #backward后a和b的grad产生了数值
    e = c.sum()
    e.backward(retain_graph=True)
    b.grad
    #输出:tensor([2., 2., 2.]).b的grad进行了两次backward后进行了累加.
    f = c.sum()
    b.grad
    #输出:tensor([2., 2., 2.])
    #只进行计算不backward,梯度不更新

    Tensor.data和Tensor.detach()

    如过tensor的数值需要参与计算又不想参与到计算图的更新中,计算的时候可以用tensor.data,这样既能利用tensor的数值,又不会更新梯度。

    import torch as t
     
    a = t.ones(3,4,requires_grad=True)
    b = t.rand(3,4,requires_grad=True)
     
    a.data.requires_grad
    #输出:False. a.data独立于计算图之外
     
    c = a.data * b.data
    d = c.sum()
    d.backward()
    #输出:RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
    #因为独立于计算图之外,requires_grad = False所以不能backward()

    当tensor.data被修改的时候,tensor也会同步的被修改,此时用该tensor进行计算并backward的时候梯度的值就不再准确了,因为tensor已经被修改了!

    import torch as t
     
    a = t.ones(3,4,requires_grad=True)
    b = t.rand(3,4,requires_grad=True)
    c = a*b
    d = c.sum()
    a.data.sigmoid_()
    #输出:tensor([[0.7311, 0.7311, 0.7311, 0.7311],
    #        [0.7311, 0.7311, 0.7311, 0.7311],
    #        [0.7311, 0.7311, 0.7311, 0.7311]])
    #虽然对a.data进行sigmoid操作,但是a的值已经被修改了.
    d.backward()
    b.grad
    #输出:tensor([[0.7311, 0.7311, 0.7311, 0.7311],
    #        [0.7311, 0.7311, 0.7311, 0.7311],
    #        [0.7311, 0.7311, 0.7311, 0.7311]])
    #b的grad不准了,本来应该都是1!

    为了避免因为对tensor.data修改导致grad变化的情况,可以利用tensor.detach,同样可以保证tensor不参与到计算图当中,但是当tensor的值被改变的时候,再进行backward就会报错而不会有先前的因为tensor的值被改变而导致不准的情况了。

    import torch as t
     
    a = t.ones(3,4,requires_grad=True)
    b = t.rand(3,4,requires_grad=True)
    c = a * b
    d = c.sum()
    a_ = a.detach()
    a_.sigmoid_()
    a
    #输出:tensor([[0.7311, 0.7311, 0.7311, 0.7311],
    #        [0.7311, 0.7311, 0.7311, 0.7311],
    #        [0.7311, 0.7311, 0.7311, 0.7311]], requires_grad=True)
    #a的值已经发生了改变
    d.backward()
    #报错:RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation
    #因为a的值被修改了,所以不能再进行backward

    推荐用tensor.detach的方式而不是tensor.data的方式,因为这样更保险!

    autograd.grad和hook

    在计算的时候有时候我们可能会用到非叶节点的grad,但是非叶节点的grad在backward之后就会被自动清空:

    import torch as t
     
    a = t.ones(3,4,requires_grad=True)
    b = t.rand(3,4,requires_grad=True)
    c = a*b
    d = c.sum()
    d.backward()
    a.grad
    #输出:tensor([[0.3114, 0.3017, 0.8461, 0.6899],
    #        [0.3878, 0.8712, 0.2406, 0.7396],
    #        [0.6369, 0.0907, 0.4984, 0.5058]])
    c.grad
    #输出:None
    #c为非叶子节点,计算后被清空

    可以用autograd.grad和hook来处理这种情况:

    #利用autograd.grad获取中间节点梯度
    t.autograd.grad(d,c)
    #输出:(tensor([[1., 1., 1., 1.],
    #        [1., 1., 1., 1.],
    #        [1., 1., 1., 1.]]),)
    #利用hook获取中间节点梯度
    import torch as t
     
    a = t.ones(3,4,requires_grad=True)
    b = t.rand(3,4,requires_grad=True)
    c = a*b
    d = c.sum()
     
    def print_grad(grad):
        print(grad)
     
    #给c注册hook
    c_hook = c.register_hook(print_grad)
     
    d.backward()
    #输出:tensor([[1., 1., 1., 1.],
    #        [1., 1., 1., 1.],
    #        [1., 1., 1., 1.]])
     
    #移除钩子
    c_hook.remove()

    补充:关于Pytorch中autograd和backward的一些笔记

    1 Tensor

    Pytorch中所有的计算其实都可以回归到Tensor上,所以有必要重新认识一下Tensor。

    如果我们需要计算某个Tensor的导数,那么我们需要设置其.requires_grad属性为True。为方便说明,在本文中对于这种我们自己定义的变量,我们称之为叶子节点(leaf nodes),而基于叶子节点得到的中间或最终变量则可称之为结果节点。

    另外一个Tensor中通常会记录如下图中所示的属性:

    data: 即存储的数据信息

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

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

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

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

    举例:

    x = torch.rand(3, requires_grad=True)
    y = x ** 2
    z = x + x
    print(
        'x requires grad: {},  is leaf: {},  grad: {},  grad_fn: {}.'
            .format(x.requires_grad, x.is_leaf, x.grad, x.grad_fn)
    )
    print(
        'y requires grad: {},  is leaf: {},  grad: {},  grad_fn: {}.'
            .format(y.requires_grad, y.is_leaf, y.grad, y.grad_fn)
    )
    print(
        'z requires grad: {},  is leaf: {},  grad: {},  grad_fn: {}.'
            .format(z.requires_grad, z.is_leaf, z.grad, z.grad_fn)
    )

    运行结果:

    x requires grad: True, is leaf: True, grad: None, grad_fn: None.

    y requires grad: True, is leaf: False, grad: None, grad_fn: <PowBackward0 object at 0x0000021A3002CD88>.

    z requires grad: True, is leaf: False, grad: None, grad_fn: <AddBackward0 object at 0x0000021A3002CD88>.

    2 torch.autograd.backward

    如下代码:

    x = torch.tensor(1.0, requires_grad=True)
    y = torch.tensor(2.0, requires_grad=True)
    z = x**2+y
    z.backward()
    print(z, x.grad, y.grad)
    >>> tensor(3., grad_fn=<AddBackward0>) tensor(2.) tensor(1.)
    

    当 z 是一个标量,当调用它的 backward 方法后会根据链式法则自动计算出叶子节点的梯度值。

    但是如果遇到 z 是一个向量或者是一个矩阵的情况,这个时候又该怎么计算梯度呢?这种情况我们需要定义grad_tensor来计算矩阵的梯度。

    在介绍为什么使用之前我们先看一下源代码中backward的接口是如何定义的:

    torch.autograd.backward(
            tensors, 
            grad_tensors=None, 
            retain_graph=None, 
            create_graph=False, 
            grad_variables=None)

    tensor: 用于计算梯度的 tensor。也就是说这两种方式是等价的:torch.autograd.backward(z) == z.backward()

    grad_tensors: 在计算非标量的梯度时会用到。他其实也是一个tensor,它的shape一般需要和前面的tensor保持一致。

    retain_graph: 通常在调用一次 backward 后,pytorch 会自动把计算图销毁,所以要想对某个变量重复调用 backward,则需要将该参数设置为True

    create_graph: 当设置为True的时候可以用来计算更高阶的梯度

    grad_variables: 这个官方说法是 grad_variables' is deprecated. Use 'grad_tensors' instead. 也就是说这个参数后面版本中应该会丢弃,直接使用grad_tensors就好了。

    pytorch设计了grad_tensors这么一个参数。它的作用相当于“权重”。

    先看一个例子:

    x = torch.ones(2,requires_grad=True)
    z = x + 2
    z.backward()
    >>> ...
    RuntimeError: grad can be implicitly created only for scalar outputs
    

    上面的报错信息意思是只有对标量输出它才会计算梯度,而求一个矩阵对另一矩阵的导数束手无策。

    x = torch.ones(2,requires_grad=True)
    z = x + 2
    z.sum().backward()
    print(x.grad)
    >>> tensor([1., 1.])

    而grad_tensors这个参数就扮演了帮助求和的作用。

    换句话说,就是对 Z 和一个权重张量grad_tensors进行 hadamard product 后求和。这也是 grad_tensors 需要与传入的 tensor 大小一致的原因。

    x = torch.ones(2,requires_grad=True)
    z = x + 2
    z.backward(torch.ones_like(z)) # grad_tensors需要与输入tensor大小一致
    print(x.grad)
    >>> tensor([1., 1.])
    

    3 torch.autograd.grad

    torch.autograd.grad(
            outputs, 
            inputs, 
            grad_outputs=None, 
            retain_graph=None, 
            create_graph=False, 
            only_inputs=True, 
            allow_unused=False)

    看了前面的内容后再看这个函数就很好理解了,各参数作用如下:

    outputs: 结果节点,即被求导数

    inputs: 叶子节点

    grad_outputs: 类似于backward方法中的grad_tensors

    retain_graph: 同上

    create_graph: 同上

    only_inputs: 默认为True,如果为True,则只会返回指定input的梯度值。 若为False,则会计算所有叶子节点的梯度,并且将计算得到的梯度累加到各自的.grad属性上去。

    allow_unused: 默认为False, 即必须要指定input,如果没有指定的话则报错。

    注意该函数返回的是 tuple 类型。

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持站长博客。如有错误或未考虑完全的地方,望不吝赐教。

    js