PyTorch 笔记Ⅱ——PyTorch 自动求导机制

文章目录

  • Autograd: 自动求导机制
    • 张量(Tensor)
    • 梯度
  • 使用PyTorch计算梯度数值
    • Autograd
    • 简单的自动求导
    • 复杂的自动求导
    • Autograd 过程解析
    • 扩展Autograd

Autograd: 自动求导机制

PyTorch中,所有神经网络的核心是autograd 包。autograd 包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义(define-by-run)的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。

PyTorch 笔记Ⅱ——PyTorch 自动求导机制_第1张图片

张量(Tensor)

torch.Tensor是这个包的核心类。如果设置
.requires_gradTrue,那么将会追踪所有对于该张量的操作。
当完成计算后通过调用 .backward(),自动计算所有的梯度,
这个张量的所有梯度将会自动积累到 .grad 属性。

要阻止张量跟踪历史记录,可以调用.detach()方法将其与计算历史记录分离,并禁止跟踪它将来的计算记录。

为了防止跟踪历史记录(和使用内存),可以将代码块包装在with torch.no_grad():中。
在评估模型时特别有用,因为模型可能具有requires_grad = True的可训练参数,但是我们不需要梯度计算。

在自动梯度计算中还有另外一个重要的类Function.

TensorFunction互相连接并生成一个非循环图,它表示和存储了完整的计算历史。
每个张量都有一个.grad_fn属性,这个属性引用了一个创建了TensorFunction(除非这个张量是用户手动创建的,即,这个张量的
grad_fnNone)。

如果需要计算导数,你可以在Tensor上调用.backward()
如果Tensor是一个标量(即它包含一个元素数据)则不需要为backward()指定任何参数,
但是如果它有更多的元素,你需要指定一个gradient 参数来匹配张量的形状。

具体的后面会有详细说明

import torch

创建一个张量,设置 requires_grad=True 来跟踪与它相关的计算,创建一个单位矩阵

x = torch.ones(2, 2, requires_grad=True)
print(x)
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

对张量进行操作:

y = x + 2
print(y)
tensor([[3., 3.],
        [3., 3.]], grad_fn=)

结果y已经被计算出来了,所以,grad_fn已经被自动生成了。

print(y.grad_fn)

y进行一个操作,*号计算点乘,将矩阵的对应元素相乘,.mean操作是对矩阵求均值,可以利用axis指定求均值的维度

Tensor1 = torch.tensor([5.5, 3])
Tensor2 = torch.tensor([2, 6])

result = Tensor1 * Tensor2
print(result)
print(result.mean(axis=0))
tensor([11., 18.])
tensor(14.5000)

因此可以发现 o u t = 1 4 ∑ x i ∈ X 3 ( x i + 2 ) 2 out = \frac{1}{4}\sum\limits_{ {x_i} \in X} {3({x_i}} + 2{)^2} out=41xiX3(xi+2)2

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

print(z, out)
tensor([[27., 27.],
        [27., 27.]], grad_fn=) tensor(27., grad_fn=)

.requires_grad_( ... ) 可以改变现有张量的 requires_grad属性。
如果没有指定的话,默认输入的flag是 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)
False
True

梯度

反向传播
因为 out是一个标量(scalar),out.backward() 等于out.backward(torch.tensor(1))

out.backward()

打印梯度 ∂ o u t ∂ x \frac{ {\partial out}}{ {\partial x}} xout

print(x.grad)
tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])

得到矩阵 4.5.将 out叫做
Tensor o o o”.
得到
o = 1 4 ∑ i z i o = \frac{1}{4}\sum_i z_i o=41izi
z i = 3 ( x i + 2 ) 2 和 z i ∣ x i = 1 = 27 z_i = 3(x_i+2)^2 和 z_i\bigr\rvert_{x_i=1} = 27 zi=3(xi+2)2zixi=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}\bigr\rvert_{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 的梯度是一个雅可比矩阵(Jacobian matrix):

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}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{pmatrix} J=x1y1x1ymxny1xnym

