Pytorch 自动求导 autograd,backward 详解

PyTorch提供两种求梯度的方法:backward()torch.autograd.grad() ,他们的区别在于前者是给叶子节点填充.grad字段,而后者是直接返回梯度给你,我会在后面举例说明。我们通常看到的y.backward()其实等同于torch.autograd.backward(y)

梯度是以tensor为对象的,之前总结了Pytorch Tensor与形状相关的属性。另外Tensor还有与自动求导相关的属性。

Tensor自动求导相关的属性

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

  • data: 即存储的数据信息
  • requires_grad: 设置为True则表示该Tensor需要求导
  • grad: 该Tensor的梯度值,只有自动求导后,才有值,每次在计算backward时都需要将前一时刻的梯度归零,否则梯度值会一直累加。
  • grad_fn: 叶子节点通常为None,只有中间节点的 grad_fn 才有效,用于指示梯度函数是哪种类型。
  • is_leaf: 用来指示该Tensor是否是叶子节点。
    在这里插入图片描述
    图片出处: PyTorch Autograd*

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

import torch
import numpy as np
x = torch.rand(3, requires_grad=True)
y = x**2


print("x.data = {}\n\
x.requires_grad = {}\n\
x.grad = {}\n\
x.grad_fn = {}\n\
x.is_leaf = {}".format(x.data,x.requires_grad,x.grad,x.grad_fn,x.is_leaf))

print("y.data = {}\n\
y.requires_grad = {}\n\
y.grad = {}\n\
y.grad_fn = {}\n\
y.is_leaf = {}".format(x.data,x.requires_grad,x.grad,x.grad_fn,x.is_leaf))

运行结果:

x.data = tensor([0.9833, 0.2409, 0.2388])
x.requires_grad = True
x.grad = None
x.grad_fn = None
x.is_leaf = True

y.data = tensor([0.9668, 0.0580, 0.0570])
y.requires_grad = True
y.grad = None
y.grad_fn = <PowBackward0 object at 0x0000028710761EB0>
y.is_leaf = False

torch.autograd.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就好了。

举例说明:

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方法后会根据链式法则自动计算出叶子节点的梯度值。

下面我们看看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

当我们运行上面的代码的话会报错,报错信息为RuntimeError: grad can be implicitly created only for scalar outputs。

上面的报错信息意思是只有对标量输出它才会计算梯度,而求一个矩阵对另一矩阵的导数就会报错。
在这里插入图片描述那么我们只要想办法把矩阵转变成一个标量不就好了?比如我们可以对z求和,然后用求和得到的标量在对x求导,这样不会对结果有影响,例如:
在这里插入图片描述
我们可以看到对z求和后再计算梯度没有报错,结果也与预期一样:

x = torch.ones(2,requires_grad=True)
z = x + 2
z.sum().backward()
print(x.grad)

>>> tensor([1., 1.])

我们再仔细想想,对z求和不就是等价于z点乘一个一样维度的全为1的矩阵吗?即 s u m ( Z ) = d o t ( Z , I ) sum(Z)=dot(Z,I) sum(Z)=dot(Z,I) ,而这个I也就是我们需要传入的grad_tensors参数。(点乘只是相对于一维向量而言的,对于矩阵或更高为的张量,可以看做是对每一个维度做点乘)
代码如下:

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.])

弄个再复杂一点的:

x = torch.tensor([2., 1.], requires_grad=True)
y = torch.tensor([[1., 2.], [3., 4.]], requires_grad=True)

z = torch.mm(x.view(1, 2), y)
print(f"z:{z}")
z.backward(torch.Tensor([[1., 0]]), retain_graph=True)
print(f"x.grad: {x.grad}")
print(f"y.grad: {y.grad}")

>>> z:tensor([[5., 8.]], grad_fn=<MmBackward>)
x.grad: tensor([[1., 3.]])
y.grad: tensor([[2., 0.],
        [1., 0.]])

再具体一点来解释,让我们写出求导计算的雅可比矩阵, y = [ y 1 , y 2 ] y=[y_1,y_2] y=[y1,y2] 是一个向量,
J = [ ∂ y ∂ x 1 , ∂ y ∂ x 2 ] = [ ∂ y 1 ∂ x 1 ∂ y 1 ∂ x 2 ∂ y 2 ∂ x 1 ∂ y 2 ∂ x 2 ] \mathbf{J} = [\frac{\partial \mathbf{y}}{\partial x_1}, \frac{\partial \mathbf{y}}{\partial x_2}] = \left[\begin{array}{cc} \frac{\partial y_{1}}{\partial x_{1}} \frac{\partial y_{1}}{\partial x_2} \\ \frac{\partial y_{2}}{\partial x_{1}} \frac{\partial y_2}{\partial x_2} \end{array}\right] J=[x1y,x2y]=[x1y1x2y1x1y2x2y2],而我们希望最终求导的结果是 [ ∂ y 1 ∂ x 1 , ∂ y 2 ∂ x 2 ] [\frac{\partial y_1}{\partial x_1}, \frac{\partial y_2}{\partial x_2}] [x1y1,x2y2],那怎么得到呢?

注意 ∂ y 1 ∂ x 2 \frac{\partial y_1}{\partial x_2} x2y1 ∂ y 2 ∂ x 1 \frac{\partial y_2}{\partial x_1} x1y2是0。那是不是

[ ∂ y 1 ∂ x 1 , ∂ y 2 ∂ x 2 ] ⊤ = [ ∂ y 1 ∂ x 1 ∂ y 1 ∂ x 2 ∂ y 2 ∂ x 1 ∂ y 2 ∂ x 2 ] [ 1 1 ] [\frac{\partial y_1}{\partial x_1}, \frac{\partial y_2}{\partial x_2}]^\top = \left[\begin{array}{cc} \frac{\partial y_{1}}{\partial x_{1}} \frac{\partial y_{1}}{\partial x_2} \\ \frac{\partial y_{2}}{\partial x_{1}} \frac{\partial y_2}{\partial x_2} \end{array}\right] \left[\begin{array}{c} 1 \\ 1 \end{array}\right] [x1y1,x2y2]=[x1y1x2y1x1y2x2y2][11]
所以不用y.sum()的另一种方式是

x = torch.tensor([1., 2.]).requires_grad_()
y = x * x

y.backward(torch.ones_like(x))
print(x.grad)
>>>tensor([2., 4.])

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,如果没有指定的话则报错。

参考

  • https://towardsdatascience.com/pytorch-autograd-understanding-the-heart-of-pytorchs-magic-2686cd94ec95
  • https://zhuanlan.zhihu.com/p/83172023
  • https://zhuanlan.zhihu.com/p/279758736
  • AUTOMATIC DIFFERENTIATION PACKAGE - TORCH.AUTOGRAD
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值