利用淺層神經網路識別圖片中的英文

AIBigbull2050發表於2019-08-25

如果你(自認為)是一個程式設計師(非機器學習),看完本教程後,你將變得與其他只會手工編寫程式的程式設計師不同。這篇文章我們會按照下面的順序展開:

  • 淺層”與 “深度” 的區別
  • 準備資料
  • 處理資料
  • 隨機梯度下降演算法
  • 訓練測試

一、“淺層”與“深度”的區別:

這裡我們假設大家對於神經網路的結構都是瞭解的,於是我們就有一個問題需要引起重視:神經網路是萬能的嗎?或者說,對於神經網路來說,會不會存在其無法表示的問題?這個問題不是很好回答,但可以告訴大家的一點是,數學上可以證明,滿足一定條件的神經網路,可以以任意精度逼近任何函式。對淺層與深層神經網路的區分,我們基本是以隱藏層的層數來劃分的。對於隱層大於1的神經網路是深層神經網路,對於隱層等於1的網路我們稱之為淺層神經網路。在通常意義上說,深層的神經網路比淺層的神經網路要好的多,這裡面的原因有很多,其中最重要的一點是,深度神經網路可以利用 “層次化” 的資訊表達減少網路中的引數數量,而且能夠提高模型的表達能力,即靠後的網路層可以利用靠前的網路層中提取的較低層次的資訊組合成更高層次或者更加抽象的資訊。但是為了方便講解,我們本次實驗會選擇相對容易實現的淺層神經網路來進行。


二、準備資料:

為了完成我們的實驗,我們需要準備足夠的訓練資料d 構建一個淺層神經網路模型, 並且使用梯度下降演算法去最佳化我們的模型。 我們先來解決訓練資料的問題,我已經事先準備好了一些帶有標籤 (label,代表圖片上的字母是什麼,0 代表 A,1 代表 B, 依次類推) 的訓練圖片。

我們訓練資料整理在一個資料夾下一共有 60000 張圖片,每張圖片的尺寸為 17*17,包含一個不等寬的大寫英文字母。train.txt檔案有 40000 行,每行的格式為 "圖片路徑 標籤",代表一張有標籤訓練圖片,validate.txt和test.txt檔案格式與train.txt類似,且都包含 10000 行。train.txt、validate.txt和test.txt分別對應是我們本次實驗的訓練集,驗證集和測試集。在實際研究中,我們也必須是這樣劃分資料集的。

我們透過梯度下降演算法利用訓練集來對模型中的引數進行最佳化,為了檢驗這些引數是否足夠 “好”,可以透過觀察訓練過程中的損失函式值來判斷,但透過損失函式值來判斷有一個問題,就是我們的模型可能只是“記住” 了所有的訓練資料,而不是真正的學會了訓練資料中所包含的問題本身的性質。就像是如果我們考試時總是出原題,那笨學生只要把所有題目都記住也一樣可以取得高分。所以為了檢驗我們的模型是在 “學習” 而不是在“死記硬背”,我們再使用與訓練集不同的驗證集對模型進行測試,當模型對驗證集的分類準確率也比較高時,就可以認為我們的模型是真正的在 “學習”,此時我們稱我們的模型擁有較好的泛化效能(generalization)-- 能夠正確的對未曾見過的測試樣例做出正確的預測。然而這裡還是有一個問題,別忘了除了模型裡的引數,我們還手動設定了超引數,我們的超引數也有可能只能適應一部分資料,所以為了避免這種情況,需要再設定一個與訓練集和驗證集都不同的測試集,測試在當前超引數的設定下,我們的模型具有良好的泛化效能。


三、處理資料:

對於圖片資料,我們首先需要將它們轉換成輸入向量的形式,並且由於我們是有監督學習,每張圖片的標籤也必須與對應的圖片向量一一對應。 編寫資料預處理如下:

利用淺層神經網路識別圖片中的英文

我們的預處理指令碼接收兩個引數,第一個引數src對應之前我們提到的train.txt、validate.txt和test.txt,我們從src中讀取圖片的路徑和它的標籤。第二個引數dst代表我們將預處理好的圖片資料儲存到哪裡,我們直接使用 np.save() 函式將陣列儲存到npy檔案。

注意原始圖片中只有 0 和 255 兩種灰度值,我們的程式碼對圖片灰度值除以了 255,將圖片矩陣轉換成了只包含 0-1 值的矩陣。同時我們將圖片矩陣轉換成了列向量,注意這裡的列向量的尺寸是 img.sizex1 而不是 img.size,即我們其實是使用矩陣的形式表示向量,這樣可以方便我們之後的運算。

