鏈接:https://zhuanlan.zhihu.com/p/21407711
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
譯者注:本文智能單元首發,譯自斯坦福CS231n課程筆記Backprop Note,課程教師Andrej Karpathy授權翻譯。本篇教程由杜客翻譯完成,堃堃和鞏子嘉進行校對修改。譯文含公式和代碼,建議PC端閱讀。
原文如下:
內容列表:
- 簡介
- 簡單表達式和理解梯度
- 復合表達式,鏈式法則,反向傳播
- 直觀理解反向傳播
- 模塊:Sigmoid例子
- 反向傳播實踐:分段計算
- 回傳流中的模式
- 用戶向量化操作的梯度
- 小結
簡介
目標:本節將幫助讀者對反向傳播形成直觀而專業的理解。反向傳播是利用鏈式法則遞歸計算表達式的梯度的方法。理解反向傳播過程及其精妙之處,對於理解、實現、設計和調試神經網絡非常關鍵。
問題陳述:這節的核心問題是:給定函數 ,其中
是輸入數據的向量,需要計算函數
關於
的梯度,也就是
。
目標:之所以關注上述問題,是因為在神經網絡中對應的是損失函數(
),輸入
里面包含訓練數據和神經網絡的權重。舉個例子,損失函數可以是SVM的損失函數,輸入則包含了訓練數據
、權重
和偏差
。注意訓練集是給定的(在機器學習中通常都是這樣),而權重是可以控制的變量。因此,即使能用反向傳播計算輸入數據
上的梯度,但在實踐為了進行參數更新,通常也只計算參數(比如
)的梯度。然而
的梯度有時仍然是有用的:比如將神經網絡所做的事情可視化便於直觀理解的時候,就能用上。
如果讀者之前對於利用鏈式法則計算偏微分已經很熟練,仍然建議瀏覽本篇筆記。因為它呈現了一個相對成熟的反向傳播視角,在該視角中能看見基於實數值回路的反向傳播過程,而對其細節的理解和收獲將幫助讀者更好地通過本課程。
簡單表達式和理解梯度
從簡單表達式入手可以為復雜表達式打好符號和規則基礎。先考慮一個簡單的二元乘法函數。對兩個輸入變量分別求偏導數還是很簡單的:

解釋:牢記這些導數的意義:函數變量在某個點周圍的極小區域內變化,而導數就是變量變化導致的函數在該方向上的變化率。

注意等號左邊的分號和等號右邊的分號不同,不是代表分數。相反,這個符號表示操作符被應用於函數
,並返回一個不同的函數(導數)。對於上述公式,可以認為
值非常小,函數可以被一條直線近似,而導數就是這條直線的斜率。換句話說,每個變量的導數指明了整個表達式對於該變量的值的敏感程度。比如,若
,則
,
的導數
。這就說明如果將變量
的值變大一點,整個表達式的值就會變小(原因在於負號),而且變小的量是
變大的量的三倍。通過重新排列公式可以看到這一點(
)。同樣,因為
,可以知道如果將
的值增加
,那么函數的輸出也將增加(原因在於正號),且增加量是
。
函數關於每個變量的導數指明了整個表達式對於該變量的敏感程度。
如上所述,梯度是偏導數的向量,所以有
。即使是梯度實際上是一個向量,仍然通常使用類似“x上的梯度”的術語,而不是使用如“x的偏導數”的正確說法,原因是因為前者說起來簡單。
我們也可以對加法操作求導:

