CS231n 2017 Lecture 4: Introduction to Neural Networks 隨堂筆記

本筆記中的所有取自課件的多媒體版權屬於斯坦福大學。


今天要講的東西是反向傳播和神經網路。計算任意複雜函數的解析梯度,要用到一個叫計算圖的框架。基本上計算圖就是可以用這類圖來表示任意函數,其中圖的節點表示要執行的每一步計算。例如之前講過的線性分類器的例子,輸入是X和W,相乘的節點表示矩陣乘法,即參數W和輸入X的乘積,輸出得分向量。還有另外一個計算節點表示hinge loss,計算損失項 L_{i} ,還有一個正則項在右下角。最後的損失項是W與X的乘積加上正則項的和。這麼做的好處是一旦能用計算圖來表示一個函數,就能使用反向傳播技術,遞歸地調用鏈式法則來計算計算圖中的每個變數的梯度。

當開始涉及到非常複雜的函數時,這些方法就非常有用。比如說後面會講到的卷積神經網路,這裡最上面的是輸入的圖片,輸入必須要經過中間的很多的轉換層才能最終到達最下面的損失函數。

又比如神經圖靈機的計算圖是更加的誇張,這是另外一種的深度學習模型,可以看到下圖中的計算圖特別的複雜,如果沿著時間來展開的話,你想要計算任意的中間變數的梯度,這幾乎是不切實際的。

我們從一個簡單的例子來描述反向傳播是如何運作的。假設有一個函數  f=(x+y)*z ,我們要找到函數輸出對應的任一變數的梯度。第一步是用計算圖來表示整個函數。這個例子的計算圖在右邊,可以看到首先是「add」節點,即x+y,然後是「multiple」節點,即 (x+y)*z 。現在要做這個網路的前向傳播,給定了每個變數及對應的值,這裡是x=-2,y=5,z=-4。將這些值寫到計算圖裡,然後計算得到中間值x+y=3,最後通過相乘的節點得到最後一個節點的值是-12。在這裡給每個中間變數一個名字,把加法的中間變數為q,然後是節點 f=q*z 用到了中間節點q。下面也把q對x和q對y的梯度 frac{partial{q}}{partial{x}}frac{partial{q}}{partial{y}} 寫出來了,分別都是1,f對q的梯度 frac{partial{f}}{partial{q}} 是z,f對z的梯度 frac{partial{f}}{partial{z}} 是q。現在我們要求f對x,y和z的梯度 frac{partial{f}}{partial{x}}frac{partial{f}}{partial{y}}frac{partial{f}}{partial{z}}

現在我們從計算圖的最後開始,從後往前計算所有的梯度。我們要計算輸出最後一個變數f的梯度,顯然f對f的梯度 frac{partial{f}}{partial{f}} 是1。

接下來從後往前,現在想計算在z上的梯度,從左邊看到梯度 frac{partial{f}}{partial{z}} 等於q,而q得值是3,所以 frac{partial{f}}{partial{z}} 等於3。

接著計算 frac{partial{f}}{partial{q}} ,從左邊看到是等於z,而z的值是-4。

繼續沿著計算圖從後往前計算,現在要計算 frac{partial{f}}{partial{y}} ,但在這種情況下關於y的梯度跟f並沒有直接的關係,但是它通過q這個中間節點與f連接。所以我們可以利用鏈式法則,也就是說 frac{partial{f}}{partial{y}} 可以寫作 frac{partial{f}}{partial{q}}frac{partial{q}}{partial{y}} 。這個想法是為了找到y對f的影響,現在已經知道了q對f的影響,接著把 frac{partial{f}}{partial{q}} 與y對q的影響相結合。從左邊看到 frac{partial{q}}{partial{y}} 是等於1,所以 frac{partial{f}}{partial{y}} 等於-4。

求解x上的梯度可以使用和求解y的梯度同樣的方法, frac{partial{f}}{partial{q}}frac{partial{q}}{partial{x}} 等於-4。