我們可以使用以下命令將圖片轉換成 npy 檔案:

main('train.txt','train.npy')

main('test.txt','test.npy')

main('validate.txt','validate.npy')

預處理好了訓練資料之後,我們還需要將資料讀入我們的神經網路,為了一致性,我們將讀入資料的操作放到一個資料層裡面,資料層程式碼如下:

利用淺層神經網路識別圖片中的英文

這裡我們使用的是隨機梯度下降演算法(stochastic gradient descent)。在實際的深度學習訓練過程當中,我們每次計算梯度並更新引數值時,總是一次性計算多個輸入資料的梯度,並將這些梯度求平均值,再使用這個平均值對引數進行更新。這樣做可以利用平行計算來提高訓練速度。我們將一次性一起計算的一組資料稱為一個batch。同時,我們稱所有訓練圖片都已參與一遍訓練的一個週期稱為一個epoch。每個epoch結束時,我們會將訓練資料重新打亂,這樣可以獲得更好的訓練效果。我們通常會訓練多個epoch。

在上次實驗中,我們實現了一個全連線 FullyConnect 層,但是那段程式碼只能處理輸出是一個標量的情況,對於輸出是多個節點的情況無法處理。而且當一個 batch 中包含多個訓練圖片資料時,那段程式碼更是無法正常工作。

所以我們需要重新編寫我們的全連線層,由於 batch 的引入,這時的全連線層要難了很多

利用淺層神經網路識別圖片中的英文

為了理解上面的程式碼,我們以一個包含 100 個訓練輸入資料的 batch 為例,分析一下具體執行流程: 我們的 l_x 為輸入單個資料向量的長度,在這裡是 17* 17=289,l_y 代表全連線層輸出的節點數量,由於大寫英文字母有 26 個,所以這裡的 l_y=26。 所以,我們的 self.weights 的尺寸為 26*289, self.bias 的尺寸為 26* 1(self.bias 也是透過矩陣形式表示的向量)。forward() 函式的輸入 x 在這裡的尺寸就是 100*289* 1(batch_size 向量長度  1)。backward() 函式的輸入 d 代表從前面的網路層反向傳遞回來的 “部分梯度值”,其尺寸為 100*26* 1(batch_size 輸出層節點數 l_y*1)。

forward() 函式里的程式碼比較好理解,由於這裡的 x 包含了多組資料,所以要對每組資料分別進行計算。

backward() 函式里的程式碼就不太好理解了,ddw 儲存的是對於每組輸入資料,損失函式對於引數的梯度。由於這裡的引數是一個 26* 289 的矩陣,所以,我們需要求損失函式對矩陣的導數。(對矩陣求導可能大部分本科生都不會。但其實也不難,如果你線性代數功底可以,可以嘗試推導矩陣求導公式。)不過這裡有一個簡便的方法去推斷對矩陣求導時應該如何計算:由於這裡的引數矩陣本身是 26*289 的,那損失函式對於它的梯度(即損失函式對引數矩陣求導的結果)的尺寸也一定是 26* 289 的。而這裡每組輸入資料的尺寸是 289*1,每組資料對應的部分梯度尺寸為 26* 1, 要得到一個 26*289 尺寸的梯度矩陣,就只能是一個 26* 1 尺寸的矩陣乘以一個 1*289 尺寸的矩陣,需要對輸入資料進行轉置。所以這裡計算的是np.dot(dd,xx.T)。 對一個 batch 裡的資料分別求得梯度之後,按照隨機梯度下降演算法的要求,我們需要對所有梯度求平均值,得到 self.dw, 其尺寸為 26*289,剛好與我們的 self.weights 匹配。

由於全連線層對 bias 的部分導數為 1,所以這裡對於 bias 的梯度 self.bias 就直接等於從之前的層反向傳回來的梯度的平均值。 損失函式對於輸入 x 的梯度值 self.dx 的求解與 self.dw 類似。由於輸入資料 self.x 中的一個資料的尺寸為 289* 1,self.weights 的尺寸為 26*289, dd 的尺寸為 26* 1, 所以需要對 self.weights 進行轉置。即 “289*1=(289* 26)(26*1)”。

最後是使用梯度更新引數,注意這裡的 self.lr 即為前面我們提到過的學習速率alpha,它是一個需要我們手工設定的超引數。

這裡的矩陣求導確實不太好處理,容易出錯,請你仔細分析每一個變數代表的含義,如果對一個地方不清楚,請回到前面看看相關的概念是如何定義的。