這就是說,無論其值如何,的導數均為1。這是有道理的,因為無論增加
中任一個的值,函數
的值都會增加,並且增加的變化率獨立於
的具體值(情況和乘法操作不同)。取最大值操作也是常常使用的:
上式是說,如果該變量比另一個變量大,那么梯度是1,反之為0。例如,若,那么max是4,所以函數對於
就不敏感。也就是說,在
上增加
,函數還是輸出為4,所以梯度是0:因為對於函數輸出是沒有效果的。當然,如果給
增加一個很大的量,比如大於2,那么函數
的值就變化了,但是導數並沒有指明輸入量有巨大變化情況對於函數的效果,他們只適用於輸入量變化極小時的情況,因為定義已經指明:
。
使用鏈式法則計算復合表達式
現在考慮更復雜的包含多個函數的復合函數,比如。雖然這個表達足夠簡單,可以直接微分,但是在此使用一種有助於讀者直觀理解反向傳播的方法。將公式分成兩部分:
和
。在前面已經介紹過如何對這分開的兩個公式進行計算,因為
是
和
相乘,所以
,又因為
是
加
,所以
。然而,並不需要關心中間量
的梯度,因為
沒有用。相反,函數
關於
的梯度才是需要關注的。鏈式法則指出將這些梯度表達式鏈接起來的正確方式是相乘,比如
。在實際操作中,這只是簡單地將兩個梯度數值相乘,示例代碼如下:
# 設置輸入值 x = -2; y = 5; z = -4 # 進行前向傳播 q = x + y # q becomes 3 f = q * z # f becomes -12 # 進行反向傳播: # 首先回傳到 f = q * z dfdz = q # df/dz = q, 所以關於z的梯度是3 dfdq = z # df/dq = z, 所以關於q的梯度是-4 # 現在回傳到q = x + y dfdx = 1.0 * dfdq # dq/dx = 1. 這里的乘法是因為鏈式法則 dfdy = 1.0 * dfdq # dq/dy = 1
最后得到變量的梯度[dfdx, dfdy, dfdz],它們告訴我們函數f對於變量[x, y, z]的敏感程度。這是一個最簡單的反向傳播。一般會使用一個更簡潔的表達符號,這樣就不用寫df了。這就是說,用dq來代替dfdq,且總是假設梯度是關於最終輸出的。
這次計算可以被可視化為如下計算線路圖像:
————————————————————————————————————————
上圖的真實值計算線路展示了計算的視覺化過程。前向傳播從輸入計算到輸出(綠色),反向傳播從尾部開始,根據鏈式法則遞歸地向前計算梯度(顯示為紅色),一直到網絡的輸入端。可以認為,梯度是從計算鏈路中回流。
————————————————————————————————————————
反向傳播的直觀理解
反向傳播是一個優美的局部過程。在整個計算線路圖中,每個門單元都會得到一些輸入並立即計算兩個東西:1. 這個門的輸出值,和2.其輸出值關於輸入值的局部梯度。門單元完成這兩件事是完全獨立的,它不需要知道計算線路中的其他細節。然而,一旦前向傳播完畢,在反向傳播的過程中,門單元門將最終獲得整個網絡的最終輸出值在自己的輸出值上的梯度。鏈式法則指出,門單元應該將回傳的梯度乘以它對其的輸入的局部梯度,從而得到整個網絡的輸出對該門單元的每個輸入值的梯度。
這里對於每個輸入的乘法操作是基於鏈式法則的。該操作讓一個相對獨立的門單元變成復雜計算線路中不可或缺的一部分,這個復雜計算線路可以是神經網絡等。
下面通過例子來對這一過程進行理解。加法門收到了輸入[-2, 5],計算輸出是3。既然這個門是加法操作,那么對於兩個輸入的局部梯度都是+1。網絡的其余部分計算出最終值為-12。在反向傳播時將遞歸地使用鏈式法則,算到加法門(是乘法門的輸入)的時候,知道加法門的輸出的梯度是-4。如果網絡如果想要輸出值更高,那么可以認為它會想要加法門的輸出更小一點(因為負號),而且還有一個4的倍數。繼續遞歸並對梯度使用鏈式法則,加法門拿到梯度,然后把這個梯度分別乘到每個輸入值的局部梯度(就是讓-4乘以x和y的局部梯度,x和y的局部梯度都是1,所以最終都是-4)。可以看到得到了想要的效果:如果x,y減小(它們的梯度為負),那么加法門的輸出值減小,這會讓乘法門的輸出值增大。
因此,反向傳播可以看做是門單元之間在通過梯度信號相互通信,只要讓它們的輸入沿着梯度方向變化,無論它們自己的輸出值在何種程度上升或降低,都是為了讓整個網絡的輸出值更高。
模塊化:Sigmoid例子
上面介紹的門是相對隨意的。任何可微分的函數都可以看做門。可以將多個門組合成一個門,也可以根據需要將一個函數分拆成多個門。現在看看一個表達式:

在后面的課程中可以看到,這個表達式描述了一個含輸入x和權重w的2維的神經元,該神經元使用了sigmoid激活函數。但是現在只是看做是一個簡單的輸入為x和w,輸出為一個數字的函數。這個函數是由多個門組成的。除了上文介紹的加法門,乘法門,取最大值門,還有下面這4種:




其中,函數使用對輸入值進行了常量
的平移,
將輸入值擴大了常量
倍。它們是加法和乘法的特例,但是這里將其看做一元門單元,因為確實需要計算常量
的梯度。整個計算線路如下:
———————————————————————————————————————

使用sigmoid激活函數的2維神經元的例子。輸入是[x0, x1],可學習的權重是[w0, w1, w2]。一會兒會看見,這個神經元對輸入數據做點積運算,然后其激活數據被sigmoid函數擠壓到0到1之間。
————————————————————————————————————————
在上面的例子中可以看見一個函數操作的長鏈條,鏈條上的門都對w和x的點積結果進行操作。該函數被稱為sigmoid函數。sigmoid函數關於其輸入的求導是可以簡化的(使用了在分子上先加后減1的技巧):


