pytorch 自动微分基础原理

1,036 阅读14分钟

我正在参加「掘金·启航计划」


PyTorch 的Autograd功能是 PyTorch 灵活快速地构建机器学习项目的一部分。它允许在复杂计算中快速轻松地计算多个偏导数(也称为 梯度) 。该操作是基于反向传播的神经网络学习的核心。

autograd 的强大之处在于它在运行时动态跟踪你的计算,这意味着如果你的模型有决策(判断)分支,或者只有在运行时才知道长度的循环,计算仍然会被正确跟踪,你会得到正确的梯度来驱动学习。这一点,再加上你的模型是用 Python 构建的这一事实,提供了比依赖于对 结构更严格(more rigidly-structured)的模型 进行静态分析来计算梯度的框架更大的灵活性。

1.我们需要 Autograd 做什么?

机器学习模型是一个函数,具有输入和输出。在本次讨论中,我们将 输入 视为 i 维向量x\vec{x},其中每一个元素记为xix_{i} 。然后我们可以将模型M表示为 输入的向量函数y=Mx\vec{y}=\vec{M}\vec{x},(我们将 M 的输出值视为一个向量,因为一般来说,一个模型可能有任意数量的输出)。

由于我们将主要在训练的上下文中讨论 autograd,因此我们感兴趣的输出将是模型的损失。损失函数L(y)=L(M(x))L(\vec{y}) = L(\vec{M}(\vec{x}))是模型输出的单值标量函数。该函数表示我们模型的预测与特定输入的理想输出之间的差距。注意:在此处之后,我们通常会在上下文清晰的地方省略矢量符号,比如: 用 y 表示 y\vec{y}

在训练模型时,我们希望最小化损失。在完美模型的理想化情况下,这意味着调整其学习权重——即函数的可调参数——使得所有输入的损失为零。在现实世界中,这意味着微调学习权重的迭代过程,直到我们看到我们对各种输入都获得了可容忍的损失。

我们如何决定微调权重的大小和方向?我们希望最小化损失,这意味着使损失相对于输入的一阶导数等于 0Lx=0\frac{\partial L}{\partial x} =0.

回想一下,损失不是直接来自输入,而是模型输出的函数(而输出输入的直接函数),Lx = L(y)x\frac{\partial L}{\partial x} = \frac{\partial L(\vec{y})}{\partial x}, 根据微积分的链式法则,我们有 L(y)x=Lyyx=LyM(x)x\frac{\partial L(\vec{y})}{\partial x} =\frac{\partial L}{\partial y}\frac{\partial y}{\partial x}= \frac{\partial L}{\partial y}\frac{\partial M(\vec{x})}{\partial x}.

M(x)x\frac{\partial M(\vec{x})}{\partial x} 是事情变得复杂的地方。模型输出相对于其输入的偏导数,如果我们再次使用链式法则展开表达式,将包含模型中许多局部偏导数,这是由每个相乘的学习权重、每个激活函数以及每个其他数学变换的产生的. 每个这样的偏导数的完整表达式是通过计算图的每条可能路径的局部梯度的乘积之和,该计算图以我们试图测量其梯度的变量结束。(同路径相乘分支相加)

一般,我们对学习权重上的梯度很感兴趣——它们告诉我们改变每个权重的方向以使损失函数更接近于零。

由于这种局部导数的数量(每个对应于通过模型计算图的单独路径)将随着神经网络的深度呈指数增长,因此计算它们的复杂性也会增加。这就是 autograd 的用武之地:它跟踪每次计算的历史。PyTorch 模型中的每个计算张量都带有其输入张量的历史记录以及用于创建它的函数。结合旨在作用于张量的 PyTorch 函数,每个函数都有一个用于计算它们自己的导数的内置实现,这极大地加快了学习所需的局部导数的计算。

2.一个简单的例子

上面说了很多理论 - 但在实践中使用的 autograd 什么样的呢?

让我们从一个简单的例子开始。首先,我们将做一些导入来展示我们的结果:

# %matplotlib inline

import torch

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import math

