目录

Autograd 机制

此说明将概述 autograd 的工作原理,并记录 操作。了解所有这些并非绝对必要,但我们建议 熟悉它,因为它会帮助你更高效、更清晰地写作 程序,并可以帮助您进行调试。

autograd 如何对历史记录进行编码

Autograd 是反向自动微分系统。概念 autograd 会录制一个图表,记录 执行作时的数据,为您提供有向无环图 其叶子是输入张量,根是输出张量。 通过从根到叶跟踪此图表,您可以自动 使用链式法则计算梯度。

在内部,autograd 将此图表示为对象图(实际上是表达式),可以将其用于计算 评估图形。在计算前向传递时,autograd 同时执行请求的计算并构建图形 表示计算梯度的函数(每个Functionapply().grad_fntorch.Tensor是此图的入口点)。 当前向传球完成后,我们在 backwards 传递来计算梯度。

需要注意的重要一点是,该图是在每个 迭代,而这正是允许使用任意 Python 控制的原因 flow 语句,这些语句可以在 每次迭代。您不必在编码之前对所有可能的路径进行编码 启动培训 - 您运行的内容就是您的差异化。

保存的张量

某些作需要在正向传递期间保存中间结果 以执行向后传递。例如,函数xx2x\mapsto x^2保存输入xx来计算梯度。

定义自定义 Python 时Function,可用于保存 张量并检索它们 在向后传球期间。有关更多信息,请参阅扩展 PyTorchsave_for_backward()saved_tensors

对于 PyTorch 定义的作(例如torch.pow()),张量为 根据需要自动保存。您可以探索 (用于教育或调试 目的)通过查找其 以前缀 .grad_fn_saved

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

在前面的代码中,引用与 x 相同的 Tensor 对象。 但情况可能并非总是如此。例如:y.grad_fn._saved_self

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 打包了张量 保存并将其解压缩到不同的 Tensor 中以供读取。在这里, 从 access 获得的 Tensor 是不同的 Tensor 对象(但它们仍共享相同的存储空间)。y.grad_fn._saved_resulty

一个 Tensor 是否会被打包到不同的 Tensor 对象中取决于 是否是自身grad_fn的输出,即实现细节 可能会发生变化,用户不应依赖。

您可以控制 PyTorch 如何使用 Hook 对保存的张量进行打包/解包。

不可微函数的梯度

使用自动微分的梯度计算仅在使用的每个初等函数都是可微分的时才有效。 不幸的是,我们在实践中使用的许多函数都没有这个属性(例如 , 或 at )。 为了尝试减少不可微函数的影响,我们通过按顺序应用以下规则来定义初等运算的梯度:relusqrt0

  1. 如果该函数是可微分的,因此在当前点存在梯度,则使用它。

  2. 如果函数是凸的(至少是局部的),则使用 minimum 范数的子梯度(这是最陡的下降方向)。

  3. 如果函数是凹的(至少是局部的),请使用 minimum 范数的超级梯度(考虑 -f(x) 并应用前一点)。

  4. 如果定义了函数,则通过连续性定义当前点的梯度(请注意,这里是可能的,例如 )。如果可能有多个值,请任意选择一个值。infsqrt(0)

  5. 如果未定义函数(例如,当 input 为 时,或大多数函数),则用作梯度的值是任意的(我们也可能引发错误,但不能保证)。大多数函数将用作渐变,但出于性能原因,某些函数将使用其他值(例如)。sqrt(-1)log(-1)NaNNaNlog(-1)

  6. 如果函数不是确定性映射(即它不是数学函数),它将被标记为不可微分。如果用于需要在环境之外 grad 的张量,这将使其向后出错。no_grad

在本地禁用梯度计算

Python 提供了多种机制来在本地禁用渐变 计算:

要禁用整个代码块的渐变,可以使用上下文管理器 就像 no-grad 模式和推理模式一样。 要从梯度计算中更细粒度地排除子图, 有设置张量的字段。requires_grad

下面,除了讨论上述机制外,我们还描述了 evaluation mode () ),一种实际未使用的方法 禁用梯度计算,但由于其名称,经常与这三者混淆。nn.Module.eval()

设置requires_grad

requires_grad是一个标志,默认为 false,除非包装 在 中,允许精细排除 梯度计算的子图。它在 向前和向后传递:nn.Parameter