一般来说,torch.autograd就是用来计算 v e c t o r − J a c o b i a n − p r o d u c t vector-Jacobian-product vectorJacobianproduct的工具。也就是说,给定任一向量 v = ( v 1    v 2    ⋯    v m ) T v=(v_{1}\;v_{2}\;\cdots\;v_{m})^{T} v=(v1v2vm)T ,计算 v ⋅ J v\cdot J vJ ,如果 v v v 恰好是标量函数 l = g ( y ⃗ ) l=g(\vec{y}) l=g(y ) 的梯度,也就是说 v = ( ∂ l ∂ y 1    ⋯    ∂ l ∂ y m ) T v=(\frac{\partial l}{\partial y_{1}}\;\cdots\;\frac{\partial l}{\partial y_{m}})^{T} v=(y1lyml)T,那么根据链式法则, v e c t o r − J a c o b i a n − p r o d u c t vector-Jacobian-product vectorJacobianproduct x ⃗ \vec{x} x l l l 的梯度:

J ⋅ v = ( ∂ y 1 ∂ x 1 ⋯ ∂ y m ∂ x 1 ⋮ ⋱ ⋮ ∂ y 1 ∂ x n ⋯ ∂ y m ∂ x n ) ( ∂ l ∂ y 1 ⋮ ∂ l ∂ y m ) = ( ∂ l ∂ x 1 ⋮ ∂ l ∂ x n ) J\cdot v = \begin{pmatrix} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{1}} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_{1}}{\partial x_{n}} & \cdots & \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} Jv=x1y1xny1x1ymxnymy1lyml=x1lxnl

v e c t o r − J a c o b i a n − p r o d u c t vector-Jacobian-product vectorJacobianproduct 这种特性使得将外部梯度返回到具有非标量输出的模型变得非常方便。

现在让我们来看一个 v e c t o r − J a c o b i a n − p r o d u c t vector-Jacobian-product vectorJacobianproduct的例子

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

y = x * 2
i = 0
while y.data.norm() < 1000:
    i += 1
    print('已计算'+str(i)+'次')
    y = y * 2

print(y)
已计算1次
已计算2次
已计算3次
已计算4次
已计算5次
已计算6次
已计算7次
已计算8次
已计算9次
已计算10次
tensor([-150.5787, 1158.3252,  227.0515], grad_fn=)

在这个情形中,y不再是个标量。torch.autograd无法直接计算出完整的雅可比行列,但是如果我们只想要 v e c t o r − J a c o b i a n − p r o d u c t vector-Jacobian-product vectorJacobianproduct,只需将向量作为参数传入backward

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

print(x.grad)
tensor([4.0960e+02, 4.0960e+03, 4.0960e-01])

如果.requires_grad=True但是你又不希望进行autograd的计算,
那么可以将变量包裹在 with torch.no_grad()中:

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

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

推荐阅读:

autogradFunction 的官方文档

下载 Python 源代码:autograd_tutorial.py

下载 Jupyter 源代码:autograd_tutorial.ipynb

import torch
torch.__version__
'1.6.0'

使用PyTorch计算梯度数值

PyTorch的Autograd模块实现了深度学习的算法中的向传播求导数,在张量(Tensor类)上的所有操作,Autograd都能为他们自动提供微分,简化了手动计算导数的复杂过程。

在0.4以前的版本中,Pytorch使用Variable类来自动计算所有的梯度Variable类主要包含三个属性:
data:保存Variable所包含的Tensor;grad:保存data对应的梯度,grad也是个Variable,而不是Tensor,它和data的形状一样;grad_fn:指向一个Function对象,这个Function用来反向传播计算输入的梯度。

从0.4起, Variable 正式合并入Tensor类,通过Variable嵌套实现的自动微分功能已经整合进入了Tensor类中。虽然为了代码的兼容性还是可以使用Variable(tensor)这种方式进行嵌套,但是这个操作其实什么都没做。

所以,以后的代码建议直接使用Tensor类进行操作,因为官方文档中已经将Variable设置成过期模块。

要想通过Tensor类本身就支持了使用autograd功能,只需要设置.requries_grad=True

Variable类中的的grad和grad_fn属性已经整合进入了Tensor类中

Autograd

在张量创建时,通过设置 requires_grad 标识为Ture来告诉Pytorch需要对该张量进行自动求导,PyTorch会记录该张量的每一步操作历史并自动计算