接下来,我们将创建一个输入张量,其中包含[0,2π]区间上的均匀间隔值 ,并指定requires_grad=True。(像大多数创建张量的函数一样,torch.linspace()接受一个可选requires_grad选项。)设置此标志意味着在随后的每次计算中,autograd 将在该计算的输出张量中累积计算历史。

a = torch.linspace(0., 2. * math.pi, steps=25, requires_grad=True)
print(a)
tensor([0.0000, 0.2618, 0.5236, 0.7854, 1.0472, 1.3090, 1.5708, 1.8326, 2.0944,
        2.3562, 2.6180, 2.8798, 3.1416, 3.4034, 3.6652, 3.9270, 4.1888, 4.4506,
        4.7124, 4.9742, 5.2360, 5.4978, 5.7596, 6.0214, 6.2832],
       requires_grad=True)

接下来,我们将执行计算,并根据其输入绘制其输出图像:

b = torch.sin(a)
plt.plot(a.detach(), b.detach())

输出图像如下:

image.png

[<matplotlib.lines.Line2D object at 0x7f562e69a790>]

让我们仔细看看张量b。当我们打印它时,我们会看到一个指示标志grad_fn 它正在跟踪其计算历史:

print(b)
tensor([ 0.0000e+00,  2.5882e-01,  5.0000e-01,  7.0711e-01,  8.6603e-01,
         9.6593e-01,  1.0000e+00,  9.6593e-01,  8.6603e-01,  7.0711e-01,
         5.0000e-01,  2.5882e-01, -8.7423e-08, -2.5882e-01, -5.0000e-01,
        -7.0711e-01, -8.6603e-01, -9.6593e-01, -1.0000e+00, -9.6593e-01,
        -8.6603e-01, -7.0711e-01, -5.0000e-01, -2.5882e-01,  1.7485e-07],
       grad_fn=<SinBackward0>)

grad_fn=<SinBackward0>给了我们一个指示标志,当我们执行反向传播步骤并计算梯度时,我们需要计算sin(x)sin(x)对于所有这些张量的输入的导数。

让我们执行一些更多的计算:

c = 2 * b
print(c)

d = c + 1
print(d)
tensor([ 0.0000e+00,  5.1764e-01,  1.0000e+00,  1.4142e+00,  1.7321e+00,
         1.9319e+00,  2.0000e+00,  1.9319e+00,  1.7321e+00,  1.4142e+00,
         1.0000e+00,  5.1764e-01, -1.7485e-07, -5.1764e-01, -1.0000e+00,
        -1.4142e+00, -1.7321e+00, -1.9319e+00, -2.0000e+00, -1.9319e+00,
        -1.7321e+00, -1.4142e+00, -1.0000e+00, -5.1764e-01,  3.4969e-07],
       grad_fn=<MulBackward0>)
tensor([ 1.0000e+00,  1.5176e+00,  2.0000e+00,  2.4142e+00,  2.7321e+00,
         2.9319e+00,  3.0000e+00,  2.9319e+00,  2.7321e+00,  2.4142e+00,
         2.0000e+00,  1.5176e+00,  1.0000e+00,  4.8236e-01, -3.5763e-07,
        -4.1421e-01, -7.3205e-01, -9.3185e-01, -1.0000e+00, -9.3185e-01,
        -7.3205e-01, -4.1421e-01,  4.7684e-07,  4.8236e-01,  1.0000e+00],
       grad_fn=<AddBackward0>)

最后,让我们计算单个单元素(即标量)输出。当你调用 .backward()一个不带参数的张量时,它期望调用张量只包含一个元素,就像计算损失函数时的情况一样。

out = d.sum()
print(out)
tensor(25., grad_fn=<SumBackward0>)

与张量一起存储的grad_fn中有一个属性next_functions ,允许我们遍历计算图的所有路径一直返回到其原始输入。我们可以在下面看到,对dgrad_fn属性深入研究 向我们展示了所有先前张量的梯度函数。请注意, a.grad_fn报告为None,表示这是函数的输入,没有自己的历史记录。