在前向传递期间,如果满足以下条件,则作仅记录在后向图中 至少有一个 Importing Tensor 需要 grad。 在向后传递 () 期间,只有 的叶张量才会将梯度累积到其字段中。.backward()requires_grad=True.grad

需要注意的是,即使每个张量都有此标志,设置它也只对叶张量(没有 的张量 ,例如 a 的参数)有意义。 非叶张量(具有 的张量 )是具有 与它们关联的后向图。因此,将需要它们的梯度 作为中间结果来计算叶张量的梯度,其中 需要 grad。从这个定义可以清楚地看出,所有非叶张量 将自动具有 。grad_fnnn.Modulegrad_fnrequire_grad=True

设置应该是您控制哪些部分的主要方式 是梯度计算的一部分,例如,如果需要 在模型微调期间冻结预训练模型的某些部分。requires_grad

要冻结模型的某些部分,只需应用于 您不希望更新的参数。如上所述, 因为使用这些参数作为输入的计算不会记录在 forward 传递时,它们不会在 backward 中更新其字段 传递,因为它们本来就不会成为 backward 图的一部分,因为 期望。.requires_grad_(False).grad

因为这是这样常见的模式,所以也可以设置为 模块级别。 当应用于模块时,对所有 模块的参数(默认情况下具有)。requires_gradnn.Module.requires_grad_().requires_grad_()requires_grad=True

Grad 模式

除了设置之外,还有三种可能的模式 可以从 Python 启用,这可能会影响 PyTorch 中的计算方式 由 autograd 内部处理:默认模式(grad 模式)、no-grad 模式、 和 inference 模式,所有这些都可以通过上下文管理器和 装饰。requires_grad

默认模式(Grad 模式)

“默认模式”实际上是我们隐含的模式,当没有其他模式时,如 no-grad 和 inference 模式。与 “no-grad mode” 默认模式有时也称为 “grad mode”。

关于默认模式,要了解的最重要的一点是它是唯一的 模式,该模式生效。 始终被覆盖 以处于其他两种模式。requires_gradrequires_gradFalse

无毕业生模式

no-grad 模式下的计算表现得好像没有任何 inputs 需要 grad 一样。 换句话说,no-grad 模式下的计算永远不会记录在反向图中 即使有具有 .require_grad=True

当您需要执行不应该执行的作时,启用 no-grad 模式 由 autograd 录制,但您仍希望使用这些输出 稍后在 grad 模式下进行计算。此上下文管理器可以方便地 禁用代码块或函数的渐变,而不使用 必须临时将张量设置为 have ,然后 返回 。requires_grad=FalseTrue

例如,在编写优化器时,no-grad 模式可能很有用:当 执行训练更新您想要更新参数 就地作,而无需 Autograd 记录更新。 您还打算使用更新的参数进行 grad 模式。

torch.nn.init 中的实现也 在初始化参数时依赖 no-grad 模式,以避免 Autograd 跟踪。

推理模式

推理模式是 no-grad 模式的极端版本。就像在 no-grad 中一样 模式下,推理模式下的计算不会记录在反向图中,但 启用推理模式将使 PyTorch 能够进一步加快您的模型速度。 这种更好的运行时间有一个缺点:在推理模式下创建的张量 将无法用于 autograd 之后记录的计算 退出推理模式。

在执行不需要的计算时启用推理模式 记录在反向图中,并且您不打算使用张量 在推理模式下创建,用于稍后由 Autograd 记录的任何计算。

建议您在代码的各个部分试用推理模式 不需要 Autograd 跟踪(例如,数据处理和模型评估)。 如果开箱即用 对于您的使用案例,这是一个免费的性能胜利。如果您在 启用推理模式,检查您是否没有使用在 退出推理后由 Autograd 记录的计算中的推理模式 模式。如果您无法避免此类使用,您可以随时切换回来 设置为 no-grad 模式。

有关推理模式的详细信息,请参阅 推理模式

有关推理模式的实现详细信息,请参阅 RFC-0011-InferenceMode

评估模式 (nn.Module.eval())

求值模式实际上不是一种在本地禁用梯度计算的机制。 无论如何,它都包含在这里,因为它有时会被混淆为这样的机制。

