forward 设计
本页解释标量驱动函数 diff 和 value_and_diff 计算什么、代价如何,以及为什么它们是作用于 Dual[T] -> Dual[T] 的普通函数。
设计目标
为最常见的情形——单变量函数的导数——提供一行代码即可调用的 API,隐藏变量的种子设定,同时把全部语义保留在 Dual[T] 中,使驱动函数不可能与该类型不一致。
数学背景
前向模式
程序通过中间量 计算 ,每个中间量都是对先前中间量的一次运算。前向模式在每个 旁边携带其导数 ,并按与程序相同的顺序用链式法则更新它:
对偶数算术执行的恰好就是这种更新: 就是 Dual::new(v_k, dv_k),dual 设计 中的规则就是各个 的偏导数。种子 就是 Dual::variable(x)。
通过嵌套得到高阶导数
如果 f 对其标量类型是泛型的,它就可以作用于 Dual[Dual[T]]。用 Dual[T] 上的对偶数对 求导,得到
因为 diff(f, ·) 本身就是一个由(内层的)对偶运算构建的程序。外层和内层的切向分量属于不同的类型,因此无类型嵌套前向模式中经典的扰动混淆(内层导数吸收了外层的扰动)会被类型检查器拒绝。11 J. M. Siskind 与 B. A. Pearlmutter 的 “Perturbation confusion and referential transparency”(IFL 2005)描述了无类型实现中的这一问题。 每嵌套一层,分量数就翻倍,因此 阶导数的代价是普通求值的 倍。
设计决策
函数,而非 trait 或包装类型
问题。 用户应如何把函数交给驱动函数?
选项。 由用户类型实现的 trait;一个作用于 Double 的函数外加单独的导数;一个作用于 Dual[T] 的闭包。
选择。 闭包 (Dual[T]) -> Dual[T]。MoonBit 的闭包开销很小,该类型明确表明函数必须针对对偶数编写,而泛型函数无需包装即可在 Dual[T] 处实例化。
最小约束
value_and_diff 只需要 T : One,用于种子切向分量。其他一切约束都由 f 内部的运算提出,由编译器在调用处检查。
值来自同一次求值
值与导数读自同一次对偶求值。根据投影同态,它与 f 在 T 上计算出的值逐位相同,因此无需第二次求值。
放在独立的包中
驱动函数位于 forward 中,使 dual 包保持为纯粹的数值类型,再由根包重新导出它们。forward 只依赖 dual 和 luna-generic。
正确性与不变量
value_and_diff(f, x) == (f(variable(x)).value(), f(variable(x)).tangent()),而diff(f, x)是它的第二个分量。- 对于由环运算、除数非零的除法以及在其定义域内的初等函数构成的程序,结果就是 ,其舍入误差界见 dual 设计。
- 代价是在
Dual[T]上对f求值一次,比在T上求值一次多出一个较小的常数因子。
被否决的方案
- 在驱动函数内部做数值微分。 出于 dual 设计 中推导的精度原因而被否决。
- 用于 阶导数的
diff_n。 对泛型函数而言嵌套已经能给出它,而截断泰勒类型才是高效的方案;两者都不构成目前再增加一个 API 的理由。
边界
- 仅支持标量输入。多个输入可由 linalg 驱动函数处理,或手动用
Dual::new设定种子。 - 没有带检查的变体:由
f决定是否使用带检查的运算,带检查的f会返回它自己的Result。 - 没有高阶驱动函数,也没有反向模式。
Footnotes
-
J. M. Siskind 与 B. A. Pearlmutter 的 “Perturbation confusion and referential transparency”(IFL 2005)描述了无类型实现中的这一问题。 ↩