从零开始实现可微编程(Differentiable Programming)
Max Slater
计算机图形学、编程和数学
从零开始实现可微编程(Differentiable Programming)
可微编程已成为一个热门的研究课题,这不仅是因为诸如 TensorFlow、PyTorch 和 JAX 等机器学习框架的普及。许多机器学习以外的领域也发现可微编程是解决优化问题的有用工具。在计算机图形学中,可微渲染、可微物理模拟 和 神经表示 都越来越受欢迎。 本文在 3Blue1Brown 的Summer of Math Exposition 2 中获得了荣誉提名!
预备知识
微分
一切都始于你在微积分课堂上学到的定义:
f′(x)=limh→0f(x+h)−f(x)h f^{\prime}(x) = \lim_{h\rightarrow 0} \frac{f(x + h) - f(x)}{h} f′(x)=h→0limhf(x+h)−f(x)
换句话说,导数计算当 xxx 受到无穷小扰动时,f(x)f(x)f(x) 变化了多少。如果 fff 是一个从 R↦R\mathbb{R} \mapsto \mathbb{R}R↦R 的一维函数,导数 f′(x)f^{\prime}(x)f′(x) 返回 fff 在 xxx 处的图的斜率。
然而,还有另一种视角可以在更高维度上提供更好的直觉。 如果我们将 fff 视为从其域中的点到其范围中的点的映射,我们可以将 f′(x)f^{\prime}(x)f′(x) 视为从基于 xxx 的_向量_到基于 f(x)f(x)f(x) 的_向量_ 的映射。
一对一
在一维中,这种区别有点微妙,因为一维“向量”只是一个数字。 尽管如此,计算 f′(x)f^{\prime}(x)f′(x) 向我们展示了放置在 xxx 处的向量在被 fff 变换时如何缩放。 这就是 fff 在 xxx 处的斜率。
多对一
如果我们考虑一个函数 g(x,y):R2↦Rg(x,y) : \mathbb{R}^2 \mapsto \mathbb{R}g(x,y):R2↦R (两个输入,一个输出),这种视角将变得更加清晰。 我们可以对 ggg 相对于任何特定输入求导,这称为偏导数:
gx(x,y)=limh→0g(x+h,y)−g(x,y)h g_x(x,y) = \lim_{h\rightarrow0} \frac{g(x+h,y) - g(x,y)}{h} gx(x,y)=h→0limhg(x+h,y)−g(x,y)
函数 gxg_xgx 计算给定 xxx 变化时 ggg 的变化。 通过将 gxg_xgx 与相应的 gyg_ygy 结合起来,我们生成 ggg 的 梯度:
∇g(x,y)=[gx(x,y)gy(x,y)] \nabla g(x,y) = \begin{bmatrix}g_x(x,y) & g_y(x,y)\end{bmatrix} ∇g(x,y)=[gx(x,y)gy(x,y)]
也就是说,∇g(x,y)\nabla g(x,y)∇g(x,y) 告诉我们如果我们改变 xxx 或 yyy,ggg 将如何变化。 如果我们将 ∇g(x,y)\nabla g(x,y)∇g(x,y) 与差分 Δx,Δy\Delta x,\Delta yΔx,Δy 的列向量相乘,我们将得到它们对 ggg 的综合影响:
∇g(x,y)[ΔxΔy]=Δxgx(x,y)+Δygy(x,y) \nabla g(x,y) \begin{bmatrix}\Delta x\\\Delta y\end{bmatrix} = \Delta xg_x(x,y) + \Delta yg_y(x,y) ∇g(x,y)[ΔxΔy]=Δxgx(x,y)+Δygy(x,y)
很容易将梯度视为另一个向量。 然而,通常将梯度视为 高阶函数 很有用:∇g\nabla g∇g 是一个函数,当在 x,yx,yx,y 处计算时,会给我们 另一个函数,该函数将基于 x,yx,yx,y 的 向量 转换为基于 g(x,y)g(x,y)g(x,y) 的_向量_。
碰巧的是,梯度返回的函数是线性的,因此可以表示为矩阵乘法。
梯度通常被解释为“最陡上升方向”。 为什么呢? 当我们在点 x,yx,yx,y 处计算梯度和一个向量 Δx,Δy\Delta x, \Delta yΔx,Δy 时,结果是 ggg 输出的变化。 如果我们 最大化 ggg 相对于输入向量的变化,我们将得到使输出增加最快的方向。
由于梯度函数只是与 [gx(x,y)gy(x,y)]\begin{bmatrix}g_x(x,y) & g_y(x,y)\end{bmatrix}[gx(x,y)gy(x,y)] 的乘积,因此很容易找到使结果最大化的方向 [ΔxΔy]\begin{bmatrix}\Delta x & \Delta y\end{bmatrix}[ΔxΔy]:它平行于 [gx(x,y)gy(x,y)]\begin{bmatrix}g_x(x,y) & g_y(x,y)\end{bmatrix}[gx(x,y)gy(x,y)]。 这意味着梯度向量实际上是最陡上升方向。
方向导数
另一个重要的术语是 方向导数,它计算函数沿任意方向的导数。 它是偏导数的推广,偏导数沿坐标轴计算方向导数。 Dvf(x) D_{\mathbf{v}}f(x) Dvf(x) 上面,我们发现我们的“梯度函数”可以表示为与梯度向量的点积。 实际上,这就是方向导数: Dvf(x)=∇f(x)⋅v D_{\mathbf{v}}f(x) = \nabla f(x) \cdot \mathbf{v} Dvf(x)=∇f(x)⋅v 这再次说明了为什么梯度向量是最陡上升方向:它是使方向导数最大化的 v\mathbf{v}v。 请注意,在弯曲空间中,梯度的“最陡上升”定义仍然成立,但方向导数变得比点积更复杂。
多对多
为了完整起见,让我们检查一下这种视角如何扩展到多变量的向量值函数。 考虑 h(x,y):R2↦R2h(x,y) : \mathbb{R}^2 \mapsto \mathbb{R}^2h(x,y):R2↦R2 (两个输入,两个输出)。
h(x,y)=[h1(x,y)h2(x,y)] h(x,y) = \begin{bmatrix}h_1(x,y)\\h_2(x,y)\end{bmatrix} h(x,y)=[h1(x,y)h2(x,y)]
我们可以获取 hhh 的每个部分的梯度:
∇h1(x,y)=[h1x(x,y)h1y(x,y)]∇h2(x,y)=[h2x(x,y)h2y(x,y)] \begin{align*} \nabla h_1(x,y) &= \begin{bmatrix}h_{1_x}(x,y)& h_{1_y}(x,y)\end{bmatrix} \\ \nabla h_2(x,y) &= \begin{bmatrix}h_{2_x}(x,y)& h_{2_y}(x,y)\end{bmatrix} \end{align*} ∇h1(x,y)∇h2(x,y)=[h1x(x,y)h1y(x,y)]=[h2x(x,y)h2y(x,y)]
什么对象可以表示 ∇h\nabla h∇h? 我们可以从每个分量的梯度构建一个矩阵,称为 雅可比矩阵(Jacobian):
∇h(x,y)=[h1x(x,y)h1y(x,y)h2x(x,y)h2y(x,y)] \nabla h(x,y) = \begin{bmatrix}h_{1_x}(x,y) & h_{1_y}(x,y)\\ h_{2_x}(x,y) & h_{2_y}(x,y)\end{bmatrix} ∇h(x,y)=[h1x(x,y)h2x(x,y)h1y(x,y)h2y(x,y)]
上面,我们认为梯度(当在 x,yx,yx,y 处计算时)会给我们从输入 向量 到输出 向量 的映射。 这里仍然如此:雅可比矩阵是一个 2x2 矩阵,因此它将 2D Δx,Δy\Delta x,\Delta yΔx,Δy 向量转换为 2D Δh1,Δh2\Delta h_1, \Delta h_2Δh1,Δh2 向量。
添加更多维度开始使我们的函数难以可视化,但我们始终可以依靠导数来告诉我们输入向量(即输入的变化)如何映射到输出向量(即输出的变化)。
链式法则
我们需要理解的微分的最后一个方面是如何对函数组合进行微分。
h(x)=g(f(x)) ⟹ h′(x)=g′(f(x))⋅f′(x) h(x) = g(f(x)) \implies h^\prime(x) = g^\prime(f(x))\cdot f^\prime(x) h(x)=g(f(x))⟹h′(x)=g′(f(x))⋅f′(x)
我们可以通过一些分析来证明这个事实,但是通过将导数视为高阶函数,这种关系更容易理解。 在这种视角下,链式法则本身只是一个函数组合。
例如,让我们假设 h(x):R2↦Rh(\mathbf{x}) : \mathbb{R}^2 \mapsto \mathbb{R}h(x):R2↦R 由 f(x):R2↦R2f(\mathbf{x}) : \mathbb{R}^2 \mapsto \mathbb{R}^2f(x):R2↦R2 和 g(x):R2↦Rg(\mathbf{x}) : \mathbb{R}^2 \mapsto \mathbb{R}g(x):R2↦R 组成。
为了将 Δx\Delta \mathbf{x}Δx 向量转换为 Δh\Delta hΔh 向量,我们可以首先使用 ∇f\nabla f∇f 将 Δx\Delta \mathbf{x}Δx 映射到基于 f(x)f(\mathbf{x})f(x) 的 Δf\Delta \mathbf{f}Δf。 然后,我们可以使用 ∇g\nabla g∇g 将 Δf\Delta \mathbf{f}Δf 映射到基于 g(f(x))g(f(\mathbf{x}))g(f(x)) 的 Δg\Delta gΔg。
因为我们的导数/梯度/雅可比矩阵是线性函数,所以我们分别将它们表示为标量/向量/矩阵。 这意味着我们可以使用典型的线性代数乘法规则轻松地组合它们。 象征性地写出上面的例子:
∇h(x)=∇g(f(x))⋅∇f(x)=[gx1(f(x))gx2(f(x))][f1x1(x)f1x2(x)f2x1(x)f2x2(x)]=[gx1(f(x))f1x1(x)+gx2(f(x))f2x1(x)gx1(f(x))f1x2(x)+gx2(f(x))f2x2(x)] \begin{align*} \nabla h(\mathbf{x}) &= \nabla g(f(\mathbf{x}))\cdot \nabla f(\mathbf{x}) \\ &= \begin{bmatrix}g_{x_1}(f(\mathbf{x}))& g_{x_2}(f(\mathbf{x}))\end{bmatrix} \begin{bmatrix}f_{1_{x_1}}(\mathbf{x}) & f_{1_{x_2}}(\mathbf{x})\\ f_{2_{x_1}}(\mathbf{x}) & f_{2_{x_2}}(\mathbf{x})\end{bmatrix} \\ &= \begin{bmatrix}g_{x_1}(f(\mathbf{x}))f_{1_{x_1}}(\mathbf{x}) + g_{x_2}(f(\mathbf{x}))f_{2_{x_1}}(\mathbf{x}) & g_{x_1}(f(\mathbf{x}))f_{1_{x_2}}(\mathbf{x}) + g_{x_2}(f(\mathbf{x}))f_{2_{x_2}}(\mathbf{x})\end{bmatrix} \end{align*} ∇h(x)=∇g(f(x))⋅∇f(x)=[gx1(f(x))gx2(f(x))][f1x1(x)f2x1(x)f1x2(x)f2x2(x)]=[gx1(f(x))f1x1(x)+gx2(f(x))f2x1(x)gx1(f(x))f1x2(x)+gx2(f(x))f2x2(x)]
结果是一个 2D 向量,表示将 2D 向量转换为 1D 向量的梯度。 组合函数 hhh 有两个输入和一个输出,这是正确的。 我们还可以注意到,每个项都对应于应用于从 xxx 的分量到 hhh 的不同计算路径的链式法则。
优化
我们将重点介绍微分通过梯度下降在优化中的应用,这通常用于机器学习和计算机图形学。 优化问题始终涉及计算以下表达式:
arg minxf(x) \underset{\mathbf{x}}{\arg\!\min} f(\mathbf{x}) xargminf(x)
这仅仅意味着“找到导致 fff 的可能最小值对应的 x\mathbf{x}x”。 函数 fff(通常是标量值)传统上被称为“能量”,或者在机器学习中称为“损失函数”。 通常会强制执行额外的约束来限制 x\mathbf{x}x 的有效选项,但我们现在将忽略约束优化。
解决优化问题的一种方法是迭代地沿着 fff 的梯度“向下”走。 这种算法称为梯度下降:
- 选择一个初始猜测 xˉ\mathbf{\bar{x}}xˉ。
- 重复:
- 计算梯度 ∇f(xˉ)\nabla f(\mathbf{\bar{x}})∇f(xˉ)。
- 沿着梯度方向移动:xˉ←xˉ−τ∇f(xˉ)\mathbf{\bar{x}} \leftarrow \mathbf{\bar{x}} - \tau\nabla f(\mathbf{\bar{x}})xˉ←xˉ−τ∇f(xˉ)。
- while ∣∇f(xˉ)∣>ϵ|\nabla f(\mathbf{\bar{x}})| > \epsilon∣∇f(xˉ)∣>ϵ。
给定一些起点 x\mathbf{x}x,计算 ∇f(x)\nabla f(\mathbf{x})∇f(x) 将给出从 x\mathbf{x}x 开始可以最快地增加 fff 的方向。 因此,如果我们将我们的点 x\mathbf{x}x 沿着负梯度移动一小段距离 τ\tauτ,我们将减小 fff 的值。 数字 τ\tauτ 被称为步长(或在 ML 中,称为学习率)。 通过迭代这个过程,我们最终会找到一个 x\mathbf{x}x 使得 ∇f(x)≃0\nabla f(\mathbf{x}) \simeq 0∇f(x)≃0,这有望成为最小值。
这种对梯度下降的描述使优化听起来很容易,但实际上有很多可能出错的地方。 当梯度下降终止时,结果只需要是 fff 的临界点,即 fff 变得平坦的地方。 这意味着我们可能会停留在最大值(不太可能)、鞍点(可能)或 局部 最小值(可能)。 在局部最小值处,沿任何方向移动 x\mathbf{x}x 都会增加 f(x)f(\mathbf{x})f(x) 的值,但 f(x)f(\mathbf{x})f(x) 不一定 是 fff 可以在全局范围内取得的最小值。
|
|
---|---|---
最大值| 鞍点| 局部最小值
如果 τ\tauτ 太大,梯度下降也可能会发散(即永远不会终止)。 因为梯度只是 fff 的线性近似,如果我们沿着它走得太远,我们可能会跳过 fff 行为的变化——甚至最终 增加 fff 和 ∇f\nabla f∇f。 另一方面,我们使 τ\tauτ 越小,我们的算法收敛所需的时间就越长。 请注意,我们假设 fff 有一个下界,并且它首先在有限的 x\mathbf{x}x 处达到它。
|
---|---
发散| 收敛缓慢
此处介绍的算法是梯度下降的最基本形式:许多研究都致力于设计更有可能收敛到合理结果的损失函数和下降算法。 添加约束、损失函数项和更新规则的做法称为 正则化。 事实上,优化本身就是一个完整的领域:如果您想了解更多信息,有很多文献可供参考,尤其是在机器学习中。 这篇交互式文章 解释了 动量 是一个很好的例子。
代码微分
现在我们了解了微分,让我们继续编程。 到目前为止,我们只考虑了数学函数,但我们可以轻松地将我们的视角转换为程序。 为简单起见,我们将仅考虑 纯 函数,即输出仅取决于其参数的函数(没有状态)。 如果您的程序实现了相对简单的数学表达式,那么手动编写另一个计算其导数的函数并不太困难。 但是,如果您的程序是一个深度神经网络或物理模拟呢? 手动区分这样的东西是不可行的,因此我们必须求助于自动区分的算法。 有几种用于区分程序的技术。 我们将首先研究数值微分和符号微分,这两种方法都存在于计算机存在的时间一样长。 但是,这些方法与我们现在所知的 自动微分 算法不同,我们将在稍后讨论。
数值微分
数值微分是最直接的技术:它只是近似导数的定义。 f′(x)=limh→0f(x+h)−f(x)h≃f(x+0.001)−f(x)0.001 f^\prime(x) = \lim_{h\rightarrow 0} \frac{f(x+h)-f(x)}{h} \simeq \frac{f(x+0.001)-f(x)}{0.001} f′(x)=h→0limhf(x+h)−f(x)≃0.001f(x+0.001)−f(x) 通过选择一个小的 hhh,我们所要做的就是在 xxx 和 x+hx+hx+h 处计算 fff。 这种技术也称为通过 有限差分 进行微分。 将数值微分实现为高阶函数非常容易。 它甚至不需要修改要区分的函数:
function numerical_diff(f, h) {
return function (x) {
return (f(x + h) - f(x)) / h;
}
}
let df = numerical_diff(f, 0.001);
您可以编辑以下 JavaScript 示例,其中 fff 以蓝色绘制,numerical_diff(fff, 0.001) 以紫色绘制。 请注意,使用控制流不是问题。 function f(x) { // Edit me! if(x < Math.PI / 2) return x; return x * Math.sin(x); }Differentiate
不幸的是,有限差分有一个很大的问题:它们只能在一个方向上计算 fff 的导数。 如果我们的输入是高维的,那么计算 fff 的完整梯度在计算上变得不可行,因为我们必须单独计算每个维度的 fff。 也就是说,如果您只需要计算 fff 的一个方向导数,那么完整的梯度是多余的:而是计算 f(x)f(\mathbf{x})f(x) 和 f(x+Δx)f(\mathbf{x} + \Delta\mathbf{x})f(x+Δx) 之间的有限差分,其中 Δx\Delta\mathbf{x}Δx 是您感兴趣的方向上的一个小步骤。 最后,请始终记住,数值微分只是一个近似值:我们没有计算 h→0h \rightarrow 0h→0 时的实际极限。 虽然有限差分很容易实现并且对于验证其他结果非常有用,但该技术通常应该被另一种方法所取代。
符号微分
符号微分涉及将 fff 的表示转换为 f′f^\primef′ 的表示。 与数值微分不同,这需要以特定于域的语言指定 fff,其中每个语法构造都有已知的微分规则。 但是,这种限制并不是那么糟糕——我们可以创建一个编译器,为我们区分符号语言中的表达式。 这是在诸如 Mathematica 之类的计算机代数包中使用的技术。 例如,我们可以创建一个简单的多项式语言,该语言可以使用一小组递归规则进行符号微分。
d(n) -> 0
d(x) -> 1
d(Add(a, b)) -> Add(d(a), d(b))
d(Times(a, b)) -> Add(Times(d(a), b), Times(a, d(b)))
试试以下实现: function f(x) { // Edit me! return Add(Times(x, x), Add(x, x)); }Differentiate| ``` function df(x) { return Add(Add(Times(x, 1), Times(1, x)), Add(1, 1)); }
---|---
如果我们希望我们的可微语言支持更多操作,我们可以简单地添加更多微分规则。 例如,为了支持三角函数:
d(sin a) -> Times(d(a), cos a) d(cos a) -> Times(d(a), Times(-1, sin a))
不幸的是,有一个问题:f′f^\primef′ 的表示的大小可能会变得非常大。 让我们写另一个递归关系,计算表达式中项的数量:
Terms(n) -> 1 Terms(x) -> 1 Terms(Add(a, b)) -> Terms(a) + Terms(b) + 1 Terms(Times(a, b)) -> Terms(a) + Terms(b) + 1
然后证明 `Terms(a) <= Terms(d(a))`,即微分一个表达式不会减少项的数量:
Base Cases: Terms(d(n)) -> 1 | Definition Terms(n) -> 1 | Definition => Terms(n) <= Terms(d(n)) Terms(d(x)) -> 1 | Definition Terms(x) -> 1 | Definition => Terms(x) <= Terms(d(x)) Inductive Case for Add: Terms(Add(a, b)) -> Terms(a) + Terms(b) + 1 | Definition Terms(d(Add(a, b))) -> Terms(d(a)) + Terms(d(b)) + 1 | Definition Terms(a) <= Terms(d(a)) | Hypothesis Terms(b) <= Terms(d(b)) | Hypothesis => Terms(Add(a, b)) <= Terms(d(Add(a, b))) Inductive Case for Times: Terms(Times(a, b)) -> Terms(a) + Terms(b) + 1 | Definition Terms(d(Times(a, b))) -> Terms(a) + Terms(b) + 3 + Terms(d(a)) + Terms(d(b)) | Definition => Terms(Times(a, b)) <= Terms(d(Times(a, b)))
如果 `df` 的大小与 `f` 的大小成线性关系,那么这个结果可能是可以接受的,但事实并非如此。 每当我们区分一个 `Times` 表达式时,结果中的项数至少会 _加倍_。 这意味着 `df` 的大小随着我们组合的 `Times` 的数量呈 _指数级_ 增长。 您可以通过在 JavaScript 示例中嵌套多个 `Times` 来演示这种现象。
因此,符号微分通常在我们感兴趣的规模上是不可行的。 但是,如果它适用于您的用例,它可能会非常有用。
# 自动微分(Automatic Differentiation)
我们终于准备好讨论现代可微编程中实际使用的自动微分算法:自动微分! 自动微分有两种风格,每种风格都以计算导数的方向命名。
## 前向模式(Forward Mode)
前向模式自动微分通过计算精确导数而不构建可能呈指数级大的 f′f^\primef′ 表示来改进我们的两种较旧技术。 它基于 _对偶数_ 的数学定义。
### 对偶数
对偶数有点像复数:它们是通过将新量 ϵ\epsilonϵ [附加](https://thenumb.at/Autodiff/<https:/en.wikipedia.org/wiki/Field_extension>) 到实数来定义的。 但是与 i2=−1i^2 = -1i2=−1 的复数不同,对偶数使用 ϵ2=0\epsilon^2 = 0ϵ2=0。
特别是,我们可以使用对偶数的 ϵ\epsilonϵ 部分来表示标量部分的导数。 如果我们将每个变量 xxx 替换为 x+x′ϵx + x^\prime\epsilonx+x′ϵ,我们将发现对偶算术自然地表达了导数是如何组合的:
加法:
(x+x′ϵ)+(y+y′ϵ)=(x+y)+(x′+y′)ϵ (x + x^\prime\epsilon) + (y + y^\prime\epsilon) = (x + y) + (x^\prime + y^\prime)\epsilon (x+x′ϵ)+(y+y′ϵ)=(x+y)+(x′+y′)ϵ
乘法:
(x+x′ϵ)∗(y+y′ϵ)=xy+xy′ϵ+x′yϵ+x′y′ϵ2=xy+(x′y+xy′)ϵ \begin{align*} (x + x^\prime\epsilon) * (y + y^\prime\epsilon) &= xy + xy^\prime\epsilon + x^\prime y\epsilon + x^\prime y^\prime\epsilon^2 \\\ &= xy + (x^\prime y + xy^\prime)\epsilon \end{align*} (x+x′ϵ)∗(y+y′ϵ)=xy+xy′ϵ+x′yϵ+x′y′ϵ2=xy+(x′y+xy′)ϵ
除法:
x+x′ϵy+y′ϵ=xy+x′yϵ1+y′yϵ=(xy+x′yϵ)(1−y′yϵ)=xy+x′y−xy′y2ϵ \begin{align*} \frac{x + x^\prime\epsilon}{y + y^\prime\epsilon} &= \frac{\frac{x}{y}+\frac{x^\prime}{y}\epsilon}{1+\frac{y^\prime}{y}\epsilon} \\\ &= \left(\frac{x}{y}+\frac{x^\prime}{y}\epsilon\right)\left(1-\frac{y^\prime}{y}\epsilon\right) \\\ &= \frac{x}{y} + \frac{x^\prime y - xy^\prime}{y^2}\epsilon \end{align*} y+y′ϵx+x′ϵ=1+yy′ϵyx+yx′ϵ=(yx+yx′ϵ)(1−yy′ϵ)=yx+y2x′y−xy′ϵ
链式法则也适用:f(x+x′ϵ)=f(x)+f′(x)x′ϵf(x + x^\prime\epsilon) = f(x) + f^\prime(x)x^\prime\epsilonf(x+x′ϵ)=f(x)+f′(x)x′ϵ 对于任何光滑函数 fff。 为了证明这个事实,让我们首先证明该属性对于正整数求幂成立。
基本情况:(x+x′ϵ)1=x1+1x0x′ϵ(x+x^\prime\epsilon)^1 = x^1 + 1x^0x^\prime\epsilon(x+x′ϵ)1=x1+1x0x′ϵ
假设:(x+x′ϵ)n=xn+nxn−1x′ϵ(x+x^\prime\epsilon)^n = x^n + nx^{n-1}x^\prime\epsilon(x+x′ϵ)n=xn+nxn−1x′ϵ
归纳:
(x+x′ϵ)n+1=(xn+nxn−1x′ϵ)(x+x′ϵ)=xn+1+xnx′ϵ+nxnx′ϵ+nxn−1x′2ϵ2=xn+1+(n+1)xnx′ϵ \begin{align*} (x+x^\prime\epsilon)^{n+1} &= (x^n + nx^{n-1}x^\prime\epsilon)(x+x^\prime\epsilon) \tag{Hypothesis}\\\ &= x^{n+1} + x^nx^\prime\epsilon + nx^nx^\prime\epsilon + nx^{n-1}x^{\prime^2}\epsilon^2\\\ &= x^{n+1} + (n+1)x^nx^\prime\epsilon \end{align*} (x+x′ϵ)n+1=(xn+nxn−1x′ϵ)(x+x′ϵ)=xn+1+xnx′ϵ+nxnx′ϵ+nxn−1x′2ϵ2=xn+1+(n+1)xnx′ϵ(假设)
(x+x′ϵ)n+1=(xn+nxn−1x′ϵ)(x+x′ϵ)=xn+1+xnx′ϵ+nxnx′ϵ+nxn−1x′2ϵ2=xn+1+(n+1)xnx′ϵ \begin{align*} (x+x^\prime\epsilon)^{n+1} &= (x^n + nx^{n-1}x^\prime\epsilon)(x+x^\prime\epsilon) \\\&\tag{Hypothesis}\\\ &= x^{n+1} + x^nx^\prime\epsilon + nx^nx^\prime\epsilon + nx^{n-1}x^{\prime^2}\epsilon^2\\\ &= x^{n+1} + (n+1)x^nx^\prime\epsilon \end{align*} (x+x′ϵ)n+1=(xn+nxn−1x′ϵ)(x+x′ϵ)=xn+1+xnx′ϵ+nxnx′ϵ+nxn−1x′2ϵ2=xn+1+(n+1)xnx′ϵ(假设)
我们可以使用这个结果来证明任何光滑函数 fff 的相同属性。 检查 fff 在零处的泰勒展开式(也称为其 _Maclaurin 级数_):
f(x)=∑n=0∞f(n)(0)xnn!=f(0)+f′(0)x+f′′(0)x22!+f′′′(0)x33!+… f(x) = \sum_{n=0}^\infty \frac{f^{(n)}(0)x^n}{n!} = f(0) + f^\prime(0)x + \frac{f^{\prime\prime}(0)x^2}{2!} + \frac{f^{\prime\prime\prime}(0)x^3}{3!} + \dots f(x)=n=0∑∞n!f(