接下來做的是在反向傳播演算法中,我們有所有這些節點在計算圖中,每個節點只知道與它直接相連接的節點,所以在每個節點上,我們有連接這個節點的輸入,也就是流入這個節點的值,也有這個節點的直接的輸出。這裡輸入是x和y,輸出是z。我們也知道這個節點的梯度,可以計算出z在x的梯度( frac{partial{z}}{partial{x}} )和z在y的梯度( frac{partial{z}}{partial{y}} )。我們來看看反向傳播演算法中發生了什麼,從圖的最後開始使用我們的方法從最後一直到開始,當我們到達每一個節點,每一個節點都會得到從上游返回的梯度,而這個梯度是對這個節點的輸出的求導,當在反向傳播中到達這個節點,我們就計算出了最終的損失值L關於z的梯度。接下來是要去找到關於節點的輸入的梯度,即在x和y上的梯度。然後就是和上面的例子一樣,用鏈式法則去計算,根據鏈式法則,損失函數關於x的梯度就是 frac{partial{L}}{partial{z}} 乘以 frac{partial{z}}{partial{x}} 。在鏈式法則中,通常把上游的梯度值傳回來,再乘以本地梯度值,從而得到關於輸入的梯度值。類似地,損失函數關於x的梯度就是 frac{partial{L}}{partial{z}} 乘以 frac{partial{z}}{partial{y}}

一旦計算出這些結果,我們可以把結果傳遞到前面的節點或者是直接相連的節點。這裡主要的事情是要在每一個節點上計算所需本地梯度,然後跟蹤這個梯度在反向傳播過程中,接收從上游傳回來的這個梯度值,然後直接用這個值乘以本地梯度,這就得到我們想要傳回連接點的值。在下一個節點進行反向傳播時,就不用考慮除了直接相連的節點之外的任何東西。

下面是另外一個更複雜的例子,在這個例子中f是關於w和x的函數。通常第一步是把它轉換成一個計算圖,在這個計算圖中可以看出將w和x相乘,也就是 w_{0}*x_{0}+w_{1}*x_{1}+w_{2} 。然後將相加的結果取負值,再求關於它的自然指數再加上1,最後對這個結果整體取倒數。圖中的綠色數字是前向傳播中每個節點的計算結果值。這裡的底部有一些之後會用到的微分表達式。

現在要對這個計算圖進行反向傳播。首先從計算圖的最後開始,輸出最終變數在某個方向上的梯度是1(輸出對輸出的梯度,即 frac{partial{f}}{partial{f}} )。

然後反向進行下一步。可以知道傳回上游梯度值就是下圖的紅框中(也就是1),現在要找到本地梯度,這個節點是 frac{1}{x} ,所以 frac{partial{f}}{partial{x}}=-frac{1}{x^{2}} ,接著把它代入在前向傳播中得到的x的值1.37,最後關於這個變數的梯度就是 -frac{1}{1.37^{2}}	imes 1=-0.53

接著反向回到下一個節點,這時從上游傳回來的梯度是-0.53,因為這個節點是加1,參照下面紅框中的微分表達式,所有這裡的本地梯度是 frac{partial{f}}{partial{x}}=1 ,利用鏈式法則,上游傳回來的梯度乘以本地的梯度值,即 -0.53	imes 1=-0.53

繼續反向傳播的下一步,這裡有一個指數,參照下面紅框中的微分表達式,所有這裡的本地梯度是 frac{partial{f}}{partial{x}}=e^{x} ,這是從上游傳回來的梯度是-0.53,再乘以本地梯度 e^{-1} ,結果為-0.20。

到了下一個節點,這個節點乘以了-1,參照下面紅框中的微分表達式,所有這裡的本地梯度是 frac{partial{f}}{partial{x}}=-1 ,這是從上游傳回來的梯度是-0.20,再乘以本地梯度-1,結果為0.20。

下一個節點是加法運算的節點,這裡有兩個分支都與這個節點相連,這裡從上游傳回來的梯度是0.20,加法運算中對每個輸入梯度正好是1,所以這裡每個分支的本地梯度都是1,再乘以上游傳回來的梯度即為 1	imes 0.2=0.2

