- 深度學習進階:自然語言處理
- (日)齋藤康毅
- 7781字
- 2021-02-07 09:25:56
1.3 神經網絡的學習
不進行神經網絡的學習,就做不到“好的推理”。因此,常規的流程是,首先進行學習,然后再利用學習好的參數進行推理。所謂推理,就是對上一節介紹的多類別分類等問題給出回答的任務。而神經網絡的學習的任務是尋找最優參數。本節我們就來研究神經網絡的學習。
1.3.1 損失函數
在神經網絡的學習中,為了知道學習進行得如何,需要一個指標。這個指標通常稱為損失(loss)。損失指示學習階段中某個時間點的神經網絡的性能。基于監督數據(學習階段獲得的正確解數據)和神經網絡的預測結果,將模型的惡劣程度作為標量(單一數值)計算出來,得到的就是損失。
計算神經網絡的損失要使用損失函數(loss function)。進行多類別分類的神經網絡通常使用交叉熵誤差(cross entropy error)作為損失函數。此時,交叉熵誤差由神經網絡輸出的各類別的概率和監督標簽求得。
現在,我們來求一下之前一直在研究的那個神經網絡的損失。這里,我們將Softmax層和Cross Entropy Error層新添加到網絡中。用Softmax層求Softmax函數的值,用Cross Entropy Error層求交叉熵誤差。如果基于“層視角”來繪制此時的網絡結構,則如圖1-12所示。

圖1-12 使用了損失函數的神經網絡的層結構
在圖1-12中,X是輸入數據,t是監督標簽,L是損失。此時,Softmax層的輸出是概率,該概率和監督標簽被輸入Cross Entropy Error層。
下面,我們來介紹一下Softmax函數和交叉熵誤差。首先,Softmax函數可由下式表示:

式(1.6)是當輸出總共有n個時,計算第k個輸出yk時的算式。這個yk是對應于第k個類別的Softmax函數的輸出。如式(1.6)所示,Softmax函數的分子是得分sk的指數函數,分母是所有輸入信號的指數函數的和。
Softmax函數輸出的各個元素是0.0~1.0的實數。另外,如果將這些元素全部加起來,則和為1。因此,Softmax的輸出可以解釋為概率。之后,這個概率會被輸入交叉熵誤差。此時,交叉熵誤差可由下式表示:

這里,tk是對應于第k個類別的監督標簽。log是以納皮爾數e為底的對數(嚴格地說,應該記為loge)。監督標簽以one-hot向量的形式表示,比如t=(0, 0, 1)。
one-hot向量是一個元素為1,其他元素為0的向量。因為元素1對應正確解的類,所以式(1.7)實際上只是在計算正確解標簽為1的元素所對應的輸出的自然對數(log)。
另外,在考慮了mini-batch處理的情況下,交叉熵誤差可以由下式表示:

這里假設數據有N筆,tnk表示第n筆數據的第k維元素的值,ynk表示神經網絡的輸出,tnk表示監督標簽。
式(1.8)看上去有些復雜,其實只是將表示單筆數據的損失函數的式(1.7)擴展到了N筆數據的情況。用式(1.8)除以N,可以求單筆數據的平均損失。通過這樣的平均化,無論mini-batch的大小如何,都始終可以獲得一致的指標。
本書將計算Softmax函數和交叉熵誤差的層實現為Softmax with Loss層(通過整合這兩個層,反向傳播的計算會變簡單)。因此,學習階段的神經網絡具有如圖1-13所示的層結構。

圖1-13 使用Softmax with Loss層輸出損失
如圖1-13所示,本書使用Softmax with Loss層。這里我們省略對其實現的說明,代碼在common/layers.py中,感興趣的讀者可以參考。此外,前作《深度學習入門:基于Python的理論與實現》的4.2節中也詳細介紹了Softmax with Loss層。
1.3.2 導數和梯度
神經網絡的學習的目標是找到損失盡可能小的參數。此時,導數和梯度非常重要。這里我們來簡單說明一下導數和梯度。
現在,假設有一個函數y=f(x)。此時,y關于x的導數記為。這個
的意思是變化程度,具體來說,就是x的微?。▏栏竦刂v,“微小”為無限?。┳兓瘯е?span id="uegt3ku" class="italic">y發生多大程度的變化。
比如函數y=x2,其導數可以解析性地求出,即。這個導數結果表示x在各處的變化程度。實際上,如圖1-14所示,它相當于函數的斜率。