从功能上讲,(或等效地)完全 与 no-grad 模式和推理模式正交。如何影响 您的模型完全依赖于模型中使用的特定模块,并且 它们是否定义了任何特定于训练模式的行为。module.eval()module.train(False)model.eval()

您负责致电,如果您的 model 依赖于model.eval()model.train()torch.nn.Dropouttorch.nn.BatchNorm2d那可能会表现 根据训练模式的不同,例如,为了避免更新您的 BatchNorm 对验证数据运行统计信息。

建议您始终在以下情况下使用 训练以及评估模型(验证/测试)时 如果您不确定您的模型是否具有特定于训练模式的行为,因为 您正在使用的模块可能会更新为在训练中表现不同,并且 eval 模式。model.train()model.eval()

使用 autograd 进行就地作

在 autograd 中支持就地作是一件困难的事情,我们不鼓励这样做 它们在大多数情况下的使用。Autograd 激进的缓冲区释放和重用使 它非常有效,并且很少有就地作的情况 实际上,将内存使用量降低了任何显著的量。除非您正在作 在内存压力很大的情况下,您可能永远不需要使用它们。

限制就地作适用性的主要原因有两个:

  1. 就地作可能会覆盖计算所需的值 梯度。

  2. 每个就地作实际上都需要实现重写 计算图。异地版本只需分配新对象和 保留对旧图形的引用,而就地作需要 将所有输入的创建者更改为表示 此作。这可能很棘手,尤其是在有许多 Tensor 的情况下 引用相同的存储(例如,通过索引或转置创建), 如果 修改后的输入被任何其他 引用。FunctionTensor

就地正确性检查

每个张量都保留一个版本计数器,每次 在任何作中标记为脏。当 Function 保存任何张量进行 backward 时, 它们包含的 Tensor 的版本计数器也会被保存。访问后,将检查它,以及它是否大于保存的值 引发错误。这可确保在就地使用 函数,并且没有看到任何错误,则可以确保计算出的 梯度是正确的。self.saved_tensors

多线程 Autograd

