自动求梯度 - 基于 PyTorch

梯度是几乎所有深度学习算法的重要步骤,尽管计算这些微分是直截了当的,仅需要一些简单的推导,但而对于复杂的模型,通过手工计算更新参数是非常痛苦的也非常容易出错。

深度学习框架通过 自动计算梯度 加快了这一工作,实际上,基于我们设计的模型,系统会构建一个 计算图(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 维的向量,xx 的点积计算返回一个标量并赋值给 y。下一步,我们可以通过调用反向传播函数自动地计算出对于 x 的每一部分 y 的梯度值,并将其输出。

y.backward()
x.grad
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 的微分最自然地解释是一个矩阵。对于高阶和高维的 yx 微分的结果可能是更高纬度的张量。

但是,这些非同寻常的对象会出现在高级的机器学习(包括深度学习)中,当我们说起向量的反向传播时,我们是尝试去计算关于每一批量的训练样本的损失函数的导数。在这里,我们的意图不是计算微分矩阵,而是对于每一个批量的样本单独地计算偏导数的和。

# 在非标量上调用 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 被计算为 yx 的函数。现在,想象我们像计算 z 关于 x 的梯度,但是由于某些原因要将 y 视为常数,并且只考虑了在 y 被计算之后 x 的作用。

在这里,我们可以分离 y 返回一个和 y 有着相同值并且将任何关于如何计算 y 的计算图的信息丢弃的新变量 u 。换句话说,梯度不会通过 u 反向流向 x 。因此,跟随反向传播函数计算 z = u * x 关于 xu 作为常数的偏导数,而不是计算 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 进行反向传播,最终结果得到梯度。