给定一个模型作为一个编程例程,这样我们就可以计算对于任何, 我对关于. 重要的是可以是任何模型,例如机器学习模型、神经网络或来自商业软件的模型,并且该模型应该始终具有计算任何给定输入的输出的能力。
当然,更多地了解该模型将提供更多关于用于敏感性分析的方法的提示。但是以抽象的方式笼统地谈论,例如,一个人可能会扰乱一个观点到并计算. 也可以使用中心差分方案。
您是否知道执行此任务的任何其他方法?
给定一个模型作为一个编程例程,这样我们就可以计算对于任何, 我对关于. 重要的是可以是任何模型,例如机器学习模型、神经网络或来自商业软件的模型,并且该模型应该始终具有计算任何给定输入的输出的能力。
当然,更多地了解该模型将提供更多关于用于敏感性分析的方法的提示。但是以抽象的方式笼统地谈论,例如,一个人可能会扰乱一个观点到并计算. 也可以使用中心差分方案。
您是否知道执行此任务的任何其他方法?
使用自动微分计算一般程序的导数的行为被称为可微分规划。整个范围内有许多等级的可微分编程,其中一些仅限于可以静态表示的模型,到像Julia中的系统(并在Swift中尝试过),这些系统允许对用该语言编写的程序进行一般微分。
实现这一点的数学原理是将自动微分理解为雅可比向量或雅可比转置向量乘积的简单组合。MIT 18.337 科学机器学习课程笔记对此进行了详细介绍,特别是关于正向模式、反向模式和最终可微编程的部分。这个想法的关键是你想要计算在哪里是雅可比行列式和是一些向量,因为等价于方向导数。如果您的计算程序是两个函数调用,,然后简单地通过线性代数你有,因此您可以通过各个步骤计算整个程序的雅可比向量积。前向模式自动微分可以通过定义对偶数算术来完成(把它想象成一个二维数,就像一个复数),其中. 如果你在一组原语上定义算术,比如说,,等,然后您的程序由所述原语组成,这将区分整个通用程序。这是ForwardDiff.jl等系统的基础。例如,Julia 编程语言有一个库ChainRules.jl,它允许注册该语言中任何函数(Julia Base 或任何包中的函数)的原始派生定义,以便 AD 系统在其派生程序构造中。请参阅Lyndon White 关于为 ChainRules.jl 定义原语的视频作为很好的介绍。
虽然用于处理一般动态程序的反向模式自动微分要复杂得多,但可以做到。反向模式AD相当于计算, 或者,因此通过线性代数,您可以看到组合是相反的:这意味着您必须弄清楚如何采用程序的正向路径,然后在每个操作中拉出一个反向的向量。请注意,如果返回一个标量和是一,那么梯度,这是与“反向传播”的关系以及在机器学习中的使用。在反向模式 AD 中,原语是这些操作,那么任何由基元组成的程序都是可微的。存在一种纯粹解释形式的可微编程,称为基于跟踪的 AD,其中程序的单次运行构建该运行期间使用的操作的磁带,以及转发程序的静态描述然后是微分的。因为这需要为每个反向传递一个新代码,所以您通常无法编译该传递(除非已知运行时间比编译时间长得多),您会失去正向传递中的优化(因为您必须动态分配调用树),并且由于缺乏对转义和别名(例如,突变)的一般分析,许多构造不能得到很好的支持。由于这些原因,通常将性能集中在大型内核调用(如矩阵乘法)的机器学习框架使用这种技术,这里的一个著名软件是 PyTorch。这可以处理某些形式的动态,但对于较小的成本函数调用有很大的限制(例如,由标量运算定义的非线性函数,如微分方程定义中常见的),
更通用的自动微分形式可以通过在称为 SSA(单一静态分配)的降低编译器表示上的源代码转换来完成。Mike Innes 关于 SSA 级微分的文章是一个很好的介绍,Keno Fischer 的关于在 Diffractor 中使用类别理论的光学的演讲是在编译器中进行更深入的一个很好的例子,而Billy 和 Valentin 对 Enzyme 的讨论讨论了与编译器转换的可组合性. 从这些部分中,您可以注意到,要正确有效地执行此操作,您需要将转义分析、thunk 的死代码消除等编译器分析思想直接混合到 AD 代码生成机制中,这就是这样做的原因与编译器交互的样式。
SSA 形式的有趣之处在于,就像 Mike 的论文在其标题“不要展开伴随”中所暗示的那样,SSA 已经有一个 O(1) 表示的动态结构(如循环),它可以保持完整。例如,如果您考虑 while 循环while x < 1
,派生程序可以将其替换为相同的检查,该检查由向堆栈添加布尔值的动作进行。然后反向模式传递是一个 for 循环for i in 1:length(stack)
,然后反向运行 while 循环的内部,用它的向量转置雅可比积替换每个操作。您可以类似地分析通用编程结构来定义它们的原语。锁定线程的反向模式导数是什么?原来是锁定线程,因为当你反向运行“lock f(x) unlock”程序时,反向解锁”程序。
因此,使用 ChainRules.jl 等用于定义语言范围原语的混合,AD 系统直接作用于语言的低级中间表示(Zygote.jl、Diffractor.jl,甚至更低的 LLVM IR Enzyme.jl),你可以设计一个语言系统,使其在程序上完全可区分,而无需他们选择加入系统。当然,由于其设计限制,每个 AD 系统都有其注意事项(我在一篇关于粘合 AD 系统的博文和另一篇关于准静态图表示的限制的博文中更详细地讨论了这一点),所以它仍然是一个持续研究的主题让它如此字面上每个程序都可以调用derivative(f,x)
并期望它起作用。但这既不是不可能的,也是整个社区都在努力的事情。
最后让我提一下,在编译器传递过程中考虑数学转换除了为导数生成代码之外还有很多用途。它可以检测稀疏模式、使数值模型更稳定、减少浮点运算、为“侵入式”不确定性量化方法生成代码等等。这里的一般概念是出于数学目的对函数的抽象解释,这确实是将所有这些编译器技术联系在一起的关键原则。
[顺便说一句,因为我知道有人会问,为什么 Swift For TensorFlow很好地解释了为什么 Google 将 Swift 和 Julia 用于可微编程而不是 Python。尽管 IMO 的讨论有些间接。一个更直接的原因是,要使其工作,您需要处理一个程序表示,该程序表示 (a) 具有一个 SSA 形式的 IR,该 IR 可以进行推理和优化(因为这就是您将要从和到的代码生成) 和 (b) 支持的原语数量最少。因此,大多数库都是用该语言编写的,并且它的表示是静态的(Swift、Fortran、C++)或几乎是静态的,并且语言设计是为了确保编译器分析(Julia)。Python 对于许多事情来说都是一种奇妙的语言,因为它的活力允许许多自然结构,但是它的中间表示并不是让编译器完全分析的(例如,对象可以随时添加字段,因此您需要进行非本地分析才能知道在反向构造对象时要包含哪些字段)并且它通常使用对 C 库的大量调用,每个调用都需要 AD 原语才能获得支持(这会降低数值稳定性,但这是另一个话题)。这就是为什么像 PyTorch 这样的机器学习框架专注于语言的子集并提供了自己的 C 库(每个都需要 AD 原语来获得支持(这会降低数值稳定性,但这是另一个话题)。这就是为什么像 PyTorch 这样的机器学习框架专注于语言的子集并提供了自己的 C 库(每个都需要 AD 原语来获得支持(这会降低数值稳定性,但这是另一个话题)。这就是为什么像 PyTorch 这样的机器学习框架专注于语言的子集并提供了自己的 C 库(torch.numpy
不是numpy
) 以最小化所需的原始支持。]