Pytorch 60分钟入门之(二) Autograd:自动求导

Autograd:自动求导

Pytorch所有的神经网络的核心是autograd库. 我们先简单了解一下, 然后训练第一个神经网络.
autograd 包提供了Tensor所有操作的自动求导(差分). 这是一个运行时定义(define-by-run)的架构, 意味着你的后向传播是根据你的代码怎样运行来定义的. 每一次的迭代都可能不同.
让我们看几个例子.

Tensor

torch.Tensor是这个包的核心类. 如果将其属性attribute.requires_grad设为True, 它就会记录所有的操作. 计算完成后, 调用.backward(), 会自动计算所有的梯度(gradients). 这个tensor的梯度会累加到.grad属性中.
为了阻止tensor记录历史, 可以调用.detach()将它和计算历史脱钩, 并阻止其下一步计算继续记录操作历史.
为了防止追踪历史(和使用存储), 你可以将代码块放在torch.no_grad():中. 这在评估一个模型时特别有用, 因为模型中有可训练的参数, 他们的requires_gradTrue. 而在评估过程中不需要计算梯度.
还有一个类在autograd实现中特别重要, Function.
TensorFunction内部联系并且建立一张非环形的图(acyclic graph). 这张图将运算的全部历史全部编码. 每一个tensor都有一个.grad_fn指向创建此tensor的Function. 除非Tensor是用户创建的, 其grad_fnNone.
如果你想计算导数, 你可以在一个Tensor上调用.backward(). 如果Tensor是个标量(也就是只有一个元素), 你不需要指定任何参数. 但是如果它有不止一个元素, 你需要指定一个匹配大小的tensor作为梯度参数.

import torch

创建一个requires_grad=True的tensor用来追踪计算历史.

x = torch.ones(2, 2, requires_grad=True)
print(x)

Out:

tensor([[1., 1.],
       			 [1., 1.]], requires_grad=True)

做一个tensor运算:

y = x + 2
print(y)

Out:

tensor([[3., 3.],
       			 [3., 3.]], grad_fn=<AddBackward0>)

y作为一个操作的结果而被创建, 所以它有grad_fn.

print(y.grad_fn)

Out:

<AddBackward0 object at 0x7f6a7f893e10>

在y上做更多运算.

z = y * y * 3
out = z.mean()
print(z, out)

Out:

tensor([[27., 27.],
       			 [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)

.requires_grad_( ... )原址改变现有Tensor的requires_grad标志位. 这个输入标志位默认是False.

a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)

Out:

False
True
<SumBackward0 object at 0x7f6a7f8a2ba8>

Gradients

现在开始反向传播. 因为out只有一个标量, out.backward()等效于out.backward(torch.tensor(1.)).

out.backward()

打印梯度 d(out)/dx

print(x.grad)

Out:

tensor([[4.5000, 4.5000],
        		[4.5000, 4.5000]])

你应该得到一个4.5的矩阵. 令tensorout为" o o o". 我们得到 o = 1 2 ∑ i z i o=\frac{1}{2}\sum_iz_i o=21izi, z i = 3 ( x i + 2 ) 2 z_i=3(x_i+2)^2 zi=3(xi+2)2, z i ∣ x i = 1 = 27 z_i|_{x_i=1}=27 zixi=1=27. 所以, ∂ o ∂ x i = 3 2 ( x i + 2 ) \frac{\partial o}{\partial x_i}=\frac{3}{2}(x_i+2) xio=23(xi+2), 所以, ∂ o ∂ x i ∣ x i = 1 = 9 2 = 4.5 \frac{\partial o}{\partial x_i}|_{x_i=1}=\frac{9}{2}=4.5 xioxi=1=29=4.5.

在数学上, 有一个向量函数 y ⃗ = f ( x ⃗ ) \vec{y} = f(\vec{x}) y =f(x ), 那么 y ⃗ \vec{y} y 相对于 x ⃗ \vec{x} x 的梯度就是一个Jocobian 矩阵:
J = ( ∂ y 1 ∂ x 1 . . . ∂ y 1 ∂ x n ⋮ ⋯ ⋮ ∂ y m ∂ x 1 . . . ∂ y m ∂ x n ) J=\begin{pmatrix} \frac{\partial y_1}{\partial x_1} & ...&\frac{\partial y_1}{\partial x_n} \\ \vdots &\cdots&\vdots\\ \frac{\partial y_m}{\partial x_1} & ...&\frac{\partial y_m}{\partial x_n} \end{pmatrix} J=x1y1x1ym......xny1xnym

一般地, torch.autograd是一个计算向量和Jacobian矩阵乘积的引擎. 也就是, 给定一个向量 v = ( v 1 , v 2 , ⋯   , v m ) T v=(v_1, v_2, \cdots, v_m)^T v=(v1,v2,,vm)T, 计算乘积 v T ⋅ J v^T\cdot J vTJ. 如果 v v v是一个标量函数 l = g ( y ⃗ ) l=g(\vec y) l=g(y )的梯度, 也就是, v = ( ∂ l ∂ y 1 , ⋯   , ∂ l ∂ y 1 ) v=(\frac{\partial l}{\partial y_1}, \cdots, \frac{\partial l}{\partial y_1}) v=(y1l,,y1l), 那么根据链式法则(chain rule), 向量和Jacobian矩阵乘积的结果就是 l l l x ⃗ \vec{x} x 的梯度:

J T ⋅ v = ( ∂ y 1 ∂ x 1 . . . ∂ y 1 ∂ x n ⋮ ⋱ ⋮ ∂ y m ∂ x 1 . . . ∂ y m ∂ x n ) ( ∂ l ∂ y 1 ⋮ ∂ l ∂ y m ) = ( ∂ l ∂ x 1 ⋮ ∂ l ∂ x n ) J^T\cdot v=\begin{pmatrix} \frac{\partial y_1}{\partial x_1} & ...&\frac{\partial y_1}{\partial x_n} \\ \vdots &\ddots&\vdots\\ \frac{\partial y_m}{\partial x_1} & ...&\frac{\partial y_m}{\partial x_n} \end{pmatrix} \begin{pmatrix} \frac{\partial l}{\partial y_1} \\ \vdots \\ \frac{\partial l}{\partial y_m} \end{pmatrix}= \begin{pmatrix} \frac{\partial l}{\partial x_1} \\ \vdots \\ \frac{\partial l}{\partial x_n} \end{pmatrix} JTv=x1y1x1ym......xny1xnymy1lyml=x1lxnl

(注意 v T ⋅ J v^T\cdot J vTJ输出行向量, 可以看做是J^T\cdot v所输出的列向量)

向量-Jacobian矩阵乘积的性质可以很方便的给非标量输出的模型传递外部梯度.
现在看一个向量-Jacobian矩阵乘积的例子:

x = torch.randn(3, requires_grad=True)

y = x * 2
while y.data.norm() < 1000:
    y = y * 2

print(y)

Out:

tensor([  367.6527, -1195.3390,  -546.3927], grad_fn=<MulBackward0>)

Now in this case y is no longer a scalar. torch.autograd could not compute the full Jacobian directly, but if we just want the vector-Jacobian product, simply pass the vector to backward as argument:
这里y不是标量. torch.autograd不能直接计算全部的Jacobian, 但是如果需要向量-Jacobian矩阵乘积, 简单地传递一个向量参数给backward即可:

v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)

