Gradcheck 机制¶
本笔记概述了 gradcheck()
和 gradgradcheck()
函数的工作原理。
它将涵盖实值和复值函数的前向和后向模式 AD,以及高阶导数。本笔记还涵盖了 gradcheck 的默认行为以及传递 fast_mode=True
参数的情况(以下称为快速 gradcheck)。
符号和背景信息¶
在本笔记中,我们将使用以下约定
, , , , , , 和 是实值向量,而 是复值向量,可以用两个实值向量重写为 。
和 是两个整数,我们将分别用于输入和输出空间的维度。
是我们的基本实数到实数函数,使得 。
是我们的基本复数到实数函数,使得 。
对于简单的实数到实数情况,我们用 表示与 关联的 Jacobian 矩阵,大小为 。此矩阵包含所有偏导数,使得位置 的条目包含 。然后,后向模式 AD 计算对于给定的大小为 的向量 ,数量 。另一方面,前向模式 AD 计算对于给定的大小为 的向量 ,数量 。
对于包含复数值的函数,情况要复杂得多。我们在此仅提供要点,完整描述可在 复数自动求导 中找到。
对于所有实值损失函数,满足复数可微性(柯西-黎曼方程)的约束过于严格,因此我们选择使用 Wirtinger 演算。在 Wirtinger 演算的基本设置中,链式法则需要访问 Wirtinger 导数(以下称为 )和共轭 Wirtinger 导数(以下称为 )。 和 都需要传播,因为通常情况下,尽管它们的名字如此,但其中一个并不是另一个的复共轭。
为了避免必须传播这两个值,对于后向模式 AD,我们始终假设正在计算其导数的函数是实值函数,或者是较大的实值函数的一部分。此假设意味着我们在后向传播期间计算的所有中间梯度也与实值函数相关联。实际上,当进行优化时,此假设并不具有限制性,因为此类问题需要实值目标(因为复数没有自然排序)。
在此假设下,使用 和 定义,我们可以证明 (我们在此使用 表示复共轭),因此实际上只需要“向后传递”两个值中的一个,因为另一个值可以很容易地恢复。为了简化内部计算,PyTorch 使用 作为它向后传递的值,并在用户请求梯度时返回。与实数情况类似,当输出实际上在 中时,后向模式 AD 不计算 ,而仅计算给定向量 的 。
对于前向模式 AD,我们使用类似的逻辑,在这种情况下,假设该函数是更大函数的一部分,其输入位于 中。在此假设下,我们可以做出类似的声明,即每个中间结果都对应于一个函数,其输入位于 中,在这种情况下,使用 和 定义,我们可以证明对于中间函数,。为了确保前向和反向模式在单维函数的基本情况下计算相同的量,前向模式还计算 。与实数情况类似,当输入实际上在 中时,前向模式 AD 不计算 ,而只计算 ,对于给定的向量 。
默认反向模式梯度检查行为¶
实数到实数函数¶
为了测试函数 ,我们以两种方式重建完整的雅可比矩阵 (大小为 ):解析方式和数值方式。解析版本使用我们的反向模式 AD,而数值版本使用有限差分。然后逐元素比较两个重建的雅可比矩阵是否相等。
默认实数输入数值评估¶
如果我们考虑一维函数的简单情况 (),那么我们可以使用来自 维基百科文章 的基本有限差分公式。我们使用“中心差分”以获得更好的数值性质
这个公式很容易推广到多个输出 () 的情况,通过使 成为大小为 的列向量,例如 。在这种情况下,上述公式可以按原样重用,并且仅需两次评估用户函数(即 和 )即可近似完整的雅可比矩阵。
处理具有多个输入 () 的情况计算量更大。在这种情况下,我们逐个循环遍历所有输入,并将 扰动应用于 的每个元素。这使我们能够逐列重建 矩阵。
默认实数输入解析评估¶
对于解析评估,我们使用上述事实,即反向模式 AD 计算 。对于具有单个输出的函数,我们只需使用 即可通过单次反向传递恢复完整的雅可比矩阵。
对于具有多个输出的函数,我们采用 for 循环,遍历每个输出,其中每个 是一个 one-hot 向量,对应于每个输出。这允许逐行重建 矩阵。
复数到实数函数¶
为了测试函数 ,其中 ,我们重建包含 的(复数值)矩阵。
默认复数输入数值评估¶
首先考虑 的简单情况。我们从 这篇研究论文(第 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 已经精确计算出两倍的 导数,因此当有多个实数输出时,我们只需在此处使用与实数到实数情况相同的技巧,逐行重建矩阵。
快速反向模式梯度检查¶
虽然上述梯度检查的公式很好,既可以确保正确性,又可以确保可调试性,但它速度非常慢,因为它重建了完整的雅可比矩阵。本节介绍了一种更快地执行梯度检查的方法,而不会影响其正确性。可以通过在检测到错误时添加特殊逻辑来恢复可调试性。在这种情况下,我们可以运行默认版本,该版本重建完整矩阵,以向用户提供完整的细节信息。
这里的高层次策略是找到一个标量量,该标量量可以通过数值方法和解析方法有效地计算,并且能够充分代表慢速梯度检查计算的完整矩阵,以确保它可以捕获雅可比矩阵中的任何差异。
实数到实数函数的快速梯度检查¶
我们想要在这里计算的标量量是 ,对于给定的随机向量 和随机单位范数向量 。
对于数值评估,我们可以有效地计算
然后,我们执行这个向量和 之间的点积,以获得感兴趣的标量值。
对于解析版本,我们可以使用反向模式自动微分来直接计算 。然后,我们执行与 的点积,以得到期望的值。
复数到实数函数的快速梯度检查¶
与实数到实数的情况类似,我们希望对完整矩阵进行降维。但是 矩阵是复数值的,因此在这种情况下,我们将与复数标量进行比较。
由于在数值情况下有效计算的某些约束,并且为了尽量减少数值评估的次数,我们计算以下(尽管令人惊讶的)标量值
其中 , 和 。
快速复数输入数值评估¶
我们首先考虑如何使用数值方法计算 。为此,请记住我们正在考虑 ,其中 ,并且 ,我们将其重写如下
在这个公式中,我们可以看到 和 可以使用与实数到实数情况的快速版本相同的方式进行评估。一旦这些实数值的量被计算出来,我们可以重建右侧的复向量,并与实数值的 向量进行点积运算。
快速复数输入解析评估¶
对于解析情况,事情会更简单,我们将公式重写为
因此,我们可以利用反向模式 AD 为我们提供了一种有效计算 的方法,然后将实部与 进行点积运算,虚部与 进行点积运算,最后重建最终的复数标量 。
为什么不使用复数 呢?¶
此时,您可能想知道为什么我们不选择复数 ,而只是执行降维计算 。 为了深入探讨这一点,在本段中,我们将使用复数版本的 ,记为 。 使用这样的复数 ,问题在于当进行数值评估时,我们需要计算
这将需要对实数到实数的有限差分进行四次评估(是上述方法的两倍)。由于这种方法没有更多的自由度(相同数量的实值变量),并且我们尝试在此处获得最快的评估速度,因此我们使用上述另一种公式。
快速 gradcheck 用于具有复数输出的函数¶
就像在慢速情况下一样,我们考虑两个实值函数,并对每个函数使用上述适当的规则。
Gradgradcheck 实现¶
PyTorch 还提供了一个实用程序来验证二阶梯度。这里的目标是确保反向传播的实现也是可微的,并且计算结果是正确的。
此功能通过考虑函数 并使用上面定义的 gradcheck 来检查此函数。请注意,在这种情况下 只是一个与 类型相同的随机向量。
快速版本的 gradgradcheck 是通过对同一函数 使用快速版本的 gradcheck 来实现的。