可以看到梯度計算簡單了很多。舉個例子,sigmoid表達式輸入為1.0,則在前向傳播中計算出輸出為0.73。根據上面的公式,局部梯度為(1-0.73)*0.73~=0.2,和之前的計算流程比起來,現在的計算使用一個單獨的簡單表達式即可。因此,在實際的應用中將這些操作裝進一個單獨的門單元中將會非常有用。該神經元反向傳播的代碼實現如下:
w = [2,-3,-3] # 假設一些隨機數據和權重 x = [-1, -2] # 前向傳播 dot = w[0]*x[0] + w[1]*x[1] + w[2] f = 1.0 / (1 + math.exp(-dot)) # sigmoid函數 # 對神經元反向傳播 ddot = (1 - f) * f # 點積變量的梯度, 使用sigmoid函數求導 dx = [w[0] * ddot, w[1] * ddot] # 回傳到x dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # 回傳到w # 完成!得到輸入的梯度
實現提示:分段反向傳播。上面的代碼展示了在實際操作中,為了使反向傳播過程更加簡潔,把向前傳播分成不同的階段將是很有幫助的。比如我們創建了一個中間變量dot,它裝着w和x的點乘結果。在反向傳播的時,就可以(反向地)計算出裝着w和x等的梯度的對應的變量(比如ddot,dx和dw)。
本節的要點就是展示反向傳播的細節過程,以及前向傳播過程中,哪些函數可以被組合成門,從而可以進行簡化。知道表達式中哪部分的局部梯度計算比較簡潔非常有用,這樣他們可以“鏈”在一起,讓代碼量更少,效率更高。
反向傳播實踐:分段計算
看另一個例子。假設有如下函數:

