語音學習筆記13------談談史丹佛大學卷積神經網路之反向傳播

塵封的記憶0發表於2017-03-14

史丹佛大學課程原文連結:http://cs231n.github.io/optimization-2/ 

介紹

動機 在這個部分我們通過一些“直覺”來理解反向傳播,也就是一種使用鏈式(求導)法則遞迴地計算梯度表示式的方法。理解其中的精妙之處非常重要,幫助理解,高效地開發,設計,除錯神經網路。

問題陳述 前面學到的核心問題是:我們有一個函式f(x),其中x是一個輸入向量,我們希望計算fx處的梯度(也就是f(x))。

動機 我們之所以對這個問題如此感興趣的原因是神經網路,f對應的是損失函式(L),輸入x會包括訓練資料和神經網路的權值。例如,使用SVM損失函式計算損失,輸入包括訓練資料(xi,yi),i=1...N和權重與偏置W,b。注意到(在機器學習中很常見的)我們認為訓練資料是給定的,並且不可改變,我們能修改的只有權重。因此,即使我們可以很輕易地使用反向傳播計算輸入xi對應的梯度,我們也只會計算引數的梯度(例如W,b)以用來更新引數。但是我們後面會看到,xi的梯度有的時候仍然是有用的,比如我們想要將神經網路正在做的事情視覺化出來的時候。 
如果你在上課之前就已經知道如何使用鏈式法則得到梯度,我仍舊希望你可以留在這裡,因為我們展示了作為實值傳遞圈逆流的反向傳播的一些很成熟的想法(it presents a rarely developed view of backpropagation as backward flow in real-valued circuits),你可能從其中得到很多幫助。

簡單的表示式和梯度的理解

我們由淺入深地學習一下。想象一個簡單的乘積函式f(x,y)=xy。求出他們的偏導數非常簡單 

f(x,y)=xyfx=yfy=x

解釋 導數的意義是:在某個點的無限小的距離內,函式值沿一個變數變化方向的變化率: 
df(x)dx=limh0f(x+h)f(x)h

等式左邊的除號和等式右邊的除號不一樣,它不是除號。這裡的操作ddx作用在f上,返回一個不同的函式(導數)。一個很好的思路就是,當h很小的時候,這個函式很好的近似了一個直線,而導數就是它的斜率。也就是說,每個變數上的導數給出了這個值附近整個表示式的敏感度。例如,如果x=4,y=3,那麼f(x,y)=12,並且導數xfx=3。這告訴我們如果我們將這個變數增加一點點,對於整個表示式來說,是減少的(因為前面的負號),並且按照增加量的三倍減少。我們重新整理上面的等式可以看到f(x+h)=f(x)+hdf(x)dx。類似的,由於fy=4,我們在y上新增一個非常小的值h,最終整個輸出會增加4h

每個變數的導數表明了這個值對整個表示式的敏感度

像上面提到的,梯度f是向量的偏導數,所以我們有f=[fx,fy]=[y,x]。 雖然梯度事實上是一個向量,為了簡化表達我們仍然會用“x上的梯度”而不是“x上的偏導”。

我們也可以匯出加法函式的偏導: 

f(x,y)=x+yfx=1fy=1

也就是說,x,y對應的導數都與x,y的值無關,這是因為增加x,y的值都會使得輸出f增加,而增長率和x,y事實上是多少是無關的。最後我們用的比較多的是最大操作: 
f(x,y)=(x,y)fx=1(x>=y)fy=1(y>=x)

That is, the (sub)gradient is 1 on the input that was larger and 0 on the other input. 直覺上來說,如果輸入是x=4,y=2,那麼最大值是4,而函式對y的值並不敏感。如果我們增加一個很小的h,函式仍舊輸出4,所以梯度是0,也就是無影響。當然我們如果給y加上一個很大的數(比如比2大),f的值就會改變,但是導數並不會描述輸入在這麼大的變化下有什麼影響。他們只描述非常小的,接近於0的輸入,也就是定義中的limh0

用鏈式求導法則混合表示式