x = torch.rand(5, 5, requires_grad=True)
x
tensor([[0.1595, 0.3191, 0.2781, 0.6355, 0.0536],
        [0.7235, 0.1022, 0.3043, 0.3100, 0.6912],
        [0.4470, 0.0810, 0.0477, 0.3036, 0.3335],
        [0.8119, 0.2347, 0.3242, 0.0228, 0.9589],
        [0.6572, 0.5987, 0.8049, 0.8557, 0.1556]], requires_grad=True)
y = torch.rand(5, 5, requires_grad=True)
y
tensor([[0.7061, 0.8268, 0.0092, 0.8704, 0.9093],
        [0.9566, 0.3723, 0.2447, 0.9608, 0.2034],
        [0.8147, 0.8910, 0.2384, 0.3672, 0.1902],
        [0.1308, 0.6554, 0.6274, 0.6656, 0.0203],
        [0.4652, 0.4639, 0.4498, 0.9452, 0.2066]], requires_grad=True)

PyTorch会自动追踪和记录对于张量的所有操作,当计算完成后调用.backward()方法自动计算梯度并且将计算结果保存到grad属性中。

z=torch.sum(x+y)
z
tensor(23.4058, grad_fn=)

在张量进行操作后,grad_fn已经被赋予了一个新的函数,这个函数引用了一个创建了这个Tensor类的Function对象。
Tensor和Function互相连接生成了一个非循环图,它记录并且编码了完整的计算历史。每个张量都有一个.grad_fn属性,如果这个张量是用户手动创建的那么这个张量的grad_fn是None。

下面我们来调用反向传播函数,计算其梯度

简单的自动求导

z.backward()
print(x.grad)
print(y.grad)

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

如果Tensor类表示的是一个标量(即它包含一个元素的张量),则不需要为backward()指定任何参数,但是如果它有更多的元素,则需要指定一个gradient参数,它是形状匹配的张量。
以上的 z.backward()相当于是z.backward(torch.tensor(1.))的简写。
这种参数常出现在图像分类中的单标签分类,输出一个标量代表图像的标签。

复杂的自动求导

x = torch.rand(5, 5, requires_grad=True)
y = torch.rand(5, 5, requires_grad=True)
z= x**2+y**3
z
tensor([[0.0176, 0.5407, 0.9507, 1.2308, 0.8023],
        [1.4538, 1.0718, 0.9246, 1.3767, 0.6727],
        [0.2848, 0.1618, 0.1929, 0.0155, 0.6381],
        [0.1712, 0.2966, 0.8279, 1.1194, 1.0610],
        [0.6224, 0.8024, 1.1009, 0.7295, 0.0825]], grad_fn=)
x
tensor([[0.0698, 0.4408, 0.9211, 0.8096, 0.6934],
        [0.8280, 0.9740, 0.9241, 0.9074, 0.2516],
        [0.5331, 0.2179, 0.2037, 0.1245, 0.6559],
        [0.4098, 0.5446, 0.2732, 0.8793, 0.4946],
        [0.7590, 0.8078, 0.4609, 0.8535, 0.2584]], requires_grad=True)
#我们的返回值不是一个标量,所以需要输入一个大小相同的张量作为参数,这里我们用ones_like函数根据x生成一个张量
z.backward(torch.ones_like(x))
# 2x
print(x.grad)
tensor([[0.1396, 0.8817, 1.8421, 1.6191, 1.3869],
        [1.6560, 1.9480, 1.8481, 1.8148, 0.5033],
        [1.0662, 0.4358, 0.4074, 0.2491, 1.3119],
        [0.8196, 1.0891, 0.5464, 1.7587, 0.9892],
        [1.5180, 1.6156, 0.9218, 1.7070, 0.5167]])
2*x
tensor([[0.1396, 0.8817, 1.8421, 1.6191, 1.3869],
        [1.6560, 1.9480, 1.8481, 1.8148, 0.5033],
        [1.0662, 0.4358, 0.4074, 0.2491, 1.3119],
        [0.8196, 1.0891, 0.5464, 1.7587, 0.9892],
        [1.5180, 1.6156, 0.9218, 1.7070, 0.5167]], grad_fn=)