圖1-14 y=x2的導數表示x在各處的斜率
在圖1-14中,我們求了關于x這一個變量的導數,其實同樣可以求關于多個變量(多變量)的導數。假設有函數L=f(x),其中L是標量,x是向量。此時,L關于xi(x的第i個元素)的導數可以寫成
。另外,也可以求關于向量的其他元素的導數,我們將其整理如下:

像這樣,將關于向量各個元素的導數羅列到一起,就得到了梯度(gradient)。
另外,矩陣也可以像向量一樣求梯度。假設W是一個m×n的矩陣,則函數L=g(W)的梯度如下所示:

如式(1.10)所示,L關于W的梯度可以寫成矩陣(準確地說,矩陣的梯度的定義如上所示)。這里的重點是,W和具有相同的形狀。利用“矩陣和其梯度具有相同形狀”這一性質,可以輕松地進行參數的更新和鏈式法則(后述)的實現。
嚴格地說,本書使用的“梯度”一詞與數學中的“梯度”是不同的。數學中的梯度僅限于關于向量的導數。而在深度學習領域,一般也會定義關于矩陣和張量的導數,稱為“梯度”。
1.3.3 鏈式法則
學習階段的神經網絡在給定學習數據后會輸出損失。這里我們想得到的是損失關于各個參數的梯度。只要得到了它們的梯度,就可以使用這些梯度進行參數更新。那么,神經網絡的梯度怎么求呢?這就輪到誤差反向傳播法出場了。
理解誤差反向傳播法的關鍵是鏈式法則。鏈式法則是復合函數的求導法則,其中復合函數是由多個函數構成的函數。
現在,我們來學習鏈式法則。這里考慮y=f(x)和z=g(y)這兩個函數。如z=g(f(x))所示,最終的輸出z由兩個函數計算而來。此時,z關于x的導數可以按下式求得:

如式(1.11)所示,z關于x的導數由y=f(x)的導數和z=g(y)的導數之積求得,這就是鏈式法則。鏈式法則的重要之處在于,無論我們要處理的函數有多復雜(無論復合了多少個函數),都可以根據它們各自的導數來求復合函數的導數。也就是說,只要能夠計算各個函數的局部的導數,就能基于它們的積計算最終的整體的導數。
可以認為神經網絡是由多個函數復合而成的。誤差反向傳播法會充分利用鏈式法則來求關于多個函數(神經網絡)的梯度。
1.3.4 計算圖
下面,我們將研究誤差反向傳播法。不過在此之前,作為準備工作,我們先來介紹一下計算圖的相關內容。計算圖是計算過程的圖形表示。圖1-15所示為計算圖的一個例子。

圖1-15 z=x+y的計算圖
如圖1-15所示,計算圖通過節點和箭頭來表示。這里,“+”表示加法,變量x和y寫在各自的箭頭上。像這樣,在計算圖中,用節點表示計算,處理結果有序(本例中是從左到右)流動。這就是計算圖的正向傳播。
使用計算圖,可以直觀地把握計算過程。另外,這樣也可以直觀地求梯度。這里重要的是,梯度沿與正向傳播相反的方向傳播,這個反方向的傳播稱為反向傳播。
這里我想先說明一下反向傳播的全貌。雖然我們處理的是z=x+y這一計算,但是在該計算的前后,還存在其他的“某種計算”(圖1-16)。另外,假設最終輸出的是標量L(在神經網絡的學習階段,計算圖的最終輸出是損失,它是一個標量)。

圖1-16 加法節點構成“復雜計算”的一部分
我們的目標是求L關于各個變量的導數(梯度)。這樣一來,計算圖的反向傳播就可以繪制成圖1-17。

