自动微分(Automatic Differentiation)
目錄
什么是自動微分
手動求解法
數值微分法
符號微分法
自動微分法
自動微分Forward Mode
自動微分Reverse Mode
參考引用
現代深度學習系統中(比如MXNet, TensorFlow等)都用到了一種技術——自動微分。在此之前,機器學習社區中很少發揮這個利器,一般都是用Backpropagation進行梯度求解,然后進行SGD等進行優化更新。手動實現過backprop算法的同學應該可以體會到其中的復雜性和易錯性,一個好的框架應該可以很好地將這部分難點隱藏于用戶視角,而自動微分技術恰好可以優雅解決這個問題。接下來我們將一起學習這個優雅的技術:-)。本文主要來源于陳天奇在華盛頓任教的課程CSE599G1: Deep Learning System和《Automatic differentiation in machine learning: a survey》。
什么是自動微分
微分求解大致可以分為4種方式:
- 手動求解法(Manual Differentiation)
- 數值微分法(Numerical Differentiation)
- 符號微分法(Symbolic Differentiation)
- 自動微分法(Automatic Differentiation)
為了講明白什么是自動微分,我們有必要了解其他方法,做到有區分有對比,從而更加深入理解自動微分技術
手動求解法
手動求解其實就對應我們傳統的backprop算法,我們求解出梯度公式,然后編寫代碼,代入實際數值,得出真實的梯度。在這樣的方式下,每一次我們修改算法模型,都要修改對應的梯度求解算法,因此沒有很好的辦法解脫用戶手動編寫梯度求解的代碼,這也是為什么我們需要自動微分技術的原因。
數值微分法
數值微分法是根據導數的原始定義:
那么只要h hh取很小的數值,比如0.0001,那么我們可以很方便求解導數,并且可以對用戶隱藏求解過程,用戶只要給出目標函數和要求解的梯度的變量,程序可以自動給出相應的梯度,這也是某種意義上的“自動微分”?。不幸的是,數值微分法計算量太大,求解速度是這四種方法中最慢的,更加雪上加霜的是,它引起的roundoff error和truncation error使其更加不具備實際應用場景,為了彌補缺點,便有如下center difference approximation:
可惜并不能完全消除truncation error,只是將誤差減小。雖然數值微分法有如上缺點,但是由于它實在是太簡單實現了,于是很多時候,我們利用它來檢驗其他算法的正確性,比如在實現backprop的時候,我們用的"gradient check"就是利用數值微分法。
符號微分法
符號微分是代替我們第一種手動求解法的過程,利用代數軟件,實現微分的一些公式比如:
然后對用戶提供的具有closed form的數學表達式進行“自動微分"求解,什么是具有closed form的呢?也就是必須能寫成完整數學表達式的,不能有編程語言中的循環結構,條件結構等。因此如果能將問題轉化為一個純數學符號問題,我們能利用現有的代數軟件進行符號微分求解,這種程度意義上的“自動微分"其實已經很完美了。然而缺點我們剛剛也提及過了,就是必須要有closed form的數學表達式,另一個有名的缺點是“表達式膨脹"(expression swell)問題,如果不加小心就會使得問題符號微分求解的表達式急速“膨脹",導致最終求解速度變慢,對于這個問題請看如下圖:
稍不注意,符號微分求解就會如上中間列所示,表達式急劇膨脹,導致問題求解也隨著變慢。
自動微分法
終于輪到我們的主角登場,自動微分的存在依賴于它識破如下事實:
所有數值計算歸根結底是一系列有限的可微算子的組合
自動微分法是一種介于符號微分和數值微分的方法:數值微分強調一開始直接代入數值近似求解;符號微分強調直接對代數進行求解,最后才代入問題數值;自動微分將符號微分法應用于最基本的算子,比如常數,冪函數,指數函數,對數函數,三角函數等,然后代入數值,保留中間結果,最后再應用于整個函數。因此它應用相當靈活,可以做到完全向用戶隱藏微分求解過程,由于它只對基本函數或常數運用符號微分法則,所以它可以靈活結合編程語言的循環結構,條件結構等,使用自動微分和不使用自動微分對代碼總體改動非常小,并且由于它的計算實際是一種圖計算,可以對其做很多優化,這也是為什么該方法在現代深度學習系統中得以廣泛應用。
自動微分Forward Mode
考察如下函數:
我們可以將其轉化為如下計算圖:
轉化成如上DAG(有向無環圖)結構之后,我們可以很容易分步計算函數的值,并求取它每一步的導數值:
上表中左半部分是從左往右每個圖節點的求值結果,右半部分是每個節點對于的求導結果,比如,注意到每一步的求導都利用到上一步的求導結果,這樣不至于重復計算,因此也不會產生像符號微分法的"expression swell"問題。
自動微分的forward mode非常符合我們高數里面學習的求導過程,只要您對求導法則還有印象,理解forward mode自不在話下。如果函數輸入輸出為:
那么利用forward mode只需計算一次如上表右邊過程即可,非常高效。對于輸入輸出映射為如下的:
這樣一個有n個輸入的函數,求解函數梯度需要n遍如上計算過程。然而實際算法模型中,比如神經網絡,通常輸入輸出是極其不成比例的,也就是:
那么利用forward mode進行自動微分就太低效了,因此便有下面要介紹的reverse mode。
自動微分Reverse Mode
如果您理解神經網絡的backprop算法,那么恭喜你,自動微分的backward mode其實就是一種通用的backprop算法,也就是backprop是reverse mode自動微分的一種特殊形式。從名字可以看出,reverse mode和forward mode是一對相反過程,reverse mode從最終結果開始求導,利用最終輸出對每一個節點進行求導,其過程如下紅色箭頭所示:
其具體計算過程如下表所示:
上表左邊和之前的forward mode一致,用于求解函數值,右邊則是reverse mode的計算過程,注意必須從下網上看,也就是一開始先計算輸出對于節點的導數,用表示,這樣的記號可以強調我們對當前計算結果進行緩存,以便用于后續計算,而不必重復計算。由鏈式法則我們可以計算輸出對于每個節點的導數。
比如對于節點v3 :
用另一種記法便得到:
比如對于節點v0:
如果用另一種記法,便可得出:
和backprop算法一樣,我們必須記住前向時當前節點發出的邊,然后在后向傳播的時候,可以搜集所有受到當前節點影響節點。
如上的計算過程,對于像神經網絡這種模型,通常輸入是上萬到上百萬維,而輸出損失函數是1維的模型,只需要一遍reverse mode的計算過程,便可以求出輸出對于各個輸入的導數,從而輕松求取梯度用于后續優化更新。
#自動微分的實現
這里主要講解reverse mode的實現方式,forward mode的實現基本和reverse mode一致,但是由于機器學習算法中大部分用reverse mode才可以高效求解,所以它是我們理解的重心。代碼設計輪廓來源于CSE599G1的作業,通過分析完成作業,可以展示自動微分的簡潔性和靈活可用性。
首先自動微分會將問題轉化成一種有向無環圖,因此我們必須構造基本的圖部件,包括節點和邊。可以先看看節點是如何實現的:
首先節點可以分為三種:
- 常數節點
- 變量節點
- 帶操作算子節點
因此Node類中定義了op成員用于存儲節點的操作算子,const_attr代表節點的常數值,name是節點的標識,主要用于調試。
對于邊的實現則簡單的多,每個節點只要知道本身的輸入節點即可,因此用inputs來描述節點的關系。
有了如上的定義,利用操作符重載,我們可以很簡單構造一個計算圖,舉一個簡單的例子:
對于如上函數,只要重載加法和乘法操作符,我們可以輕松得到如下計算圖:
操作算子是自動微分最重要的組成部分,接下來我們重點介紹,先上代碼:
從定義可以看出,所有實際計算都落在各個操作算子中,上面代碼應該抽象一些,我們來舉一個乘法算子的例子加以說明:
我們重點講解一下gradient方法,它接收兩個參數,一個是node,也就是當前要計算的節點,而output_grad則是后面節點傳來的,我們來看看它到底是啥玩意,對于如下例子:
return [node.inputs[1] * output_grad, node.inputs[0] * output_grad]再來介紹一個特殊的op——PlaceHolderOp,它的作用就如同名字,起到占位符的作用,也就是自動微分中的變量,它不會參與實際計算,只等待用戶給他提供實際值,因此他的實現如下:
了解了節點和操作算子的定義,接下來我們考慮如何協調執行運算。首先是如何計算函數值,對于一幅計算圖,由于節點與節點之間的計算有一定的依賴關系,比如必須先計算node1之后才可以計算node2,那么如何能正確處理好計算關系呢?一個簡單的方式是對圖節點進行拓撲排序,這樣可以保證需要先計算的節點先得到計算。這部分代碼由Executor掌控:
Executor是實際計算圖的引擎,用戶提供需要計算的圖和實際輸入,Executor計算相應的值和梯度。
如何從計算圖中計算函數的值,上面我們已經介紹了,接下來是如何自動計算梯度。reverse mode的自動微分,要求從輸出到輸入節點,按照先后依賴關系,對各個節點求取輸出對于當前節點的梯度,那么和我們上面介紹的剛好相反,為了得到正確計算節點順序,我們可以將圖節點的拓撲排序倒序即可。代碼也很簡單,如下所示:
這里先介紹一個新的算子——oneslike_op。他是一個和numpy自帶的oneslike函數一樣的算子,作用是構造reverse梯度圖的起點,因為最終輸出關于本身的梯度就是一個和輸出shape一樣的全1數組,引入oneslike_op可以使得真實計算得以延后,因此gradients方法最終返回的不是真實的梯度,而是梯度計算圖,然后可以復用Executor,計算實際的梯度值。
緊接著是根據輸出節點,獲得倒序的拓撲排序序列,然后遍歷序列,構造實際的梯度計算圖。我們重點來介紹node_to_output_grad和node_to_output_grads_list這兩個字典的意義。
先關注node_to_output_grads_list,他key是節點,value是一個梯度列表,代表什么含義呢?先看如下部分計算圖:
而對于Executor而言,它并不知道此時的圖是否被反轉,它只關注用戶實際輸入,還有計算相應的值而已。
#自動梯度的應用
有了上面的大篇幅介紹,我們其實已經實現了一個簡單的自動微分引擎了,接下來看如何使用:
使用相當簡單,我們像編寫普通程序一樣,對變量進行各種操作,只要提供要求導數的變量,還有提供實際輸入,引擎可以正確給出相應的梯度值。
下面給出一個根據自動微分訓練Logistic Regression的例子:
看到吧,用戶可以完全感受不到微分求解過程,真正做到自動微分!?完整實現代碼可戳此處。
參考引用
- CSE599G1: Deep Learning System
- Automatic differentiation in machine learning: a survey
————————————————
版權聲明:本文為CSDN博主「Carl-Xie」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/aws3217150/article/details/70214422
總結
以上是生活随笔為你收集整理的自动微分(Automatic Differentiation)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: pythonChallenge:第1关
- 下一篇: Keras: 多输入及混合数据输入的神经