autograd 引擎负责运行所有向后作 计算向后传递所必需的。本节将介绍所有详细信息 这可以帮助您在多线程环境中充分利用它。(这是 仅与 PyTorch 1.6+ 相关,因为以前版本中的行为不同。

用户可以使用多线程代码(例如 Hogwild 训练)来训练他们的模型,并且 不会阻塞并发向后计算,示例代码可以是:

# 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 并发

C++当您在多个 threads 的 CPU 上,您期望看到额外的并发,而不是 在执行期间按特定顺序序列化所有向后调用 (PyTorch 1.6 之前的行为)。backward()grad()

非确定性

如果你同时调用多个线程,但使用 共享输入(即 Hogwild CPU 训练)。由于参数会自动 在线程之间共享,梯度累积可能会在 跨线程的向后调用,因为两个向后调用可能会 access 和 try 以累积相同的属性。这在技术上是不安全的,并且 它可能会导致 Racing 条件,并且结果可能无效。backward().grad

但是,如果您使用多线程方法 驱动整个训练过程,但使用共享参数,使用 多线程应该考虑到线程模型,并且应该期望 发生。用户可以使用功能 APItorch.autograd.grad()自 计算梯度,而不是避免不确定性。backward()

图形保留

如果 autograd 图的一部分在线程之间共享,即先运行 forward single thread 的一部分,然后在多个线程中运行 second part, 然后共享 Graph 的第一部分。在本例中,不同的线程 execute 或在同一图表上可能存在 在一个线程的动态中销毁图形,另一个线程将 崩溃。Autograd 会向用户发送错误,类似于没有 out 的两次调用,并让用户知道 他们应该使用 .grad()backward()backward()retain_graph=Trueretain_graph=True

Autograd 节点上的线程安全

由于 Autograd 允许调用方线程驱动其向后执行 潜在的并行性,因此我们必须确保 CPU 上的线程安全 parallel backward 共享部分/全部 GraphTask。

由于 GIL,自定义 Python 会自动成为线程安全的。 对于内置的 C++ Autograd 节点(例如 AccumulateGrad、CopySlices)和自定义节点,Autograd 引擎使用线程互斥锁来确保 可能具有 write/read 状态的 autograd 节点上的线程安全。autograd.Functionautograd::Function

C++ 钩子上没有线程安全

Autograd 依赖于用户编写线程安全的 C++ 钩子。如果你想要钩子 要在多线程环境中正确应用,您需要编写 正确的线程锁定代码,以确保 hook 是线程安全的。

复数的 Autograd

简短版本:

  • 当您使用 PyTorch 区分任何函数时f(z)f(z)具有复杂域和/或共域, 梯度的计算假设该函数是更大的实值的一部分 loss 函数g(nput)=Lg(input)=L.计算出的梯度为Lz\frac{\partial L}{\partial z^*}(注意 z 的共轭),其负数恰好是 STEEPEST DESCENT 的方向 用于 Gradient Descent 算法。因此,所有现有的优化器都可以在 具有复杂参数的盒子。

  • 此约定与 TensorFlow 的 complex 约定 微分,但与 JAX 不同(JAX 计算Lz\frac{\partial L}{\partial z}).

  • 如果你有一个 real-to-real 函数,它在内部使用 complex 作,这里的约定并不重要:您总是会得到 如果实施它,您将获得相同的结果 只有真正的作。

如果您对数学细节感到好奇,或者想知道怎么做 要在 PyTorch 中定义复杂衍生品,请继续阅读。

什么是复杂衍生品?

复微分性的数学定义采用 limit 定义,并将其推广为对 复数。考虑一个函数f:CCf: ℂ → ℂ,

f(z=x+yj)=u(x,y)+v(x,y)j`f(z=x+yj) = u(x, y) + v(x, y)j`

哪里uuvv是两个变量实值函数。

使用导数定义,我们可以写成:

f(z)=h0,hCf(z+h)f(z)hf'(z) = \lim_{h \to 0, h \in C} \frac{f(z+h) - f(z)}{h}

为了存在这个限制,不仅必须uuvv必须是 real 微分,但ff还必须满足 Cauchy-Riemann 方程。在 换句话说:为实部和虚部计算的极限 (hh) 必须相等。这是一个限制性更强的条件。

复数可微函数通常称为全态 功能。他们乖巧,拥有所有美好的特性 您已经从真正的可微函数中看到了,但实际上没有 在优化领域使用。对于优化问题,只有真正的值目标 函数用于研究社区,因为复数不是任何 ordered 字段,因此具有 Complex Value Loss 没有多大意义。

还证明,没有有趣的实值目标满足 Cauchy-Riemann 方程。所以具有同态函数的理论不可能是 用于优化,因此大多数人使用 Wirtinger 微积分。

Wirtinger 微积分出现 ...

所以,我们有了这个伟大的复微分理论,并且 holomorphic 函数,我们根本无法使用其中的任何一个,因为许多 的常用函数不是全态的。什么是穷人 数学家要做吗?好吧,Wirtinger 观察到,即使f(z)f(z)不是全态的,可以将其重写为双变量函数f(z,z)f(z, z*)它总是全态的。这是因为真实和 的 imaginary 的组成部分zz可以用zzzz^*如:

Re(z)=z+z2m(z)=zz2j\begin{aligned} Re(z) &= \frac {z + z^*}{2} \\ Im(z) &= \frac {z - z^*}{2j} \end{aligned}

Wirtinger 微积分建议学习f(z,z)f(z, z^*)相反,它是 保证为 holomorphic,如果ff是实数可微分的(另一个 可以把它看作是坐标系的变化,从f(x,y)f(x, y)f(z,z)f(z, z^*).)此函数具有偏导数z\frac{\partial }{\partial z}z\frac{\partial}{\partial z^{*}}. 我们可以使用链式规则来建立一个 这些偏导数与偏导数之间的关系 导数 w.r.t.,实部和虚部zz.

x=zxz+zxz=z+zy=zyz+zyz=1j(zz)\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 * (\frac{\partial }{\partial z} - \frac{\partial }{\partial z^*}) \end{aligned}

从上面的方程式中,我们得到:

z=1/2(x1jy)z=1/2(x+1jy)\begin{aligned} \frac{\partial }{\partial z} &= 1/2 * (\frac{\partial }{\partial x} - 1j * \frac{\partial }{\partial y}) \\ \frac{\partial }{\partial z^*} &= 1/2 * (\frac{\partial }{\partial x} + 1j * \frac{\partial }{\partial y}) \end{aligned}

这是维辛格微积分的经典定义,你可以在维基百科上找到。

这种变化有很多美好的后果。

  • 首先,Cauchy-Riemann 方程可以简单地说fz=0\frac{\partial f}{\partial z^*} = 0(也就是说,函数ff可以写入 完全就zz,而不引用zz^*).

  • 正如我们稍后将看到的,另一个重要的(但有点违反直觉的)结果是,当我们对实际价值的损失进行优化时,我们应该 take 的 take 的 API 表达式为Lossz\frac{\partial Loss}{\partial z^*}(不是Lossz\frac{\partial Loss}{\partial z}).

如需更多阅读,请查看:https://arxiv.org/pdf/0906.4835.pdf

Wirtinger Calculus 在优化中有何用处?

音频和其他领域的研究人员更常用地使用 Gradient descent 来优化具有复变量的实值损失函数。 通常,这些人将实值和虚值视为分开的 可以更新的频道。对于步长α/2\alpha/2和损失LL,我们可以在R2ℝ^2:

xn+1=xn(α/2)Lxyn+1=yn(α/2)Ly\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}