# d=2*sin(a) +1
print('d:')
print(d.grad_fn)
print(d.grad_fn.next_functions)
print(d.grad_fn.next_functions[0][0].next_functions)
print(d.grad_fn.next_functions[0][0].next_functions[0][0].next_functions)
print(d.grad_fn.next_functions[0][0].next_functions[0][0].next_functions[0][0].next_functions)
print('\nc:')
print(c.grad_fn)
print('\nb:')
print(b.grad_fn)
print('\na:')
print(a.grad_fn)
d:
<AddBackward0 object at 0x7f562d83da50>
((<MulBackward0 object at 0x7f562d83db10>, 0), (None, 0))
((<SinBackward0 object at 0x7f562d83db10>, 0), (None, 0))
((<AccumulateGrad object at 0x7f562d83da90>, 0),)
()

c:
<MulBackward0 object at 0x7f562d83dad0>

b:
<SinBackward0 object at 0x7f562d83db10>

a:
None

有了所有这些机制,我们如何获得输出的导数?您在输出上调用backward()方法,并检查输入的 grad属性以查看梯度:

# 在输出上执行backward
out.backward()
# 在输入上查看grad
print(a.grad)
# 绘制图像
plt.plot(a.detach(), a.grad.detach())

导数2cos(a)2 * cos(a)图像如下: autogradyt 教程

tensor([ 2.0000e+00,  1.9319e+00,  1.7321e+00,  1.4142e+00,  1.0000e+00,
         5.1764e-01, -8.7423e-08, -5.1764e-01, -1.0000e+00, -1.4142e+00,
        -1.7321e+00, -1.9319e+00, -2.0000e+00, -1.9319e+00, -1.7321e+00,
        -1.4142e+00, -1.0000e+00, -5.1764e-01,  2.3850e-08,  5.1764e-01,
         1.0000e+00,  1.4142e+00,  1.7321e+00,  1.9319e+00,  2.0000e+00])

[<matplotlib.lines.Line2D object at 0x7f562e559f10>]

回想一下我们到达这里所采取的计算步骤:

a = torch.linspace(0., 2. * math.pi, steps=25, requires_grad=True)
# b=sin(a)
b = torch.sin(a)
# c=2*sin(a)
c = 2 * b
# d=2*sin(a) +1
d = c + 1
out = d.sum()

正如我们在计算 d时所做的那样,添加一个常数不会改变导数。那么 叶子节点 c=2b=2sin(a)c = 2 * b = 2 * sin(a) ,其导数应该是2cos(a)2 * cos(a). 看看上面的图表,这就是我们所看到的。

请注意,只有计算图的叶子节点才计算其梯度。例如,如果你尝试 print(c.grad)你会返回 None的。在这个简单的例子中,只有输入a是叶子节点,所以只有 a 计算了梯度。

3.训练中的 Autograd

我们已经简要了解了 autograd 的工作原理,但是当它用于预期目的时会是什么样子呢?让我们定义一个小模型并检查它在单个训练批次后如何变化。首先,定义一些常量、模型以及一些输入和输出的样例值:

BATCH_SIZE = 16
DIM_IN = 1000
HIDDEN_SIZE = 100
DIM_OUT = 10
# 定义模型
class TinyModel(torch.nn.Module):

    def __init__(self):
        super(TinyModel, self).__init__()

        self.layer1 = torch.nn.Linear(1000, 100)
        self.relu = torch.nn.ReLU()
        self.layer2 = torch.nn.Linear(100, 10)

    def forward(self, x):
        x = self.layer1(x)
        x = self.relu(x)
        x = self.layer2(x)
        return x
# 输入样例
some_input = torch.randn(BATCH_SIZE, DIM_IN, requires_grad=False)
# 输出样例
ideal_output = torch.randn(BATCH_SIZE, DIM_OUT, requires_grad=False)
# 创建一个模型
model = TinyModel()

您可能会注意到的一件事是我们从未指定 requires_grad=True模型的层。在torch.nn.Module 的子类中 ,它总是假设我们想要跟踪层权重的梯度以进行学习。