現在我們考慮更加複雜包括多種操作的表示式,例如f(x,y,z)=(x+y)z。這個表示式仍然簡單到可以直接區分出來,但是我們只是為理解反向傳播特別設計一個方法。這個表示式可以分成兩個表示式:q=x+y,f=qz。我們知道如何獨立地求出兩個表示式的導數。f只是qz的積,所以fq=zfz=q,並且qx,y的和,所以qx=1qy=1。但是我們並不會關心中間兩q的梯度,我們非常關心f以及他的輸入x,y,z的梯度。鏈式法則告訴我們如何正確地將梯度表示式合在一起。例如,fx=fqqx。在實際操作中,這只是一個簡單的兩個數相乘的例子。我們看下例:

# set some inputs
x = -2; y = 5; z = -4

# perform the forward pass
q = x + y # q becomes 3
f = q * z # f becomes -12

# perform the backward pass (backpropagation) in reverse order:
# first backprop through f = q * z
dfdz = q # df/dz = q, so gradient on z becomes 3
dfdq = z # df/dq = z, so gradient on q becomes -4
# now backprop through q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. And the multiplication here is the chain rule!
dfdy = 1.0 * dfdq # dq/dy = 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

最後我們將梯度儲存在變數中[dfdx,dfdy,dfdz],也就是告訴我們x,y,z對於函式f的敏感度。這是最簡單的反向傳播的例子。我們想要一些更加簡明的標記方法,這樣我們就不用一直寫df這種東西了。現在我們將dfdq簡化成dq,並且約定這個梯度始終對應最終輸出。

計算過程也可以很好的用電路圖表達出來


這裡寫圖片描述 
上圖中顯示了計算過程的電路圖。前向傳播從輸入開始計算輸出(綠色)。反向傳播則從最後開始遞迴地為每一個輸入應用鏈式法則計算梯度(紅色)。梯度可以看做沿著電路圖反向流動。


直觀理解反向傳播

反向傳播是非常精妙的操作,電路圖中的每個門都接收一些輸入,然後立刻計算兩個東西:1、輸出值,2、計算輸出對於輸入值的梯度。這些門的計算都是完全獨立的,不必瞭解整個電路圖中的分佈情況。但是,一旦前向傳播結束,在反向傳播的過程中,這些門會習得自己的輸出對於整個電路圖的輸出的梯度。鏈式法則中指出,這些門應將梯度乘進所有輸入梯度中。

這額外的乘操作(對每個輸入)是因為鏈式法則可以將一個簡單的相對無用的門轉變成一個複雜電路例如整個神經網路中的一個替代品。(This extra multiplication (for each input) due to the chain rule can turn a single and relatively useless gate into a cog in a complex circuit such as an entire neural network.)

我們再從例子裡理解這一切如何運作。“和”門接受兩個輸入[-2,5],計算輸出為3。由於這個門的計算為和運算,所以對每個輸入的梯度都是+1。剩下的部分則進行積運算,結果為-12。在反向遞迴計算梯度的過程中,和門(積門的一個輸入)學習到他對於輸出的梯度為-4。如果我們將這個電路圖人格化為想要輸出更高的值,那麼我們就希望和門輸出的結果要小一些,並且是4倍關係。接下來,和門將所有輸入都乘上梯度-4。如果x,y減小,在減小,和門輸出的結果是減小的,但是總輸出是增大的。 
反向傳播可以認為是不同的門之間的通訊,以決定他們是想讓輸出更高還是更低(還有多快地增高或降低),以影響最終輸出。

模組化:以Sigmoid為例

上面我們說到的門是胡編亂造的。任何可識別的函式都可以像門一樣運作,我們也可以將許多門放到一個門中去,或者將一個函式分解為若干個門。我們看下一個例子: 

f(w,x)=11+e(w0x0+w1x1+w2)

後面我們可以看到,這個式子描述了使用sigmoid的二維神經元(有輸入x以及權重w),但是現在我們把他想像成簡單的w,x輸入都是單個數字。這個函式由幾個門組成。上面只介紹了和,積以及最大操作,下面有一些其他的操作: 
f(x)=1xdfdx=1/x2

fc(x)=c+xdfdx=1

f(x)=exdfdx=ex