这些方程如何转化为复空间C?

zn+1=xn(α/2)Lx+1j(yn(α/2)Ly)=znα1/2(Lx+jLy)=znαLz\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 * (\frac{\partial L}{\partial x} + j \frac{\partial L}{\partial y}) \\ &= z_n - \alpha * \frac{\partial L}{\partial z^*} \end{aligned}

发生了一件非常有趣的事情:维廷格微积分告诉我们 我们可以将上面的复数变量更新公式简化为仅 参考共轭 Wirtinger 导数Lz\frac{\partial L}{\partial z^*},这正是我们在优化中采取的步骤。

因为共轭 Wirtinger 导数为我们提供了实值损失函数的正确步骤,所以 PyTorch 为您提供了这个导数 当您区分具有实际价值损失的函数时。

PyTorch 如何计算共轭 Wirtinger 导数?

通常,我们的导数公式将 grad_output 作为输入, 表示我们已经输入的 Vector-Jacobian 积 computed,又名Ls\frac{\partial L}{\partial s^*}哪里LL是整个计算的损失(产生真正的损失),而ss是我们函数的输出。这里的目标是计算Lz\frac{\partial L}{\partial z^*}哪里zz是 函数。事实证明,在实际亏损的情况下,我们可以 只计算Lz\frac{\partial L}{\partial z^*}, 尽管链式法则意味着我们还需要 有权访问Lz\frac{\partial L}{\partial z^*}.如果需要帮助, 要跳过此推导,请查看本节中的最后一个方程 ,然后跳到下一部分。

让我们继续合作f:CCf: ℂ → ℂ定义为f(z)=f(x+yj)=u(x,y)+v(x,y)jf(z) = f(x+yj) = u(x, y) + v(x, y)j.如上所述, Autograd 的 gradient 约定以 Real 优化为中心 有值损失函数,所以我们假设ff是 larger 的一部分 实值损失函数gg.使用链式法则,我们可以写成:

(1)Lz=Luuz+Lvvz\frac{\partial L}{\partial z^*} = \frac{\partial L}{\partial u} * \frac{\partial u}{\partial z^*} + \frac{\partial L}{\partial v} * \frac{\partial v}{\partial z^*}

现在使用 Wirtinger 导数定义,我们可以写成:

Ls=1/2(LuLvj)Ls=1/2(Lu+Lvj)\begin{aligned} \frac{\partial L}{\partial s} = 1/2 * (\frac{\partial L}{\partial u} - \frac{\partial L}{\partial v} j) \\ \frac{\partial L}{\partial s^*} = 1/2 * (\frac{\partial L}{\partial u} + \frac{\partial L}{\partial v} j) \end{aligned}

这里需要注意的是,由于uuvv是真实的 函数和LL是真实的,根据我们的假设ff是一个 作为实值函数的一部分,我们有:

(2)(Ls)=Ls(\frac{\partial L}{\partial s})^* = \frac{\partial L}{\partial s^*}

Ls\frac{\partial L}{\partial s}等于gr一个d_outputgrad\_output^*.

求解上述方程Lu\frac{\partial L}{\partial u}Lv\frac{\partial L}{\partial v},我们得到:

