https://blog.csdn.net/oxuzhenyi/article/details/73026807
使用淺層神經網絡識別圖片中的英文字母
一、實驗介紹
1.1 實驗內容
本次實驗我們正式開始我們的項目:使用神經網絡識別圖片中的英文字母。
激動人心的時刻到了,我們將運用神經網絡的魔力,解決一個無法使用手工編程解決的問題。如果你(自認為)是一個程序員,本次實驗結束后,你將變得與其他只會手工編寫程序的程序員不同。
1.2 實驗知識點
- “淺層”與“深度”的區別
- 泛化性能
- 隨機梯度下降算法
- 如何對矩陣求導
- 編寫我們的損失層
1.3 實驗環境
- python 2.7
- numpy 1.12.1
- scipy 0.19.0
二、實驗步驟
2.1 是“淺層”好還是“深度”好?
2.1.1 神經網絡的潛能
這里先插入一個問題,我們一開始直接把神經網絡的模型結構告訴了大家,但有一個問題似乎被忽視掉了:神經網絡是萬能的嗎?或者說,對於神經網絡來說,會不會存在其無法表示的問題?這個問題不是很好回答,但可以告訴大家的一點是,數學上可以證明,滿足一定條件的神經網絡,可以以任意精度逼近任何函數。這里給出了一個直觀的解釋為什么神經網絡有這樣的能力。所以,神經網絡確實是非常強大。
2.1.2 為什么“深度”更好
界定多"深"才算深度學習的標准不一,一種較常見的界定方法是,我們將神經網絡除輸入和輸出層之外的層叫做隱層(hidden layer)
,當隱層的數量大於1時,就可以稱之為深度學習。我們第一次實驗所放的第一張神經網絡結構圖,只有一個隱層,可以稱之為“淺層神經網絡”,本次實驗將會實現的神經網絡模型就會是類似的結構。
“深度”神經網絡要比“淺層”神經網絡更好,這里面的原因有很多,其中最重要的一點是,深度神經網絡可以利用“層次化”的信息表達減少網絡中的參數數量,而且能夠提高模型的表達能力,即靠后的網絡層可以利用靠前的網絡層中提取的較低層次的信息組合成更高層次或者更加抽象的信息。
2.2 准備訓練數據
2.2.1 獲取訓練數據
為了完成我們的項目,我們需要准備足夠的訓練數據data
, 構建一個淺層神經網絡模型model
, 並且使用梯度下降算法learn
去優化我們的模型。
我們先來解決訓練數據的問題,我已經事先准備好了一些帶有標簽(label,代表圖片上的字母是什么,0代表A,1代表B,依次類推)的訓練圖片,你可以直接運行以下命令下載並解壓它們:
-
wget http://labfile.oss.aliyuncs.com/courses/814/data.tar.gz
-
tar zxvf data.tar.gz
解壓之后,我們得到了一個文件夾pic
和三個分別名為train
、validate
、test
的txt
格式文件,pic
文件夾下一共有60000張圖片,每張圖片的尺寸為17*17,包含一個不等寬的大寫英文字母。train.txt
文件有40000行,每行的格式為"圖片路徑 標簽",代表一張有標簽訓練圖片,validate.txt
和test.txt
文件格式與train.txt
類似,且都包含10000行。
你可以使用cat
命令查看這三個文件中的內容:
cat train.txt
2.2.2 訓練、驗證和測試 & 泛化性能
train.txt
、validate.txt
和test.txt
將我們的數據划分成了三個部分。進行這樣的划分是有原因的,在實際運用深度學習解決分類問題的過程中,我們總是將數據划分為訓練集
、驗證集
和測試集
。
我們的學習算法learn
利用訓練集
來對模型中的參數進行優化,為了檢驗這些參數是否足夠“好”,可以通過觀察訓練過程中的損失函數值來判斷,但通過損失函數值來判斷有一個問題,就是我們的模型可能只是“記住”了所有的訓練數據,而不是真正的學會了訓練數據中所包含的問題本身的性質。就像是如果我們考試時總是出原題,那笨學生只要把所有題目都記住也一樣可以取得高分。
所以為了檢驗我們的模型是在“學習”而不是在“死記硬背”,我們再使用與訓練集不同的驗證集
對模型進行測試,當模型對驗證集的分類准確率也比較高時,就可以認為我們的模型是真正的在“學習”,此時我們稱我們的模型擁有較好的泛化性能(generalization)
--能夠正確的對未曾見過的測試樣例做出正確的預測。
然而這里還是有一個問題,別忘了除了模型里的參數
,我們還手動設置了超參數
,我們的超參數也有可能只能適應一部分數據,所以為了避免這種情況,需要再設置一個與訓練集和驗證集都不同的測試集
,測試在當前超參數的設置下,我們的模型具有良好的泛化性能
。
2.2.3 預處理訓練數據
對於圖片數據,我們首先需要將它們轉換成輸入向量的形式,並且由於我們是有監督學習,每張圖片的標簽也必須與對應的圖片向量一一對應。
編寫數據預處理腳本preprocess.py
如下:
# Created by wz on 17-3-23. # encoding=utf-8 import sys from scipy import misc import numpy as np def main(): l = len(sys.argv) if l < 2: # 檢查參數的數量是否足夠 print'eg: python img2pkl.py list.txt dst.npy\n' \ 'convert image to npy\n' return src = sys.argv[1] dst = sys.argv[2] if l > 2 else 'data.pkl' with open(src, 'r') as f: # 讀取圖片列表 list = f.readlines() data = [] labels = [] for i in list: name, label = i.strip('\n').split(' ') # 將圖片列表中的每一行拆分成圖片名和圖片標簽 print name + ' processed' img = misc.imread(name) # 將圖片讀取出來,存入一個矩陣 img /= 255 # 將圖片轉換為只有0、1值的矩陣 img.resize((img.size, 1)) # 為了之后的運算方便,我們將圖片存儲到一個img.size*1的列向量里面 data.append(img) labels.append(int(label)) print 'write to npy' np.save(dst, [data, labels]) # 將訓練數據以npy的形式保存到成本地文件 print 'completed' if __name__ == '__main__': main()
讀入圖片數據需要scipy
模塊,使用以下命令安裝:
sudo pip install scipy
我們的預處理腳本接收兩個參數,第一個參數src
對應之前我們提到的train.txt
、validate.txt
和test.txt
,我們從src
中讀取圖片的路徑和它的標簽。第二個參數dst
代表我們將預處理好的圖片數據保存到哪里,我們直接使用np.save()函數將數組保存到npy
文件。
注意原始圖片中只有0和255兩種灰度值,我們的代碼對圖片灰度值除以了255,將圖片矩陣轉換成了只包含0-1值的矩陣。同時我們將圖片矩陣轉換成了列向量,注意這里的列向量的尺寸是img.sizex1而不是img.size,即我們其實是使用矩陣的形式表示向量,這樣可以方便我們之后的運算。
我們可以使用以下命令將圖片轉換成npy文件:
python preprocess.py train.txt train.npy
python preprocess.py validate.txt validate.npy
python preprocess.py test.txt test.npy
然后你會發現生成了3個文件
2.3 編寫數據層 & 隨機梯度下降算法
預處理好了訓練數據之后,我們還需要將數據讀入我們的神經網絡,為了一致性,我們將讀入數據的操作放到一個數據層
里面。創建layers.py
文件,數據層代碼如下:
import numpy as np class Data: def __init__(self, name, batch_size): # 數據所在的文件名name和batch中圖片的數量batch_size with open(name, 'rb') as f: data = np.load(f) self.x = data[0] # 輸入x self.y = data[1] # 預期正確輸出y self.l = len(self.x) self.batch_size = batch_size self.pos = 0 # pos用來記錄數據讀取的位置 def forward(self): pos = self.pos bat = self.batch_size l = self.l if pos + bat >= l: # 已經是最后一個batch時,返回剩余的數據,並設置pos為開始位置0 ret = (self.x[pos:l], self.y[pos:l]) self.pos = 0 index = range(l) np.random.shuffle(index) # 將訓練數據打亂 self.x = self.x[index] self.y = self.y[index] else: # 不是最后一個batch, pos直接加上batch_size ret = (self.x[pos:pos + bat], self.y[pos:pos + bat]) self.pos += self.batch_size return ret, self.pos # 返回的pos為0時代表一個epoch已經結束 def backward(self, d): # 數據層無backward操作 pass
這里先要介紹梯度下降算法
的實際運用版本:隨機梯度下降算法(stochastic gradient descent)
。在實際的深度學習訓練過程當中,我們每次計算梯度並更新參數值時,總是一次性計算多個輸入數據的梯度,並將這些梯度求平均值,再使用這個平均值對參數進行更新。這樣做可以利用並行計算來提高訓練速度。我們將一次性一起計算的一組數據稱為一個batch
。同時,我們稱所有訓練圖片都已參與一遍訓練的一個周期稱為一個epoch
。每個epoch
結束時,我們會將訓練數據重新打亂,這樣可以獲得更好的訓練效果。我們通常會訓練多個epoch
。
2.3 編寫一次處理一個batch的全連接層 & 對矩陣求導的竅門
在上次實驗中,我們實現了一個全連接FullyConnect層,但是那段代碼只能處理輸出是一個標量的情況,對於輸出是多個節點的情況無法處理。而且當一個batch中包含多個訓練圖片數據時,那段代碼更是無法正常工作。
所以我們需要重新編寫我們的全連接層,由於batch的引入,這時的全連接層要難了很多:
class FullyConnect: def __init__(self, l_x, l_y): # 兩個參數分別為輸入層的長度和輸出層的長度 self.weights = np.random.randn(l_y, l_x) / np.sqrt(l_x) # 使用隨機數初始化參數,請暫時忽略這里為什么多了np.sqrt(l_x) self.bias = np.random.randn(l_y, 1) # 使用隨機數初始化參數 self.lr = 0 # 先將學習速率初始化為0,最后統一設置學習速率 def forward(self, x): self.x = x # 把中間結果保存下來,以備反向傳播時使用 self.y = np.array([np.dot(self.weights, xx) + self.bias for xx in x]) # 計算全連接層的輸出 return self.y # 將這一層計算的結果向前傳遞 def backward(self, d): ddw = [np.dot(dd, xx.T) for dd, xx in zip(d, self.x)] # 根據鏈式法則,將反向傳遞回來的導數值乘以x,得到對參數的梯度 self.dw = np.sum(ddw, axis=0) / self.x.shape[0] self.db = np.sum(d, axis=0) / self.x.shape[0] self.dx = np.array([np.dot(self.weights.T, dd) for dd in d]) # 更新參數 self.weights -= self.lr * self.dw self.bias -= self.lr * self.db return self.dx # 反向傳播梯度
為了理解上面的代碼,我們以一個包含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
,它是一個需要我們手工設定的超參數。
這里的矩陣求導確實不太好處理,容易出錯,請你仔細分析每一個變量代表的含義,如果對一個地方不清楚,請回到前面看看相關的概念是如何定義的。
2.4 激活函數層
由於numpy能夠同時處理標量和矩陣的情況,所以我們之前寫的激活函數sigmoid層可以不用修改直接使用:
class Sigmoid: def __init__(self): # 無參數,不需初始化 pass def sigmoid(self, x): return 1 / (1 + np.exp(-x)) def forward(self, x): self.x = x self.y = self.sigmoid(x) return self.y def backward(self, d): sig = self.sigmoid(self.x) self.dx = d * sig * (1 - sig) return self.dx # 反向傳遞梯度
sigmoid函數將輸出限制在0到1之間,剛好可以作為概率看待。這里我們有26個輸入節點,經過sigmoid層計算之后,哪個輸出節點的數值最大,就認為圖片上最有可能是該節點代表的字母。比如如果輸出層第0個節點值最大,就認為圖片上的字母是“A”, 如果第25個節點的值最大,就認為圖片上的字母是“Z”。
注意一般在計算神經網絡的深度時我們一般不把激活層算進去,但這里為了編程方便,也將激活函數視為單獨的一層。
2.5 損失函數層
之前我們講解過二次損失函數quadratic loss
的定義,這里我們來實現它:
class QuadraticLoss: def __init__(self): pass def forward(self, x, label): self.x = x self.label = np.zeros_like(x) # 由於我們的label本身只包含一個數字,我們需要將其轉換成和模型輸出值尺寸相匹配的向量形式 for a, b in zip(self.label, label): a[b] = 1.0 # 只有正確標簽所代表的位置概率為1,其他為0 self.loss = np.sum(np.square(x - self.label)) / self.x.shape[0] / 2 # 求平均后再除以2是為了表示方便 return self.loss def backward(self): self.dx = (self.x - self.label) / self.x.shape[0] # 2被抵消掉了 return self.dx
在隨機梯度下降算法
里,每次前向計算和反向傳播都會計算包含多個輸入數據的一個batch。所以損失函數值在隨后也要除以batch中包含的數據數量, 即self.x.shape[0]
,同時這里除以了2, 這個地方的2可以和對二次損失函數求導后多出來的系數2抵消掉。所以,我們的損失函數變成了:
2.6 准確率層
前面我們提到過,為了判斷經過訓練的模型是否具有良好的泛化性能
,需要使用驗證集和測試集對模型的效果進行檢驗。所以我們還需要一個計算准確率的層:
class Accuracy: def __init__(self): pass def forward(self, x, label): # 只需forward self.accuracy = np.sum([np.argmax(xx) == ll for xx, ll in zip(x, label)]) # 對預測正確的實例數求和 self.accuracy = 1.0 * self.accuracy / x.shape[0] return self.accuracy
如果我們的神經網絡的輸出層中,概率最大的節點的下標與實際的標簽label相等,則預測正確。預測正確的數量除以總的數量,就得到了正確率。
2.7 構建神經網絡
我們已經寫好了所有必須的網絡層,並所有網絡層都放到一個layers.py
文件里。
接下來我們要使用這些層構建出一個完整的神經網絡,方法很簡單,按順序把它們“堆疊”起來就可以了,就像搭積木一樣,創建shallow.py
文件:
# encoding=utf-8 from layers import * def main(): datalayer1 = Data('train.npy', 1024) # 用於訓練,batch_size設置為1024 datalayer2 = Data('validate.npy', 10000) # 用於驗證,所以設置batch_size為10000,一次性計算所有的樣例 inner_layers = [] inner_layers.append(FullyConnect(17 * 17, 26)) inner_layers.append(Sigmoid()) losslayer = QuadraticLoss() accuracy = Accuracy() for layer in inner_layers: layer.lr = 1000.0 # 為所有中間層設置學習速率 epochs = 20 for i in range(epochs): print 'epochs:', i losssum = 0 iters = 0 while True: data, pos = datalayer1.forward() # 從數據層取出數據 x, label = data for layer in inner_layers: # 前向計算 x = layer.forward(x) loss = losslayer.forward(x, label) # 調用損失層forward函數計算損失函數值 losssum += loss iters += 1 d = losslayer.backward() # 調用損失層backward函數層計算將要反向傳播的梯度 for layer in inner_layers[::-1]: # 反向傳播 d = layer.backward(d) if pos == 0: # 一個epoch完成后進行准確率測試 data, _ = datalayer2.forward() x, label = data for layer in inner_layers: x = layer.forward(x) accu = accuracy.forward(x, label) # 調用准確率層forward()函數求出准確率 print 'loss:', losssum / iters print 'accuracy:', accu break if __name__ == '__main__': main()
由於FullyConnect層和Sigmoid層在網絡中的調用方式一模一樣,所以把它們存到一個列表里,使用循環的方式調用。同時由於Sigmoid層一般不計入神經網絡的深度,所以我們將這個列表命名為inner_layers
而不是hidden_layers
以免混淆。
datalayer1
數據層用來輸出訓練集數據,datalayer2
數據層用來輸出驗證集數據。accuracy
層用來在每個epoch結束時計算驗證集上的准確率。
上面的代碼里只有一個隱層,構建的神經網絡屬於淺層神經網絡,所以我們把這段代碼存儲在shallow.py
文件里。
preprocess.py
layers.py
shallow.py
三個文件可以使用以下命令獲取:
-
wget http://labfile.oss.aliyuncs.com/courses/814/code.tar.gz
-
tar zxvf code.tar.gz
2.8 訓練神經網絡
終於,我們排除萬難,准備好了訓練數據,構建好了我們的淺層神經網絡,也寫好了訓練算法,終於可以開始訓練了!在terminal里輸入:
python shallow.py
這里設置學習速率為1000(實際當中很少看到大於1的學習速率,下次實驗我們會解釋為什么這里的學習速率需要這么大),你可以嘗試將學習速率改變成其他的值,觀察損失函數值和准確率的變化情況。
我們看到每個epoch結束時,會先輸出在訓練集上的損失函數值,再輸出在驗證集上的准確率。
20個epoch結束時,准確率大概會在0.9左右(為了節省時間這里只訓練了20個epoch,你可以加大epochs的數值,看看最高能到多少,我這里測試大概是在0.93),這非常令人振奮不是嗎!一個原本通過手工編程不可解的圖片分類問題,(幾乎)被我們解決了,0.9的准確率已經可以應用在一些實際的項目中了(比如這里),而且我們模型中的參數都是自動設定的,我們只是編寫了模型和訓練算法部分的代碼。
而且,我們的代碼具有很好的可擴展性,一方面我們可以很方便的向神經網絡中添加更多的網絡層使之成為真真的“深度神經網絡”,另一方面我們也可以很方便的將我們的模型運用到其他圖片分類問題當中,我們只編寫了一次代碼,就有可能能夠解決多種問題!不過,我要告訴你的是,我們的神經網絡的性能還沒有被完全發掘出來,我們的准確率還可以更高!這次實驗的最開始我們提到過,深度神經網絡會比淺層神經網絡擁有更好的性能,下次實驗,我們會嘗試使用深度神經網絡來提高我們的模型性能,進行真正的
深度學習
!三、實驗總結
這次實驗我們編寫了數據預處理腳本、數據輸入網絡層、能夠處理批量數據的FullyConnect層、損失函數層和准確率層,使用這些層構建出了只有一個隱層的淺層神經網絡,並使用這個神經網絡訓練得到了一個效果已經很不錯的模型。
在此課程的一開始,我就強調本課程不要求很高的數學水平,但是我相信你在實驗的過程中還是逐漸的體會到了(尤其是編寫FullyConnect層對矩陣求導數的時候),要想理解深度學習的原理,必須要具備一定的數學基礎,數學就像是一把強大的戰斧,幫你掃清一個個障礙,使原本不可解的問題變得可解。所以如果你想從事深度學習相關的工作,甚至進行深度學習領域的研究的話,請務必要重視學習相關數學知識。
本次實驗,我們學習了:
- 深度神經網絡比淺層神經網絡更好
- 泛化性能是指一個模型能夠正確預測未曾見過的樣例的能力
- 隨機梯度下降算法在每輪(epoch)訓練開始時將所有數據打亂,每次訓練一次性計算多個樣例的平均值並使用平均值對參數進行更新
四、課后作業
- 修改上面的代碼,在訓練過程中打印出FullyConnect層的梯度值,觀察這些值的大小。
- 增大epoch的值,測試我們的神經網絡最高能達到多少准確率。