如果我们查看模型的层,我们可以检查权重的值,并验证尚未计算其梯度:

# 打印权重
print(model.layer2.weight[0][0:10]) # just a small slice
# 验证尚未计算其梯度
print(model.layer2.weight.grad)
tensor([ 0.0443, -0.0774,  0.0950, -0.0238, -0.0356,  0.0176,  0.0295,  0.0138,
        -0.0617, -0.0156], grad_fn=<SliceBackward0>)
# 验证尚未计算其梯度
None

让我们看看当我们运行一个训练批次时情况如何变化。对于损失函数,我们使用predictionideal_output之间的欧几里得距离的平方,并且我们将使用基本的随机梯度下降优化器。

# 随机梯度下降优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
# 从输入预测输出
prediction = model(some_input)
# 计算损失=欧几里得距离的平方
loss = (ideal_output - prediction).pow(2).sum()
print(loss)
tensor(144.2516, grad_fn=<SumBackward0>)

现在,让我们调用loss.backward()看看会发生什么:

loss.backward()
print(model.layer2.weight[0][0:10])
print(model.layer2.weight.grad[0][0:10])
# 打印权重 print(model.layer2.weight[0][0:10])

tensor([ 0.0443, -0.0774,  0.0950, -0.0238, -0.0356,  0.0176,  0.0295,  0.0138,
        -0.0617, -0.0156], grad_fn=<SliceBackward0>)
        
# 打印梯度 print(model.layer2.weight.grad[0][0:10])

tensor([ 4.5724, -0.1287,  2.4772,  0.4667,  2.1017,  5.8762,  0.8660, -1.4688,
         0.4730,  0.3111])

我们可以看到已经为每个学习权重计算了梯度,但是权重保持不变,因为我们还没有运行优化器。优化器负责根据计算的梯度更新模型权重。

# 运行优化器
optimizer.step()
# 再次打印权重
print(model.layer2.weight[0][0:10])
# 再次打印梯度
print(model.layer2.weight.grad[0][0:10])
# 再次打印权重

tensor([ 0.0398, -0.0773,  0.0925, -0.0243, -0.0377,  0.0117,  0.0286,  0.0152,
        -0.0622, -0.0159], grad_fn=<SliceBackward0>)
        
# 再次打印梯度

tensor([ 4.5724, -0.1287,  2.4772,  0.4667,  2.1017,  5.8762,  0.8660, -1.4688,
         0.4730,  0.3111])

您应该看到layer2的权重发生了变化。

关于这个示例过程的一件重要的事情:在调用optimizer.step()之后 ,你需要调用optimizer.zero_grad(),否则每次运行loss.backward()时,学习权重上的梯度都会累积:

print(model.layer2.weight.grad[0][0:10])

for i in range(0, 5):
    prediction = model(some_input)
    loss = (ideal_output - prediction).pow(2).sum()
    loss.backward()

print(model.layer2.weight.grad[0][0:10])

# 清空梯度
optimizer.zero_grad()

print(model.layer2.weight.grad[0][0:10])
tensor([ 4.5724, -0.1287,  2.4772,  0.4667,  2.1017,  5.8762,  0.8660, -1.4688,
         0.4730,  0.3111])
         
         
tensor([ 22.6788,   0.8147,  -2.3859,   3.7174,  12.8808,  20.8097,   7.0952,
        -16.3539,  14.0011,  -4.4302])
        
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

运行上面的程序片段后,你应该看到,运行 loss.backward()多次后,大部分梯度的幅度会大得多。在运行下一个训练批次之前未能将梯度归零将导致梯度以这种方式爆炸,从而导致不正确和不可预测的学习结果。

4.关闭和打开 Autograd

在某些情况下,您需要对是否启用 autograd 进行细粒度控制。有多种方法可以做到这一点,具体取决于具体情况。

最简单的方法是直接更改requires_grad张量上的标志:

a = torch.ones(2, 3, requires_grad=True)
print(a)

b1 = 2 * a
print(b1)

a.requires_grad = False
b2 = 2 * a
print(b2)
tensor([[1., 1., 1.],
        [1., 1., 1.]], requires_grad=True)
tensor([[2., 2., 2.],
        [2., 2., 2.]], grad_fn=<MulBackward0>)
tensor([[2., 2., 2.],
        [2., 2., 2.]])

在上面的程序片段中,我们看到它b1有一个grad_fn(即,跟踪的计算历史),这是我们所期望的,因为它是从一个张量导出的,a它打开了 autograd。当我们用 a.requires_grad = False显式关闭 autograd 时,不再跟踪计算历史,正如我们在计算b2时看到的那样。

如果您只需要暂时关闭 autograd,更好的方法是使用torch.no_grad()

a = torch.ones(2, 3, requires_grad=True) * 2
b = torch.ones(2, 3, requires_grad=True) * 3

c1 = a + b
print(c1)

with torch.no_grad():
    c2 = a + b

print(c2)

c3 = a * b
print(c3)
tensor([[5., 5., 5.],
        [5., 5., 5.]], grad_fn=<AddBackward0>)
tensor([[5., 5., 5.],
        [5., 5., 5.]])
tensor([[6., 6., 6.],
        [6., 6., 6.]], grad_fn=<MulBackward0>)

torch.no_grad()也可以用作函数或方法的装饰器:

def add_tensors1(x, y):
    return x + y

@torch.no_grad()
def add_tensors2(x, y):
    return x + y


a = torch.ones(2, 3, requires_grad=True) * 2
b = torch.ones(2, 3, requires_grad=True) * 3

c1 = add_tensors1(a, b)
print(c1)

c2 = add_tensors2(a, b)
print(c2)
tensor([[5., 5., 5.],
        [5., 5., 5.]], grad_fn=<AddBackward0>)
tensor([[5., 5., 5.],
        [5., 5., 5.]])

有一个相应的上下文管理器,torch.enable_grad()用于在尚未打开 autograd 时打开它。它也可以用作装饰器。

最后,您可能有一个需要梯度跟踪的张量,但您想要一个不需要梯度跟踪的副本。为此,我们有Tensor对象的 detach() 方法——它创建一个张量副本,这个副本与计算历史是分离的。

x = torch.rand(5, requires_grad=True)
y = x.detach()

print(x)
print(y)
tensor([0.9463, 0.6709, 0.4693, 0.7836, 0.9490], requires_grad=True)
tensor([0.9463, 0.6709, 0.4693, 0.7836, 0.9490])

当我们想绘制一些张量时,我们在上面用了detach方法。这是因为matplotlib需要一个 NumPy 数组作为输入,并且对于 requires_grad=True 的张量没有启用从 PyTorch 张量到 NumPy 数组的隐式转换。制作一个分离的副本可以让我们继续使用。

4.1 Autograd 和 In-place操作

到目前为止,在本笔记本的每个示例中,我们都使用变量来捕获计算的中间值。Autograd 需要这些中间值来执行梯度计算。因此,在使用 autograd 时必须小心使用In-place操作。 这样做会破坏您在backward()调用中计算导数所需的信息。如果您尝试对需要 autograd 的叶变量进行In-place操作,PyTorch 甚至会阻止您,如下所示。

注意事项:

以下代码单元引发运行时错误。这是意料之中的。

a = torch.linspace(0., 2. * math.pi, steps=25, requires_grad=True)
# 注意此处是  sin_ 而不是 sin,带_ 函数是 In-place操作,即直接替换
torch.sin_(a)

5.Autograd 配置文件

Autograd 会详细跟踪计算的每一步。这样的计算历史与时间信息相结合,将成为一个方便的分析器 - 而 autograd 具有该功能。这是一个快速示例用法:

device = torch.device('cpu')
run_on_gpu = False
if torch.cuda.is_available():
    device = torch.device('cuda')
    run_on_gpu = True

x = torch.randn(2, 3, requires_grad=True)
y = torch.rand(2, 3, requires_grad=True)
z = torch.ones(2, 3, requires_grad=True)

with torch.autograd.profiler.profile(use_cuda=run_on_gpu) as prf:
    for _ in range(1000):
        z = (z / x) * y

print(prf.key_averages().table(sort_by='self_cpu_time_total'))
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------
                       Name    Self CPU %      Self CPU   CPU total %     CPU total  CPU time avg     Self CUDA   Self CUDA %    CUDA total  CUDA time avg    # of Calls
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------
                  aten::div        49.46%       4.735ms        49.46%       4.735ms       4.735us       8.661ms        50.90%       8.661ms       8.661us          1000
                  aten::mul        47.91%       4.586ms        47.91%       4.586ms       4.586us       8.354ms        49.10%       8.354ms       8.354us          1000
    cudaGetDeviceProperties         2.32%     222.000us         2.32%     222.000us     222.000us       0.000us         0.00%       0.000us       0.000us             1
      cudaDeviceSynchronize         0.31%      30.000us         0.31%      30.000us      30.000us       0.000us         0.00%       0.000us       0.000us             1
         cudaGetDeviceCount         0.00%       0.000us         0.00%       0.000us       0.000us       0.000us         0.00%       0.000us       0.000us             1
---------------------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------  ------------
Self CPU time total: 9.573ms
Self CUDA time total: 17.015ms

分析器还可以标记各个代码子块,通过输入张量形状分解数据,并将数据导出为 Chrome 跟踪工具文件。有关 API 的完整详细信息,请参阅 文档

6.高级主题:更多 Autograd 细节和高级 API

如果您有一个具有 n 维输入和 m 维输出的函数,y=f(x)\vec{y}=f(\vec{x}),,完整梯度是每个输出相对于每个输入的导数矩阵,称为 雅可比矩阵:

J=(y1x1y1xnymx1ymxn)\mathbf{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}

假设有第二个函数,l=g(x)l=g(\vec{x}),它采用 m 维输入(即与上面的输出相同的维数),并返回一个标量输出,您可以表示其梯度相对于y\vec{y}作为列向量,

v=(ly1lym)Tv = \begin{pmatrix} \frac{\partial l}{\partial y_{1}} & \cdots & \frac{\partial l}{\partial y_{m}} \end{pmatrix} ^{T}

-- 这实际上只是一个单列雅可比行列式。 更具体地说,将第一个函数想象为 PyTorch 模型(可能有很多输入和很多输出),第二个函数作为损失函数(模型的输出作为输入,损失值作为标量输出)。

如果我们将第一个函数的雅可比乘以第二个函数的梯度,并应用链式法则,我们得到:

JTv=(l1x1y1xnymx1ymxn)T(ly1lym)=(y1x1ymx1y1xnymxn)(ly1lym)=(lx1lxm) \mathbf{J}^{T}·v = \begin{pmatrix} \frac{\partial l_{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}^{T} \begin{pmatrix} \frac{\partial l}{\partial y_{1}} \\ \vdots \\ \frac{\partial l}{\partial y_{m}} \end{pmatrix} = \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_{m}} \end{pmatrix}

注意:您也可以使用等效操作vTJv^{T}\cdot J,并返回一个行向量。

得到的列向量是第二个函数相对于第一个函数的输入的梯度——或者在我们的模型和损失函数的情况下,是损失相对于模型输入的梯度。

参考下图理解:同路相乘,分支相加

image.png

lx1=ly1y1x1+ly2y2x1+ly3y3x1+...+lymymx1\frac{\partial l}{\partial x_{1}}=\frac{\partial l}{\partial y_{1}}\frac{\partial y_{1}}{\partial x_{1}} + \frac{\partial l}{\partial y_{2}}\frac{\partial y_{2}}{\partial x_{1}} + \frac{\partial l}{\partial y_{3}}\frac{\partial y_{3}}{\partial x_{1}} +... + \frac{\partial l}{\partial y_{m}}\frac{\partial y_{m}}{\partial x_{1}}
lx2=ly1y1x2+ly2y2x2+ly3y3x2+...+lymymx2\frac{\partial l}{\partial x_{2}}= \frac{\partial l}{\partial y_{1}}\frac{\partial y_{1}}{\partial x_{2}} + \frac{\partial l}{\partial y_{2}}\frac{\partial y_{2}}{\partial x_{2}} + \frac{\partial l}{\partial y_{3}}\frac{\partial y_{3}}{\partial x_{2}} +... + \frac{\partial l}{\partial y_{m}}\frac{\partial y_{m}}{\partial x_{2}}