(3)Lu=Ls+LsLv=1j(LsLs)\begin{aligned} \frac{\partial L}{\partial u} = \frac{\partial L}{\partial s} + \frac{\partial L}{\partial s^*} \\ \frac{\partial L}{\partial v} = -1j * (\frac{\partial L}{\partial s} - \frac{\partial L}{\partial s^*}) \end{aligned}

(3) 代入 (1) 中,我们得到:

Lz=(Ls+Ls)uz1j(LsLs)vz=Ls(uz+vzj)+Ls(uzvzj)=Ls(u+vj)z+Ls(u+vj)z=Lssz+Lssz\begin{aligned} \frac{\partial L}{\partial z^*} &= (\frac{\partial L}{\partial s} + \frac{\partial L}{\partial s^*}) * \frac{\partial u}{\partial z^*} - 1j * (\frac{\partial L}{\partial s} - \frac{\partial L}{\partial s^*}) * \frac{\partial v}{\partial z^*} \\ &= \frac{\partial L}{\partial s} * (\frac{\partial u}{\partial z^*} + \frac{\partial v}{\partial z^*} j) + \frac{\partial L}{\partial s^*} * (\frac{\partial u}{\partial z^*} - \frac{\partial v}{\partial z^*} j) \\ &= \frac{\partial L}{\partial s^*} * \frac{\partial (u + vj)}{\partial z^*} + \frac{\partial L}{\partial s} * \frac{\partial (u + vj)^*}{\partial z^*} \\ &= \frac{\partial L}{\partial s} * \frac{\partial s}{\partial z^*} + \frac{\partial L}{\partial s^*} * \frac{\partial s^*}{\partial z^*} \\ \end{aligned}

使用 (2),我们得到:

(4)Lz=(Ls)sz+Ls(sz)=(gr一个d_output)sz+gr一个d_output(sz)\begin{aligned} \frac{\partial L}{\partial z^*} &= (\frac{\partial L}{\partial s^*})^* * \frac{\partial s}{\partial z^*} + \frac{\partial L}{\partial s^*} * (\frac{\partial s}{\partial z})^* \\ &= \boxed{ (grad\_output)^* * \frac{\partial s}{\partial z^*} + grad\_output * {(\frac{\partial s}{\partial z})}^* } \\ \end{aligned}

最后一个方程式是编写你自己的梯度的重要方程式, 因为它将我们的导数公式分解成一个更简单的公式 手动计算。

如何为复杂函数编写自己的导数公式?

上面的方框方程为我们提供了所有 复杂函数的导数。但是,我们仍然需要 计算sz\frac{\partial s}{\partial z}sz\frac{\partial s}{\partial z^*}. 有两种方法可以执行此作:

  • 第一种方法是直接使用 Wirtinger 导数的定义并计算sz\frac{\partial s}{\partial z}sz\frac{\partial s}{\partial z^*}由 用sx\frac{\partial s}{\partial x}sy\frac{\partial s}{\partial y}(您可以按正常方式计算)。

  • 第二种方法是使用变量更改 trick 并重写f(z)f(z)作为双变量函数f(z,z)f(z, z^*)和 compute 共轭 Wirtinger 导数,通过处理zzzz^*作为自变量。这通常更容易;例如,如果所讨论的函数是 holomorphic,则只有zz将使用(并且sz\frac{\partial s}{\partial z^*}将为零)。

让我们考虑函数f(z=x+yj)=cz=c(x+yj)f(z = x + yj) = c * z = c * (x+yj)例如,其中cRc \in ℝ.

使用第一种方法来计算 Wirtinger 导数,我们有。

sz=1/2(sxsyj)=1/2(c(c1j)1j)=csz=1/2(sx+syj)=1/2(c+(c1j)1j)=0\begin{aligned} \frac{\partial s}{\partial z} &= 1/2 * (\frac{\partial s}{\partial x} - \frac{\partial s}{\partial y} j) \\ &= 1/2 * (c - (c * 1j) * 1j) \\ &= c \\ \\ \\ \frac{\partial s}{\partial z^*} &= 1/2 * (\frac{\partial s}{\partial x} + \frac{\partial s}{\partial y} j) \\ &= 1/2 * (c + (c * 1j) * 1j) \\ &= 0 \\ \end{aligned}

使用 (4)grad_output = 1.0(这是在 PyTorch 中对标量输出调用时使用的默认 grad 输出值),我们得到:backward()

