cs231n Lecture 4 反向傳播筆記

內容列表:

英文原版鏈接

  • 簡介
  • 簡單表達式和理解梯度
  • 複合表達式,鏈式法則,反向傳播
  • 直觀理解反向傳播
  • 模塊:Sigmoid例子
  • 反向傳播實踐:分段計算
  • 回傳流中的模式
  • 用戶向量化操作的梯度
  • 小結

注:中文翻譯出自知乎專欄“智能單元”,在其基礎上增加了個人批註

簡介

目標:本節將幫助讀者對反向傳播(backpropagation)形成直觀而專業的理解。反向傳播是利用鏈式法則(chain rule)遞歸計算表達式的梯度的方法。理解反向傳播過程及其精妙之處,對於理解、實現、設計和調試神經網絡非常關鍵。

問題陳述:這節的核心問題是:給定函數f(x) ,其中x是輸入數據的向量,需要計算函數 f 關於 x 的梯度,也就是
f(x) \nabla f(x)
(批註 這節課講反向傳播算法,其實就是教會我們如何求梯度,在前面幾節課在講解折葉損失函數和交叉熵函數時,求梯度是一個難點,我在做作業1時怎麼也很頭疼,看了這節課視頻之後,瞬間豁然開朗。課件cs231n的課程和作業都安排的非常用心)

目標:之所以關注上述問題,是因爲在神經網絡中 f 對應的是損失函數(L),輸入 x 裏面包含訓練數據和神經網絡的權重。舉個例子,損失函數可以是SVM的損失函數,輸入則包含了訓練數據(xi, yi), i=1…N、權重 W 和偏差 b 。注意訓練集是給定的(在機器學習中通常都是這樣),而權重是可以控制的變量。因此,即使能用反向傳播計算輸入數據 xi 上的梯度,但在實踐爲了進行參數更新,通常也只計算參數(比如 W, b)的梯度。然而 xi
的梯度有時仍然是有用的:比如將神經網絡所做的事情可視化便於直觀理解的時候,就能用上。

如果讀者之前對於利用鏈式法則計算偏微分已經很熟練,仍然建議瀏覽本篇筆記。因爲它呈現了一個相對成熟的反向傳播視角,在該視角中能看見基於實數值迴路的反向傳播過程,而對其細節的理解和收穫將幫助讀者更好地通過本課程。

簡單表達式和理解梯度

從簡單表達式入手可以爲複雜表達式打好符號和規則基礎。先考慮一個簡單的二元乘法函數 f(x,y)=xy 。對兩個輸入變量分別求偏導數還是很簡單的:

f(x,y)=xydfdx=ydfdy=x \displaystyle f(x,y)=xy \to \frac {df}{dx}=y \quad \frac {df}{dy}=x

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

df(x)dx=limh0f(x+h)f(x)h \frac{df(x)}{dx}= lim_{h\to 0}\frac{f(x+h)-f(x)}{h}

注意等號左邊的除號和等號右邊的除號不同,不是代表除法。相反,這個符號表示操作符
ddx \frac{d}{dx}
被應用於函數 f ,並返回一個不同的函數(導數)。對於上述公式,可以認爲 h 值非常小,函數可以被一條直線近似,而導數就是這條直線的斜率。換句話說,每個變量的導數指明瞭整個表達式對於該變量的值的敏感程度。比如,若x=4,y=-3,則f(x,y)=-12,x的導數
fx=3 \frac{\partial f}{\partial x}=-3
這就說明如果將變量x的值變大一點,整個表達式的值就會變小(原因在於負號),而且變小的量是x變大的量的三倍。通過重新排列公式可以看到這一點(
f(x+h)=f(x)+hdf(x)dx f(x+h)=f(x)+h \frac{df(x)}{dx}
)。同樣,因爲
fy=4 \frac{\partial f}{\partial y}=4
可以知道如果將y的值增加 h,那麼函數的輸出也將增加(原因在於正號),且增加量是 4h

函數關於每個變量的導數指明瞭整個表達式對於該變量的敏感程度。