圖1-17 計算圖的反向傳播
如圖1-17所示,反向傳播用藍色的粗箭頭表示,在箭頭的下方標注傳播的值。此時,傳播的值是指最終的輸出L關于各個變量的導數。在這個例子中,關于z的導數是,關于x和y的導數分別是
和
。
接著,該鏈式法則出場了。根據剛才復習的鏈式法則,反向傳播中流動的導數的值是根據從上游(輸出側)傳來的導數和各個運算節點的局部導數之積求得的。因此,在上面的例子中,,
。
這里,我們來處理z=x+y這個基于加法節點的運算。此時,分別解析性地求得,
。因此,如圖1-18所示,加法節點將上游傳來的值乘以1,再將該梯度向下游傳播。也就是說,只是原樣地將從上游傳來的梯度傳播出去。

圖1-18 加法節點的正向傳播(左圖)和反向傳播(右圖)
像這樣,計算圖直觀地表示了計算過程。另外,通過觀察反向傳播的梯度的流動,可以幫助我們理解反向傳播的推導過程。
在構成計算圖的運算節點中,除了這里見到的加法節點之外,還有很多其他的運算節點。下面,我們將介紹幾個典型的運算節點。
1.3.4.1 乘法節點
乘法節點是z=x×y這樣的計算。此時,導數可以分別求出,即和
。因此,如圖1-19所示,乘法節點的反向傳播會將“上游傳來的梯度”乘以“將正向傳播時的輸入替換后的值”。

圖1-19 乘法節點的正向傳播(左圖)和反向傳播(右圖)
另外,在目前為止的加法節點和乘法節點的介紹中,流過節點的數據都是“單變量”。但是,不僅限于單變量,也可以是多變量(向量、矩陣或張量)。當張量流過加法節點(或者乘法節點)時,只需獨立計算張量中的各個元素。也就是說,在這種情況下,張量的各個元素獨立于其他元素進行對應元素的運算。
1.3.4.2 分支節點
如圖1-20所示,分支節點是有分支的節點。

圖1-20 分支節點的正向傳播(左圖)和反向傳播(右圖)
嚴格來說,分支節點并沒有節點,只有兩根分開的線。此時,相同的值被復制并分叉。因此,分支節點也稱為復制節點。如圖1-20所示,它的反向傳播是上游傳來的梯度之和。
1.3.4.3 Repeat節點
分支節點有兩個分支,但也可以擴展為N個分支(副本),這里稱為Repeat節點。現在,我們嘗試用計算圖繪制一個Repeat節點(圖1-21)。

圖1-21 Repeat節點的正向傳播(上圖)和反向傳播(下圖)
如圖1-21所示,這個例子中將長度為D的數組復制了N份。因為這個Repeat節點可以視為N個分支節點,所以它的反向傳播可以通過N個梯度的總和求出,如下所示。
>>> import numpy as np >>> D , N = 8 , 7 >>> x = np.random.randn(1 , D) # 輸入 >>> y = np.repeat(x , N , axis=0) # 正向傳播 >>> dy = np.random.randn(N , D) # 假設的梯度 >>> dx = np.sum(dy , axis=0 , keepdims=True) # 反向傳播
這里通過np.repeat()方法進行元素的復制。上面的例子中將復制N次數組x。通過指定axis,可以指定沿哪個軸復制。因為反向傳播時要計算總和,所以使用NumPy的sum()方法。此時,通過指定axis來指定對哪個軸求和。另外,通過指定keepdims=True,可以維持二維數組的維數。在上面的例子中,當keepdims=True時,np.sum()的結果的形狀是(1, D);當keepdims=False時,形狀是(D,)。
NumPy的廣播會復制數組的元素。這可以通過Repeat節點來表示。
1.3.4.4 Sum節點
Sum節點是通用的加法節點。這里考慮對一個N×D的數組沿第0個軸求和。此時,Sum節點的正向傳播和反向傳播如圖1-22所示。