對於乘法節點,某個輸入的梯度值恰好是另一個輸入的值,因此對 w_{0} ,從上游傳回來的梯度是0.20,本地梯度是-1,所以結果為-0.20。類似對 x_{0} ,從上游傳回來的梯度是0.20,本地梯度是2,所以結果為0.40。

相比於推導任意參數的梯度的解析表達式,使用計算圖會更簡單的原因是。從上面的計算可以看出,只要得到的本地梯度的表達式,我們就可以往表達式中填充每個值,然後使用鏈式法則從後往前乘以這些值,得到對所有變數的梯度。因此這裡,我們也可以用相同的方法得到 w_{1}x_{1} 方向上的梯度是-0.40和-0.60。當創建這些計算圖示,我們可以用想要的任意間隔距離來定義計算節點。所以在這種情況下,可以把它分解成最簡單的形式,例如上面的例子中分解成加法和乘法運算。但實際上,只要能寫出節點的本地梯度,這是可以把一些節點組合在一起形成更複雜的節點。例如下面這個sigmoid函數,已經定義如右上角所示關於x的sigmoid函數,我們可以寫出這個梯度的表達式並計算它的梯度。接著我們可以得到計算圖中構成這個sigmoid函數的計算節點,然後用一個大的sigmoid節點替換掉。這就說明了只要能寫出本地梯度,你可以組合一些稍微複雜的任意節點。這也是想用多少數學計算去得到一個簡潔的計算圖和想要每個梯度有多簡單之間的權衡。

現在我們把下圖的節點組合成一個新的sigmoid函數節點,這實際上是等價的,我們可以把數值代入到表達式中,這裡sigmoid函數的輸入是0.73,新的sigmoid節點的本地梯度表達式是 (1-x)*x ,所以可以得到這個梯度的結果是0.2,這和之前沒有組合之前的計算結果是一樣的。

又到了問題時間,如果節點是求兩個分支的最大值,那麼梯度是什麼?例如下面的例子中,z是2,w是-1,兩個取最大值得到輸出是2,如果現在對max節點的梯度的上游梯度是2,可以把這個想像成一個梯度路由器,加法節點回傳相同的梯度給進來的兩個分支,max節點將獲取梯度並且將其回傳到它其中的值較大的一個分支。這是因為如果查看前向傳播,可以看到只有最大值能被傳遞到後面的計算圖。所以只有這個最大值真正影響到函數計算的唯一值,反向傳播的時候也只去通過這個計算分支來調整它。

對於乘法節點的本地梯度基本上是另一個變數的值。這裡可以把它認為是一個梯度轉換器,我們獲取上游梯度然後根據另外一個分支對其進行縮放。

另外一個需要說明的事情是當有一個節點連接到多個節點時,梯度會在這個節點累加。在這些分支上,根據多元鏈式法則,我們只會獲取這些每個節點返回的上游梯度值,並將它們加起來,得到回傳到這個節點上的總的上游的梯度。如果要對這個節點進行一點改變,當正通過這個圖進行前向傳播時,它會在前向傳播中影響到所有連接到這個節點的節點。然後當進行反向傳播的時候,所有傳回的梯度都會影響到這個節點。

上面講述的是變數是一維時候的情況,現在來討論一下變數是高維向量的情況。假設有x,y,z三個向量,這時整個計算流程還是一樣的,唯一的區別在於剛才的梯度變成了Jacobian矩陣,所以現在是一個包含了每個變數里各元素導數的矩陣,比如z在每個x元素方向上的梯度。

再看下面一個例子,有一個4096個元素的向量作為輸入,中間的運算節點時對每個元素求最大值的運算,具體是輸入向量中的每個元素和零之間最大值,最後的輸入也是一個包含4096個元素。

問題來了,在這種情況下,Jacobian矩陣的size是多少?Jacobian矩陣每一行都是偏導數,矩陣的每個元素是輸出向量的每個元素對輸入向量每個元素分別求偏導數的結果,所以答案是4096×4096。