print(x.grad)

Out:

tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])

设置.requires_grad=True, 阻止autograd追踪Tensor的历史. 将代码块放在with torch.no_grad():里:

print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
    print((x ** 2).requires_grad)

Out:

True
True
False

或者使用detach()获取一个内容相同但不需要求梯度的新tensor:

print(x.requires_grad)
y = x.detach()
print(y.requires_grad)
print(x.eq(y).all())

Out:

True
False
tensor(True)

稍后阅读:
autograd.Function的文档在:https://pytorch.org/docs/stable/autograd.html#function

Total running time of the script: ( 0 minutes 3.509 seconds)

公式编辑语法: https://katex.org/docs/supported.html

补充1. 如何自定义autograd 和nn?

简单来说, 需要继承Function基类并override forward()和backward()函数. 这两个函数都是static的.

官方文档:
https://pytorch.org/docs/stable/notes/extending.html
补充解释:
https://blog.csdn.net/Hungryof/article/details/78346304

补充2. 如何理解autograd.Function和Tensor的联系?

pytorch0.4之前版本有Variable和Tensor的区别. 现在两者统一, 使用Tensor即可.
torch.Tensor有几个属性/方法和autograd相关:

  1. grad, 存储梯度值(累加)
  2. requires_grad, (是否需要计算梯度)
  3. is_leaf, (是否叶节点, 是叶节点的tensor不从其他tensor运算得来)
  4. backward(), (计算当前tensor相对于叶节点tensor的梯度)
  5. detach(), 得到新tensor, 不再求梯度
  6. detach_(), 使tensor成为一个叶节点.
  7. register_hook(), 注册一个backward hook, 用以求梯度.
  8. retain_grad()

官方文档:
https://pytorch.org/docs/stable/autograd.html

补充3. 如何理解自动求导和DAG图的关系?

autograd机制能够记录作用于Tensor上的所有操作,生成一个动态计算图。图的叶子节点是输入的数据,根节点是输出的结果。当在根节点调用.backward()的时候就会从根到叶应用链式法则计算梯度。默认情况下,只有.requires_grad和is_leaf两个属性都为True的节点才会被计算导数,并存储到grad中。

我们已经知道PyTorch使用动态计算图(DAG)记录计算的全过程,那么DAG是怎样建立的呢?一些博客认为DAG的节点是Tensor(或说Variable),这其实是不准确的。DAG的节点是Function对象,边表示数据依赖,从输出指向输入。因此Function类在PyTorch自动微分中位居核心地位,但是用户通常不会直接去使用,导致人们对Function类了解并不多。

每当对Tensor施加一个运算的时候,就会产生一个Function对象,它产生运算的结果,记录运算的发生,并且记录运算的输入。Tensor使用.grad_fn属性记录这个计算图的入口。反向传播过程中,autograd引擎会按照逆序,通过Function的backward依次计算梯度。
在这里插入图片描述
https://www.cnblogs.com/cocode/p/10746347.html

补充4. 为什么.grad设计成累加模式?

backward函数本身没有返回值,它计算出来的梯度存放在叶子节点的grad属性中。PyTorch文档中提到,如果grad属性不为空,新计算出来的梯度值会直接加到旧值上面。

为什么不直接覆盖旧的结果呢?这是因为有些Tensor可能有多个输出,那么就需要调用多个backward。叠加的处理方式使得backward不需要考虑之前有没有被计算过导数,只需要加上去就行了,这使得设计变得更简单。因此我们用户在反向传播之前,常常需要用zero_grad函数对导数手动清零,确保计算出来的是正确的结果。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值