圖1-22 Sum節點的正向傳播(上圖)和反向傳播(下圖)
如圖1-22所示,Sum節點的反向傳播將上游傳來的梯度分配到所有箭頭上。這是加法節點的反向傳播的自然擴展。下面,和Repeat節點一樣,我們也來展示一下Sum節點的實現示例,如下所示。
>>> import numpy as np >>> D , N = 8 , 7 >>> x = np.random.randn(N , D) # 輸入 >>> y = np.sum(x , axis=0 , keepdims=True) # 正向傳播 >>> dy = np.random.randn(1 , D) # 假設的梯度 >>> dx = np.repeat(dy , N , axis=0) # 反向傳播
如上所示,Sum節點的正向傳播通過np.sum()方法實現,反向傳播通過np.repeat()方法實現。有趣的是,Sum節點和Repeat節點存在逆向關系。所謂逆向關系,是指Sum節點的正向傳播相當于Repeat節點的反向傳播,Sum節點的反向傳播相當于Repeat節點的正向傳播。
1.3.4.5 MatMul節點
本書將矩陣乘積稱為MatMul節點。MatMul是Matrix Multiply的縮寫。因為MatMul節點的反向傳播稍微有些復雜,所以這里我們先進行一般性的介紹,再進行直觀的解釋。
為了解釋MatMul節點,我們來考慮y=xW這個計算。這里,x、W、y的形狀分別是1×D、D×H、1×H(圖1-23)。

圖1-23 MatMul節點的正向傳播:矩陣的形狀顯示在各個變量的上方
此時,可以按如下方式求得關于x的第i個元素的導數。

式(1.12)的表示變化程度,即當xi發生微小的變化時,L會有多大程度的變化。如果此時改變xi,則向量y的所有元素都會發生變化。另外,因為y的各個元素會發生變化,所以最終L也會發生變化。因此,從xi到L的鏈式法則的路徑有多個,它們的和是
。
式(1.12)仍可進一步簡化。利用,將其代入式(1.12):

由式(1.13)可知,由向量
和W的第i行向量的內積求得。從這個關系可以導出下式:

如式(1.14)所示,可由矩陣乘積一次求得。這里,WT的T表示轉置矩陣。對式(1.14)進行形狀檢查,結果如圖1-24所示。

圖1-24 矩陣乘積的形狀檢查
如圖1-24所示,矩陣形狀的演變是正確的。由此,可以確認式(1.14)的計算是正確的。然后,我們可以反過來利用它(為了保持形狀合規)來推導出反向傳播的數學式(及其實現)。為了說明這個方法,我們再次考慮矩陣乘積的計算y=xW。不過,這次考慮mini-batch處理,假設x中保存了N筆數據。此時,x、W、y的形狀分別是N×D、D×H、N×H,反向傳播的計算圖如圖1-25所示。

圖1-25 MatMul節點的反向傳播
那么,將如何計算呢?此時,和
相關的變量(矩陣)是上游傳來的
和W。為什么說和W有關系呢?考慮到乘法的反向傳播的話,就容易理解了。因為乘法的反向傳播中使用了“將正向傳播時的輸入替換后的值”。同理,矩陣乘積的反向傳播也使用“將正向傳播時的輸入替換后的矩陣”。之后,留意各個矩陣的形狀求矩陣乘積,使它們的形狀保持合規。如此,就可以導出矩陣乘積的反向傳播,如圖1-26所示。
如圖1-26所示,通過確認矩陣的形狀,可以推導矩陣乘積的反向傳播的數學式。這樣一來,我們就推導出了MatMul節點的反向傳播。現在我們將MatMul節點實現為層,如下所示(common/layers.py)。

圖1-26 通過確認矩陣形狀,推導反向傳播的數學式
class MatMul: def__init__(self, W): self.params = [W] self.grads = [np.zeros_like(W)] self.x = None def forward(self, x): W, = self.params out = np.dot(x, W) self.x = x return out def backward(self, dout): W, = self.params dx = np.dot(dout, W.T) dW = np.dot(self.x.T, dout) self.grads[0][...] = dW return dx
MatMul層在params中保存要學習的參數。另外,以與其對應的形式,將梯度保存在grads中。在反向傳播時求dx和dw,并在實例變量grads中設置權重的梯度。
另外,在設置梯度的值時,像grads[0][...] = dW這樣,使用了省略號。由此,可以固定NumPy數組的內存地址,覆蓋NumPy數組的元素。
和省略號一樣,這里也可以進行基于grads[0] = dW的賦值。不同的是,在使用省略號的情況下會覆蓋掉NumPy數組。這是淺復制(shallow copy)和深復制(deep copy)的差異。grads[0] = dW的賦值相當于淺復制,grads[0][...] = dW的覆蓋相當于深復制。
省略號的話題稍微有些復雜,我們舉個例子來說明。假設有a和b兩個NumPy數組。
>>> a = np.array([1 , 2 , 3]) >>> b = np.array([4 , 5 , 6])
這里,不管是a = b,還是a[...] = b,a都被賦值[4,5,6]。但是,此時a指向的內存地址不同。我們將內存(簡化版)可視化,如圖1-27所示。

