目录

Autograd 机制

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

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

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

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

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

保存的张量

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

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

对于 PyTorch 定义的操作(例如 ),张量为 根据需要自动保存。您可以探索 (用于教育或调试 目的)通过查找其 以前缀 .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 对保存的张量进行打包/解包。

在本地禁用梯度计算

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()model.eval()

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

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

但是,如果您使用多线程方法 驱动整个训练过程,但使用共享参数,使用 多线程应该考虑到线程模型,并且应该期望 发生。用户可以使用函数式 API 来 计算梯度,而不是避免不确定性。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-manager 来注册一对 钩子,该钩子将应用于在 那个背景。

例:

# 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 的全面开发人员文档

查看文档

教程

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

查看教程

资源

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

查看资源