如上所述,梯度 ∇f
是偏導數的向量,所以有
f(x)=[fx,fy]=[y,x] \nabla f(x)=[\frac{\partial f}{\partial x},\frac{\partial f}{\partial y}]=[y,x]
即使是梯度實際上是一個向量,仍然通常使用類似“x上的梯度”的術語,而不是使用如“x的偏導數”的正確說法,原因是因爲前者說起來簡單。

我們也可以對加法操作求導:

f(x,y)=x+ydfdx=1dfdy=1 \displaystyle f(x,y)=x+y \to \frac {df}{dx}=1\quad\frac {df}{dy}=1

這就是說,無論其值如何,x,y的導數均爲1。這是有道理的,因爲無論增加x,y中任一個的值,函數f的值都會增加,並且增加的變化率獨立於x,y的具體值(情況和乘法操作不同)。取最大值操作也是常常使用的:

f(x,y)=max(x,y)dfdx=1(x>=y)dfdy=1(y>=x) \displaystyle f(x,y)=max(x,y) \to \frac {df}{dx}=1 (x>=y) \quad\frac {df}{dy}=1 (y>=x)

上式是說,如果該變量比另一個變量大,那麼梯度是1,反之爲0(這裏還是空心1)。例如,若x=4, y=2,那麼max是4,所以函數對於y就不敏感。也就是說,在y上增加h,函數還是輸出爲4,所以梯度是0:因爲對於函數輸出是沒有效果的。當然,如果給y增加一個很大的量,比如大於2,那麼函數f的值就變化了,但是導數並沒有指明輸入量有巨大變化情況對於函數的效果,他們只適用於輸入量變化極小時的情況,因爲定義已經指明:lim_h->0

使用鏈式法則計算複合表達式

現在考慮更復雜的包含多個函數的複合函數,比如f(x,y,z)=(x+y)z。雖然這個表達足夠簡單,可以直接微分,但是在此使用一種有助於讀者直觀理解反向傳播的方法。將公式分成兩部分:q=x+y和f=qz。在前面已經介紹過如何對這分開的兩個公式進行計算,因爲f是q和z相乘,所以
fq=z,fz=q \displaystyle\frac{\partial f}{\partial q}=z,\frac{\partial f}{\partial z}=q
,又因爲q是x加y,所以
qx=1,qy=1 \displaystyle\frac{\partial q}{\partial x}=1,\frac{\partial q}{\partial y}=1
然而,並不需要關心中間量q的梯度,因爲
fq \frac{\partial f}{\partial q}
沒有用。相反,函數f關於x,y,z的梯度纔是需要關注的。鏈式法則指出將這些梯度表達式鏈接起來的正確方式是相乘,比如
fx=fqqx \displaystyle\frac{\partial f}{\partial x}=\frac{\partial f}{\partial q}\frac{\partial q}{\partial x}
在實際操作中,這只是簡單地將兩個梯度數值相乘,示例代碼如下:

# 設置輸入值
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,且總是假設梯度是關於最終輸出的。

這次計算可以被可視化爲如下計算線路圖像:

image

上圖的真實值計算線路展示了計算的視覺化過程。前向傳播從輸入計算到輸出(綠色),反向傳播從尾部開始,根據鏈式法則遞歸地向前計算梯度(顯示爲紅色),一直到網絡的輸入端。可以認爲,梯度是從計算鏈路中迴流。


(批註 從這裏開始引入計算圖,視頻中老師反覆強調了計算圖的重要性,以及在實際工程中,第一步就是將前向和反向推導轉換成計算圖,同時,在tensorflow中,我們也可以看到每個計算都轉換成計算圖,這也是因爲計算圖方式去求梯度的便利性)

反向傳播的直觀理解

反向傳播是一個優美的局部過程。在整個計算線路圖中,每個門單元都會得到一些輸入並立即計算兩個東西:1. 這個門的輸出值,和2.其輸出值關於輸入值的局部梯度(local gradient)。門單元完成這兩件事是完全獨立的,它不需要知道計算線路中的其他細節。然而,一旦前向傳播完畢,在反向傳播的過程中,門單元門將最終獲得整個網絡的最終輸出值在自己的輸出值上的梯度。鏈式法則指出,門單元應該將回傳的梯度乘以它對其的輸入的局部梯度,從而得到整個網絡的輸出對該門單元的每個輸入值的梯度

