Autograd 机制¶
此说明将概述 autograd 的工作原理,并记录 操作。了解所有这些并非绝对必要,但我们建议 熟悉它,因为它会帮助你更高效、更清晰地写作 程序,并可以帮助您进行调试。
autograd 如何对历史记录进行编码¶
Autograd 是反向自动微分系统。概念 autograd 会录制一个图表,记录 执行操作时的数据,为您提供有向无环图 其叶子是输入张量,根是输出张量。 通过从根到叶跟踪此图表,您可以自动 使用链式法则计算梯度。
在内部,autograd 将此图表示为对象图(实际上是表达式),可以将其用于计算
评估图形。在计算前向传递时,autograd
同时执行请求的计算并构建图形
表示计算梯度的函数(每个的属性是此图的入口点)。
当前向传球完成后,我们在
backwards 传递来计算梯度。
Function
apply()
.grad_fn
需要注意的重要一点是,该图是在每个 迭代,而这正是允许使用任意 Python 控制的原因 flow 语句,这些语句可以在 每次迭代。您不必在编码之前对所有可能的路径进行编码 启动培训 - 您运行的内容就是您的差异化。
保存的张量¶
某些操作需要在正向传递期间保存中间结果 以执行向后传递。例如,函数保存输入来计算梯度。
定义自定义 Python 时,您可以使用保存
张量并检索它们
在向后传球期间。有关更多信息,请参阅扩展 PyTorch。
save_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_result
y
一个 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_fn
nn.Module
grad_fn
require_grad=True
设置应该是您控制哪些部分的主要方式
是梯度计算的一部分,例如,如果需要
在模型微调期间冻结预训练模型的某些部分。requires_grad
要冻结模型的某些部分,只需应用于
您不希望更新的参数。如上所述,
因为使用这些参数作为输入的计算不会记录在
forward 传递时,它们不会在 backward 中更新其字段
传递,因为它们本来就不会成为 backward 图的一部分,因为
期望。.requires_grad_(False)
.grad
因为这是这样常见的模式,所以也可以设置为
模块级别。
当应用于模块时,对所有
模块的参数(默认情况下具有)。requires_grad
nn.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_grad
requires_grad
False
无毕业生模式¶
no-grad 模式下的计算表现得好像没有任何 inputs 需要 grad 一样。
换句话说,no-grad 模式下的计算永远不会记录在反向图中
即使有具有 .require_grad=True
当您需要执行不应该执行的操作时,启用 no-grad 模式
由 autograd 录制,但您仍希望使用这些输出
稍后在 grad 模式下进行计算。此上下文管理器可以方便地
禁用代码块或函数的渐变,而不使用
必须临时将张量设置为 have ,然后
返回 。requires_grad=False
True
例如,在编写优化器时,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 激进的缓冲区释放和重用使 它非常有效,并且很少有就地操作的情况 实际上,将内存使用量降低了任何显著的量。除非您正在操作 在内存压力很大的情况下,您可能永远不需要使用它们。
限制就地操作适用性的主要原因有两个:
就地操作可能会覆盖计算所需的值 梯度。
每个就地操作实际上都需要实现重写 计算图。异地版本只需分配新对象和 保留对旧图形的引用,而就地操作需要 将所有输入的创建者更改为表示 此操作。这可能很棘手,尤其是在有许多 Tensor 的情况下 引用相同的存储(例如,通过索引或转置创建), 如果 修改后的输入被任何其他 引用。
Function
Tensor
就地正确性检查¶
每个张量都保留一个版本计数器,每次
在任何操作中标记为脏。当 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=True
retain_graph=True
Autograd 节点上的线程安全¶
由于 Autograd 允许调用方线程驱动其向后执行 潜在的并行性,因此我们必须确保 CPU 上的线程安全 parallel backward 共享部分/全部 GraphTask。
由于 GIL,自定义 Python 会自动成为线程安全的。
对于内置的 C++ Autograd 节点(例如 AccumulateGrad、CopySlices)和自定义,Autograd 引擎使用线程互斥锁来保护
可能具有 write/read 状态的 autograd 节点上的线程安全。autograd.function
autograd::Function
C++ 钩子上没有线程安全¶
Autograd 依赖于用户编写线程安全的 C++ 钩子。如果你想要钩子 要在多线程环境中正确应用,您需要编写 正确的线程锁定代码,以确保 hook 是线程安全的。
复数的 Autograd¶
简短版本:
当您使用 PyTorch 区分任何函数时具有复杂域和/或共域, 梯度的计算假设该函数是更大的实值的一部分 loss 函数.计算出的梯度为(注意 z 的共轭),其负数恰好是 STEEPEST DESCENT 的方向 用于 Gradient Descent 算法。因此,所有现有的优化器都可以在 具有复杂参数的盒子。
此约定与 TensorFlow 的 complex 约定 微分,但与 JAX 不同(JAX 计算).
如果你有一个 real-to-real 函数,它在内部使用 complex 操作,这里的约定并不重要:您总是会得到 如果实施它,您将获得相同的结果 只有真正的操作。
如果您对数学细节感到好奇,或者想知道怎么做 要在 PyTorch 中定义复杂衍生品,请继续阅读。
什么是复杂衍生品?¶
复微分性的数学定义采用 limit 定义,并将其推广为对 复数。考虑一个函数,
哪里和是两个变量实值函数。
使用导数定义,我们可以写成:
为了存在这个限制,不仅必须和必须是 real 微分,但还必须满足 Cauchy-Riemann 方程。在 换句话说:为实部和虚部计算的极限 () 必须相等。这是一个限制性更强的条件。
复数可微函数通常称为全态 功能。他们乖巧,拥有所有美好的特性 您已经从真正的可微函数中看到了,但实际上没有 在优化领域使用。对于优化问题,只有真正的值目标 函数用于研究社区,因为复数不是任何 ordered 字段,因此具有 Complex Value Loss 没有多大意义。
还证明,没有有趣的实值目标满足 Cauchy-Riemann 方程。所以具有同态函数的理论不可能是 用于优化,因此大多数人使用 Wirtinger 微积分。
Wirtinger 微积分出现 ...¶
所以,我们有了这个伟大的复微分理论,并且 holomorphic 函数,我们根本无法使用其中的任何一个,因为许多 的常用函数不是全态的。什么是穷人 数学家要做吗?好吧,Wirtinger 观察到,即使不是全态的,可以将其重写为双变量函数它总是全态的。这是因为真实和 的 imaginary 的组成部分可以用和如:
Wirtinger 微积分建议学习相反,它是 保证为 holomorphic,如果是实数可微分的(另一个 可以把它看作是坐标系的变化,从自.)此函数具有偏导数和. 我们可以使用链式规则来建立一个 这些偏导数与偏导数之间的关系 导数 w.r.t.,实部和虚部.
从上面的方程式中,我们得到:
这是维辛格微积分的经典定义,你可以在维基百科上找到。
这种变化有很多美好的后果。
首先,Cauchy-Riemann 方程可以简单地说(也就是说,函数可以写入 完全就,而不引用).
正如我们稍后将看到的,另一个重要的(但有点违反直觉的)结果是,当我们对实际价值的损失进行优化时,我们应该 take 的 take 的 API 表达式为(不是).
如需更多阅读,请查看:https://arxiv.org/pdf/0906.4835.pdf
Wirtinger Calculus 在优化中有何用处?¶
音频和其他领域的研究人员更常用地使用 Gradient descent 来优化具有复变量的实值损失函数。 通常,这些人将实值和虚值视为分开的 可以更新的频道。对于步长和损失,我们可以在:
这些方程如何转化为复空间?
发生了一件非常有趣的事情:维廷格微积分告诉我们 我们可以将上面的复数变量更新公式简化为仅 参考共轭 Wirtinger 导数,这正是我们在优化中采取的步骤。
因为共轭 Wirtinger 导数为我们提供了实值损失函数的正确步骤,所以 PyTorch 为您提供了这个导数 当您区分具有实际价值损失的函数时。
PyTorch 如何计算共轭 Wirtinger 导数?¶
通常,我们的导数公式将 grad_output 作为输入, 表示我们已经输入的 Vector-Jacobian 积 computed,又名哪里是整个计算的损失(产生真正的损失),而是我们函数的输出。这里的目标是计算哪里是 函数。事实证明,在实际亏损的情况下,我们可以 只计算, 尽管链式法则意味着我们还需要 有权访问.如果需要帮助, 要跳过此推导,请查看本节中的最后一个方程 ,然后跳到下一部分。
让我们继续合作定义为.如上所述, Autograd 的 gradient 约定以 Real 优化为中心 有值损失函数,所以我们假设是 larger 的一部分 实值损失函数.使用链式法则,我们可以写成:
(1)¶
现在使用 Wirtinger 导数定义,我们可以写成:
这里需要注意的是,由于和是真实的 函数和是真实的,根据我们的假设是一个 作为实值函数的一部分,我们有:
(2)¶
即等于.
求解上述方程和,我们得到:
(3)¶
使用 (2),我们得到:
(4)¶
最后一个方程式是编写你自己的梯度的重要方程式, 因为它将我们的导数公式分解成一个更简单的公式 手动计算。
如何为复杂函数编写自己的导数公式?¶
上面的方框方程为我们提供了所有 复杂函数的导数。但是,我们仍然需要 计算和. 有两种方法可以执行此操作:
第一种方法是直接使用 Wirtinger 导数的定义并计算和由 用和(您可以按正常方式计算)。
第二种方法是使用变量更改 trick 并重写作为双变量函数和 compute 共轭 Wirtinger 导数,通过处理和作为自变量。这通常更容易;例如,如果所讨论的函数是 holomorphic,则只有将使用(并且将为零)。
让我们考虑函数例如,其中.
使用第一种方法来计算 Wirtinger 导数,我们有。
使用 (4) 和 grad_output = 1.0(这是在 PyTorch 中对标量输出调用时使用的默认 grad 输出值),我们得到:backward()
使用第二种方法计算 Wirtinger 导数,我们直接得到:
再次使用 (4),我们得到.如您所见,第二种方式涉及较少的计算,并且 更方便,计算更快。
已保存张量的钩子¶
您可以通过定义一对 / 钩子来控制保存的张量的打包/解包方式。该函数应将张量作为其单个参数
但可以返回任何 Python 对象(例如另一个张量、一个元组,甚至是一个
string 包含文件名)。该函数将
参数的输出 and 应返回要在
向后传递。返回的 tensor 只需要有
与作为输入传递给 的 Tensor 的内容相同。特别
任何与 Autograd 相关的元数据都可以忽略,因为它们将在
打开。pack_hook
unpack_hook
pack_hook
unpack_hook
pack_hook
unpack_hook
pack_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_hook
unpack_hook
警告
禁止对任何函数的输入执行就地操作 因为它们可能会导致意想不到的副作用。如果 PyTorch 的 input 的 input 被就地修改,但不会捕获 unpack 钩子的 input 被就地修改。
为保存的张量注册钩子¶
您可以通过对对象调用 method 在保存的张量上注册一对钩子。这些对象作为 a 的属性公开,并以前缀开头。register_hooks()
SavedTensor
grad_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_hook
unpack_hook
y.grad_fn._saved_self
警告
如果您在保存的
张量已被释放(即在调用 backward 之后),调用
它是被禁止的。
PyTorch 大多数情况下会抛出错误,但可能会失败
在某些情况下,可能会出现未定义的行为。SavedTensor
register_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 共享同一存储空间(不执行复制)。x
y.grad_fn._saved_self
y.grad_fn._saved_other