在實際應用中會遇到更大的Jacobian矩陣,因為要進行mini batch例如一次同時有100個變數輸入,將這些放到運算節點以提高運算效率,那這個Jacobian矩陣的行和列都要再擴大100倍,這時的Jacobian矩陣就是409600×409600,這樣大規模的矩陣運算起來非常困難。

所以實際運算時,多數情況下不需要計算這麼大的Jacobian矩陣,根據Jacobian矩陣的特點,如果我們思考一下這裡的計算,是對每個元素求最大值,想想每個編導計算的結果,輸入的某個元素和輸出的某個元素之間有什麼對應關係。因為是對每個元素分別運算,比如輸入的第一個元素只與輸出里的第一個元素有聯繫,所以算出的Jacobian矩陣將會是一個對角矩陣。實際上不需要把整個矩陣全部寫出來,只要求輸出向量關於x的偏導,然後把結果作為梯度填入其中。

下面來看看一個更具體的例子,有關W和x函數等於W與x的乘積的L2範數,這裡假設x是一個n維向量,W是一個 n	imes n 的矩陣。第一步先把計算圖寫出來,然後對變數賦上一些值,例如W是一個 2	imes 2 的矩陣,x是一個二維向量,W與x相乘後的中間結果記作q,q等於W乘以x可以寫成元素相乘的形式,現在可以用q表示出f,對第二個節點有 f(q) 等於q的L2範數,也就是等於 q_{1}^2+q_{2}^2 ,接著把計算結果填入計算圖中得到最後的輸出結果0.116。

現在開始計算反向傳播,首先輸出關於輸出的梯度是1,接著反向前進到下一個節點,這裡q是一個二維向量,我們想找到q的每個元素是怎樣影響f最後的值。先來觀察一下頁面底部f的表達式,可以看到f在特定的 q_{i} 方向上的梯度正好是兩倍的 q_{i} 。我們有表達式關於每個 q_{i} 元素的,寫出它的向量形式,即為2倍的q向量。所以我們得到梯度是0.44和0.52組成的向量,這裡向量的梯度總是和原向量保持相同大小,每個梯度的元素代表著這個特定元素對最後函數的影響。

現在計算關於W的梯度,想要計算q對w的本地梯度,先看看w的每個元素對q的每個元素的影響,根據前面所講的這裡梯度應該是一個Jacobian矩陣。在這個乘法公式中q等於W乘以x,q的第一個元素對應 W_{11} ,所以 q_{1}W_{11} 的梯度結果是 x_{1} 。我們可以把這個推廣到關於 w_{ij}q_{k} 的梯度為 x_{j} ,如果要想找到f對每個 w_{ij} 的梯度,還是使用鏈式法則,得到梯度的表示式為 2q_{i}x_{j} ,或者是向量形式 
abla_{W}f=2qcdot x^{T} 。要記著一個很重要的事情是要檢查變數梯度的向量大小要和變數向量的大小要保持一致,這在實際中是非常有用的完備性檢查(這裡1是指示函數,如果k=i就是1,否則為0)。

最後是計算f對x的梯度,依舊是利用鏈式法則得到下面的表達式,或者向量形式,代入向量q和矩陣W的值得到結果,可以看到這個梯度的向量也是和向量x的大小保持一致。

這樣的計算是非常像模塊化計算。在計算圖中觀察每個本地節點並計算本地梯度,然後使用鏈式法則將上游的梯度反向傳回。下面的代碼可以看作是一個前向和反向傳播的API,在前向傳播的過程中計算節點的輸出,然後在反向傳播過程中計算梯度。對整個計算圖,可以迭代圖中的所有節點得到整個圖的前向傳播,接著按照順序的拓撲結構運算所有的輸入。然後反向傳播以相反的方向通過所有的節點,從而得到每個節點的反向傳播。

下面再來看看偏導數的運行,例如輸入x乘以y,輸出z,然後反向傳播是輸入上游梯度dz,輸出是從x和y上傳遞下去的梯度dx和dy。在下面的代碼中可以看到我們需要緩存前向傳播時候的數值,因為這些數值在反向傳播計算中會被多次用到。