這裏對於每個輸入的乘法操作是基於鏈式法則的。該操作讓一個相對獨立的門單元變成複雜計算線路中不可或缺的一部分,這個複雜計算線路可以是神經網絡等。

下面通過例子來對這一過程進行理解。加法門收到了輸入[-2, 5],計算輸出是3。既然這個門是加法操作,那麼對於兩個輸入的局部梯度都是+1。網絡的其餘部分計算出最終值爲-12。在反向傳播時將遞歸地使用鏈式法則,算到加法門(是乘法門的輸入)的時候,知道加法門的輸出的梯度是-4。如果網絡如果想要輸出值更高,那麼可以認爲它會想要加法門的輸出更小一點(因爲負號),而且還有一個4的倍數。繼續遞歸併對梯度使用鏈式法則,加法門拿到梯度,然後把這個梯度分別乘到每個輸入值的局部梯度(就是讓-4乘以x和y的局部梯度,x和y的局部梯度都是1,所以最終都是-4)。可以看到得到了想要的效果:如果x,y減小(它們的梯度爲負),那麼加法門的輸出值減小,這會讓乘法門的輸出值增大。

因此,反向傳播可以看做是門單元之間在通過梯度信號相互通信,只要讓它們的輸入沿着梯度方向變化,無論它們自己的輸出值在何種程度上升或降低,都是爲了讓整個網絡的輸出值更高。

模塊化:Sigmoid例子

上面介紹的門是相對隨意的。任何可微分的函數都可以看做門。可以將多個門組合成一個門,也可以根據需要將一個函數分拆成多個門。現在看看一個表達式:

f(w,x)=11+e(w0x0+w1x1+w2) \displaystyle f(w,x)=\frac{1}{1+e^{-(w_0x_0+w_1x_1+w_2)}}

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

f(x)=1xdfdx=1/x2 \displaystyle f(x)=\frac{1}{x} \to \frac{df}{dx}=-1/x^2

fc(x)=c+xdfdx=1 \displaystyle f_c(x)=c+x \to \frac{df}{dx}=1\displaystyle

f(x)=exdfdx=ex f(x)=e^x \to \frac{df}{dx}=e^x

fa(x)=axdfdx=a \displaystyle f_a(x)=ax \to \frac{df}{dx}=a

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


image

使用sigmoid激活函數的2維神經元的例子。輸入是[x0, x1],可學習的權重是[w0, w1, w2]。一會兒會看見,這個神經元對輸入數據做點積運算,然後其激活數據被sigmoid函數擠壓到0到1之間。

在上面的例子中可以看見一個函數操作的長鏈條,鏈條上的門都對w和x的點積結果進行操作。該函數被稱爲sigmoid函數 σ(x)。sigmoid函數關於其輸入的求導是可以簡化的(使用了在分子上先加後減1的技巧):

σ(x)=11+exdσ(x)dx=ex(1+ex)2=(1+ex11+ex)(11+ex)=(1σ(x))σ(x) \displaystyle\sigma(x)=\frac{1}{1+e^{-x}} \displaystyle\to\frac{d\sigma(x)}{dx}=\frac{e^{-x}}{(1+e^{-x})^2}=(\frac{1+e^{-x}-1}{1+e^{-x}})(\frac{1}{1+e^{-x}})=(1-\sigma(x))\sigma(x)

可以看到梯度計算簡單了很多。舉個例子,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)。

本節的要點就是展示反向傳播的細節過程,以及前向傳播過程中,哪些函數可以被組合成門,從而可以進行簡化。知道表達式中哪部分的局部梯度計算比較簡潔非常有用,這樣他們可以“鏈”在一起,讓代碼量更少,效率更高。

反向傳播實踐:分段計算

看另一個例子。假設有如下函數:

f(x,y)=x+σ(y)σ(x)+(x+y)2 \displaystyle f(x,y)=\frac{x+\sigma(y)}{\sigma(x)+(x+y)^2}