我们可以使用with torch.no_grad()上下文管理器临时禁止对已设置requires_grad=True的张量进行自动求导。这个方法在测试集计算准确率的时候会经常用到,例如:

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

使用.no_grad()进行嵌套后,代码不会跟踪历史记录,也就是说保存的这部分记录会减少内存的使用量并且会加快少许的运算速度。

Autograd 过程解析

为了说明Pytorch的自动求导原理,我们来尝试分析一下PyTorch的源代码,虽然Pytorch的 Tensor和 TensorBase都是使用CPP来实现的,但是可以使用一些Python的一些方法查看这些对象在Python的属性和状态。
Python的 dir() 返回参数的属性、方法列表。z是一个Tensor变量,看看里面有哪些成员变量。

dir(z)
['T',
 '__abs__',
 '__add__',
 '__and__',
 '__array__',
 '__array_priority__',
 '__array_wrap__',
 '__bool__',
 '__class__',
 '__contains__',
 '__deepcopy__',
 '__delattr__',
 '__delitem__',
 '__dict__',
 '__dir__',
 '__div__',
 '__doc__',
 '__eq__',
 '__float__',
 '__floordiv__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__iadd__',
 '__iand__',
 '__idiv__',
 '__ifloordiv__',
 '__ilshift__',
 '__imul__',
 '__index__',
 '__init__',
 '__init_subclass__',
 '__int__',
 '__invert__',
 '__ior__',
 '__ipow__',
 '__irshift__',
 '__isub__',
 '__iter__',
 '__itruediv__',
 '__ixor__',
 '__le__',
 '__len__',
 '__long__',
 '__lshift__',
 '__lt__',
 '__matmul__',
 '__mod__',
 '__module__',
 '__mul__',
 '__ne__',
 '__neg__',
 '__new__',
 '__nonzero__',
 '__or__',
 '__pow__',
 '__radd__',
 '__rdiv__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__reversed__',
 '__rfloordiv__',
 '__rmul__',
 '__rpow__',
 '__rshift__',
 '__rsub__',
 '__rtruediv__',
 '__setattr__',
 '__setitem__',
 '__setstate__',
 '__sizeof__',
 '__str__',
 '__sub__',
 '__subclasshook__',
 '__truediv__',
 '__weakref__',
 '__xor__',
 '_backward_hooks',
 '_base',
 '_cdata',
 '_coalesced_',
 '_dimI',
 '_dimV',
 '_grad',
 '_grad_fn',
 '_indices',
 '_is_view',
 '_make_subclass',
 '_nnz',
 '_update_names',
 '_values',
 '_version',
 'abs',
 'abs_',
 'absolute',
 'absolute_',
 'acos',
 'acos_',
 'acosh',
 'acosh_',
 'add',
 'add_',
 'addbmm',
 'addbmm_',
 'addcdiv',
 'addcdiv_',
 'addcmul',
 'addcmul_',
 'addmm',
 'addmm_',
 'addmv',
 'addmv_',
 'addr',
 'addr_',
 'align_as',
 'align_to',
 'all',
 'allclose',
 'angle',
 'any',
 'apply_',
 'argmax',
 'argmin',
 'argsort',
 'as_strided',
 'as_strided_',
 'as_subclass',
 'asin',
 'asin_',
 'asinh',
 'asinh_',
 'atan',
 'atan2',
 'atan2_',
 'atan_',
 'atanh',
 'atanh_',
 'backward',
 'baddbmm',
 'baddbmm_',
 'bernoulli',
 'bernoulli_',
 'bfloat16',
 'bincount',
 'bitwise_and',
 'bitwise_and_',
 'bitwise_not',
 'bitwise_not_',
 'bitwise_or',
 'bitwise_or_',
 'bitwise_xor',
 'bitwise_xor_',
 'bmm',
 'bool',
 'byte',
 'cauchy_',
 'ceil',
 'ceil_',
 'char',
 'cholesky',
 'cholesky_inverse',
 'cholesky_solve',
 'chunk',
 'clamp',
 'clamp_',
 'clamp_max',
 'clamp_max_',
 'clamp_min',
 'clamp_min_',
 'clone',
 'coalesce',
 'conj',
 'contiguous',
 'copy_',
 'cos',
 'cos_',
 'cosh',
 'cosh_',
 'cpu',
 'cross',
 'cuda',
 'cummax',
 'cummin',
 'cumprod',
 'cumsum',
 'data',
 'data_ptr',
 'deg2rad',
 'deg2rad_',
 'dense_dim',
 'dequantize',
 'det',
 'detach',
 'detach_',
 'device',
 'diag',
 'diag_embed',
 'diagflat',
 'diagonal',
 'digamma',
 'digamma_',
 'dim',
 'dist',
 'div',
 'div_',
 'dot',
 'double',
 'dtype',
 'eig',
 'element_size',
 'eq',
 'eq_',
 'equal',
 'erf',
 'erf_',
 'erfc',
 'erfc_',
 'erfinv',
 'erfinv_',
 'exp',
 'exp_',
 'expand',
 'expand_as',
 'expm1',
 'expm1_',
 'exponential_',
 'fft',
 'fill_',
 'fill_diagonal_',
 'flatten',
 'flip',
 'fliplr',
 'flipud',
 'float',
 'floor',
 'floor_',
 'floor_divide',
 'floor_divide_',
 'fmod',
 'fmod_',
 'frac',
 'frac_',
 'gather',
 'ge',
 'ge_',
 'geometric_',
 'geqrf',
 'ger',
 'get_device',
 'grad',
 'grad_fn',
 'gt',
 'gt_',
 'half',
 'hardshrink',
 'has_names',
 'histc',
 'ifft',
 'imag',
 'index_add',
 'index_add_',
 'index_copy',
 'index_copy_',
 'index_fill',
 'index_fill_',
 'index_put',
 'index_put_',
 'index_select',
 'indices',
 'int',
 'int_repr',
 'inverse',
 'irfft',
 'is_coalesced',
 'is_complex',
 'is_contiguous',
 'is_cuda',
 'is_distributed',
 'is_floating_point',
 'is_leaf',
 'is_meta',
 'is_mkldnn',
 'is_nonzero',
 'is_pinned',
 'is_quantized',
 'is_same_size',
 'is_set_to',
 'is_shared',
 'is_signed',
 'is_sparse',
 'isclose',
 'isfinite',
 'isinf',
 'isnan',
 'istft',
 'item',
 'kthvalue',
 'layout',
 'le',
 'le_',
 'lerp',
 'lerp_',
 'lgamma',
 'lgamma_',
 'log',
 'log10',
 'log10_',
 'log1p',
 'log1p_',
 'log2',
 'log2_',
 'log_',
 'log_normal_',
 'log_softmax',
 'logaddexp',
 'logaddexp2',
 'logcumsumexp',
 'logdet',
 'logical_and',
 'logical_and_',
 'logical_not',
 'logical_not_',
 'logical_or',
 'logical_or_',
 'logical_xor',
 'logical_xor_',
 'logsumexp',
 'long',
 'lstsq',
 'lt',
 'lt_',
 'lu',
 'lu_solve',
 'map2_',
 'map_',
 'masked_fill',
 'masked_fill_',
 'masked_scatter',
 'masked_scatter_',
 'masked_select',
 'matmul',
 'matrix_power',
 'max',
 'mean',
 'median',
 'min',
 'mm',
 'mode',
 'mul',
 'mul_',
 'multinomial',
 'mv',
 'mvlgamma',
 'mvlgamma_',
 'name',
 'names',
 'narrow',
 'narrow_copy',
 'ndim',
 'ndimension',
 'ne',
 'ne_',
 'neg',
 'neg_',
 'nelement',
 'new',
 'new_empty',
 'new_full',
 'new_ones',
 'new_tensor',
 'new_zeros',
 'nonzero',
 'norm',
 'normal_',
 'numel',
 'numpy',
 'orgqr',
 'ormqr',
 'output_nr',
 'permute',
 'pin_memory',
 'pinverse',
 'polygamma',
 'polygamma_',
 'pow',
 'pow_',
 'prelu',
 'prod',
 'put_',
 'q_per_channel_axis',
 'q_per_channel_scales',
 'q_per_channel_zero_points',
 'q_scale',
 'q_zero_point',
 'qr',
 'qscheme',
 'rad2deg',
 'rad2deg_',
 'random_',
 'real',
 'reciprocal',
 'reciprocal_',
 'record_stream',
 'refine_names',
 'register_hook',
 'reinforce',
 'relu',
 'relu_',
 'remainder',
 'remainder_',
 'rename',
 'rename_',
 'renorm',
 'renorm_',
 'repeat',
 'repeat_interleave',
 'requires_grad',
 'requires_grad_',
 'reshape',
 'reshape_as',
 'resize',
 'resize_',
 'resize_as',
 'resize_as_',
 'retain_grad',
 'rfft',
 'roll',
 'rot90',
 'round',
 'round_',
 'rsqrt',
 'rsqrt_',
 'scatter',
 'scatter_',
 'scatter_add',
 'scatter_add_',
 'select',
 'set_',
 'shape',
 'share_memory_',
 'short',
 'sigmoid',
 'sigmoid_',
 'sign',
 'sign_',
 'sin',
 'sin_',
 'sinh',
 'sinh_',
 'size',
 'slogdet',
 'smm',
 'softmax',
 'solve',
 'sort',
 'sparse_dim',
 'sparse_mask',
 'sparse_resize_',
 'sparse_resize_and_clear_',
 'split',
 'split_with_sizes',
 'sqrt',
 'sqrt_',
 'square',
 'square_',
 'squeeze',
 'squeeze_',
 'sspaddmm',
 'std',
 'stft',
 'storage',
 'storage_offset',
 'storage_type',
 'stride',
 'sub',
 'sub_',
 'sum',
 'sum_to_size',
 'svd',
 'symeig',
 't',
 't_',
 'take',
 'tan',
 'tan_',
 'tanh',
 'tanh_',
 'to',
 'to_dense',
 'to_mkldnn',
 'to_sparse',
 'tolist',
 'topk',
 'trace',
 'transpose',
 'transpose_',
 'triangular_solve',
 'tril',
 'tril_',
 'triu',
 'triu_',
 'true_divide',
 'true_divide_',
 'trunc',
 'trunc_',
 'type',
 'type_as',
 'unbind',
 'unflatten',
 'unfold',
 'uniform_',
 'unique',
 'unique_consecutive',
 'unsqueeze',
 'unsqueeze_',
 'values',
 'var',
 'view',
 'view_as',
 'where',
 'zero_']

