自动求梯度
梯度是几乎所有深度学习算法的重要步骤,尽管计算这些微分是直截了当的,仅需要一些简单的推导,但而对于复杂的模型,通过手工计算更新参数是非常痛苦的也非常容易出错。
深度学习框架通过 自动计算梯度 加快了这一工作,实际上,基于我们设计的模型,系统会构建一个 计算图(computational graph) ,通过跟踪数据通过哪些操作产生的结果。自动求梯度使系统随后反向传播梯度。反向传播(back propagation) 只是意味着通过计算图进行跟踪,对每一个参数填充偏导数。
import torch
一个简单的例子
比如我们对关于列向量 \(x\) 的函数 \(y=2x^Tx\) 的梯度感兴趣,开始之前,先让我们创建一个变量 x
并赋初值。
x = torch.arange(4.0)
x
tensor([0., 1., 2., 3.])
在我们计算关于 \(x\) 的函数 \(y\) 的梯度之前,我们需要内存空间去存储它。我们在每次计算关于参数的导数时不用都申请一次新的内存,因为我们会经常对参数进行更新成千上百次,并且可能很快就溢出内存,这是很重要的。注意对于向量 \(x\) 的标量函数,它的梯度也是向量,并且具有和 \(x\) 相同的 shape
x.requires_grad_(True) # 等价于 x = torch.arange(4.0, requires_grad=True)
print(x.grad) # 默认值是 None
None
现在让我们计算 \(y\) 的值
y = 2 * torch.dot(x, x)
y
tensor(28., grad_fn=<MulBackward0>)
因为 x
是一个 4 维的向量,x
与 x
的点积计算返回一个标量并赋值给 y
。下一步,我们可以通过调用反向传播函数自动地计算出对于 x
的每一部分 y
的梯度值,并将其输出。
y.backward()
x.grad
d:\programming\miniconda3\envs\d2l\lib\site-packages\torch\autograd\__init__.py:130: UserWarning: CUDA initialization: Found no NVIDIA driver on your system. Please check that you have an NVIDIA GPU and installed a driver from http://www.nvidia.com/Download/index.aspx (Triggered internally at ..\c10\cuda\CUDAFunctions.cpp:100.)
Variable._execution_engine.run_backward(
tensor([ 0., 4., 8., 12.])
关于 \(x\) 的函数 \(y=2x^Tx\) 的梯度应该是 \(4x\),可以让我们验证一下梯度的计算是否正确。
x.grad == 4 * x
tensor([True, True, True, True])
现在,让我们计算关于 x
的其它函数。
# PyTorch 默认积累梯度,我们需要清楚之前的梯度值
x.grad.zero_()
y = x.sum()
y.backward()
x.grad
tensor([1., 1., 1., 1.])
非标量变量的反向传播
从技术上讲,当 y
不再是标量的时候,关于向量 x
的函数向量 y
的微分最自然地解释是一个矩阵。对于高阶和高维的 y
和 x
微分的结果可能是更高纬度的张量。
但是,这些非同寻常的对象会出现在高级的机器学习(包括深度学习)中,当我们说起向量的反向传播时,我们是尝试去计算关于每一批量的训练样本的损失函数的导数。在这里,我们的意图不是计算微分矩阵,而是对于每一个批量的样本单独地计算偏导数的和。
# 在非标量上调用 backward 需要传入一个 gradient 的梯度参数,指定关于 self (即调用对象)的微分梯度函数
# 在下面的例子中,我们只是简单地想要求偏导数的和,所以我们传入了 ones 的梯度是合适的
x.grad.zero_()
y = x * x
# 下面的语句等价于 y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
tensor([0., 2., 4., 6.])
分离计算
在某些情况下,我们希望将某些计算移出在计算图的记录之外。举个例子,y
是被计算为关于 x
的函数,且随后 z
被计算为 y
和 x
的函数。现在,想象我们像计算 z
关于 x
的梯度,但是由于某些原因要将 y
视为常数,并且只考虑了在 y
被计算之后 x
的作用。
在这里,我们可以分离 y
返回一个和 y
有着相同值并且将任何关于如何计算 y
的计算图的信息丢弃的新变量 u
。换句话说,梯度不会通过 u
反向流向 x
。因此,跟随反向传播函数计算 z = u * x
关于 x
将 u
作为常数的偏导数,而不是计算 z = x * x * x
关于 x
的偏导数。
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad == u
tensor([True, True, True, True])
计算关于 Python 控制流的梯度
使用自动求梯度的一个好处就是,即使计算图的函数是构建在错综复杂的 Python 控制流上(比如条件判断、循环和一些函数调用),我们仍然可以计算结果变量的梯度。在下面的片段中,注意 while
循环的迭代次数和 if
语句的判断都是以来在输入变量 a
的。
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
让我们来计算一下梯度。
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
我们现在可以分析函数 f
的定义,注意它是一个关于输入 a
的分段线性的。换句话说,对于任何的输入 a
存在某个常数 k
满足 f(a) = k * a
其中 k
的值依赖输入的 a
因此 d / a
让我们验证一下梯度的正确性。
a.grad == d / a
tensor(True)
深度学习的框架可以使导数的计算自动化。为了使用它,我们首先将梯度依附在这些关于我们想要的偏导数的变量上。我们记录目标值的计算,执行函数 backward
进行反向传播,最终结果得到梯度。