首先要說的是,這個函數完全沒用,讀者是不會用到它來進行梯度計算的,這裏只是用來作爲實踐反向傳播的一個例子,需要強調的是,如果對x或y進行微分運算,運算結束後會得到一個巨大而複雜的表達式。然而做如此複雜的運算實際上並無必要,因爲我們不需要一個明確的函數來計算梯度,只需知道如何使用反向傳播計算梯度即可。下面是構建前向傳播的代碼模式:

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)

到了表達式的最後,就完成了前向傳播。注意在構建代碼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在前向傳播的表達式中出現多次,那麼進行反向傳播的時候就要非常小心,使用+=而不是=來累計這些變量的梯度(不然就會造成覆寫)。這是遵循了在微積分中的多元鏈式法則,該法則指出如果變量在線路中分支走向不同的部分,那麼梯度在回傳的時候,就應該進行累加。

回傳流中的模式

一個有趣的現象是在多數情況下,反向傳播中的梯度可以被很直觀地解釋。例如神經網絡中最常用的加法、乘法和取最大值這三個門單元,它們在反向傳播過程中的行爲都有非常簡單的解釋。先看下面這個例子:


image

一個展示反向傳播的例子。加法操作將梯度相等地分發給它的輸入。取最大操作將梯度路由給更大的輸入。乘法門拿取輸入激活數據,對它們進行交換,然後乘以梯度。

從上例可知:

加法門單元把輸出的梯度相等地分發給它所有的輸入,這一行爲與輸入值在前向傳播時的值無關。這是因爲加法操作的局部梯度都是簡單的+1,所以所有輸入的梯度實際上就等於輸出的梯度,因爲乘以1.0保持不變。上例中,加法門把梯度2.00不變且相等地路由給了兩個輸入。

max門單元對梯度做路由。和加法門不同,取最大值門將梯度轉給其中一個輸入,這個輸入是在前向傳播中值最大的那個輸入。這是因爲在取最大值門中,最高值的局部梯度是1.0,其餘的是0。上例中,取最大值門將梯度2.00轉給了z變量,因爲z的值比w高,於是w的梯度保持爲0。

乘法門單元相對不容易解釋。它的局部梯度就是輸入值,但是是相互交換之後的,然後根據鏈式法則乘以輸出值的梯度。上例中,x的梯度是-4.00x2.00=-8.00。

非直觀影響及其結果。注意一種比較特殊的情況,如果乘法門單元的其中一個輸入非常小,而另一個輸入非常大,那麼乘法門的操作將會不是那麼直觀:它將會把大的梯度分配給小的輸入,把小的梯度分配給大的輸入。在線性分類器中,權重和輸入是進行點積 wTxi,這說明輸入數據的大小對於權重梯度的大小有影響。例如,在計算過程中對所有輸入數據樣本xi乘以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)。

使用小而具體的例子:有些讀者可能覺得向量化操作的梯度計算比較困難,建議是寫出一個很小很明確的向量化例子,在紙上演算梯度,然後對其一般化,得到一個高效的向量化操作形式。

小結

  • 對梯度的含義有了直觀理解,知道了梯度是如何在網絡中反向傳播的,知道了它們是如何與網絡的不同部分通信並控制其升高或者降低,並使得最終輸出值更高的。

  • 討論了分段計算在反向傳播的實現中的重要性。應該將函數分成不同的模塊,這樣計算局部梯度相對容易,然後基於鏈式法則將其“鏈”起來。重要的是,不需要把這些表達式寫在紙上然後演算它的完整求導公式,因爲實際上並不需要關於輸入變量的梯度的數學公式。只需要將表達式分成不同的可以求導的模塊(模塊可以是矩陣向量的乘法操作,或者取最大值操作,或者加法操作等),然後在反向傳播中一步一步地計算梯度。

(批註 視頻中有提到“雅比克矩陣(Jaconbian)”,在講義中居然沒有提及)

在下節課中,將會開始定義神經網絡,而反向傳播使我們能高效計算神經網絡各個節點關於損失函數的梯度。換句話說,我們現在已經準備好訓練神經網絡了,本課程最困難的部分已經過去了!ConvNets相比只是向前走了一小步。

參考文獻

Automatic differentiation in machine learning: a survey

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章