其實在很多深度學習的框架中都使用了這樣的模塊化設計,例如Caffe是一個流行的深度學習框架(在當前2017年目測已經不那麼流行了),可以看到它的源碼在層與層之間都是基本的計算節點,當然裡面會有更多的層數和一些更複雜的計算節點,基本上就是各種不同計算節點的列表。這些在我們構建整個網路的時候並進行前向和反向計算的時候都會被調用。

下面是Caffe中的sigmoid函數的源碼,可以看到藍色框的是根據sigmoid表達式的前向計算,紅色框是將上游梯度top_diff作為輸入與本地梯度相乘的反向傳播計算。

下面是反向傳播的一個小結。

前面已經講述了很多關於這種線性分類器的函數 f=Wx ,我們將它作為需要優化的函數來使用。如果依然想在神經網路里用最簡單的形式,那隻需將這兩個變換結合在一起,最上層有一個線性變換,然後和另外一個構成兩層神經網路。這個和第一個變換很相似, W_{1} 乘以x的結果作為中間變數,然後是0和中間變數取最大值的非線性變換,這些非線性計算是非常重要的,因為如果只是在最上面的一層將線性層組合在一起,也只能起到一個線性函數的作用,最後通過一個得分函數輸出得分向量。一般來說,神經網路就是由一組簡單的函數構成,用一種層次化的方式將它們堆疊起來,從而形成一個更複雜的非線性函數。

在前面介紹的線性分類模型中,權值矩陣W的每一行相當於一個分類表達的模板,這裡的問題是只有一個範本,但是在實際中卻會有多種樣式。所以這種多層網路中的 W_{1} 還是各種範本的集合,但是中間變數h對這些範本都有相應的得分,然後下面的一層將這些得分組合起來。例如 W_{1} 中包含了左側臉高分和右側臉的馬的範圍,而x是左側臉的馬,在h中左高右低, W_{2} 是對所有範本的得分加權求和,如果有一類的得分較高,其他的得分較低的組合對於馬都會有一個較高的得分。因此如果要找車的類別,那麼這個類別和不同顏色的車應該都有聯繫,因為矩陣 W_{2} 是h裡面所有向量的權值(所有範本的加權,讓你可以在多個範本中權衡,來得到特定分類的最後得分)。

前面的例子講了兩層神經網路,但實際上可以堆疊更多層得到任意深度的神經網路。例如我們可以重複非線性變換max函數這一過程,就能夠得到一個三層的神經網路。

下圖是一個簡單的兩層神經網路的前向和反向傳播的代碼,20行代碼搞掂。

下面是講述神經網路如何從生物學中獲得靈感,其實這種比喻是不嚴謹的,當中的聯繫也並不緊密。簡單來說,神經元,脈衝信號傳向各個神經元,多個神經元相互連接,每個神經元有多個樹突接收傳給神經元的脈衝。然後是細胞體集中處理這些傳入的信號並整合再傳出去,脈衝信號通過軸突從細胞體傳向連接的下游的神經元。

現在回到神經網路,對每個計算節點的方法與上面描述的是有點類似的。計算圖裡的節點相互連接,把信號x傳入神經元,所有的x例如 x_{0}x_{1}x_{2} 等採用例如賦予權值W的方法加權求和,再把結果放到激活函數。我們把激活函數應用在神經元的端部,其計算結果值作為輸出,把該值傳輸到和相連的神經元,

當然在進行這樣的類比是要注意到生物學上的神經元實際上要比剛才描述的要複雜得多,生物學的神經元有很多不同種類,它們的樹突會表現出非常複雜的非線性機制,而不是像前面講到神經網路中只有簡單的權值。

下面是一個兩層神經網路的前向傳播代碼的簡要實現,具體的計算細節可以查看代碼注釋。

最後這個是神經網路的小結。


推薦閱讀:

TAG:深度學習DeepLearning | 計算機視覺 |