導讀:我們在研究與應用深度學習時,會碰到一個無法繞過的內容,就是微分求導,再具體點其實就是反向傳播。如果我們只是簡單地應用深度學習、搭搭模型,那麼可以不用深究。但是如果想深入的從工程上了解深度學習及對應框架的實現,那麼瞭解程序是如何進行反向傳播,自動微分就十分重要了。本文將一步步的從簡單的手動求導一直談到自動微分。
我們目前可以將微分劃分爲四大類:Manual Differentiation、Symbolic Differentiation、Numeric Differentiation、Automatic Differentiation。
Manual Differentiation
Manual Differentiation正如字面意思所寫,就是手動對函數求導。
比如下式就是個簡單的函數。
對x求導可得:
對y求導可得:
這個例子很見到,只要瞭解基本的求導公式,都可以很迅速的解決。但是當我們面臨的是一個極其複雜的公式時,整個過程將會非常繁瑣,而且容易出錯。
Symbolic Differentiation
爲了避免人力的介入,首先提出了 Symbolic Differentiation 即符號微分。
下面也是個簡單的例子:
符號微分方式如下圖:
這個例子很簡單,但是對於複雜函數會生成非常大的圖,很難簡化,有一定的性能問題。
更重要的是,符號微分無法對任意代碼定義的函數進行求導,如下:
def my_func(a, b): z = 0 for i in range(1000): z = a * np.cos(z + i) + z * n return z
Numeric Differentiation
那怎樣才能對任意函數進行求導呢,於是引出了Numeric Differentiation,顧名思義就是用數值去近似計算,求微分。
我們知道:
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) # 24.000039999805264 df_dy = derivative(f, 3, 4, 0, 0.00001) # 10.000000000331966
數值微分裏邊,如上邊的代碼,我們至少需要執行三次f(x,y),當我們的參數有1000個時就至少要執行1001次,在大規模的神經網絡模型中,這樣的做法是很低效的。不過數值微分比較簡單,可以用來對手動求導的值進行驗證。
Automatic Differentiation
Autodiff算是在之前提到的這些方案基礎之上得到的一個兼具性能和數值穩定的方案。
1. Forward-Mode Autodiff
- Forward-Mode是數值微分和符號微分的結合
- 引入二元數ε, ϵ**2 = 0 ( ϵ ≠ 0)
- 所以f(x,y) 在(3,4)點對x的導數爲ϵ前面的係數
forward-mode autodiff 比數值微分更準確,但是也有同樣的問題,就是有n個參數要走n次圖
2. Reverse-Mode Autodiff
爲了解決Forward-Mode中需要根據參數進行圖遍歷的問題,提出了Reverse-Mode Autodiff方案。這也就是我們常說的鏈式法則、反向傳播算法(machine learning中我們常常將其等價)。
- 第一遍,先從輸入到輸出,進行前向計算
- 第二遍,從輸出到輸入,進行反向計算求偏導
- Reverse-Mode Autodiff在有大量輸入,少量輸出時是非常有力的方案
- 一次正向,一次反向可以計算所有輸入的偏導
- 一個簡易教學版的Autograd的實現: https://github.com/mattjj/autodidact
3. 相關工程實現
Autograd將numpy包了一層,提供與numpy相同的基礎ops,但自己內部創建了計算圖
import autograd.numpy as np from autograd import grad def logistic(z): # 等價形式 # np.reciprocal(np.add(1, np.exp(np.negative(z)))) return 1. / (1. + np.exp(-z)) print(logistic(1.5)) # 0.8175744761936437 print(grad(logistic)(1.5)) # 0.14914645207033284
① Vector-Jacobian Products
雅克比矩陣:
vector-Jacobian product (VJP):
對於每一個基本操作都需要定義一個VJP。
primitive_vjps = defaultdict(dict) def defvjp(fun, *vjps, **kwargs): argnums = kwargs.get('argnums', count()) for argnum, vjp in zip(argnums, vjps): primitive_vjps[fun][argnum] = vjp defvjp(anp.negative, lambda g, ans, x: -g) defvjp(anp.exp, lambda g, ans, x: ans * g) defvjp(anp.log, lambda g, ans, x: g / x) defvjp(anp.tanh, lambda g, ans, x: g / anp.cosh(x) **2) defvjp(anp.sinh, lambda g, ans, x: g * anp.cosh(x)) defvjp(anp.cosh, lambda g, ans, x: g * anp.sinh(x))
tracing the forward pass to build the computation graph上邊的代碼是截取了autodidact的部分代碼,像negative基本操作,求出倒數就是原來基礎上取負。整個過程可以劃分爲三塊。
- vector-Jacobian products for primitive ops
- backwards pass
再具體的細節就不粘貼了,有興趣的同學可以訪問我之前貼出的鏈接,幾百行代碼,麻雀雖小五臟俱全,非常適合學習。
Reference
- Géron, Aurélien. “Hands-On Machine Learning with Scikit-Learn and TensorFlow” https://www.cs.toronto.edu/~rgrosse/courses/csc321_2018/slides/lec10.pdf
- Autograd的實現: https://github.com/mattjj/autodidact
本文來自 DataFun 社區
原文鏈接: