Gradcheck 机制#
创建日期:2021 年 4 月 27 日 | 最后更新日期:2025 年 6 月 18 日
本文档概述了 gradcheck()
和 gradgradcheck()
函数的工作原理。
它将涵盖实值和复值函数以及高阶导数的前向和后向模式 AD。本文档还涵盖了 gradcheck 的默认行为以及传递 fast_mode=True
参数的情况(下文称为快速 gradcheck)。
符号和背景信息#
本文档将使用以下约定
, , , , , , 和 是实值向量, 是一个复值向量,可以写成两个实值向量 的形式。
和 是我们将用于输入和输出空间维度的两个整数。
是我们的基本实到实函数,使得 。
是我们的基本复到实函数,使得 。
对于简单的实到实情况,我们将与 关联的雅可比矩阵 写成大小为 。该矩阵包含所有偏导数,其中位置 的条目包含 。然后,后向模式 AD 计算给定大小为 的向量 的量 。另一方面,前向模式 AD 计算给定大小为 的向量 的量 。
对于包含复数值的函数,情况要复杂得多。我们只在此处提供要点,完整描述可以在 复数的 Autograd 中找到。
为了满足复数可微分性(柯西-黎曼方程)的约束对于所有实值损失函数来说都过于严格,因此我们转而使用 Wirtinger 微积分。在 Wirtinger 微积分的基本设置中,链式法则需要访问 Wirtinger 导数(下文称为 )和共轭 Wirtinger 导数(下文称为 )。 和 都需要传播,因为通常,尽管它们的名称如此,但一个并非另一个的复共轭。
为了避免必须传播两个值,对于后向模式 AD,我们始终假设正在计算导数的函数是实值函数或是一个更大的实值函数的一部分。这个假设意味着我们在后向传递过程中计算的所有中间梯度也都与实值函数相关联。在实践中,这个假设在进行优化时并不受限制,因为这样的问题需要实值目标(因为复数没有自然排序)。
在这个假设下,使用 和 的定义,我们可以证明 (这里我们使用 来表示复共轭),因此只需要通过图“向后”传播其中一个值,另一个值可以很容易地恢复。为了简化内部计算,PyTorch 使用 作为它在用户请求梯度时反向传播并返回的值。与实数情况类似,当输出实际在 中时,后向模式 AD 不计算 ,而只计算给定向量 的 。
对于前向模式 AD,我们使用类似的逻辑,在这种情况下,假设该函数是其输入在 中的较大函数的一部分。在这个假设下,我们可以做出类似的断言,即每个中间结果都对应于一个输入在 中的函数,在这种情况下,使用 和 定义,我们可以证明中间函数 。为了确保前向和后向模式在基本的一维函数情况下计算相同的量,前向模式也计算 。与实数情况类似,当输入实际在 中时,前向模式 AD 不计算 ,而只计算给定向量 的 。
默认后向模式 gradcheck 行为#
实到实函数#
为了测试函数 ,我们以两种方式重建完整的雅可比矩阵 ,大小为 :解析法和数值法。解析法使用我们的后向模式 AD,而数值法使用有限差分。然后,将两个重建的雅可比矩阵逐元素进行比较以检查相等性。
默认实数输入数值评估#
如果我们考虑一维函数()的基本情况,那么我们可以使用 维基百科文章 中的基本有限差分公式。我们使用“中心差分”以获得更好的数值特性
这个公式很容易推广到多个输出(),只需将 视为大小为 的列向量,例如 。在这种情况下,上述公式可以原样重用,并且仅通过两次用户函数评估(即 和 )近似完整的雅可比矩阵。
处理多个输入()的情况计算成本更高。在这种情况下,我们逐个遍历所有输入,并对 的每个元素逐个应用 扰动。这使我们能够逐列重建 矩阵。
默认实数输入解析评估#
对于解析评估,我们使用上述事实,即后向模式 AD 计算 。对于单个输出的函数,我们简单地使用 来通过单个后向传递恢复完整的雅可比矩阵。
对于具有多个输出的函数,我们采用 for 循环,它迭代输出,其中每个 是一个独热向量,对应于每个输出。这允许我们逐行重建 矩阵。
复到实函数#
为了测试函数 ,其中 ,我们重建包含 的(复值)矩阵。
默认复数输入数值评估#
首先考虑 的基本情况。我们从 这篇研究论文(第 3 章)中知道
请注意,在上述方程中, 和 是 导数。为了数值评估这些,我们使用上述实到实情况的方法。这使我们能够计算 矩阵,然后将其乘以 。
请注意,截至撰写本文时,代码以一种稍微复杂的方式计算此值
# Code from https://github.com/pytorch/pytorch/blob/58eb23378f2a376565a66ac32c93a316c45b6131/torch/autograd/gradcheck.py#L99-L105
# Notation changes in this code block:
# s here is y above
# x, y here are a, b above
ds_dx = compute_gradient(eps)
ds_dy = compute_gradient(eps * 1j)
# conjugate wirtinger derivative
conj_w_d = 0.5 * (ds_dx + ds_dy * 1j)
# wirtinger derivative
w_d = 0.5 * (ds_dx - ds_dy * 1j)
d[d_idx] = grad_out.conjugate() * conj_w_d + grad_out * w_d.conj()
# Since grad_out is always 1, and W and CW are complex conjugate of each other, the last line ends up computing exactly `conj_w_d + w_d.conj() = conj_w_d + conj_w_d = 2 * conj_w_d`.
默认复数输入解析评估#
由于后向模式 AD 已经精确计算了两次 导数,我们在这里简单地使用与实到实情况相同的技巧,并在存在多个实数输出时逐行重建矩阵。
快速反向模式 gradcheck#
虽然上述 gradcheck 公式在确保正确性和可调试性方面都很出色,但由于它重建了完整的雅可比矩阵,因此速度非常慢。本节介绍了一种在不影响正确性的前提下更快地执行 gradcheck 的方法。当检测到错误时,可以通过添加特殊逻辑来恢复可调试性。在这种情况下,我们可以运行重建完整矩阵的默认版本,以便向用户提供完整详细信息。
这里的高级策略是找到一个标量数量,该数量可以通过数值方法和分析方法高效计算,并且能够很好地表示由慢速 gradcheck 计算的完整矩阵,从而确保它能够捕获雅可比矩阵中的任何差异。
实数到实数函数的快速 gradcheck#
我们想在这里计算的标量数量是 ,给定一个随机向量 和一个随机单位范数向量 。
对于数值评估,我们可以高效地计算
然后,我们执行该向量与 的点积,得到感兴趣的标量值。
对于分析版本,我们可以使用反向模式 AD 直接计算 。然后,我们执行与 的点积,得到期望值。
复数到实数函数的快速 gradcheck#
类似于实数到实数的情况,我们希望对完整矩阵进行归约。但是 矩阵是复数值的,因此在这种情况下,我们将与复数标量进行比较。
由于数值情况下可以高效计算的某些限制,并为了将数值评估的数量保持在最低限度,我们计算以下(尽管令人惊讶的)标量值
其中 、 和 。
快速复数输入数值评估#
我们首先考虑如何使用数值方法计算 。为此,考虑到我们正在考虑 和 ,并且 ,我们将其重写如下
在此公式中,我们可以看到 和 可以像实数到实数情况下的快速版本一样进行评估。一旦计算出这些实数值,我们就可以重建右侧的复数向量,并与实数值 向量进行点积。
快速复数输入分析评估#
对于分析情况,事情更简单,我们将公式重写为
因此,我们可以利用反向模式 AD 为我们提供一种有效计算 的方法,然后将实部与 进行点积,将虚部与 进行点积,最后重建最终的复数标量 。
为什么不使用复数 #
此时,你可能会想,为什么我们不选择复数 ,而只是执行归约 。为了深入探讨这一点,在本段中,我们将使用 的复数版本,记为 。使用这样的复数 ,问题在于进行数值评估时,我们需要计算
这将需要对实数到实数的有限差分进行四次评估(是上述方法的两倍)。由于这种方法没有更多的自由度(相同数量的实值变量),并且我们在这里尝试获得最快的评估,因此我们使用上述另一种公式。
具有复杂输出的函数的快速梯度检查#
就像在慢速情况下一样,我们考虑两个实值函数,并对每个函数使用上述适当的规则。