# 附录 D、自动微分
> 译者:[@rickllyxu](https://github.com/rickllyxu)
这个附录解释了 TensorFlow 的自动微分功能是如何工作的,以及它与其他解决方案的对比。
假定你定义了函数 ![f(x, y) = x^2y + y + 2](https://img.kancloud.cn/84/f5/84f51a1c782f49b88f5f584262ed7d40_166x20.gif),需要得到它的偏导数 ![\frac{\partial f}{\partial x}](https://img.kancloud.cn/05/c1/05c1c279cff28e047168dd0298019c9d_22x39.gif) 和 ![\frac{\partial f}{\partial y}](https://img.kancloud.cn/d6/07/d607095498c0f94be18359c08e55d5cf_22x42.gif),以用于梯度下降或者其他优化算法。你的可选方案有手动微分法,符号微分法,数值微分法,前向自动微分,和反向自动微分。TensorFlow 实现的反向自动微分法。我们来看看每种方案。
## 手动微分法
第一个方法是拿起一直笔和一张纸,使用你的代数知识去手动的求偏导数。对于已定义的函数,求它的偏导并不太困难。你需要使用如下 5 条规则:
- 常数的导数为 0。
- ![\lambda x](https://img.kancloud.cn/bd/56/bd56d5c72306dc7ea94034f125e6f7a9_20x12.gif) 的导数为 ![\lambda](https://img.kancloud.cn/99/d3/99d394e7d0b74248114405067e0ffd51_10x12.jpg),![\lambda](https://img.kancloud.cn/99/d3/99d394e7d0b74248114405067e0ffd51_10x12.jpg) 为常数。
- ![x^{\lambda}](https://img.kancloud.cn/a9/f7/a9f7350469db3692d5e1fe7e456dc73f_18x16.gif) 的导数是 ![\lambda x^{\lambda - 1}](https://img.kancloud.cn/e4/c3/e4c3387d4b2f9262ef1fa8a82e7014d8_44x16.gif)
- 函数的和的导数,等于函数的导数的和
- ![\lambda](https://img.kancloud.cn/99/d3/99d394e7d0b74248114405067e0ffd51_10x12.jpg) 乘以函数,再求导,等于 ![\lambda](https://img.kancloud.cn/99/d3/99d394e7d0b74248114405067e0ffd51_10x12.jpg) 乘以函数的导数
从上述这些规则,可得到公式 D-1。
![公式D-1](https://img.kancloud.cn/7f/d4/7fd4c1aa6cd2a83ebd8e89ffaeaf1949_538x217.png)
这个种方法应用于更复杂函数时将变得非常罗嗦,并且有可能出错。好消息是,像刚才我们做的求数学式子的偏导数可以被自动化,通过一个称为符号微分的过程。
## 符号微分
图 D-1 展示了符号微分是如何运行在相当简单的函数上的,![g(x,y) = 5 + xy](https://img.kancloud.cn/28/62/28622fbd30b4f7340053724088d3eb65_125x18.gif)。该函数的计算图如图的左边所示。通过符号微分,我们可得到图的右部分,它代表了 ![\frac{\partial g}{\partial x} = 0 + (0 \times x + y \times 1) = y](https://img.kancloud.cn/9c/ca/9cca26166b0faea7e200d6be62a6e18d_229x39.gif),相似地也可得到关于`y`的导数。
![D-1](https://img.kancloud.cn/9b/fb/9bfb691e57ff0f864cb36823a917b144_938x571.png)
概算法先获得叶子节点的偏导数。常数 5 返回常数 0,因为常数的导数总是 0。变量`x`返回常数 1,变量`y`返回常数 0,因为 ![\frac{\partial y}{\partial x} = 0](https://img.kancloud.cn/14/a0/14a007db0bb8de434f01267c385534c7_55x39.gif)(如果我们找关于`y`的偏导数,那它将反过来)。
现在我们移动到计算图的相乘节点处,代数告诉我们,`u`和`v`相乘后的导数为 ![\frac{\partial (u \times v)}{\partial x} = \frac{\partial v}{\partial x} \times u + \frac{\partial u}{\partial x} \times v ](https://img.kancloud.cn/b8/de/b8de96a430b50464a6f5297af3224ef9_226x40.gif)。因此我们可以构造有图中大的部分,代表`0 × x + y × 1`。
最后我们往上走到计算图的相加节点处,正如 5 条规则里提到的,和的导数等于导数的和。所以我们只需要创建一个相加节点,连接我们已经计算出来的部分。我们可以得到正确的偏导数,即:![\frac{\partial g}{\partial x} = 0 + (0 \times x + y \times 1) ](https://img.kancloud.cn/10/4f/104fc8550f0e58f8a23d70bcc39384e3_195x39.gif)。
然而,这个过程可简化。对该图应用一些微不足道的剪枝步骤,可以去掉所有不必要的操作,然后我们可以得到一个小得多的只有一个节点的偏导计算图:![\frac{\partial g}{\partial x} = y](https://img.kancloud.cn/64/fa/64fa1dab8099d82d3c4b1dc3cc7888d6_55x39.gif)。
在这个例子里,简化操作是相当简单的,但对更复杂的函数来说,符号微分会产生一个巨大的计算图,该图可能很难去简化,以导致次优的性能。更重要的是,符号微分不能处理由任意代码定义的函数,例如,如下已在第 9 章讨论过的函数:
```python
def my_func(a, b):
z = 0
for i in range(100):
z = a * np.cos(z + i) + z * np.sin(b - i)
return z
```
## 数值微分
从数值上说,最简单的方案是去计算导数的近似值。回忆`h(x)`在 ![x_0](https://img.kancloud.cn/46/08/46082f7d6471c3fabb832d8f94075758_16x11.gif) 的导数 ![h^{'}(x_0)](https://img.kancloud.cn/89/14/89145d6b263966722529e2afb50b19ed_45x21.gif),是该函数在该点处的斜率,或者更准确如公式 D-2 所示。
![E_D-2](https://img.kancloud.cn/6a/98/6a98f2f1de8e30e0c4cb2911d2bc4014_622x220.png)
因此如果我们想要计算 ![f(x,y)](https://img.kancloud.cn/56/b5/56b5f89bd3ffde69293aeb8cea7e8bfd_51x18.gif) 关于`x`,在 ![x=3, y=4](https://img.kancloud.cn/e2/aa/e2aaf7b7ea4190534d4ac26a8ad006bd_94x16.gif) 处的导数,我们可以简单计算 ![f(3+\epsilon, 4) - f(3, 4)](https://img.kancloud.cn/7b/dc/7bdce5700a0fa919cbca69b64ad10271_152x18.gif) 的值,将这个结果除以 ![\epsilon](https://img.kancloud.cn/f1/f4/f1f442a329c8d3df85dce68831d660fe_7x8.jpg),且 ![\epsilon](https://img.kancloud.cn/f1/f4/f1f442a329c8d3df85dce68831d660fe_7x8.jpg) 去很小的值。这个过程正是如下的代码所要干的。
```python
def f(x, y):
return x**2*y + y + 2
def derivative(f, x, y, x_eps, y_eps):
return (f(x + x_eps, y + y_eps) - f(x, y)) / (x_eps + y_eps)
df_dx = derivative(f, 3, 4, 0.00001, 0)
df_dy = derivative(f, 3, 4, 0, 0.00001)
```
不幸的是,偏导的结果并不准确(并且可能在求解复杂函数时更糟糕)。上述正确答案分别是 24 和 10 ,但我们得到的是:
```python
>>> print(df_dx)
24.000039999805264
>>> print(df_dy)
10.000000000331966
```
注意到为了计算两个偏导数, 我们不得不调用`f()`至少三次(在上述代码里我们调用了四次,但可以优化)。如果存在 1000 个参数,我们将会调用`f()`至少 1001 次。当处理大的神经网络时,这样的操作很没有效率。
然而,数值微分实现起来如此简单,以至于它是检查其他方法正确性的优秀工具。例如,如果它的结果与您手动计算的导数不同,那么你的导数可能包含错误。
## 前向自动微分
前向自动微分既不是数值微分,也不是符号微分,但在某些方面,它是他们的爱情结晶。它依赖对偶数。对偶数是奇怪但迷人的,是 ![a + b\epsilon](https://img.kancloud.cn/7f/1b/7f1bb30d051a46e6b04ec4069b44e7d5_47x15.gif) 形式的数,这里`a`和`b`是实数,![\epsilon](https://img.kancloud.cn/f1/f4/f1f442a329c8d3df85dce68831d660fe_7x8.jpg) 是无穷小的数,满足 ![\epsilon ^ 2 = 0](https://img.kancloud.cn/c6/cc/c6cc30cdb24ab2dac1e76588c402c1dc_48x16.gif),但 ![\epsilon \ne 0](https://img.kancloud.cn/8b/0e/8b0ee45e42addceeac0f1ab949ae7b66_40x18.gif)。你可以认为对偶数 ![42 + 24\epsilon](https://img.kancloud.cn/42/9e/429ebed428aab62a7ee7fdd3f60173af_65x14.gif) 类似于有着无穷个 0 的 42.0000⋯000024(但当然这是简化后的,仅仅给你对偶数什么的想法)。一个对偶数在内存中表示为一个浮点数对,例如,![42 + 24\epsilon](https://img.kancloud.cn/42/9e/429ebed428aab62a7ee7fdd3f60173af_65x14.gif) 表示为`(42.0, 24.0)`。
对偶数可相加、相乘、等等操作,正如公式 D-3 所示。
![E_D-3](https://img.kancloud.cn/6f/41/6f41581d268f5944b1f74b9d21843986_722x183.png)
最重要的,可证明`h(a + bϵ) = h(a) + b × h'(a)ϵ`,所以计算一次`h(a + ϵ)`就得到了两个值`h(a)`和`h'(a)`。图 D-2 展示了前向自动微分如何计算 ![f(x,y)=x^2y + y + 2](https://img.kancloud.cn/84/f5/84f51a1c782f49b88f5f584262ed7d40_166x20.gif) 关于`x`,在 ![x=3, y=4](https://img.kancloud.cn/e2/aa/e2aaf7b7ea4190534d4ac26a8ad006bd_94x16.gif) 处的导数。我们所要做的一切只是计算 ![f(3+\epsilon, 4)](https://img.kancloud.cn/91/83/9183f3a9fa7479c3cbcaceb1139bac79_79x18.gif);它将输出一个对偶数,其第一部分等于 ![f(3, 4)](https://img.kancloud.cn/d6/7a/d67af86988871251de3e79dbf5af200a_50x18.gif),第二部分等于 ![f^{'}(3, 4) = \frac{\partial f}{\partial x} (3,4)](https://img.kancloud.cn/d4/ea/d4eae45d90e3dce416dff2ad3f866bc7_143x39.gif)。
![D-2](https://img.kancloud.cn/33/a5/33a54efdb1579f40dabbc1c7769c26f0_760x638.png)
为了计算 ![\frac{\partial f}{\partial y} (3,4)](https://img.kancloud.cn/a8/c5/a8c507b925ddf6cc1fd878231cee58b4_62x42.gif) 我们不得不再遍历一遍计算图,但这次前馈的值为 ![x=3, y = 4 + \epsilon](https://img.kancloud.cn/4a/8d/4a8d3512abe31ea635ae3550ba5c617d_123x16.gif)。
所以前向自动微分比数值微分准确得多,但它遭受同样的缺陷:如果有 1000 个参数,那为了计算所有的偏导数,得历经计算图 1000 次。这正是反向自动微分耀眼的地方:计算所有的偏导数,它只需要遍历计算图 2 次。
## 反向自动微分
反向自动微分是 TensorFlow 采取的方案。它首先前馈遍历计算图(即,从输入到输出),计算出每个节点的值。然后进行第二次遍历,这次是反向遍历(即,从输出到输入),计算出所有的偏导数。图 D-3 展示了第二次遍历的过程。在第一次遍历过程中,所有节点值已被计算,输入是 ![x=3, y=4](https://img.kancloud.cn/e2/aa/e2aaf7b7ea4190534d4ac26a8ad006bd_94x16.gif)。你可以在每个节点底部右方看到这些值(例如,![x \times x = 9](https://img.kancloud.cn/82/64/82648b22027f31599b4e10c56b0082eb_76x12.gif))。节点已被标号,从 ![n_1](https://img.kancloud.cn/d6/ba/d6baf45f28b535f30a864415b4130c52_17x12.jpg) 到 ![n_7](https://img.kancloud.cn/80/3f/803fb7016ca56ce07782ba9ebface325_18x11.gif)。输出节点是 ![n_7: f(3, 4) = n_7 = 42](https://img.kancloud.cn/ab/09/ab095df113f3666ac41ab7377454f4a7_168x18.gif)。
![D-3](https://img.kancloud.cn/74/a0/74a020a33d0d4a52e731bfbfd2acadce_765x590.png)
这个计算关于每个连续节点的偏导数的思想逐渐地从上到下遍历图,直到到达变量节点。为实现这个,反向自动微分强烈依赖于链式法则,如公式 D-4 所示。
![E_D-4](https://img.kancloud.cn/65/9d/659d14db2f4ca7302c4a38f5f5874176_369x113.png)
由于 ![n_7](https://img.kancloud.cn/80/3f/803fb7016ca56ce07782ba9ebface325_18x11.gif) 是输出节点,即 ![f= n_7](https://img.kancloud.cn/f2/20/f220da9bd15e8b6d9e5d1d7b4ed738b6_53x16.gif),所以 ![\frac{\partial f}{\partial n_7} = 1](https://img.kancloud.cn/ce/78/ce781ce6a126062d276e5ad50d89a52f_62x41.gif)。
接着到了图的 ![n_5](https://img.kancloud.cn/64/88/64885a24bbaaf5ea9cb3a3a6246771c2_17x11.gif) 节点:当 ![n_5](https://img.kancloud.cn/64/88/64885a24bbaaf5ea9cb3a3a6246771c2_17x11.gif) 变化时,![f](https://img.kancloud.cn/18/8e/188ee644e8202aad30eac11166858841_10x16.gif) 会变化多少?答案是 ![\frac{\partial f}{\partial n_5} = \frac{\partial f}{\partial n_7} \times \frac{\partial n_7}{\partial n_5}](https://img.kancloud.cn/5a/19/5a194c124999f2d23231948cf07219d1_139x41.gif)。我们已经知道 ![\frac{\partial f}{\partial n_7} = 1](https://img.kancloud.cn/ce/78/ce781ce6a126062d276e5ad50d89a52f_62x41.gif),因此我们只需要知道 ![\frac{\partial n_7}{\partial n_5}](https://img.kancloud.cn/a7/ff/a7ff39d50d242ac765b8a5f62fdc3c17_29x41.gif) 就行。因为 ![n_7](https://img.kancloud.cn/80/3f/803fb7016ca56ce07782ba9ebface325_18x11.gif) 是 ![n_5 + n_6](https://img.kancloud.cn/b2/2e/b22e70fa0128f957ccac6b94a2e1b03b_57x14.gif) 的和,因此可得到 ![\frac{\partial n_7}{\partial n_5} = 1](https://img.kancloud.cn/42/3f/423f200d46c9a9a9002ecd2bc082e097_62x41.gif),因此 ![\frac{\partial f}{\partial n_5}=1 \times 1 = 1](https://img.kancloud.cn/fd/0f/fd0f3fa064aaf3eabd0aedffe01a0b64_127x41.gif)。
现在前进到 ![n_4](https://img.kancloud.cn/48/2b/482b4f1dbb7f8b26c49138d2d074308d_18x11.gif):当 ![n_4](https://img.kancloud.cn/48/2b/482b4f1dbb7f8b26c49138d2d074308d_18x11.gif) 变化时,![f](https://img.kancloud.cn/18/8e/188ee644e8202aad30eac11166858841_10x16.gif) 会变化多少?答案是 ![\frac{\partial f}{\partial n_4} = \frac{\partial f}{\partial n_5} \times \frac{\partial n_5}{\partial n_4}](https://img.kancloud.cn/88/b2/88b25becabedd1329cb373a814bf7ee1_139x41.gif)。由于 ![n_5 = n_4 \times n_2](https://img.kancloud.cn/e3/54/e354e58ead9bc5ba1b63d92b2e3f8f1d_99x12.gif),我们可得到 ![\frac{\partial n_5}{\partial n_4} = n_2](https://img.kancloud.cn/ee/dc/eedc836f2242ab4526e7fe426c255820_71x41.gif),所以 ![\frac{\partial f}{\partial n_4}= 1 \times n_2 = 4](https://img.kancloud.cn/8e/5e/8e5ec50ec7baa31ae92aad71a237d9df_136x41.gif)。
这个遍历过程一直持续,此时我们达到图的底部。这时我们已经得到了所有偏导数在点 ![x=3, y=4](https://img.kancloud.cn/e2/aa/e2aaf7b7ea4190534d4ac26a8ad006bd_94x16.gif) 处的值。在这个例子里,我们得到 ![\frac{\partial f}{\partial x} = 24, \frac{\partial f}{\partial y} = 10](https://img.kancloud.cn/b6/98/b6986e1e3f178430f72c7d14a0608826_140x42.gif)。听起来很美妙!
反向自动微分是非常强大且准确的技术,尤其是当有很多输入参数和极少输出时,因为它只要求一次前馈传递加上一次反向传递,就可计算所有输出关于所有输入的偏导数。最重要的是,它可以处理任意代码定义的函数。它也可以处理那些不完全可微的函数,只要 你要求他计算的偏导数在该点处是可微的。
如果你在 TensorFlow 中实现了新算子,你想使它与现有的自动微分相兼容,那你需要提供函数,该函数用于构建一个子图,来计算关于新算子输入的偏导数。例如,假设你实现了一个计算其输入的平方的函数,平方算子 ![f(x)= x ^2](https://img.kancloud.cn/e9/60/e960ebb8a09a606aa966e44b93ea076a_75x20.gif),在这个例子中你需要提供相应的导函数 ![f^{'}(x)= 2x ](https://img.kancloud.cn/f6/89/f68957bd403ae5772d4b1cddabee6fa8_83x21.gif)。注意这个导函数不计算一个数值结果,而是用于构建子图,该子图后续将计算偏导结果。这是非常有用的,因为这意味着你可以计算梯度的梯度(为了计算二阶导数,或者甚至更高阶的导数)。