首先要說的是,這個函數完全沒用,讀者是不會用到它來進行梯度計算的,這里只是用來作為實踐反向傳播的一個例子,需要強調的是,如果對或
進行微分運算,運算結束后會得到一個巨大而復雜的表達式。然而做如此復雜的運算實際上並無必要,因為我們不需要一個明確的函數來計算梯度,只需知道如何使用反向傳播計算梯度即可。下面是構建前向傳播的代碼模式:
x = 3 # 例子數值 y = -4 # 前向傳播 sigy = 1.0 / (1 + math.exp(-y)) # 分子中的sigmoi #(1) num = x + sigy # 分子 #(2) sigx = 1.0 / (1 + math.exp(-x)) # 分母中的sigmoid #(3) xpy = x + y #(4) xpysqr = xpy**2 #(5) den = sigx + xpysqr # 分母 #(6) invden = 1.0 / den #(7) f = num * invden # 搞定! #(8)
┗|`O′|┛ 嗷~~,到了表達式的最后,就完成了前向傳播。注意在構建代碼s時創建了多個中間變量,每個都是比較簡單的表達式,它們計算局部梯度的方法是已知的。這樣計算反向傳播就簡單了:我們對前向傳播時產生每個變量(sigy, num, sigx, xpy, xpysqr, den, invden)進行回傳。我們會有同樣數量的變量,但是都以d開頭,用來存儲對應變量的梯度。注意在反向傳播的每一小塊中都將包含了表達式的局部梯度,然后根據使用鏈式法則乘以上游梯度。對於每行代碼,我們將指明其對應的是前向傳播的哪部分。
# 回傳 f = num * invden dnum = invden # 分子的梯度 #(8) dinvden = num #(8) # 回傳 invden = 1.0 / den dden = (-1.0 / (den**2)) * dinvden #(7) # 回傳 den = sigx + xpysqr dsigx = (1) * dden #(6) dxpysqr = (1) * dden #(6) # 回傳 xpysqr = xpy**2 dxpy = (2 * xpy) * dxpysqr #(5) # 回傳 xpy = x + y dx = (1) * dxpy #(4) dy = (1) * dxpy #(4) # 回傳 sigx = 1.0 / (1 + math.exp(-x)) dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below #(3) # 回傳 num = x + sigy dx += (1) * dnum #(2) dsigy = (1) * dnum #(2) # 回傳 sigy = 1.0 / (1 + math.exp(-y)) dy += ((1 - sigy) * sigy) * dsigy #(1) # 完成! 嗷~~
需要注意的一些東西:
對前向傳播變量進行緩存:在計算反向傳播時,前向傳播過程中得到的一些中間變量非常有用。在實際操作中,最好代碼實現對於這些中間變量的緩存,這樣在反向傳播的時候也能用上它們。如果這樣做過於困難,也可以(但是浪費計算資源)重新計算它們。
在不同分支的梯度要相加:如果變量x,y在前向傳播的表達式中出現多次,那么進行反向傳播的時候就要非常小心,使用+=而不是=來累計這些變量的梯度(不然就會造成覆寫)。這是遵循了在微積分中的多元鏈式法則,該法則指出如果變量在線路中分支走向不同的部分,那么梯度在回傳的時候,就應該進行累加。
回傳流中的模式
一個有趣的現象是在多數情況下,反向傳播中的梯度可以被很直觀地解釋。例如神經網絡中最常用的加法、乘法和取最大值這三個門單元,它們在反向傳播過程中的行為都有非常簡單的解釋。先看下面這個例子:
——————————————————————————————————————————
一個展示反向傳播的例子。加法操作將梯度相等地分發給它的輸入。取最大操作將梯度路由給更大的輸入。乘法門拿取輸入激活數據,對它們進行交換,然后乘以梯度。
——————————————————————————————————————————
從上例可知:
加法門單元把輸出的梯度相等地分發給它所有的輸入,這一行為與輸入值在前向傳播時的值無關。這是因為加法操作的局部梯度都是簡單的+1,所以所有輸入的梯度實際上就等於輸出的梯度,因為乘以1.0保持不變。上例中,加法門把梯度2.00不變且相等地路由給了兩個輸入。
取最大值門單元對梯度做路由。和加法門不同,取最大值門將梯度轉給其中一個輸入,這個輸入是在前向傳播中值最大的那個輸入。這是因為在取最大值門中,最高值的局部梯度是1.0,其余的是0。上例中,取最大值門將梯度2.00轉給了z變量,因為z的值比w高,於是w的梯度保持為0。
乘法門單元相對不容易解釋。它的局部梯度就是輸入值,但是是相互交換之后的,然后根據鏈式法則乘以輸出值的梯度。上例中,x的梯度是-4.00x2.00=-8.00。
非直觀影響及其結果。注意一種比較特殊的情況,如果乘法門單元的其中一個輸入非常小,而另一個輸入非常大,那么乘法門的操作將會不是那么直觀:它將會把大的梯度分配給小的輸入,把小的梯度分配給大的輸入。在線性分類器中,權重和輸入是進行點積,這說明輸入數據的大小對於權重梯度的大小有影響。例如,在計算過程中對所有輸入數據樣本
乘以1000,那么權重的梯度將會增大1000倍,這樣就必須降低學習率來彌補。這就是為什么數據預處理關系重大,它即使只是有微小變化,也會產生巨大影響。對於梯度在計算線路中是如何流動的有一個直觀的理解,可以幫助讀者調試網絡。
用向量化操作計算梯度
上述內容考慮的都是單個變量情況,但是所有概念都適用於矩陣和向量操作。然而,在操作的時候要注意關注維度和轉置操作。
矩陣相乘的梯度:可能最有技巧的操作是矩陣相乘(也適用於矩陣和向量,向量和向量相乘)的乘法操作:
# 前向傳播 W = np.random.randn(5, 10) X = np.random.randn(10, 3) D = W.dot(X) # 假設我們得到了D的梯度 dD = np.random.randn(*D.shape) # 和D一樣的尺寸 dW = dD.dot(X.T) #.T就是對矩陣進行轉置 dX = W.T.dot(dD)
提示:要分析維度!注意不需要去記憶dW和dX的表達,因為它們很容易通過維度推導出來。例如,權重的梯度dW的尺寸肯定和權重矩陣W的尺寸是一樣的,而這又是由X和dD的矩陣乘法決定的(在上面的例子中X和W都是數字不是矩陣)。總有一個方式是能夠讓維度之間能夠對的上的。例如,X的尺寸是[10x3],dD的尺寸是[5x3],如果你想要dW和W的尺寸是[5x10],那就要dD.dot(X.T)。
使用小而具體的例子:有些讀者可能覺得向量化操作的梯度計算比較困難,建議是寫出一個很小很明確的向量化例子,在紙上演算梯度,然后對其一般化,得到一個高效的向量化操作形式。
小結
-
對梯度的含義有了直觀理解,知道了梯度是如何在網絡中反向傳播的,知道了它們是如何與網絡的不同部分通信並控制其升高或者降低,並使得最終輸出值更高的。
-
討論了分段計算在反向傳播的實現中的重要性。應該將函數分成不同的模塊,這樣計算局部梯度相對容易,然后基於鏈式法則將其“鏈”起來。重要的是,不需要把這些表達式寫在紙上然后演算它的完整求導公式,因為實際上並不需要關於輸入變量的梯度的數學公式。只需要將表達式分成不同的可以求導的模塊(模塊可以是矩陣向量的乘法操作,或者取最大值操作,或者加法操作等),然后在反向傳播中一步一步地計算梯度。
在下節課中,將會開始定義神經網絡,而反向傳播使我們能高效計算神經網絡各個節點關於損失函數的梯度。換句話說,我們現在已經准備好訓練神經網絡了,本課程最困難的部分已經過去了!ConvNets相比只是向前走了一小步。