Autograd (自动梯度)是Pytorch能够快速又灵活地构建机器学习模型的关键。它能够用来快速而简单地计算复杂函数的多重偏导数,它是基于反向传播的神经网络学习的核心。
Autograd的强大之处在于,它能在程序运行时而不是编译时动态追踪计算,意味着即使你的模型的分支或者循环的长度或层数直到运行时才确定,它仍然能够正确的追踪计算,你将得到正确的梯度去驱动模型的学习。如果你的模型是用python构建的,在梯度计算上它就能比基于统计分析的结构固定的学习框架提供更强大的灵活度。
- 我们用Autograd来干啥?
机器学习模型是一个有输入有输出的函数。在本篇的讨论范围内,我们把输入看做一个n维向量
, 把输出也看做是一个向量
(为什么可以当成向量呢?因为从广泛意义上讲,模型可以有任意数量的输出),则有
模型的Loss(损失或误差)则为
Loss是一个标量函数,它表达的是模型的预测值与实际的lables之间的差异。
从这里开始,后面我们会忽略y的向量符号,用
代替
在模型的训练中,我们希望使loss达到最小。最理想的情况是,我们调整模型的可调参数,即权重,能够使得对所有的输入,loss都为零。在真实世界里,它是指我们调整权重,使得对于宽泛的输入,模型的总损失足够小,在可接受的范围。
我们怎么判断权重该调整的方向和大小呢?损失L最小,意味着L的一阶偏导数等于0,
我们知道,损失L不是输入的直接函数,而是输出的之间函数,
。根据链式法则有
=
。
使得事情变得复杂。如果我们再用链式法则去展开表达式,需要涉及到模型中每个权重的偏导数,每个激活函数的偏导数,以及每个数学变换的偏导数。每个偏导数的完整表达式是计算图中的每个可能路径的局部梯度的乘积之和,以我们试图测量其梯度的变量结束。
我们对各学习权重的梯度感兴趣,它告诉我们该如何调整各个学习梯度,以使得损失趋向于零。
由于这种局部导数的数量(每一个都对应于通过模型计算图的一条单独路径)将倾向于随着神经网络的深度呈指数增长,因此计算它们的复杂性也是如此。这就是autograd的用武之地:它追踪每一次计算的历史。PyTorch模型中的每个计算张量都包含其输入张量的历史以及用于创建它的函数。结合作用于张量的PyTorch函数都有一个用于计算自身导数的内置实现这一事实,这大大加快了学习所需的局部导数的计算。
- 一个简单的例子
先导入一些稍后会用到的库
代码语言:javascript复制import torch
import math
from matplotlib import pyplot as plt
像所有其它创建tensor的函数一样,linspace()接受一个可选参数requires_grad。
设置此标志为True意味着在接下来的每一次计算中,autograd将在该计算的输出张量中累积计算历史。
代码语言:javascript复制>>> a = torch.linspace(0., 2. * math.pi, steps=25, requires_grad=True)
>>> a
tensor([0.0000, 0.3307, 0.6614, 0.9921, 1.3228, 1.6535, 1.9842, 2.3149, 2.6456,
2.9762, 3.3069, 3.6376, 3.9683, 4.2990, 4.6297, 4.9604, 5.2911, 5.6218,
5.9525, 6.2832], requires_grad=True
代码语言:javascript复制>>> b = torch.sin(a)
>>> b
tensor([ 0.0000e 00, 3.2470e-01, 6.1421e-01, 8.3717e-01, 9.6940e-01,
9.9658e-01, 9.1577e-01, 7.3572e-01, 4.7595e-01, 1.6459e-01,
-1.6459e-01, -4.7595e-01, -7.3572e-01, -9.1577e-01, -9.9658e-01,
-9.6940e-01, -8.3717e-01, -6.1421e-01, -3.2470e-01, 1.7485e-07],
grad_fn=<SinBackward>)
# 注意 grad_fn处 是 <SinBackward>
代码语言:javascript复制>>> c = 2 * b
>>> c
tensor([ 0.0000e 00, 6.4940e-01, 1.2284e 00, 1.6743e 00, 1.9388e 00,
1.9932e 00, 1.8315e 00, 1.4714e 00, 9.5189e-01, 3.2919e-01,
-3.2919e-01, -9.5189e-01, -1.4714e 00, -1.8315e 00, -1.9932e 00,
-1.9388e 00, -1.6743e 00, -1.2284e 00, -6.4940e-01, 3.4969e-07],
grad_fn=<MulBackward0>)
# 注意 grad_fn处 是 <MulBackward0>
代码语言:javascript复制>>> d = c 1
>>> d
tensor([ 1.0000, 1.6494, 2.2284, 2.6743, 2.9388, 2.9932, 2.8315, 2.4714,
1.9519, 1.3292, 0.6708, 0.0481, -0.4714, -0.8315, -0.9932, -0.9388,
-0.6743, -0.2284, 0.3506, 1.0000], grad_fn=<AddBackward0>)
# 注意 grad_fn处 是 <AddBackward0>
代码语言:javascript复制>>> out = d.sum()
>>> out
tensor(20.0000, grad_fn=<SumBackward0>)
# 注意 grad_fn处 是 <SumBackward0>
代码语言:javascript复制>>> d.grad_fn # 对应 d = c 1
<AddBackward0 object at 0x00000274BEB678D0>
>>> d.grad_fn.next_functions # 对应 c = 2 * b
((<MulBackward0 object at 0x00000274E60235F8>, 0), (None, 0))
>>> d.grad_fn.next_functions[0][0].next_functions #对应 b = torch.sin(a)
((<SinBackward object at 0x00000274E60235F8>, 0), (None, 0))
>>> d.grad_fn.next_functions[0][0].next_functions[0][0].next_functions
((<AccumulateGrad object at 0x00000274BEB678D0>, 0),)
代码语言:javascript复制>>> c.grad_fn
<MulBackward0 object at 0x00000274E692A898>
代码语言:javascript复制>>> b.grad_fn
<SinBackward object at 0x00000274E60235F8>
代码语言:javascript复制>>> a.grad_fn
None # 叶子节点没有grad_fn
代码语言:javascript复制
代码语言:javascript复制# d.backward() # RuntimeError("grad can be implicitly created only for scalar outputs")
>>> out.backward() # 对输出调用backward函数后
>>> a.grad # 接口获得其对叶子节点的梯度
tensor([ 2.0000, 1.8916, 1.5783, 1.0939, 0.4910, -0.1652, -0.8034, -1.3546, -1.7589, -1.9727, -1.9727, -1.7589, -1.3546, -0.8034, -0.1652, 0.4910, 1.0939, 1.5783, 1.8916, 2.0000])
>>> b.grad # 如果访问非叶子节点的梯度,则返回None,并触发一个警告
None
UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won't be populated during autograd.backward(). If you indeed want the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. See github.com/pytorch/pytorch/pull/30531 for more information.
在这个简单的例子中, 仅仅只有输入 a 是一个叶子节点, 所以只有它才有梯度计算。
绘图
代码语言:javascript复制# 直接用 a 和 b 绘图会报错:
# RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
plt.plot(a.data, b.data, "r-", label="Original")
# 或用 plt.plot(a.detach(), b.detach(), "r-")
# detach 函数返回一个新的张量,从当前的计算图脱离
# plt.show()
plt.plot(a.data, a.grad.data, 'b-', label='Grad')
plt.legend()
plt.show()