Lz=10+1c=c\frac{\partial L}{\partial z^*} = 1 * 0 + 1 * c = c

使用第二种方法计算 Wirtinger 导数,我们直接得到:

sz=(cz)z=csz=(cz)z=0\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}

再次使用 (4),我们得到Lz=c\frac{\partial L}{\partial z^*} = c.如您所见,第二种方式涉及较少的计算,并且 更方便,计算更快。

跨域函数呢?

某些函数从复杂输入映射到实际输出,反之亦然。 这些函数构成了 (4) 的特例,我们可以使用 链式法则:

  • f:CRf: ℂ → ℝ,我们得到:

    Lz=2gr一个d_outputsz\frac{\partial L}{\partial z^*} = 2 * grad\_output * \frac{\partial s}{\partial z^{*}}
  • f:RCf: ℝ → ℂ,我们得到:

    Lz=2Re(gr一个d_outsz)\frac{\partial L}{\partial z^*} = 2 * Re(grad\_out^* * \frac{\partial s}{\partial z^{*}})

已保存张量的钩子

您可以通过定义一对 / 钩子来控制保存的张量的打包/解包方式。该函数应将张量作为其单个参数 但可以返回任何 Python 对象(例如另一个张量、一个元组,甚至是一个 string 包含文件名)。该函数将 参数的输出 and 应返回要在 向后传递。返回的 tensor 只需要有 与作为输入传递给 的 Tensor 的内容相同。特别 任何与 Autograd 相关的元数据都可以忽略,因为它们将在 打开。pack_hookunpack_hookpack_hookunpack_hookpack_hookunpack_hookpack_hook

此类对的一个例子是:

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)

请注意,不应删除临时文件,因为它 可能会被多次调用:临时文件应处于活动状态的时间 ,因为返回的 SelfDeletingTempFile 对象处于活动状态。在上面的示例中, 我们通过在不再需要临时文件时关闭临时文件来防止它泄漏 (在删除 SelfDeletingTempFile 对象时)。unpack_hook

注意

我们保证这只会被调用一次,但可以 被调用的次数与向后传递所需的次数相同,我们希望它能够 每次返回相同的数据。pack_hookunpack_hook

警告

禁止对任何函数的输入执行就地作 因为它们可能会导致意想不到的副作用。如果 PyTorch 的 input 的 input 被就地修改,但不会捕获 unpack 钩子的 input 被就地修改。

为保存的张量注册钩子

您可以通过对对象调用 method 在保存的张量上注册一对钩子。这些对象作为 a 的属性公开,并以前缀开头。register_hooks()SavedTensorgrad_fn_raw_saved_

x = torch.randn(5, requires_grad=True)
y = x.pow(2)
y.grad_fn._raw_saved_self.register_hooks(pack_hook, unpack_hook)

一旦注册了对,就会调用该方法。 每次保存的张量需要时,都会调用该方法 访问,通过 或 在 backward 期间访问 通过。pack_hookunpack_hooky.grad_fn._saved_self

警告

如果您在保存的 张量已被释放(即在调用 backward 之后),调用 它是被禁止的。 PyTorch 大多数情况下会抛出错误,但可能会失败 在某些情况下,可能会出现未定义的行为。SavedTensorregister_hooks()

为保存的张量注册默认钩子

或者,您可以使用 context-managersaved_tensors_hooks注册一对 钩子,该钩子将应用于在 那个背景。

例:

# 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

# Example what NOT to do

net = nn.DataParallel(model)
with torch.autograd.graph.saved_tensors_hooks(pack_hook, unpack_hook):
    output = net(input)

请注意,使用这些钩子会禁用所有优化以减少 Tensor 对象创建。例如:

with torch.autograd.graph.saved_tensors_hooks(lambda x: x, lambda x: x):
    x = torch.randn(5, requires_grad=True)
    y = x * x

如果没有钩子, , 和 都引用同一个 tensor 对象。 使用钩子,PyTorch 会将 x 打包和解压缩为两个新的 Tensor 对象 与原始 X 共享同一存储空间(不执行复制)。xy.grad_fn._saved_selfy.grad_fn._saved_other

文档

访问 PyTorch 的全面开发人员文档

查看文档

教程

获取面向初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并解答您的问题

查看资源