返回很多,我们直接排除掉一些Python中特殊方法(以__开头和结束的)和私有方法(以_开头的,直接看几个比较主要的属性:
.is_leaf:记录是否是leaf节点。通过这个属性来确定这个变量的类型
在官方文档中所说的“graph leaves”,“leaf variables”,都是指像xy这样的手动创建的、而非运算得到的变量,这些变量成为创建变量。
z这样的,是通过计算后得到的结果称为结果变量。

一个变量是创建变量还是结果变量是通过.is_leaf来获取的。

print("x.is_leaf="+str(x.is_leaf))
print("z.is_leaf="+str(z.is_leaf))
x.is_leaf=True
z.is_leaf=False

x是手动创建的没有通过计算,所以他被认为是一个leaf节点也就是一个创建变量,而z是通过xy的一系列计算得到的,所以不是leaf结点也就是结果变量。

为什么我们执行z.backward()方法会更新x.grady.grad呢?
.grad_fn属性记录的就是这部分的操作,虽然.backward()方法也是CPP实现的,但是可以通过Python来进行简单的探索。

grad_fn:记录并且编码了完整的计算历史

z.grad_fn

grad_fn是一个AddBackward0类型的变量 AddBackward0这个类也是用Cpp来写的,但是我们从名字里就能够大概知道,他是加法(ADD)的反反向传播(Backward),看看里面有些什么东西

dir(z.grad_fn)
['__call__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '_register_hook_dict',
 'metadata',
 'name',
 'next_functions',
 'register_hook',
 'requires_grad']

next_functions就是grad_fn的精华

z.grad_fn.next_functions
((, 0), (, 0))

next_functions是一个tuple of tuple of PowBackward0 and int。

为什么是2个tuple ?
因为我们的操作是z= x**2+y**3 刚才的AddBackward0是相加,而前面的操作是乘方 PowBackward0。tuple第一个元素就是x相关的操作记录

xg = z.grad_fn.next_functions[0][0]
dir(xg)
['__call__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '_register_hook_dict',
 'metadata',
 'name',
 'next_functions',
 'register_hook',
 'requires_grad']

继续深挖

x_leaf=xg.next_functions[0][0]
type(x_leaf)
AccumulateGrad

在PyTorch的反向图计算中,AccumulateGrad类型代表的就是leaf节点类型,也就是计算图终止节点。AccumulateGrad类中有一个.variable属性指向leaf节点。

x_leaf.variable
tensor([[0.0698, 0.4408, 0.9211, 0.8096, 0.6934],
        [0.8280, 0.9740, 0.9241, 0.9074, 0.2516],
        [0.5331, 0.2179, 0.2037, 0.1245, 0.6559],
        [0.4098, 0.5446, 0.2732, 0.8793, 0.4946],
        [0.7590, 0.8078, 0.4609, 0.8535, 0.2584]], requires_grad=True)

这个.variable的属性就是我们的生成的变量x

print("x_leaf.variable的id:"+str(id(x_leaf.variable)))
print("x的id:"+str(id(x)))
x_leaf.variable的id:2559778408632
x的id:2559778408632
assert(id(x_leaf.variable)==id(x))

这样整个规程就很清晰了:

  1. 当我们执行z.backward()的时候。这个操作将调用z里面的grad_fn这个属性,执行求导的操作。
  2. 这个操作将遍历grad_fn的next_functions,然后分别取出里面的Function(AccumulateGrad),执行求导操作。这部分是一个递归的过程直到最后类型为leaf节点。
  3. 计算出结果以后,将结果保存到他们对应的variable 这个变量所引用的对象(x和y)的 grad这个属性里面。
  4. 求导结束。所有的叶节点的grad变量都得到了相应的更新

最终当我们执行完c.backward()之后,a和b里面的grad值就得到了更新。

扩展Autograd

如果需要自定义autograd扩展新的功能,就需要扩展Function类。因为Function使用autograd来计算结果和梯度,并对操作历史进行编码。
在Function类中最主要的方法就是forward()backward()他们分别代表了前向传播和反向传播。

一个自定义的Function需要一下三个方法:

__init__ (optional):如果这个操作需要额外的参数则需要定义这个Function的构造函数,不需要的话可以忽略。

forward():执行前向传播的计算代码

backward():反向传播时梯度计算的代码。 参数的个数和forward返回值的个数一样,每个参数代表传回到此操作的梯度。
# 引入Function便于扩展
from torch.autograd.function import Function
# 定义一个乘以常数的操作(输入参数是张量)
# 方法必须是静态方法,所以要加上@staticmethod 
class MulConstant(Function):
    @staticmethod 
    def forward(ctx, tensor, constant):
        # ctx 用来保存信息这里类似self,并且ctx的属性可以在backward中调用
        ctx.constant=constant
        return tensor *constant
    @staticmethod
    def backward(ctx, grad_output):
        # 返回的参数要与输入的参数一样.
        # 第一个输入为3x3的张量,第二个为一个常数
        # 常数的梯度必须是 None.
        return grad_output, None 

定义完我们的新操作后,我们来进行测试

a=torch.rand(3,3,requires_grad=True)
b=MulConstant.apply(a,5)
print("a:"+str(a))
print("b:"+str(b)) # b为a的元素乘以5
a:tensor([[0.8143, 0.9200, 0.1687],
        [0.4485, 0.6652, 0.0561],
        [0.6099, 0.4022, 0.3129]], requires_grad=True)
b:tensor([[4.0716, 4.5998, 0.8436],
        [2.2425, 3.3259, 0.2806],
        [3.0496, 2.0112, 1.5646]], grad_fn=)

反向传播,返回值不是标量,所以backward方法需要参数

b.backward(torch.ones_like(a))
a.grad
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])

梯度因为1

参考链接:PyTorch官方教程中文版
参考链接:yunjey-pytorch-tutorial

你可能感兴趣的:(PyTorch基础笔记,Pytorch,Autograd,自动求导)