fa(x)=axdfdx=a

函式fa,fc用常量a倍乘了輸入,用常量c擴大了輸入。理論上他們是加與乘運算的特殊形式,但是我們以新的一元門引入他們,因為我們確實需要常量c和a的梯度。下面是完整的電路圖:


這裡寫圖片描述
以sigmoid為啟用函式的二維神經元電路圖。輸入是[x0,x1],可學習的 權重[w0,w1,w2]。後面我們會看到,神經元進行點積計算,然後啟用函式sigmoid將結果溫柔地壓進0到1之間。


在上面的例子中,我們看到了依據w,x點乘結果的一長串操作。這些操作實現的是叫sigmoid函式σ(x)。事實上,我們可以使用函式本身化簡它的導數: 

σ(x)=11+exdσ(x)dx=ex(1+ex)2=(1+ex11+ex)(11+ex)=(1σ(x))σ(x)

梯度變得異常的簡單。比如,sigmoid在前向傳播的時候獲得的輸入時1.0,得到的輸出是0.73。領域梯度(local gradient)就是(1-0.73)*0.73~=0.2,在前面曾經計算過一個表示式。所以,在實際操作中,將這些操作整合在一個門中是非常有效的。我們看這個神經元的反向傳播實現:

w = [2,-3,-3] # assume some random weights and data
x = [-1, -2]

# forward pass
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid function

# backward pass through the neuron (backpropagation)
ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
dx = [w[0] * ddot, w[1] * ddot] # backprop into x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
# we're done! we have the gradients on the inputs to the circuit
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

實現過程的提示:分段反向傳播 我們前面提到過,將前向傳播的計算結果儲存下來可以讓返向傳播更加容易實現。比如我們建立了中間變數dot,其中儲存了wx的點積結果。在反向傳播的過程中再依次(反向地)計算各個對應的儲存了各個梯度的變數(比如ddotdw,dx)。

這個部分的重點在於反向傳播的細節如何實現,我們哪些部分認作為門是出於方便的考量。為了更好的使用鏈式法則,用更少的程式碼將梯度整合在一起,我們應該瞭解哪些部分表示式可以很方便地得到梯度。

反向傳播實戰:分段計算

我們來看另一個例子。假設我們有如下表示式: 

f(x,y)=x+σ(y)σ(x)+(x+y)2

這裡交代一下,這個函式沒有任何實際意義,甚至你都不知道你為什麼要計算它的梯度,這裡用到它僅僅是因為它是一個反向傳播實際計算的好例子罷了。如果你陷入了對x或者y求導的泥潭裡,你會得到一個非常長,非常複雜的結果。但是這樣做是完全沒必要的,因為我們不需要一個明明確確的表示式來衡量梯度。我們只需要知道怎樣計算他們就好。下面就是我們計算前向傳播的過程:

x = 3 # example values
y = -4

# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # sigmoid in numerator   #(1)
num = x + sigy # numerator                               #(2)
sigx = 1.0 / (1 + math.exp(-x)) # sigmoid in denominator #(3)
xpy = x + y                                              #(4)
xpysqr = xpy**2                                          #(5)
den = sigx + xpysqr # denominator                        #(6)
invden = 1.0 / den                                       #(7)
f = num * invden # done!                                 #(8)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

哦喲~最後我們終於把前向傳播過程完成了。注意到我們儲存了許多中間變數,每個變數都是我們已知梯度的簡單表示式。因此,反向計算就很簡單了:我們沿著路一個一個變數的返回(sigy,num,sigx,xpy.xpysqr.den.invden)。我們使用d開頭的變數儲存該變數對於輸出結果的梯度。另外,注意我們計算的每一個反向傳播都會影響到最終梯度表達,並且我們使用乘操作將他們連線在一起。每一行我們都標註出對應的前向傳播過程:

# backprop f = num * invden
dnum = invden # gradient on numerator                             #(8)
dinvden = num                                                     #(8)
# backprop invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden                                #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden                                                #(6)
dxpysqr = (1) * dden                                              #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr                                        #(5)
# backprop xpy = x + y
dx = (1) * dxpy                                                   #(4)
dy = (1) * dxpy                                                   #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below  #(3)
# backprop num = x + sigy
dx += (1) * dnum                                                  #(2)
dsigy = (1) * dnum                                                #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy                                 #(1)
# done! phew
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

