https://blog.csdn.net/oxuzhenyi/article/details/73026807html
本次實驗咱們正式開始咱們的項目:使用神經網絡識別圖片中的英文字母。python
激動人心的時刻到了,咱們將運用神經網絡的魔力,解決一個沒法使用手工編程解決的問題。若是你(自認爲)是一個程序員,本次實驗結束後,你將變得與其餘只會手工編寫程序的程序員不一樣。git
這裏先插入一個問題,咱們一開始直接把神經網絡的模型結構告訴了你們,但有一個問題彷佛被忽視掉了:神經網絡是萬能的嗎?或者說,對於神經網絡來講,會不會存在其沒法表示的問題?這個問題不是很好回答,但能夠告訴你們的一點是,數學上能夠證實,知足必定條件的神經網絡,能夠以任意精度逼近任何函數。這裏給出了一個直觀的解釋爲何神經網絡有這樣的能力。因此,神經網絡確實是很是強大。程序員
界定多"深"纔算深度學習的標準不一,一種較常見的界定方法是,咱們將神經網絡除輸入和輸出層以外的層叫作隱層(hidden layer)
,當隱層的數量大於1時,就能夠稱之爲深度學習。咱們第一次實驗所放的第一張神經網絡結構圖,只有一個隱層,能夠稱之爲「淺層神經網絡」,本次實驗將會實現的神經網絡模型就會是相似的結構。
「深度」神經網絡要比「淺層」神經網絡更好,這裏面的緣由有不少,其中最重要的一點是,深度神經網絡能夠利用「層次化」的信息表達減小網絡中的參數數量,並且可以提升模型的表達能力,即靠後的網絡層能夠利用靠前的網絡層中提取的較低層次的信息組合成更高層次或者更加抽象的信息。github
爲了完成咱們的項目,咱們須要準備足夠的訓練數據data
, 構建一個淺層神經網絡模型model
, 而且使用梯度降低算法learn
去優化咱們的模型。
咱們先來解決訓練數據的問題,我已經事先準備好了一些帶有標籤(label,表明圖片上的字母是什麼,0表明A,1表明B,依次類推)的訓練圖片,你能夠直接運行如下命令下載並解壓它們:算法
解壓以後,咱們獲得了一個文件夾pic
和三個分別名爲train
、validate
、test
的txt
格式文件,pic
文件夾下一共有60000張圖片,每張圖片的尺寸爲17*17,包含一個不等寬的大寫英文字母。train.txt
文件有40000行,每行的格式爲"圖片路徑 標籤",表明一張有標籤訓練圖片,validate.txt
和test.txt
文件格式與train.txt
相似,且都包含10000行。編程
你可使用cat
命令查看這三個文件中的內容:數組
cat train.txt
train.txt
、validate.txt
和test.txt
將咱們的數據劃分紅了三個部分。進行這樣的劃分是有緣由的,在實際運用深度學習解決分類問題的過程當中,咱們老是將數據劃分爲訓練集
、驗證集
和測試集
。bash
咱們的學習算法learn
利用訓練集
來對模型中的參數進行優化,爲了檢驗這些參數是否足夠「好」,能夠經過觀察訓練過程當中的損失函數值來判斷,但經過損失函數值來判斷有一個問題,就是咱們的模型可能只是「記住」了全部的訓練數據,而不是真正的學會了訓練數據中所包含的問題自己的性質。就像是若是咱們考試時老是出原題,那笨學生只要把全部題目都記住也同樣能夠取得高分。網絡
因此爲了檢驗咱們的模型是在「學習」而不是在「死記硬背」,咱們再使用與訓練集不一樣的驗證集
對模型進行測試,當模型對驗證集的分類準確率也比較高時,就能夠認爲咱們的模型是真正的在「學習」,此時咱們稱咱們的模型擁有較好的泛化性能(generalization)
--可以正確的對不曾見過的測試樣例作出正確的預測。
然而這裏仍是有一個問題,別忘了除了模型裏的參數
,咱們還手動設置了超參數
,咱們的超參數也有可能只能適應一部分數據,因此爲了不這種狀況,須要再設置一個與訓練集和驗證集都不一樣的測試集
,測試在當前超參數的設置下,咱們的模型具備良好的泛化性能
。
對於圖片數據,咱們首先須要將它們轉換成輸入向量的形式,而且因爲咱們是有監督學習,每張圖片的標籤也必須與對應的圖片向量一一對應。
編寫數據預處理腳本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個文件
預處理好了訓練數據以後,咱們還須要將數據讀入咱們的神經網絡,爲了一致性,咱們將讀入數據的操做放到一個數據層
裏面。建立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
。
在上次實驗中,咱們實現了一個全鏈接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
,它是一個須要咱們手工設定的超參數。
這裏的矩陣求導確實不太好處理,容易出錯,請你仔細分析每個變量表明的含義,若是對一個地方不清楚,請回到前面看看相關的概念是如何定義的。
因爲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」。
注意通常在計算神經網絡的深度時咱們通常不把激活層算進去,但這裏爲了編程方便,也將激活函數視爲單獨的一層。
以前咱們講解過二次損失函數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抵消掉。因此,咱們的損失函數變成了:
前面咱們提到過,爲了判斷通過訓練的模型是否具備良好的泛化性能
,須要使用驗證集和測試集對模型的效果進行檢驗。因此咱們還須要一個計算準確率的層:
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相等,則預測正確。預測正確的數量除以總的數量,就獲得了正確率。
咱們已經寫好了全部必須的網絡層,並全部網絡層都放到一個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
三個文件可使用如下命令獲取:
終於,咱們排除萬難,準備好了訓練數據,構建好了咱們的淺層神經網絡,也寫好了訓練算法,終於能夠開始訓練了!在terminal裏輸入:
python shallow.py
這裏設置學習速率爲1000(實際當中不多看到大於1的學習速率,下次實驗咱們會解釋爲何這裏的學習速率須要這麼大),你能夠嘗試將學習速率改變成其餘的值,觀察損失函數值和準確率的變化狀況。
咱們看到每一個epoch結束時,會先輸出在訓練集上的損失函數值,再輸出在驗證集上的準確率。
20個epoch結束時,準確率大概會在0.9左右(爲了節省時間這裏只訓練了20個epoch,你能夠加大epochs的數值,看看最高能到多少,我這裏測試大概是在0.93),這很是使人振奮不是嗎!一個本來經過手工編程不可解的圖片分類問題,(幾乎)被咱們解決了,0.9的準確率已經能夠應用在一些實際的項目中了(好比這裏),並且咱們模型中的參數都是自動設定的,咱們只是編寫了模型和訓練算法部分的代碼。
並且,咱們的代碼具備很好的可擴展性,一方面咱們能夠很方便的向神經網絡中添加更多的網絡層使之成爲真真的「深度神經網絡」,另外一方面咱們也能夠很方便的將咱們的模型運用到其餘圖片分類問題當中,咱們只編寫了一次代碼,就有可能可以解決多種問題!
不過,我要告訴你的是,咱們的神經網絡的性能尚未被徹底發掘出來,咱們的準確率還能夠更高!此次實驗的最開始咱們提到過,深度神經網絡會比淺層神經網絡擁有更好的性能,下次實驗,咱們會嘗試使用深度神經網絡來提升咱們的模型性能,進行真正的深度學習
!
此次實驗咱們編寫了數據預處理腳本、數據輸入網絡層、可以處理批量數據的FullyConnect層、損失函數層和準確率層,使用這些層構建出了只有一個隱層的淺層神經網絡,並使用這個神經網絡訓練獲得了一個效果已經很不錯的模型。
在此課程的一開始,我就強調本課程不要求很高的數學水平,可是我相信你在實驗的過程當中仍是逐漸的體會到了(尤爲是編寫FullyConnect層對矩陣求導數的時候),要想理解深度學習的原理,必需要具有必定的數學基礎,數學就像是一把強大的戰斧,幫你掃清一個個障礙,使本來不可解的問題變得可解。因此若是你想從事深度學習相關的工做,甚至進行深度學習領域的研究的話,請務必要重視學習相關數學知識。
本次實驗,咱們學習了: