# 自动求导机制 > 您可以使用[保存张量的钩子](#saved-tensors-hooks-doc)来控制PyTorch如何进行打包/解包。 这个笔记将介绍自动求导的工作原理以及记录操作的概述。虽然不是严格必要理解所有这些,但我们建议熟悉它,因为这将帮助您编写更高效、更清洁的程序,并可以帮助您调试。 ## 自动求导如何编码历史[](#how-autograd-encodes-the-history "跳转到此标题") 自动求导是一个反向自动微分系统。概念上,autograd在执行操作时记录创建数据的所有操作的图,为您提供一个有向无环图,其叶子是输入张量,根是输出张量。通过从根到叶子跟踪这个图,您可以使用链式法则自动计算梯度。 在内部,autograd将这个图表示为`Function`对象(实际上是表达式)的图,可以`apply()`来计算评估图的结果。在计算前向传播时,autograd同时执行请求的计算并构建一个表示计算梯度的函数的图(每个[`torch.Tensor`](../tensors.html#torch.Tensor "torch.Tensor")的`.grad_fn`属性是这个图的入口点)。完成前向传播后,我们在反向传播中评估这个图以计算梯度。 需要注意的一点是,在每次迭代中,图形都是从头开始重新创建的,这正是允许使用任意Python控制流语句的原因,这些语句可以在每次迭代中改变图形的整体形状和大小。您不必在启动训练之前编码所有可能的路径 - 您运行的就是您要求导数的内容。 ### 已保存的张量 一些操作需要在前向传播期间保存中间结果,以便执行反向传播。例如,函数$x\mapsto x^2$x↦x2 保存输入$x$x 来计算梯度。 当定义自定义Python [`Function`](../autograd.html#torch.autograd.Function "torch.autograd.Function")时,您可以使用`save_for_backward()`在前向传播期间保存张量,并使用`saved_tensors`在反向传播期间检索它们。有关更多信息,请参阅[扩展PyTorch](extending.html)。 对于PyTorch定义的操作(例如[`torch.pow()`](../generated/torch.pow.html#torch.pow "torch.pow")),张量会根据需要自动保存。您可以探索(用于教育或调试目的)通过查找以前缀`_saved`开头的属性来了解某个`grad_fn`保存了哪些张量。 ```py x = torch.randn(5, requires_grad=True) y = x.pow(2) print(x.equal(y.grad_fn._saved_self)) # True print(x is y.grad_fn._saved_self) # True ``` 在前面的代码中,`y.grad_fn._saved_self`指的是与x相同的张量对象。但这并不总是这样。例如: ```py x = torch.randn(5, requires_grad=True) y = x.exp() print(y.equal(y.grad_fn._saved_result)) # True print(y is y.grad_fn._saved_result) # False ``` 在内部,为了防止引用循环,PyTorch在保存时*打包*张量,并在读取时*解包*为不同的张量。在这里,通过访问`y.grad_fn._saved_result`获得的张量对象与`y`是不同的张量对象(但它们仍然共享相同的存储)。 张量是否会打包为不同的张量对象取决于它是否是其自己`grad_fn`的输出,这是一个实现细节,可能会发生变化,用户不应依赖于此。 ## 不可微函数的梯度[](#gradients-for-non-differentiable-functions "跳转到此标题") 使用自动微分进行梯度计算仅在每个使用的基本函数可微时有效。不幸的是,我们在实践中使用的许多函数都没有这个性质(例如在`0`处的`relu`或`sqrt`)。为了尝试减少不可微函数的影响,我们通过按照以下规则定义基本操作的梯度来实现: 1. 如果函数是可微的,因此在当前点存在梯度,请使用它。 1. 如果函数是凸的(至少在局部),请使用最小范数的次梯度(这是最陡下降方向)。 1. 如果函数是凹的(至少在局部),则使用最小范数的超梯度(考虑-f(x)并应用前面的点)。 1. 如果函数已定义,请通过连续性在当前点定义梯度(请注意,这里可能是`inf`,例如对于`sqrt(0)`)。如果可能有多个值,请任意选择一个。 1. 如果函数未定义(例如`sqrt(-1)`,`log(-1)`或大多数函数在输入为`NaN`时),则用作梯度的值是任意的(我们也可能引发错误,但不能保证)。大多数函数将使用`NaN`作为梯度,但出于性能原因,某些函数将使用其他值(例如`log(-1)`)。 1. 如果函数不是确定性映射(即不是[数学函数](https://en.wikipedia.org/wiki/Function_(mathematics))),它将被标记为不可微。如果在`no_grad`环境之外使用需要梯度的张量,则在反向传播中将出现错误。##局部禁用梯度计算[](#locally-disabling-gradient-computation "跳转到此标题的永久链接") 有几种机制可用于在Python中局部禁用梯度计算: 要在整个代码块中禁用梯度,有像无梯度模式和推断模式这样的上下文管理器。为了更细粒度地排除梯度计算中的子图,可以设置张量的`requires_grad`字段。 除了讨论上述机制之外,我们还描述了评估模式(`nn.Module.eval()`),这是一种不用于禁用梯度计算的方法,但由于其名称,经常与这三种方法混淆。 ### 设置`requires_grad` `requires_grad`是一个标志,默认为false,*除非包装在* `nn.Parameter`中,允许对梯度计算中的子图进行细粒度排除。它在前向和反向传播中都生效: 在前向传播期间,只有在其输入张量中至少有一个需要梯度的情况下,操作才会记录在反向图中。在反向传播(`.backward()`)期间,只有`requires_grad=True`的叶子张量才会将梯度累积到其`.grad`字段中。 重要的是要注意,即使每个张量都有这个标志,*设置*它只对叶子张量(没有`grad_fn`的张量,例如`nn.Module`的参数)有意义。非叶子张量(具有`grad_fn`的张量)是具有与之关联的反向图的张量。因此,它们的梯度将作为计算需要梯度的叶子张量的梯度的中间结果。从这个定义可以清楚地看出,所有非叶子张量将自动具有`require_grad=True`。 设置`requires_grad`应该是您控制模型哪些部分参与梯度计算的主要方式,例如,如果您需要在模型微调期间冻结部分预训练模型。 要冻结模型的部分,只需将`.requires_grad_(False)`应用于您不希望更新的参数。正如上面所述,由于使用这些参数作为输入的计算不会在前向传播中记录,因此它们在反向传播中不会更新其`.grad`字段,因为它们本来就不会成为反向图的一部分,这正是所期望的。 由于这是一个常见模式,`requires_grad`也可以在模块级别使用`nn.Module.requires_grad_()`进行设置。当应用于模块时,`.requires_grad_()`会对模块的所有参数(默认情况下具有`requires_grad=True`)生效。 ### Grad模式 除了设置`requires_grad`外,还有三种可以从Python中选择的grad模式,可以影响PyTorch中autograd内部处理计算的方式:默认模式(grad模式)、无梯度模式和推理模式,所有这些模式都可以通过上下文管理器和装饰器进行切换。 | 模式 | 排除在反向图中记录的操作 | 跳过额外的autograd跟踪开销 | 在启用模式时创建的张量可以在grad模式中使用 | 示例 | | --- | --- | --- | --- | --- | | 默认 | | | ✓ | 前向传递 | | 无梯度 | ✓ | | ✓ | 优化器更新 | | 推理 | ✓ | ✓ | | 数据处理,模型评估 | ### 默认模式(grad模式) “默认模式”是我们在没有启用其他模式(如无梯度模式和推理模式)时隐式处于的模式。与“无梯度模式”相对应,“默认模式”有时也被称为“grad模式”。 关于默认模式最重要的一点是它是唯一一个`requires_grad`生效的模式。在另外两种模式中,`requires_grad`总是被覆盖为`False`。 ### 无梯度模式 在无梯度模式下的计算行为就好像没有任何输入需要梯度一样。换句话说,在无梯度模式下的计算永远不会被记录在反向图中,即使有`require_grad=True`的输入也是如此。 当您需要执行不应被autograd记录的操作,但仍希望稍后在grad模式中使用这些计算的输出时,请启用无梯度模式。这个上下文管理器使得在不必临时将张量设置为`requires_grad=False`,然后再设置为`True`的情况下,方便地禁用一段代码或函数的梯度。 例如,当编写优化器时,无梯度模式可能很有用:在执行训练更新时,您希望在不被autograd记录的情况下就地更新参数。您还打算在下一个前向传递中使用更新后的参数进行计算。 在初始化参数时,[torch.nn.init](../nn.init.html#nn-init-doc)中的实现也依赖于无梯度模式,以避免在就地更新初始化参数时进行autograd跟踪。 ### 推理模式 推理模式是无梯度模式的极端版本。就像在无梯度模式中一样,在推理模式中的计算不会被记录在反向图中,但启用推理模式将使PyTorch加速您的模型。这种更好的运行时性能伴随着一个缺点:在推理模式中创建的张量将无法在退出推理模式后用于由autograd记录的计算。 当您执行不需要在反向图中记录的计算,并且您不打算在稍后由autograd记录的任何计算中使用在推理模式中创建的张量时,请启用推理模式。 建议您在代码中不需要autograd跟踪的部分尝试推理模式(例如数据处理和模型评估)。如果它适用于您的用例,那么这是一个免费的性能提升。如果在启用推理模式后遇到错误,请检查您是否在退出推理模式后使用了在推理模式中创建的张量进行autograd记录的计算。如果您无法避免在您的情况下使用这种用法,您可以随时切换回无梯度模式。 有关推理模式的详细信息,请参见[推理模式](https://pytorch.org/cppdocs/notes/inference_mode.html)。 有关推理模式的实现细节,请参阅[RFC-0011-InferenceMode](https://github.com/pytorch/rfcs/pull/17)。 ### 评估模式(`nn.Module.eval()`) 评估模式不是一种本地禁用梯度计算的机制。它在这里包含是因为有时会被误解为这样的机制。 从功能上讲,`module.eval()`(或等效地`module.train(False)`)与无梯度模式和推断模式完全无关。`model.eval()`如何影响您的模型完全取决于您的模型中使用的特定模块以及它们是否定义了任何特定于训练模式的行为。 如果您的模型依赖于诸如[`torch.nn.Dropout`](../generated/torch.nn.Dropout.html#torch.nn.Dropout "torch.nn.Dropout")和[`torch.nn.BatchNorm2d`](../generated/torch.nn.BatchNorm2d.html#torch.nn.BatchNorm2d "torch.nn.BatchNorm2d")等模块,这些模块可能会根据训练模式的不同而表现不同,例如,为了避免在验证数据上更新您的BatchNorm运行统计数据,您需要调用`model.eval()`和`model.train()`。 建议在训练时始终使用`model.train()`,在评估模型(验证/测试)时使用`model.eval()`,即使您不确定您的模型是否具有特定于训练模式的行为,因为您使用的模块可能会更新以在训练和评估模式下表现不同。 ## 使用autograd的原地操作 在自动求导中支持原地操作是一件困难的事情,我们不鼓励在大多数情况下使用它们。自动求导的积极缓冲区释放和重用使其非常高效,只有在极度内存压力下,原地操作才会显著降低内存使用量。除非您在极度内存压力下操作,否则您可能永远不需要使用它们。 有两个主要原因限制了原地操作的适用性: 1. 原地操作可能会覆盖计算梯度所需的值。 1. 每个原地操作都需要实现重写计算图。非原地版本只是分配新对象并保留对旧图的引用,而原地操作需要将所有输入的创建者更改为代表此操作的`Function`。这可能会很棘手,特别是如果有许多Tensor引用相同的存储(例如通过索引或转置创建),并且如果修改后的输入的存储被任何其他`Tensor`引用,原地函数将引发错误。 ### 原地正确性检查 每个张量都保留一个版本计数器,每次在任何操作中标记为脏时都会递增。当一个Function保存任何张量用于反向传播时,它们包含的Tensor的版本计数器也会被保存。一旦访问`self.saved_tensors`,它就会被检查,如果大于保存的值,则会引发错误。这确保了如果您使用原地函数而没有看到任何错误,您可以确信计算的梯度是正确的。 ## 多线程自动求导 自动求导引擎负责运行计算反向传播所需的所有反向操作。本节将描述所有细节,以帮助您在多线程环境中充分利用它。(这仅适用于PyTorch 1.6+,因为之前版本的行为不同。) 用户可以使用多线程代码训练他们的模型(例如,Hogwild训练),并且不会在并发反向计算上阻塞,示例代码可能是: ```py # Define a train function to be used in different threads def train_fn(): x = torch.ones(5, 5, requires_grad=True) # forward y = (x + 3) * (x + 4) * 0.5 # backward y.sum().backward() # potential optimizer update # User write their own threading code to drive the train_fn threads = [] for _ in range(10): p = threading.Thread(target=train_fn, args=()) p.start() threads.append(p) for p in threads: p.join() ``` 请注意用户应该注意的一些行为: ### CPU上的并发 当您在CPU上通过Python或C++ API在多个线程上运行`backward()`或`grad()`时,您期望看到额外的并发,而不是在执行期间按特定顺序序列化所有的反向调用(PyTorch 1.6之前的行为)。 ### 非确定性 如果您从多个线程同时调用 `backward()` 并且具有共享输入(即 Hogwild CPU 训练),则应该期望非确定性。这可能是因为参数会自动在线程之间共享,因此多个线程可能会访问并尝试在梯度累积期间累积相同的 `.grad` 属性。这在技术上是不安全的,可能会导致竞争条件,结果可能无效。 开发具有共享参数的多线程模型的用户应该考虑线程模型,并应理解上述问题。 可以使用函数式 API [`torch.autograd.grad()`](../generated/torch.autograd.grad.html#torch.autograd.grad) 来计算梯度,而不是使用 `backward()` 来避免非确定性。 ### 保留图 如果 autograd 图的一部分在多个线程之间共享,即在单个线程中运行前半部分的前向,然后在多个线程中运行第二部分,那么图的第一部分是共享的。在这种情况下,不同的线程在相同的图上执行 `grad()` 或 `backward()` 可能会出现破坏图的问题,其中一个线程会在飞行中破坏图,而另一个线程将在这种情况下崩溃。Autograd 将向用户报告类似于两次调用 `backward()` 而没有 `retain_graph=True`,并告知用户应该使用 `retain_graph=True`。 ### Autograd 节点上的线程安全 由于 Autograd 允许调用者线程驱动其向后执行以实现潜在的并行性,因此我们需要确保在 CPU 上使用并行 `backward()` 调用时的线程安全,这些调用共享 GraphTask 的部分/全部。 自定义 Python `autograd.Function` 由于 GIL 的存在自动线程安全。对于内置的 C++ Autograd 节点(例如 AccumulateGrad、CopySlices)和自定义 `autograd::Function`,Autograd 引擎使用线程互斥锁定来确保对可能具有状态写入/读取的 autograd 节点的线程安全。 ### C++ 钩子上没有线程安全性 Autograd 依赖用户编写线程安全的 C++ 钩子。如果要在多线程环境中正确应用钩子,您需要编写适当的线程锁定代码以确保钩子是线程安全的。 ## 复数的自动微分 简短版本: + 当您使用 PyTorch 对具有复数域和/或共域的任何函数 f(z) 进行微分时,梯度是在假设该函数是更大的实值损失函数 g(input)=L 的一部分的情况下计算的。计算的梯度是 ∂L/∂z*(注意 z 的共轭),其负值恰好是梯度下降算法中使用的最陡下降方向。因此,所有现有的优化器都可以直接与复数参数一起使用。 + 这个约定与 TensorFlow 对复杂微分的约定相匹配,但与 JAX 不同(它计算 ∂L/∂z)。 + 如果你有一个内部使用复杂运算的实到实函数,这里的约定并不重要:你总是会得到与仅使用实数运算实现时相同的结果。 如果你对数学细节感兴趣,或者想知道如何在PyTorch中定义复杂导数,继续阅读。 ### 什么是复杂导数? 复杂可微函数的数学定义将导数的极限定义推广到复数上。考虑一个函数 > 其中u和v是两个变量的实值函数,j是虚数单位。 使用导数定义,我们可以写成: > 为了使这个极限存在,不仅必须u和v是实可微的,而且f也必须满足柯西-黎曼方程。换句话说:为实部和虚部步长计算的极限(h)必须相等。这是一个更严格的条件。 复杂可微函数通常被称为全纯函数。它们表现良好,具有你从实可微函数中看到的所有好的性质,但在优化领域实际上没有什么用。在优化问题中,研究社区只使用实值目标函数,因为复数不属于任何有序域,因此具有复值损失并没有太多意义。 Im(z) = (z - z*) / 2j ### 同时,没有有趣的实值目标满足柯西-黎曼方程。因此,全纯函数的理论不能用于优化,大多数人因此使用威廉格微积分。威廉格微积分进入画面... 所以,我们有这个复可微和全纯函数的伟大理论,但我们根本无法使用它,因为许多常用函数都不是全纯的。一个可怜的数学家该怎么办呢?威廉格观察到,即使f(z)不是全纯的,也可以将其重写为一个两个变量的函数f(z, z*),这个函数总是全纯的。这是因为z的实部和虚部可以用z和z*来表示: > Re(z) = (z + z*) / 2 Wirtinger微积分建议研究f(z, z*),如果f是实可微的,则保证是全纯的(另一种思考方式是将坐标系从f(x, y)变换为f(z, z*))。这个函数有偏导数∂z∂和∂z*∂。我们可以使用链式法则建立这些偏导数与z的实部和虚部的偏导数之间的关系。 > $\begin{aligned} \frac{\partial }{\partial x} &= \frac{\partial z}{\partial x} * \frac{\partial }{\partial z} + \frac{\partial z^*}{\partial x} * \frac{\partial }{\partial z^*} \\ &= \frac{\partial }{\partial z} + \frac{\partial }{\partial z^*} \\ \\ \frac{\partial }{\partial y} &= \frac{\partial z}{\partial y} * \frac{\partial }{\partial z} + \frac{\partial z^*}{\partial y} * \frac{\partial }{\partial z^*} \\ &= 1j * \left(\frac{\partial }{\partial z} - \frac{\partial }{\partial z^*}\right) \end{aligned}$ 从上面的方程中,我们得到: > $\begin{aligned} \frac{\partial }{\partial z} &= 1/2 * \left(\frac{\partial }{\partial x} - 1j * \frac{\partial }{\partial y}\right) \\ \frac{\partial }{\partial z^*} &= 1/2 * \left(\frac{\partial }{\partial x} + 1j * \frac{\partial }{\partial y}\right) \end{aligned}$ ∂z∂​∂z∗∂​​=1/2∗(∂x∂​−1j∗∂y∂​)=1/2∗(∂x∂​+1j∗∂y∂​)​ 这是您在[Wikipedia](https://en.wikipedia.org/wiki/Wirtinger_derivatives)上找到的Wirtinger微积分的经典定义。 这种变化有很多美好的结果。 + 首先,柯西-黎曼方程简单地表明$\frac{\partial f}{\partial z^*} = 0$∂z∗∂f​=0(也就是说,函数$f$f可以完全用$z$z来表示,而不涉及$z^*$z∗)。 + 另一个重要的(有些违反直觉的)结果是,当我们在实值损失上进行优化时,进行变量更新时应该采取的步骤由$\frac{\partial Loss}{\partial z^*}$∂z∗∂Loss​给出(而不是$\frac{\partial Loss}{\partial z}$∂z∂Loss​)。 更多阅读,请查看:[https://arxiv.org/pdf/0906.4835.pdf](https://arxiv.org/pdf/0906.4835.pdf) ### Wirtinger微积分在优化中有什么用处?[](#how-is-wirtinger-calculus-useful-in-optimization "Permalink to this heading") 研究人员在音频和其他领域更常见地使用梯度下降来优化具有复杂变量的实值损失函数。通常,这些人将实部和虚部视为可以更新的独立通道。对于步长 $\alpha/2$α/2 和损失 $L$L,我们可以在 $ℝ^2$R2 中写出以下方程: > $\begin{aligned} x_{n+1} &= x_n - (\alpha/2) * \frac{\partial L}{\partial x} \\ y_{n+1} &= y_n - (\alpha/2) * \frac{\partial L}{\partial y} \end{aligned}$ xn+1​=xn​−(α/2)∗∂x∂L​yn+1​=yn​−(α/2)∗∂y∂L​​ 这些方程如何转化为复数空间 $ℂ$C? > $\begin{aligned} z_{n+1} &= x_n - (\alpha/2) * \frac{\partial L}{\partial x} + 1j * (y_n - (\alpha/2) * \frac{\partial L}{\partial y}) \\ &= z_n - \alpha * 1/2 * \left(\frac{\partial L}{\partial x} + j \frac{\partial L}{\partial y}\right) \\ &= z_n - \alpha * \frac{\partial L}{\partial z^*} \end{aligned}$ zn+1​​=xn​−(α/2)∗∂x∂L​+1j∗(yn​−(α/2)∗∂y∂L​)=zn​−α∗1/2∗(∂x∂L​+j∂y∂L​)=zn​−α∗∂z∗∂L​​ 发生了一件非常有趣的事情:Wirtinger微积分告诉我们,我们可以将上面的复变量更新公式简化为只涉及共轭Wirtinger导数$\frac{\partial L}{\partial z^*}$∂z∗∂L​,这样我们就得到了优化中所采取的确切步骤。 因为共轭Wirtinger导数给出了实值损失函数的正确步骤,所以当您对具有实值损失的函数进行微分时,PyTorch会给出这个导数。 ### PyTorch如何计算共轭Wirtinger导数?[](#how-does-pytorch-compute-the-conjugate-wirtinger-derivative "Permalink to this heading") 通常,我们的导数公式将grad_output作为输入,表示我们已经计算过的传入向量雅可比乘积,即,∂s∗∂L​,其中L是整个计算的损失(产生实际损失),s是我们函数的输出。这里的目标是计算∂z∗∂L​,其中z是函数的输入。事实证明,在实际损失的情况下,我们可以仅仅计算∂s∗∂L​,即使链式法则暗示我们也需要访问∂s∂L​。如果您想跳过这个推导,请查看本节中的最后一个方程,然后跳到下一节。 让我们继续使用f:C→C定义为f(z)=f(x+yj)=u(x,y)+v(x,y)j。如上所述,autograd的梯度约定围绕着针对实值损失函数的优化,因此让我们假设f是更大的实值损失函数g的一部分。使用链式法则,我们可以写成: > ∂z∗∂L​=∂u∂L​∗∂z∗∂u​+∂v∂L​∗∂z∗∂v​ 现在使用Wirtinger导数定义,我们可以写成: > (1) 这里需要注意,由于u和v是实函数,而L根据我们假设f是实值函数的一部分,我们有: > (2)$\left( \frac{\partial L}{\partial s} \right)^* = \frac{\partial L}{\partial s^*}$ (∂s∂L​)∗=∂s∗∂L​ 即,$\frac{\partial L}{\partial s}$等于$grad\_output^*$。 解上述方程得到$\frac{\partial L}{\partial u}$和$\frac{\partial L}{\partial v}$: > (3)$\begin{aligned} \frac{\partial L}{\partial u} = \frac{\partial L}{\partial s} + \frac{\partial L}{\partial s^*} \\ \frac{\partial L}{\partial v} = -1j * \left(\frac{\partial L}{\partial s} - \frac{\partial L}{\partial s^*}\right) \end{aligned}$ ∂u∂L​=∂s∂L​+∂s∗∂L​∂v∂L​=−1j∗(∂s∂L​−∂s∗∂L​)​ 将[(3)](#equation-3)代入[(1)](#equation-1),我们得到: > ∂z∗∂L​​=(∂s∂L​+∂s∗∂L​)∗∂z∗∂u​−1j∗(∂s∂L​−∂s∗∂L​)∗∂z∗∂v​=∂s∂L​∗(∂z∗∂u​+∂z∗∂v​j)+∂s∗∂L​∗(∂z∗∂u​−∂z∗∂v​j)=∂s∗∂L​∗∂z∗∂(u+vj)​+∂s∂L​∗∂z∗∂(u+vj)∗​=∂s∂L​∗∂z∗∂s​+∂s∗∂L​∗∂z∗∂s∗​​ 使用[(2)](#equation-2),我们得到: > ∂z∗∂L​​=(∂s∗∂L​)∗∗∂z∗∂s​+∂s∗∂L​∗(∂z∂s​)∗=(grad_output)∗∗∂z∗∂s​+grad_output∗(∂z∂s​)∗​​ 这个最后的方程式是编写自己的梯度的重要方程式,因为它将我们的导数公式分解为一个更简单的公式,容易手工计算。 ### 如何为复杂函数编写自己的导数公式? 上面的方框方程式给出了复杂函数所有导数的一般公式。然而,我们仍然需要计算∂s∂z​和∂s∗∂z​。你可以通过两种方式来做到这一点: > + 第一种方法是直接使用Wirtinger导数的定义,并通过使用∂x∂s和∂y∂s(可以以正常方式计算)来计算∂z∂s和∂z∗∂s。 > + > + 第二种方法是使用变量变换技巧,将f(z)重写为一个两个变量的函数f(z, z*),并通过将z和z*视为独立变量来计算共轭Wirtinger导数。这通常更容易;例如,如果所讨论的函数是全纯的,只会使用z(而∂z∗∂s将为零)。 让我们以一个例子来考虑函数f(z=x+yj)=c*z=c*(x+yj),其中c∈R。 使用第一种方法计算Wirtinger导数,我们有。 ∂s∂z∗​=1/2∗(∂s∂x​+∂s∂y​j)=1/2∗(c+(c∗1j)∗1j)=0 使用第二种计算Wirtinger导数的方法,我们直接得到: > 使用[(4)](#equation-4),并且grad_output = 1.0(这是在PyTorch中对标量输出调用`backward()`时使用的默认梯度输出值),我们得到: ∂s∂z​=1/2∗(∂s∂x​−∂s∂y​j)=1/2∗(c−(c∗1j)∗1j)=c > $\begin{aligned} \frac{\partial s}{\partial z} &= \frac{\partial (c*z)}{\partial z} \\ &= c \\ \frac{\partial s}{\partial z^*} &= \frac{\partial (c*z)}{\partial z^*} \\ &= 0 \end{aligned}$∂z∂s​∂z∗∂s​​=∂z∂(c∗z)​=c=∂z∗∂(c∗z)​=0​ 再次使用[(4)](#equation-4),我们得到$\frac{\partial L}{\partial z^*} = c$∂z∗∂L​=c。如您所见,第二种方法涉及更少的计算,并且更适用于更快的计算。 ### 跨域函数呢?[](#what-about-cross-domain-functions "Permalink to this heading") 一些函数从复杂输入映射到实数输出,或者反之亦然。这些函数形成了[(4)](#equation-4)的一个特殊情况,我们可以使用链式法则推导出来: > + 对于$f: ℂ → ℝ$f:C→R,我们得到: > + > > $\frac{\partial L}{\partial z^*} = 2 * grad\_output * \frac{\partial s}{\partial z^{*}}$∂z∗∂L​=2∗grad_output∗∂z∗∂s​ > > > + 对于$f: ℝ → ℂ$f:R→C,我们得到: > + > > $\frac{\partial L}{\partial z^*} = 2 * \mathrm{Re}(grad\_output^* * \frac{\partial s}{\partial z^{*}})$ ∂z∗∂L​=2∗Re(grad_output∗∗∂z∗∂s​) ## 保存的张量的钩子 通过定义一对`pack_hook` / `unpack_hook`钩子,您可以控制[保存的张量如何打包/解包](#saved-tensors-doc)。`pack_hook`函数应该以一个张量作为其单个参数,但可以返回任何Python对象(例如另一个张量,一个元组,甚至包含文件名的字符串)。`unpack_hook`函数以`pack_hook`的输出作为其单个参数,并应返回一个张量,用于在反向传播中使用。`unpack_hook`返回的张量只需要与传递给`pack_hook`的输入张量具有相同的内容。特别地,任何与自动求导相关的元数据都可以忽略,因为它们在解包过程中将被覆盖。 一个这样的示例是: ```py class SelfDeletingTempFile(): def __init__(self): self.name = os.path.join(tmp_dir, str(uuid.uuid4())) def __del__(self): os.remove(self.name) def pack_hook(tensor): temp_file = SelfDeletingTempFile() torch.save(tensor, temp_file.name) return temp_file def unpack_hook(temp_file): return torch.load(temp_file.name) ``` 请注意,`unpack_hook`不应删除临时文件,因为它可能会被多次调用:临时文件应该在返回的SelfDeletingTempFile对象存在期间保持活动状态。在上面的示例中,我们通过在不再需要时关闭临时文件(在删除SelfDeletingTempFile对象时)来防止泄漏临时文件。 注意 我们保证`pack_hook`只会被调用一次,但`unpack_hook`可以根据反向传播的需要被调用多次,并且我们期望每次返回相同的数据。 警告 对任何函数的输入执行原地操作是禁止的,因为这可能会导致意外的副作用。如果对pack hook的输入进行了原地修改,PyTorch会抛出错误,但不会捕获对unpack hook的输入进行原地修改的情况。 ### 注册保存的张量的钩子[](#registering-hooks-for-a-saved-tensor "跳转到此标题的永久链接") 您可以通过在`SavedTensor`对象上调用`register_hooks()`方法来注册一对保存的张量上的钩子。这些对象作为`grad_fn`的属性暴露,并以`_raw_saved_`前缀开头。 ```py x = torch.randn(5, requires_grad=True) y = x.pow(2) y.grad_fn._raw_saved_self.register_hooks(pack_hook, unpack_hook) ``` 一旦注册,`pack_hook`方法将立即被调用。每当需要访问保存的张量时,`unpack_hook`方法将被调用,可以通过`y.grad_fn._saved_self`或在反向传播期间访问。 警告 如果在保存的张量被释放后(即在调用反向传播后)仍保留对`SavedTensor`的引用,则禁止调用其`register_hooks()`。PyTorch大多数情况下会抛出错误,但在某些情况下可能无法这样做,可能会出现未定义的行为。 ### 注册保存的张量的默认钩子[](#registering-default-hooks-for-saved-tensors "跳转到此标题的永久链接") 另外,您可以使用上下文管理器[`saved_tensors_hooks`](../autograd.html#torch.autograd.graph.saved_tensors_hooks "torch.autograd.graph.saved_tensors_hooks")来注册一对钩子,这些钩子将应用于在该上下文中创建的*所有*保存的张量。 示例: ```py # Only save on disk tensors that have size >= 1000 SAVE_ON_DISK_THRESHOLD = 1000 def pack_hook(x): if x.numel() < SAVE_ON_DISK_THRESHOLD: return x temp_file = SelfDeletingTempFile() torch.save(tensor, temp_file.name) return temp_file def unpack_hook(tensor_or_sctf): if isinstance(tensor_or_sctf, torch.Tensor): return tensor_or_sctf return torch.load(tensor_or_sctf.name) class Model(nn.Module): def forward(self, x): with torch.autograd.graph.saved_tensors_hooks(pack_hook, unpack_hook): # ... compute output output = x return output model = Model() net = nn.DataParallel(model) ``` 使用此上下文管理器定义的钩子是线程局部的。因此,以下代码不会产生期望的效果,因为这些钩子不会通过DataParallel。 ```py # Example what NOT to do net = nn.DataParallel(model) with torch.autograd.graph.saved_tensors_hooks(pack_hook, unpack_hook): output = net(input) ``` 请注意,使用这些钩子会禁用所有优化,以减少张量对象的创建。例如: ```py with torch.autograd.graph.saved_tensors_hooks(lambda x: x, lambda x: x): x = torch.randn(5, requires_grad=True) y = x * x ``` 没有钩子,`x`,`y.grad_fn._saved_self`和`y.grad_fn._saved_other`都指向同一个张量对象。有了钩子,PyTorch将x打包并解包为两个新的张量对象,这两个对象与原始x共享相同的存储(不执行复制)。## 后向钩子执行[](#backward-hooks-execution "Permalink to this heading") 本节将讨论不同的钩子何时触发或不触发。然后将讨论它们触发的顺序。将涵盖的钩子包括:通过[`torch.Tensor.register_hook()`](../generated/torch.Tensor.register_hook.html#torch.Tensor.register_hook "torch.Tensor.register_hook")注册到张量的后向钩子,通过[`torch.Tensor.register_post_accumulate_grad_hook()`](../generated/torch.Tensor.register_post_accumulate_grad_hook.html#torch.Tensor.register_post_accumulate_grad_hook "torch.Tensor.register_post_accumulate_grad_hook")注册到张量的后累积梯度钩子,通过[`torch.autograd.graph.Node.register_hook()`](../generated/torch.autograd.graph.Node.register_hook.html#torch.autograd.graph.Node.register_hook "torch.autograd.graph.Node.register_hook")注册到节点的后钩子,以及通过[`torch.autograd.graph.Node.register_prehook()`](../generated/torch.autograd.graph.Node.register_prehook.html#torch.autograd.graph.Node.register_prehook "torch.autograd.graph.Node.register_prehook")注册到节点的前钩子。 ### 特定钩子是否会被触发[](#whether-a-particular-hook-will-be-fired "Permalink to this heading") 通过[`torch.Tensor.register_hook()`](../generated/torch.Tensor.register_hook.html#torch.Tensor.register_hook "torch.Tensor.register_hook")注册到张量的钩子在计算该张量的梯度时执行。(请注意,这不需要执行张量的grad_fn。例如,如果张量作为`inputs`参数的一部分传递给[`torch.autograd.grad()`](../generated/torch.autograd.grad.html#torch.autograd.grad "torch.autograd.grad"),则可能不会执行张量的grad_fn,但是注册到该张量的钩子将始终被执行。) 通过[`torch.Tensor.register_post_accumulate_grad_hook()`](../generated/torch.Tensor.register_post_accumulate_grad_hook.html#torch.Tensor.register_post_accumulate_grad_hook "torch.Tensor.register_post_accumulate_grad_hook")注册到张量的钩子在该张量的梯度累积后执行,这意味着张量的grad字段已经设置。而通过[`torch.Tensor.register_hook()`](../generated/torch.Tensor.register_hook.html#torch.Tensor.register_hook "torch.Tensor.register_hook")注册的钩子在计算梯度时运行,通过[`torch.Tensor.register_post_accumulate_grad_hook()`](../generated/torch.Tensor.register_post_accumulate_grad_hook.html#torch.Tensor.register_post_accumulate_grad_hook "torch.Tensor.register_post_accumulate_grad_hook")注册的钩子只有在autograd在反向传播结束时更新张量的grad字段时才会触发。因此,后累积梯度钩子只能注册给叶子张量。在非叶子张量上通过[`torch.Tensor.register_post_accumulate_grad_hook()`](../generated/torch.Tensor.register_post_accumulate_grad_hook.html#torch.Tensor.register_post_accumulate_grad_hook "torch.Tensor.register_post_accumulate_grad_hook")注册钩子会出错,即使您调用backward(retain_graph=True)。 使用[`torch.autograd.graph.Node.register_hook()`](../generated/torch.autograd.graph.Node.register_hook.html#torch.autograd.graph.Node.register_hook)或[`torch.autograd.graph.Node.register_prehook()`](../generated/torch.autograd.graph.Node.register_prehook.html#torch.autograd.graph.Node.register_prehook)注册到`torch.autograd.graph.Node`的钩子仅在注册的节点被执行时触发。 特定节点是否执行可能取决于反向传播是使用[`torch.autograd.grad()`](../generated/torch.autograd.grad.html#torch.autograd.grad)还是[`torch.autograd.backward()`](../generated/torch.autograd.backward.html#torch.autograd.backward)调用的。具体来说,当您在注册到与作为`inputs`参数的一部分传递给[`torch.autograd.grad()`](../generated/torch.autograd.grad.html#torch.autograd.grad)或[`torch.autograd.backward()`](../generated/torch.autograd.backward.html#torch.autograd.backward)的张量对应的节点上注册钩子时,您应该注意这些差异。 如果您使用[`torch.autograd.backward()`](../generated/torch.autograd.backward.html#torch.autograd.backward),上述提到的所有钩子都将被执行,无论您是否指定了`inputs`参数。这是因为.backward()执行所有节点,即使它们对应于作为输入指定的张量。(请注意,执行作为`inputs`传递的张量对应的此额外节点通常是不必要的,但仍然会执行。此行为可能会更改;您不应该依赖它。) 另一方面,如果您使用[`torch.autograd.grad()`](../generated/torch.autograd.grad.html#torch.autograd.grad),则注册到与传递给`input`的张量对应的节点的反向钩子可能不会被执行,因为除非有另一个依赖于此节点梯度结果的输入,否则不会执行这些节点。 ### 不同钩子被触发的顺序[](#the-order-in-which-the-different-hooks-are-fired) 发生事情的顺序是: 1. 执行注册到张量的钩子 1. 执行注册到节点的前钩子(如果节点被执行)。 1. 对保留梯度的张量更新`.grad`字段 1. 节点被执行(受上述规则约束) 1. 对于累积了`.grad`的叶子张量,执行后累积梯度钩子 1. 执行注册到节点的后钩子(如果节点被执行) 如果同一类型的多个钩子注册到同一张量或节点上,则它们按照注册的顺序执行。稍后执行的钩子可以观察到先前钩子对梯度所做的修改。 ### 特殊钩子 [`torch.autograd.graph.register_multi_grad_hook()`](../autograd.html#torch.autograd.graph.register_multi_grad_hook)是使用注册到张量的钩子实现的。每个单独的张量钩子按照上面定义的张量钩子顺序触发,并且当计算最后一个张量梯度时调用注册的多梯度钩子。 [`torch.nn.modules.module.register_module_full_backward_hook()`](../generated/torch.nn.modules.module.register_module_full_backward_hook.html#torch.nn.modules.module.register_module_full_backward_hook "torch.nn.modules.module.register_module_full_backward_hook") 是使用注册到节点的钩子来实现的。在计算前向传播时,钩子被注册到与模块的输入和输出对应的grad_fn上。因为一个模块可能接受多个输入并返回多个输出,所以在前向传播之前,首先对模块的输入应用一个虚拟的自定义自动求导函数,然后将前向传播的输出返回到确保这些张量共享一个单一的grad_fn,然后我们可以将我们的钩子附加到上面。 ### 张量在原地修改时的钩子行为[](#behavior-of-tensor-hooks-when-tensor-is-modified-in-place "Permalink to this heading") 通常,注册到张量的钩子接收相对于该张量的输出的梯度,其中张量的值被视为在计算反向传播时的值。 然而,如果您将钩子注册到一个张量,然后对该张量进行原地修改,那么在原地修改之前注册的钩子同样会接收相对于该张量的输出的梯度,但是张量的值被视为在原地修改之前的值。 如果您更喜欢前一种情况的行为,您应该在对张量进行所有原地修改之后将它们注册到张量上。例如: ```py t = torch.tensor(1., requires_grad=True).sin() t.cos_() t.register_hook(fn) t.backward() ``` 此外,值得知道的是,在幕后,当钩子注册到张量时,它们实际上会永久绑定到该张量的grad_fn上,因此如果该张量随后被原地修改,即使该张量现在有一个新的grad_fn,之前在原地修改之前注册的钩子仍将继续与旧的grad_fn相关联,例如,当自动求导引擎在图中到达该张量的旧grad_fn时,它们将触发。