torch.autograd 是计算这些产品的引擎。 这就是我们在反向传播期间在学习权重上累积梯度的方式。

出于这个原因,backward()调用也可以采用可选的向量输入。该向量表示张量上的一组梯度,它们乘以它之前的自动梯度跟踪张量的雅可比行列式。让我们尝试一个带有小向量的特定示例:

x = torch.randn(3, requires_grad=True)

y = x * 2
while y.data.norm() < 1000:
    y = y * 2

print(y)
tensor([ -752.3369, -1154.9319,  -286.9966], grad_fn=<MulBackward0>)

如果我们现在尝试调用y.backward(),我们会得到一个运行时错误和一条消息,即只能为标量输出隐式计算梯度。对于多维输出,autograd 期望我们为这三个输出提供梯度,它可以乘以雅可比行列式:

v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float) # stand-in for gradients
y.backward(v)

print(x.grad)
tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])

(请注意,输出梯度都与 2 的幂有关——我们期望从重复的加倍操作即 y = y * 2中得到。)

6.1 高级 API

autograd 上有一个 API,可让您直接访问重要的微分矩阵和向量运算。特别是,它允许您为特定输入计算特定函数的雅可比矩阵和Hessian矩阵。 (Hessian 类似于 Jacobian,但表示所有偏二阶导数。雅可比矩阵是一阶偏导数矩阵)它还提供了用这些矩阵获取向量积的方法。

让我们采用一个简单函数的雅可比行列式,针对 2 个单元素输入进行评估:

def exp_adder(x, y):
    return 2 * x.exp() + 3 * y

inputs = (torch.rand(1), torch.rand(1)) # arguments for the function
print(inputs)
torch.autograd.functional.jacobian(exp_adder, inputs)
(tensor([0.9360]), tensor([0.7653]))

(tensor([[5.0995]]), tensor([[3.]]))

如果仔细观察,第一个输出应该等于2ex2e^{x} (因为exe^{x}导数是exe^{x}),第二个值应该是 3。

当然,您可以使用高阶张量执行此操作:

inputs = (torch.rand(3), torch.rand(3)) # arguments for the function
print(inputs)
torch.autograd.functional.jacobian(exp_adder, inputs)
(tensor([0.2903, 0.0750, 0.9455]), tensor([0.3400, 0.3578, 0.0708]))

(tensor([[2.6737, 0.0000, 0.0000],
        [0.0000, 2.1557, 0.0000],
        [0.0000, 0.0000, 5.1483]]), tensor([[3., 0., 0.],
        [0., 3., 0.],
        [0., 0., 3.]]))

torch.autograd.functional.hessian()方法的工作原理相同(假设您的函数是两次可微的),但返回所有二阶导数的矩阵。

如果您提供向量,还有一个函数可以直接计算向量雅可比积:

def do_some_doubling(x):
    y = x * 2
    while y.data.norm() < 1000:
        y = y * 2
    return y

inputs = torch.randn(3)
my_gradients = torch.tensor([0.1, 1.0, 0.0001])
torch.autograd.functional.vjp(do_some_doubling, inputs, v=my_gradients)
(tensor([ -564.1382, -1069.4080, -1006.6667]), tensor([1.0240e+02, 1.0240e+03, 1.0240e-01]))

torch.autograd.functional.jvp()【Jacobian-Vector product】方法执行与vjp()【Vector-Jacobian product】转置操作数相同的矩阵乘法。vhp() 和hvp()方法对vector-Hessian product 执行相同的操作。

原文连接