圖1-27 a=b和a[...]=b的區別:使用省略號時數據被覆蓋,變量指向的內存地址不變
如圖1-27所示,在a = b的情況下,a指向的內存地址和b一樣。由于實際的數據(4,5,6)沒有被復制,所以這可以說是淺復制。而在a[...] = b時,a的內存地址保持不變,b的元素被復制到a指向的內存上。這時,因為實際的數據被復制了,所以稱為深復制。
由此可知,使用省略號可以固定變量的內存地址(在上面的例子中,a的地址是固定的)。通過固定這個內存地址,實例變量grads的處理會變簡單。
在grads列表中保存各個參數的梯度。此時,grads列表中的各個元素是NumPy數組,僅在生成層時生成一次。然后,使用省略號,在不改變NumPy數組的內存地址的情況下覆蓋數據。這樣一來,將梯度匯總在一起的工作就只需要在開始時進行一次即可。
以上就是MatMul層的實現,代碼在common/layers.py中。
1.3.5 梯度的推導和反向傳播的實現
計算圖的介紹結束了,下面我們來實現一些實用的層。這里,我們將實現Sigmoid層、全連接層Affine層和Softmax with Loss層。
1.3.5.1 Sigmoid層
sigmoid函數由表示,sigmoid函數的導數由下式表示。

根據式(1.15),Sigmoid層的計算圖可以繪制成圖1-28。這里,將輸出側的層傳來的梯度()乘以sigmoid函數的導數(
),然后將這個值傳遞給輸入側的層。

圖1-28 Sigmoid層的計算圖
這里,我們省略sigmoid函數的偏導數的推導過程。相關內容會在附錄A中介紹,感興趣的讀者可以參考一下。
接下來,我們使用Python來實現Sigmoid層。參考圖1-28,可以像下面這樣進行實現(common/layers.py)。
class Sigmoid: def__init__(self): self.params, self.grads = [], [] self.out = None def forward(self, x): out = 1 / (1 + np.exp(-x)) self.out = out return out def backward(self, dout): dx = dout * (1.0 - self.out) * self.out return dx
這里將正向傳播的輸出保存在實例變量out中。然后,在反向傳播中,使用這個out變量進行計算。
1.3.5.2 Affine層
如前所示,我們通過y = np.dot(x, W) + b實現了Affine層的正向傳播。此時,在偏置的加法中,使用了NumPy的廣播功能。如果明示這一點,則Affine層的計算圖如圖1-29所示。
如圖1-29所示,通過MatMul節點進行矩陣乘積的計算。偏置被Repeat節點復制,然后進行加法運算(可以認為NumPy的廣播功能在內部進行了Repeat節點的計算)。下面是Affine層的實現(common/layers.py)。

圖1-29 Affine層的計算圖
class Affine: def__init__(self, W, b): self.params = [W, b] self.grads = [np.zeros_like(W), np.zeros_like(b)] self.x = None def forward(self, x): W, b = self.params out = np.dot(x, W) + b self.x = x return out def backward(self, dout): W, b = self.params dx = np.dot(dout, W.T) dW = np.dot(self.x.T, dout) db = np.sum(dout, axis=0) self.grads[0][...] = dW self.grads[1][...] = db return dx
根據本書的代碼規范,Affine層將參數保存在實例變量params中,將梯度保存在實例變量grads中。它的反向傳播可以通過執行MatMul節點和Repeat節點的反向傳播來實現。Repeat節點的反向傳播可以通過np.sum()計算出來,此時注意矩陣的形狀,就可以清楚地知道應該對哪個軸(axis)求和。最后,將權重參數的梯度設置給實例變量grads。以上就是Affine層的實現。
使用已經實現的MatMul層,可以更輕松地實現Affine層。這里出于復習的目的,沒有使用MatMul層,而是使用NumPy的方法進行了實現。
1.3.5.3 Softmax with Loss層
我們將Softmax函數和交叉熵誤差一起實現為Softmax with Loss層。此時,計算圖如圖1-30所示。