四、梯度下降演算法:

接下來編寫的是啟用函式:

利用淺層神經網路識別圖片中的英文

sigmoid 函式將輸出限制在 0 到 1 之間,剛好可以作為機率看待。這裡我們有 26 個輸入節點,經過 sigmoid 層計算之後,哪個輸出節點的數值最大,就認為圖片上最有可能是該節點代表的字母。比如如果輸出層第 0 個節點值最大,就認為圖片上的字母是 “A”, 如果第 25 個節點的值最大,就認為圖片上的字母是 “Z”。

注意一般在計算神經網路的深度時我們一般不把啟用層算進去,但這裡為了程式設計方便,也將啟用函式視為單獨的一層。

之前我們講解過二次損失函式quadratic loss的定義,這裡我們來實現它:

利用淺層神經網路識別圖片中的英文

在隨機梯度下降演算法裡,每次前向計算和反向傳播都會計算包含多個輸入資料的一個 batch。所以損失函式值在隨後也要除以 batch 中包含的資料數量, 即self.x.shape[0],同時這裡除以了 2, 這個地方的 2 可以和對二次損失函式求導後多出來的係數 2 抵消掉。所以,我們的損失函式變成了一種非常規的形式。

前面我們提到過,為了判斷經過訓練的模型是否具有良好的泛化效能,需要使用驗證集和測試集對模型的效果進行檢驗。所以我們還需要一個計算準確率的層:

利用淺層神經網路識別圖片中的英文

如果我們的神經網路的輸出層中,機率最大的節點的下標與實際的標籤 label 相等,則預測正確。預測正確的數量除以總的數量,就得到了正確率


五、構建神經網路訓練測試

我們已經寫好了所有必須的網路層, 接下來我們要使用這些層構建出一個完整的神經網路,方法很簡單,按順序把它們 “堆疊” 起來就可以了,就像搭積木一樣:

利用淺層神經網路識別圖片中的英文

利用淺層神經網路識別圖片中的英文

示例結果:

epochs: 0
loss: 0.5475572337223819
accuracy: 0.292
epochs: 1
loss: 0.3357296762021519
accuracy: 0.3904
epochs: 2
loss: 0.2720450330419767
accuracy: 0.5501
epochs: 3
loss: 0.22451741831232722
accuracy: 0.6553
epochs: 4
loss: 0.1959364738089137
accuracy: 0.6832
……

由於 FullyConnect 層和 Sigmoid 層在網路中的呼叫方式一模一樣,所以把它們存到一個列表裡,使用迴圈的方式呼叫。同時由於 Sigmoid 層一般不計入神經網路的深度,所以我們將這個列表命名為inner_layers而不是hidden_layers以免混淆。

datalayer1資料層用來輸出訓練集資料,datalayer2資料層用來輸出驗證集資料。accuracy層用來在每個 epoch 結束時計算驗證集上的準確率。

這裡設定學習速率為 1000(實際當中很少看到大於 1 的學習速率,下次實驗我們會解釋為什麼這裡的學習速率需要這麼大), 你可以嘗試將學習速率改變成其他的值,觀察損失函式值和準確率的變化情況。

我們看到每個 epoch 結束時,會先輸出在訓練集上的損失函式值,再輸出在驗證集上的準確率。

20 個 epoch 結束時,準確率大概會在 0.9 左右 (為了節省時間這裡只訓練了 20 個 epoch, 你可以加大 epochs 的數值,看看最高能到多少,我這裡測試大概是在 0.93),這非常令人振奮不是嗎!一個原本透過手工程式設計不可解的圖片分類問題,(幾乎)被我們解決了,0.9 的準確率已經可以應用在一些實際的專案中了(比如這裡),而且我們模型中的引數都是自動設定的,我們只是編寫了模型和訓練演算法部分的程式碼。 而且,我們的程式碼具有很好的可擴充套件性,一方面我們可以很方便的向神經網路中新增更多的網路層使之成為真真的 “深度神經網路”,另一方面我們也可以很方便的將我們的模型運用到其他圖片分類問題當中,我們只編寫了一次程式碼,就有可能能夠解決多種問題!

不過,我要告訴你的是,我們的神經網路的效能還沒有被完全發掘出來,我們的準確率還可以更高!這次實驗的最開始我們提到過,深度神經網路會比淺層神經網路擁有更好的效能,下次實驗,我們會嘗試使用深度神經網路來提高我們的模型效能,進行真正的深度學習!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946223/viewspace-2654849/,如需轉載,請註明出處,否則將追究法律責任。

相關文章