PyTorch
中,所有神经网络的核心是autograd
包。autograd
包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义(define-by-run)
的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。
torch.Tensor
是这个包的核心类。如果设置
.requires_grad
为 True
,那么将会追踪所有对于该张量的操作。
当完成计算后通过调用 .backward()
,自动计算所有的梯度,
这个张量的所有梯度将会自动积累到 .grad
属性。
要阻止张量跟踪历史记录,可以调用.detach()
方法将其与计算历史记录分离,并禁止跟踪它将来的计算记录。
为了防止跟踪历史记录(和使用内存),可以将代码块包装在with torch.no_grad():
中。
在评估模型时特别有用,因为模型可能具有requires_grad = True
的可训练参数,但是我们不需要梯度计算。
在自动梯度计算中还有另外一个重要的类Function
.
Tensor
和 Function
互相连接并生成一个非循环图,它表示和存储了完整的计算历史。
每个张量都有一个.grad_fn
属性,这个属性引用了一个创建了Tensor
的Function
(除非这个张量是用户手动创建的,即,这个张量的
grad_fn
是 None
)。
如果需要计算导数,你可以在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=41xi∈X∑3(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}} ∂x∂out
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=41i∑zi
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)2和zi∣∣xi=1=27
因此
∂ o ∂ x i = 3 2 ( x i + 2 ) \frac{\partial o}{\partial x_i} = \frac{3}{2}(x_i+2) ∂xi∂o=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 ∂xi∂o∣∣xi=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=⎝⎜⎛∂x1∂y1⋮∂x1∂ym⋯⋱⋯∂xn∂y1⋮∂xn∂ym⎠⎟⎞
一般来说,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 vector−Jacobian−product的工具。也就是说,给定任一向量 v = ( v 1 v 2 ⋯ v m ) T v=(v_{1}\;v_{2}\;\cdots\;v_{m})^{T} v=(v1v2⋯vm)T ,计算 v ⋅ J v\cdot J v⋅J ,如果 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=(∂y1∂l⋯∂ym∂l)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 vector−Jacobian−product 是 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} J⋅v=⎝⎜⎛∂x1∂y1⋮∂xn∂y1⋯⋱⋯∂x1∂ym⋮∂xn∂ym⎠⎟⎞⎝⎜⎛∂y1∂l⋮∂ym∂l⎠⎟⎞=⎝⎜⎛∂x1∂l⋮∂xn∂l⎠⎟⎞
v e c t o r − J a c o b i a n − p r o d u c t vector-Jacobian-product vector−Jacobian−product 这种特性使得将外部梯度返回到具有非标量输出的模型变得非常方便。
现在让我们来看一个 v e c t o r − J a c o b i a n − p r o d u c t vector-Jacobian-product vector−Jacobian−product的例子
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 vector−Jacobian−product,只需将向量作为参数传入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
推荐阅读:
autograd
和 Function
的官方文档
下载 Python 源代码:autograd_tutorial.py
下载 Jupyter 源代码:autograd_tutorial.ipynb
import torch
torch.__version__
'1.6.0'
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类中
在张量创建时,通过设置 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()进行嵌套后,代码不会跟踪历史记录,也就是说保存的这部分记录会减少内存的使用量并且会加快少许的运算速度。
为了说明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”,都是指像x
,y
这样的手动创建的、而非运算得到的变量,这些变量成为创建变量。
像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
是通过x
与y
的一系列计算得到的,所以不是leaf
结点也就是结果变量。
为什么我们执行z.backward()
方法会更新x.grad
和y.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))
这样整个规程就很清晰了:
leaf
节点。最终当我们执行完c.backward()之后,a和b里面的grad值就得到了更新。
如果需要自定义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