注意一些小問題:

將前向傳播過程中的變數儲存下來 前向傳播過程中的中間變數對於計算反向傳播非常有用。實際操作中需要將他們儲存下來,反向計算的時候拿出來用。如果不這麼做(或者這麼做很麻煩),重新計算他們一便就很浪費時間了。

梯度以叉狀疊加 前向表示式中出現好幾次x和y,所以我們要注意用+=操作去累計梯度,而不是=(否則就被覆蓋了)。這符合微積分中的多變數鏈式求導,如果一個變數出現在“電路圖”中的不同部分,迴流的梯度要相加。

逆流中的模式

很有趣的是,向後流動的梯度在很多時候都可以直觀地解釋出來。例如,神經網路中最常用的三個門(和,積,最大)都有非常簡單的理解方法。如下例:


這裡寫圖片描述 
示例電路圖:反向傳播計算過程中操作背後的直觀解釋。和操作的兩個操作元對於梯度的貢獻相同,求大操作的梯度依賴較大的輸入,積門則將操作元互換,並與梯度相乘。


從上面的示例中我們可以看到:

和門的不同輸入對於輸出的梯度總是相同的,不管前向傳播的時候變數的值是多少。這是因為和操作的區域性梯度是簡單的+1.0,因此所有的輸入對輸出的梯度都是相同的,因為他們的係數都是+1.0。上面的電路中,和門給到輸入的梯度都是2.00,相等且不變。 
求最大門則把梯度權值給了其中一個。與和門不同的是,他將梯度賦予了其中給一個輸入(前向傳播中有較大值的那個)。這是因為他的區域性梯度給最大值以+1.0的權值,而其他輸入則是0。例中變數zw大,所以梯度2.00賦予了zw仍然是0。 
積門不是很好解釋。它的區域性梯度是輸入(交換過的),並且在鏈式法則中與輸出的梯度相乘。上面的例子中,x的梯度是-8.00,也就是-4.00x2.00。

直觀的影響以及結果 注意到如果積門的某一個輸入很小,另一個很大。那麼它會做一些事情:它會給很小的值以很大的梯度,而給大值很小的梯度。線性分類器中,權重與輸入的計算是點乘wTxi,這就是說資料的大小對於權重梯度量級有影響。例如,將資料樣例xi乘以1000,那麼權重的梯度就會是原來的1000倍大,你就得減小學習率,以抵消這種情況。這就是為什麼預處理如此重要,有的時候還很微妙。直觀地理解梯度的傳播可以幫助你調整這些問題。

梯度的向量化操作

上面的部分都是單個變數,把概念擴充到矩陣和向量操作中,需要額外的注意量綱和轉置操作。 
矩陣相乘梯度 最投機取巧的操作可能就是矩陣矩陣相乘(包括矩陣向量相乘,向量向量相乘)了:

# forward pass
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)

# now suppose we had the gradient on D from above in the circuit
dD = np.random.randn(*D.shape) # same shape as D
dW = dD.dot(X.T) #.T gives the transpose of the matrix
dX = W.T.dot(dD)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

提示:使用量綱分析!你不必記住dWdX的表示式,因為他們很容易從維度(dimensions)中匯出來。例如,我們知道在計算過後,權重的梯度dW肯定和W形狀一樣,並且它一定依賴XdD的矩陣積。肯定有一個方法得到這種效果,所以量綱很管用。例如,X形狀為[10x3],dD形狀為[5x3],所以如果我們想要dWW有[5x10]的形狀,我們只能使用dD.dot(X.T)

研究小的、確定的例子 有的人可能發現向量化的表示式中很難匯出梯度用於更新引數。這裡推薦一些最小的向量化的例子,在紙上推匯出梯度,再將模式泛化到有效率的向量化的形式裡。



不懂的可以加我的QQ群:522869126(語音訊號處理) 歡迎你

到來哦,看了博文給點腳印唄,謝謝啦~~


相關文章