动手学习深度学习——2.5 自动微分

2.5 自动微分

  正如 【2.4 微积分】所说,微分是深度学习中几乎所有最优化算法的关键步骤。虽然求这些导数的计算过程很简单,只需要一些基本的微积分知识。但对于复杂的模型,手工计算参数的更新可能很痛苦(而且经常容易出错)。深度学习框架通过自动计算导数加快了这一工作,即自动微分(Automatic Differentiation)。在实践中,基于我们设计的模型,系统构建了一个计算图,跟踪哪些数据结合哪些操作进而产生输出。自动微分使系统能够反向传播梯度。在这里,反向传播只是意味着跟踪整个计算图,填充关于每个参数的偏导数。

2.5.1 一个简单的例子

  计算函数 y = 2 x T x y=2\pmb x^T\pmb x y=2xxxTxxx 的微分,其中 x \pmb x xxx 是列向量。那么,首先需要创建变量 x \pmb x xxx,并初始化。具体代码如下所示:

import torch

x = torch.arange(4.0)
print(x)

# 输出如下
tensor([0., 1., 2., 3.])

在计算 y y y 关于 x \pmb x xxx 的导数之前,我们需要存储空间。重要的是,我们不要在每次对参数求导时都分配新的内存,因为我们经常会成千上万次或数百万次更新相同的参数,并可能很快耗尽内存。注意,标量值函数关于向量 x \pmb x xxx 的梯度本身是向量,并且具有与 x \pmb x xxx 相同的形状。代码如下:

x.requires_grad_(True)  # Same as `x = torch.arange(4.0, requires_grad=True)`
x.grad  # The default value is None
print(x.grad)

# 输出如下
None

下面,我们来计算 y y y,具体代码如下:

y = 2 * torch.dot(x,x)
print(y)

# 输出如下
tensor(28., grad_fn=<MulBackward0>)

因为 x \pmb x xxx 是长度为 4 的向量,那么 x \pmb x xxx x \pmb x xxx 的点乘的结果就是 y y y 的值。现在,我们可以计算 y y y 关于向量 x x x 分量的梯度,并调用函数进行反向传播,具体代码如下:

y.backward()
print(x.grad)

# 输出如下
tensor([ 0.,  4.,  8., 12.])

函数 y y y 关于 x \pmb x xxx 的导数为 4 x 4\pmb x 4xxx ,下面让我们快速验证梯度计算的正确性。具体代码如下:

# 验证梯度
print(x.grad == 4 * x)

# 输出如下
tensor([True, True, True, True])

现在让我们计算另一个关于 x \pmb x xxx 的函数的梯度,代码如下:

# 另一个函数的梯度
# PyTorch accumulates the gradient in default, we need to clear the previous
# values
x.grad.zero_()
y = x.sum()
y.backward()
print(x.grad)

# 输出如下
tensor([1., 1., 1., 1.])

2.5.2 非标量函数的反向传播

  从技术上讲,当 y \pmb y yyy 不是标量时,对于向量 y \pmb y yyy 关于向量 x \pmb x xxx 的微分,最自然的解释是一个矩阵。对于高维的 y \pmb y yyy x \pmb x xxx ,微分结果可以是一个高阶张量。

  然而,尽管在高级机器学习(包括深度学习)中确实出现了更奇特的对象,但当我们反向调用一个向量时,更常见的情况是,我们试图为一批训练示例的每个组成部分计算损失函数的导数。在这里,我们的目的不是计算微分矩阵,而是计算一个批次数据中每个例子单独计算的偏导数的和。

# # 非标量函数的梯度
# 将函数 "backward()" 应用在非标量函数时,需要传递梯度参数,用于指定偏导函数关于自身的梯度.
# 在当前的例子中,我们只是想求偏导数的和,所以可以用 1 近似梯度.
x.grad.zero_()
y = x * x
# y.backward(torch.ones(len(x))) equivalent to the below
y.sum().backward()
print(x.grad)

# 输出如下
tensor([0., 2., 4., 6.])

2.5.3 分离计算

  有时,我们希望将一些计算放在计算图之外。假设 y y y x x x 的函数,而 z z z 是关于 y y y x x x 的函数。现在,我们想要计算 z z z 关于 x x x 的梯度,但是,基于某些原因,我们需要将 y y y 当作一个常量,仅仅考虑关于 x x x 的导数。

  这里,我们将 y y y 分离,并返回一个与 y y y 有相同值的新变量 u u u,但是要丢弃 y y y 在计算图中的任何信息。换句话说,梯度不会从 u u u 流向 x x x. 那么,反向传播函数计算函数 z = u ∗ x z=u*x z=ux 关于 x x x 的导数时,将 u u u 看作是常量,而不是 z = x ∗ x ∗ x z=x*x*x z=xxx 关于 x x x 的偏导数。具体代码如下:

# 分离计算
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x

z.sum().backward()
print(x.grad == u)
print('u:', u)

# 输出如下
tensor([True, True, True, True])
u:  tensor([0., 1., 4., 9.])

由于 y y y 的计算已经记录下来,我们可以随后调用 y y y 上的反向传播函数来得到 y = x ∗ x y = x * x y=xx x x x 的导数,即 2 ∗ x 2*x 2x.

x.grad.zero_()
y.sum().backward()
print(x.grad == 2 * x)

# 输出如下
tensor([True, True, True, True])

2.5.4 计算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函数。注意它是分段线性的输入。换句话说,任何一个存在一个常数系数【k】, f ( a ) = k ∗ a f(a)=k*a f(a)=ka,而【k】的值取决于输入【a】。因此【d/a】可以让我们确认梯度是正确。

print('a: ', a)
print('a.grad: ', a.grad)
print(a.grad == d / a)

# 输出如下
a:  tensor(0.6359, requires_grad=True)
a.grad:  tensor(2048.)
tensor(True)

你可能感兴趣的:(动手学习深度学习,深度学习,人工智能,自动微分)