圖1-30 Softmax with Loss層的計算圖
圖1-30的計算圖將Softmax函數記為Softmax層,將交叉熵誤差記為Cross Entropy Error層。這里假設要執行3類別分類的任務,從前一層(靠近輸入的層)接收3個輸入。
如圖1-30所示,Softmax層對輸入a1, a2, a3進行正規化,輸出y1, y2, y3。Cross Entropy Error層接收Softmax的輸出y1, y2, y3和監督標簽t1, t2, t3,并基于這些數據輸出損失L。
在圖1-30中,需要注意的是反向傳播的結果。從Softmax層傳來的反向傳播有y1-t1, y2-t2, y3-t3這樣一個很“漂亮”的結果。因為y1, y2, y3是Softmax層的輸出,t1, t2, t3是監督標簽,所以y1-t1, y2-t2, y3-t3是Softmax層的輸出和監督標簽的差分。神經網絡的反向傳播將這個差分(誤差)傳給前面的層。這是神經網絡的學習中的一個重要性質。
這里我們省略對Softmax with Loss層的實現的說明,具體代碼在common/layers.py中。另外,Softmax with Loss層的反向傳播的推導過程在前作《深度學習入門:基于Python的理論與實現》的附錄A中有詳細說明,感興趣的讀者可以參考一下。
1.3.6 權重的更新
通過誤差反向傳播法求出梯度后,就可以使用該梯度更新神經網絡的參數。此時,神經網絡的學習按如下步驟進行。
· 步驟1:mini-batch
從訓練數據中隨機選出多筆數據。
· 步驟2:計算梯度
基于誤差反向傳播法,計算損失函數關于各個權重參數的梯度。
· 步驟3:更新參數
使用梯度更新權重參數。
· 步驟4:重復
根據需要重復多次步驟1、步驟2和步驟3。
我們按照上面的步驟進行神經網絡的學習。首先,選擇mini-batch數據,根據誤差反向傳播法獲得權重的梯度。這個梯度指向當前的權重參數所處位置中損失增加最多的方向。因此,通過將參數向該梯度的反方向更新,可以降低損失。這就是梯度下降法(gradient descent)。之后,根據需要將這一操作重復多次即可。
我們在上面的步驟3中更新權重。權重更新方法有很多,這里我們來實現其中最簡單的隨機梯度下降法(Stochastic Gradient Descent,SGD)。其中,“隨機”是指使用隨機選擇的數據(mini-batch)的梯度。
SGD是一個很簡單的方法。它將(當前的)權重朝梯度的(反)方向更新一定距離。如果用數學式表示,則有:

這里將要更新的權重參數記為W,損失函數關于W的梯度記為。η表示學習率,實際上使用0.01、0.001等預先定好的值。
現在,我們來進行SGD的實現。這里考慮到模塊化,將進行參數更新的類實現在common/optimizer.py中。除了SGD之外,這個文件中還有AdaGrad和Adam等的實現。
進行參數更新的類的實現擁有通用方法update(params, grads)。這里,在參數params和grads中分別以列表形式保存了神經網絡的權重和梯度。此外,假定params和grads在相同索引處分別保存了對應的參數和梯度。這樣一來,SGD就可以像下面這樣實現(common/optimizer.py)。
class SGD: def__init__(self, lr=0.01): self.lr = lr def update(self, params, grads): for i in range(len(params)): params[i] -= self.lr * grads[i]
初始化參數lr表示學習率(learning rate)。這里將學習率保存為實例變量。然后,在update(params, grads)方法中實現參數的更新處理。
使用這個SGD類,神經網絡的參數更新可按如下方式進行(下面的代碼是不能實際運行的偽代碼)。
model = TwoLayerNet(...) optimizer = SGD() for i in range(10000): ... x_batch, t_batch = get_mini_batch(...) # 獲取mini-batch loss = model.forward(x_batch, t_batch) model.backward() optimizer.update(model.params , model.grads) ...
像這樣,通過獨立實現進行最優化的類,系統的模塊化會變得更加容易。除了SGD外,本書還實現了AdaGrad和Adam等方法,它們的實現都在common/optimizer.py中。這里省略對這些最優化方法的介紹,詳細內容請參考前作《深度學習入門:基于Python的理論與實現》的6.1節。