DeepLearning tutorial(5)CNN卷積神經(jīng)網(wǎng)絡(luò)應(yīng)用于人臉識(shí)別(詳細(xì)流程+代碼實(shí)現(xiàn))
@author:wepon
@blog:http://blog.csdn.net/u012162613/article/details/43277187
本文代碼下載地址:我的github
本文主要講解將CNN應(yīng)用于人臉識(shí)別的流程,程序基于python+numpy+theano+PIL開發(fā),采用類似LeNet5的CNN模型,應(yīng)用于olivettifaces人臉數(shù)據(jù)庫(kù),實(shí)現(xiàn)人臉識(shí)別的功能,模型的誤差降到了5%以下。本程序只是個(gè)人學(xué)習(xí)過程的一個(gè)toy implement,模型可能存在overfitting,因?yàn)闃颖拘?,這一點(diǎn)也無從驗(yàn)證。
但是,本文意在理清程序開發(fā)CNN模型的具體步驟,特別是針對(duì)圖像識(shí)別,從拿到圖像數(shù)據(jù)庫(kù),到實(shí)現(xiàn)一個(gè)針對(duì)這個(gè)圖像數(shù)據(jù)庫(kù)的CNN模型,我覺得本文對(duì)這些流程的實(shí)現(xiàn)具有參考意義。
《本文目錄》
一、olivettifaces人臉數(shù)據(jù)庫(kù)介紹
二、CNN的基本“構(gòu)件”(LogisticRegression、HiddenLayer、LeNetConvPoolLayer)
三、組建CNN模型,設(shè)置優(yōu)化算法,應(yīng)用于Olivetti Faces進(jìn)行人臉識(shí)別
四、訓(xùn)練結(jié)果以及參數(shù)設(shè)置的討論
五、利用訓(xùn)練好的參數(shù)初始化模型
六、一些需要說明的
一、olivettifaces人臉數(shù)據(jù)庫(kù)介紹
Olivetti Faces是紐約大學(xué)的一個(gè)比較小的人臉庫(kù),由40個(gè)人的400張圖片構(gòu)成,即每個(gè)人的人臉圖片為10張。每張圖片的灰度級(jí)為8位,每個(gè)像素的灰度大小位于0-255之間,每張圖片大小為64×64。如下圖,這個(gè)圖片大小是1190*942,一共有20*20張人臉,故每張人臉大小是(1190/20)*(942/20)即57*47=2679:
本文所用的訓(xùn)練數(shù)據(jù)就是這張圖片,400個(gè)樣本,40個(gè)類別,乍一看樣本好像比較小,用CNN效果會(huì)好嗎?先別下結(jié)論,請(qǐng)往下看。
要運(yùn)行CNN算法,這張圖片必須先轉(zhuǎn)化為數(shù)組(或者說矩陣),這個(gè)用到python的圖像庫(kù)PIL,幾行代碼就可以搞定,具體的方法我之前剛好寫過一篇文章,也是用這張圖,考慮到文章冗長(zhǎng),就不復(fù)制過來了,鏈接在此:《利用Python PIL、cPickle讀取和保存圖像數(shù)據(jù)庫(kù)》。
訓(xùn)練機(jī)器學(xué)習(xí)算法,我們一般將原始數(shù)據(jù)分成訓(xùn)練數(shù)據(jù)(training_set)、驗(yàn)證數(shù)據(jù)(validation_set)、測(cè)試數(shù)據(jù)(testing_set)。本程序?qū)raining_set、validation_set、testing_set分別設(shè)置為320、40、40個(gè)樣本。它們的label為0~39,對(duì)應(yīng)40個(gè)不同的人。這部分的代碼如下:
- """
- 加載圖像數(shù)據(jù)的函數(shù),dataset_path即圖像olivettifaces的路徑
- 加載olivettifaces后,劃分為train_data,valid_data,test_data三個(gè)數(shù)據(jù)集
- 函數(shù)返回train_data,valid_data,test_data以及對(duì)應(yīng)的label
- """
- def load_data(dataset_path):
- img = Image.open(dataset_path)
- img_ndarray = numpy.asarray(img, dtype='float64')/256
- faces=numpy.empty((400,2679))
- for row in range(20):
- for column in range(20):
- faces[row*20+column]=numpy.ndarray.flatten(img_ndarray [row*57:(row+1)*57,column*47:(column+1)*47])
-
- label=numpy.empty(400)
- for i in range(40):
- label[i*10:i*10+10]=i
- label=label.astype(numpy.int)
-
- #分成訓(xùn)練集、驗(yàn)證集、測(cè)試集,大小如下
- train_data=numpy.empty((320,2679))
- train_label=numpy.empty(320)
- valid_data=numpy.empty((40,2679))
- valid_label=numpy.empty(40)
- test_data=numpy.empty((40,2679))
- test_label=numpy.empty(40)
-
- for i in range(40):
- train_data[i*8:i*8+8]=faces[i*10:i*10+8]
- train_label[i*8:i*8+8]=label[i*10:i*10+8]
- valid_data[i]=faces[i*10+8]
- valid_label[i]=label[i*10+8]
- test_data[i]=faces[i*10+9]
- test_label[i]=label[i*10+9]
-
- #將數(shù)據(jù)集定義成shared類型,才能將數(shù)據(jù)復(fù)制進(jìn)GPU,利用GPU加速程序。
- def shared_dataset(data_x, data_y, borrow=True):
- shared_x = theano.shared(numpy.asarray(data_x,
- dtype=theano.config.floatX),
- borrow=borrow)
- shared_y = theano.shared(numpy.asarray(data_y,
- dtype=theano.config.floatX),
- borrow=borrow)
- return shared_x, T.cast(shared_y, 'int32')
-
-
-
- train_set_x, train_set_y = shared_dataset(train_data,train_label)
- valid_set_x, valid_set_y = shared_dataset(valid_data,valid_label)
- test_set_x, test_set_y = shared_dataset(test_data,test_label)
- rval = [(train_set_x, train_set_y), (valid_set_x, valid_set_y),
- (test_set_x, test_set_y)]
- return rval
二、CNN的基本“構(gòu)件”(LogisticRegression、HiddenLayer、LeNetConvPoolLayer)
卷積神經(jīng)網(wǎng)絡(luò)(CNN)的基本結(jié)構(gòu)就是輸入層、卷積層(conv)、子采樣層(pooling)、全連接層、輸出層(分類器)。 卷積層+子采樣層一般都會(huì)有若干個(gè),本程序?qū)崿F(xiàn)的CNN模型參考LeNet5,有兩個(gè)“卷積+子采樣層”LeNetConvPoolLayer。全連接層相當(dāng)于MLP(多層感知機(jī))中的隱含層HiddenLayer。輸出層即分類器,一般采用softmax回歸(也有人直接叫邏輯回歸,其實(shí)就是多類別的logistics regression),本程序也直接用LogisticRegression表示。
代碼太長(zhǎng),就不貼具體的了,只給出框架,具體可以下載我的代碼看看:
- #分類器,即CNN最后一層,采用邏輯回歸(softmax)
- class LogisticRegression(object):
- def __init__(self, input, n_in, n_out):
- self.W = ....
- self.b = ....
- self.p_y_given_x = ...
- self.y_pred = ...
- self.params = ...
- def negative_log_likelihood(self, y):
- def errors(self, y):
-
- #全連接層,分類器前一層
- class HiddenLayer(object):
- def __init__(self, rng, input, n_in, n_out, W=None, b=None,activation=T.tanh):
- self.input = input
- self.W = ...
- self.b = ...
- lin_output = ...
- self.params = [self.W, self.b]
-
- #卷積+采樣層(conv+maxpooling)
- class LeNetConvPoolLayer(object):
- def __init__(self, rng, input, filter_shape, image_shape, poolsize=(2, 2)):
- self.input = input
- self.W = ...
- self.b = ...
- # 卷積
- conv_out = ...
- # 子采樣
- pooled_out =...
- self.output = ...
- self.params = [self.W, self.b]
三、組建CNN模型,設(shè)置優(yōu)化算法,應(yīng)用于Olivetti Faces進(jìn)行人臉識(shí)別
上面定義好了CNN的幾個(gè)基本“構(gòu)件”,現(xiàn)在我們使用這些構(gòu)件來組建CNN模型,本程序的CNN模型參考LeNet5,具體為:input+layer0(LeNetConvPoolLayer)+layer1(LeNetConvPoolLayer)+layer2(HiddenLayer)+layer3(LogisticRegression)
這是一個(gè)串聯(lián)結(jié)構(gòu),代碼也很好寫,直接用第二部分定義好的各種layer去組建就行了,上一layer的輸出接下一layer的輸入,具體可以看看代碼evaluate_olivettifaces函數(shù)中的“建立CNN模型”部分。
CNN模型組建好了,就剩下用優(yōu)化算法求解了,優(yōu)化算法采用批量隨機(jī)梯度下降算法(MSGD),所以要先定義MSGD的一些要素,主要包括:代價(jià)函數(shù),訓(xùn)練、驗(yàn)證、測(cè)試model、參數(shù)更新規(guī)則(即梯度下降)。這部分的代碼在evaluate_olivettifaces函數(shù)中的“定義優(yōu)化算法的一些基本要素”部分。
優(yōu)化算法的基本要素也定義好了,接下來就要運(yùn)用人臉圖像數(shù)據(jù)集來訓(xùn)練這個(gè)模型了,訓(xùn)練過程有訓(xùn)練步數(shù)(n_epoch)的設(shè)置,每個(gè)epoch會(huì)遍歷所有的訓(xùn)練數(shù)據(jù)(training_set),本程序中也就是320個(gè)人臉圖。還有迭代次數(shù)iter,一次迭代遍歷一個(gè)batch里的所有樣本,具體為多少要看所設(shè)置的batch_size。關(guān)于參數(shù)的設(shè)定我在下面會(huì)討論。這一部分的代碼在evaluate_olivettifaces函數(shù)中的“訓(xùn)練CNN階段”部分。
代碼很長(zhǎng),只貼框架,具體可以下載我的代碼看看:
- def evaluate_olivettifaces(learning_rate=0.05, n_epochs=200,
- dataset='olivettifaces.gif',
- nkerns=[5, 10], batch_size=40):
-
- #隨機(jī)數(shù)生成器,用于初始化參數(shù)....
- #加載數(shù)據(jù).....
- #計(jì)算各數(shù)據(jù)集的batch個(gè)數(shù)....
- #定義幾個(gè)變量,x代表人臉數(shù)據(jù),作為layer0的輸入......
-
- ######################
- #建立CNN模型:
- #input+layer0(LeNetConvPoolLayer)+layer1(LeNetConvPoolLayer)+layer2(HiddenLayer)+layer3(LogisticRegression)
- ######################
- ...
- ....
- ......
-
- #########################
- # 定義優(yōu)化算法的一些基本要素:代價(jià)函數(shù),訓(xùn)練、驗(yàn)證、測(cè)試model、參數(shù)更新規(guī)則(即梯度下降)
- #########################
- ...
- ....
- ......
-
- #########################
- # 訓(xùn)練CNN階段,尋找最優(yōu)的參數(shù)。
- ########################
- ...
- .....
- .......
另外,值得一提的是,在訓(xùn)練CNN階段,我們必須定時(shí)地保存模型的參數(shù),這是在訓(xùn)練機(jī)器學(xué)習(xí)算法時(shí)一個(gè)經(jīng)常會(huì)做的事情,這一部分的詳細(xì)介紹我之前寫過一篇文章《DeepLearning tutorial(2)機(jī)器學(xué)習(xí)算法在訓(xùn)練過程中保存參數(shù)》。簡(jiǎn)單來說,我們要保存CNN模型中l(wèi)ayer0、layer1、layer2、layer3的參數(shù),所以在“訓(xùn)練CNN階段”這部分下面,有一句代碼:
- save_params(layer0.params,layer1.params,layer2.params,layer3.params)
這個(gè)函數(shù)具體定義為:
- #保存訓(xùn)練參數(shù)的函數(shù)
- def save_params(param1,param2,param3,param4):
- import cPickle
- write_file = open('params.pkl', 'wb')
- cPickle.dump(param1, write_file, -1)
- cPickle.dump(param2, write_file, -1)
- cPickle.dump(param3, write_file, -1)
- cPickle.dump(param4, write_file, -1)
- write_file.close()
如果在其他算法中,你要保存的參數(shù)有五個(gè)六個(gè)甚至更多,那么改一下這個(gè)函數(shù)的參數(shù)就行啦。
四、訓(xùn)練結(jié)果以及參數(shù)設(shè)置的討論
ok,上面基本介紹完了CNN模型的構(gòu)建,以及模型的訓(xùn)練,我將它們的代碼都放在train_CNN_olivettifaces.py這個(gè)源文件中,將Olivetti Faces這張圖片跟這個(gè)代碼文件放在同個(gè)目錄下,運(yùn)行這個(gè)文件,幾分鐘就可以訓(xùn)練完模型,并且在同個(gè)目錄下得到一個(gè)params.pkl文件,這個(gè)文件保存的就是最后的模型的參數(shù),方便你以后直接使用這個(gè)模型。
好了,現(xiàn)在討論一下怎么設(shè)置參數(shù),具體來說,程序中可以設(shè)置的參數(shù)包括:學(xué)習(xí)速率learning_rate、batch_size、n_epochs、nkerns、poolsize。下面逐一討論調(diào)節(jié)它們時(shí)對(duì)模型的影響。
- 調(diào)節(jié)learning_rate
學(xué)習(xí)速率learning_rate就是運(yùn)用SGD算法時(shí)梯度前面的系數(shù),非常重要,設(shè)得太大的話算法可能永遠(yuǎn)都優(yōu)化不了,設(shè)得太小會(huì)使算法優(yōu)化得太慢,而且可能還會(huì)掉入局部最優(yōu)??梢孕蜗蟮貙earning_rate比喻成走路時(shí)步子的大小,想象一下要從一個(gè)U形的山谷的一邊走到山谷最低點(diǎn),如果步子特別大,像巨人那么大,那會(huì)直接從一邊跨到另一邊,然后又跨回這邊,如此往復(fù)。如果太小了,可能你走著走著就掉入了某些小坑,因?yàn)樯铰房偸前纪共黄降模ň植孔顑?yōu)),掉入這些小坑后,如果步子還是不變,就永遠(yuǎn)走不出那個(gè)坑。
好,回到本文的模型,下面是我使用時(shí)的記錄,固定其他參數(shù),調(diào)節(jié)learning_rate:
(1)kerns=[20, 50], batch_size=40,poolsize=(2,2),learning_rate=0.1時(shí),validation-error一直是97.5%,沒降下來,分析了一下,覺得應(yīng)該是學(xué)習(xí)速率太大,跳過了最優(yōu)。
(2)nkerns=[20, 50], batch_size=40,poolsize=(2,2),learning_rate=0.01時(shí),訓(xùn)練到epoch 60多時(shí),validation-error降到5%,test-error降到15%
(3)nkerns=[20, 50], batch_size=40,poolsize=(2,2),learning_rate=0.05時(shí),訓(xùn)練到epoch 36時(shí),validation-error降到2.5%,test-error降到5%
注意,驗(yàn)證集和測(cè)試集都只有40張圖片,也就是說只有一兩張識(shí)別錯(cuò)了,還是不錯(cuò)的,數(shù)據(jù)集再大點(diǎn),誤差率可以降到更小。最后我將learning_rate設(shè)置為0.05。
PS:學(xué)習(xí)速率應(yīng)該自適應(yīng)地減小,是有專門的一些算法的,本程序沒有實(shí)現(xiàn)這個(gè)功能,有時(shí)間再研究一下。
因?yàn)槲覀儾捎胢inibatch SGD算法來優(yōu)化,所以是一個(gè)batch一個(gè)batch地將數(shù)據(jù)輸入CNN模型中,然后計(jì)算這個(gè)batch的所有樣本的平均損失,即代價(jià)函數(shù)是所有樣本的平均。而batch_size就是一個(gè)batch的所包含的樣本數(shù),顯然batch_size將影響到模型的優(yōu)化程度和速度。
回到本文的模型,首先因?yàn)槲覀僼rain_dataset是320,valid_dataset和test_dataset都是40,所以batch_size最好都是40的因子,也就是能讓40整除,比如40、20、10、5、2、1,否則會(huì)浪費(fèi)一些樣本,比如設(shè)置為30,則320/30=10,余數(shù)時(shí)20,這20個(gè)樣本是沒被利用的。并且,如果batch_size設(shè)置為30,則得出的validation-error和test-error只是30個(gè)樣本的錯(cuò)誤率,并不是全部40個(gè)樣本的錯(cuò)誤率。這是設(shè)置batch_size要注意的。特別是樣本比較少的時(shí)候。
下面是我實(shí)驗(yàn)時(shí)的記錄,固定其他參數(shù),改變batch_size:
batch_size=1、2、5、10、20時(shí),validation-error一直是97.5%,沒降下來。我覺得可能是樣本類別覆蓋率過小,因?yàn)槲覀兊臄?shù)據(jù)是按類別排的,每個(gè)類別10個(gè)樣本是連續(xù)排在一起的,batch_size等于20時(shí)其實(shí)只包含了兩個(gè)類別,這樣優(yōu)化會(huì)很慢。
因此最后我將batch_size設(shè)為40,也就是valid_dataset和test_dataset的大小了,沒辦法,原始數(shù)據(jù)集樣本太少了。一般我們都不會(huì)讓batch_size達(dá)到valid_dataset和test_dataset的大小的。
n_epochs也就是最大的訓(xùn)練步數(shù),比如設(shè)為200,那訓(xùn)練過程最多遍歷你的數(shù)據(jù)集200遍,當(dāng)遍歷了200遍你的dataset時(shí),程序會(huì)停止。n_epochs就相當(dāng)于一個(gè)停止程序的控制參數(shù),并不會(huì)影響CNN模型的優(yōu)化程度和速度,只是一個(gè)控制程序結(jié)束的參數(shù)。
20表示第一個(gè)卷積層的卷積核的個(gè)數(shù),50表示第二個(gè)卷積層的卷積核的個(gè)數(shù)。這個(gè)我也是瞎調(diào)的,暫時(shí)沒什么經(jīng)驗(yàn)可以總結(jié)。
不過從理論上來說,卷積核的個(gè)數(shù)其實(shí)就代表了特征的個(gè)數(shù),你提取的特征越多,可能最后分類就越準(zhǔn)。但是,特征太多(卷積核太多),會(huì)增加參數(shù)的規(guī)模,加大了計(jì)算復(fù)雜度,而且有時(shí)候卷積核也不是越多越好,應(yīng)根據(jù)具體的應(yīng)用對(duì)象來確定。所以我覺得,CNN雖號(hào)稱自動(dòng)提取特征,免去復(fù)雜的特征工程,但是很多參數(shù)比如這里的nkerns還是需要去調(diào)節(jié)的,還是需要一些“人工”的。
下面是我的實(shí)驗(yàn)記錄,固定batch_size=40,learning_rate=0.05,poolsize=(2,2):
(1)nkerns=[20, 50]時(shí),訓(xùn)練到epoch 36時(shí),validation-error降到2.5%,test-error降到5%
(2)nkerns=[10, 30]時(shí),訓(xùn)練到epoch 46時(shí),validation-error降到5%,test-error降到5%
(3)nkerns=[5, 10]時(shí),訓(xùn)練到epoch 38時(shí),validation-error降到5%,test-error降到7.5%
poolzize在本程序中是設(shè)置為(2,2),即從一個(gè)2*2的區(qū)域里maxpooling出1個(gè)像素,說白了就算4和像素保留成1個(gè)像素。本例程中人臉圖像大小是57*47,對(duì)這種小圖像來說,(2,2)時(shí)比較合理的。如果你用的圖像比較大,可以把poolsize設(shè)的大一點(diǎn)。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++分割線+++++++++++++++++++++++++++++++++++++++++++
上面部分介紹完了CNN模型構(gòu)建以及模型訓(xùn)練的過程,代碼都在train_CNN_olivettifaces.py里面,訓(xùn)練完可以得到一個(gè)params.pkl文件,這個(gè)文件保存的就是最后的模型的參數(shù),方便你以后直接使用這個(gè)模型。以后只需利用這些保存下來的參數(shù)來初始化CNN模型,就得到一個(gè)可以使用的CNN系統(tǒng),將人臉圖輸入這個(gè)CNN系統(tǒng),預(yù)測(cè)人臉圖的類別。
接下來就介紹怎么使用訓(xùn)練好的參數(shù)的方法,這部分的代碼放在use_CNN_olivettifaces.py文件中。
五、利用訓(xùn)練好的參數(shù)初始化模型
在train_CNN_olivettifaces.py中的LeNetConvPoolLayer、HiddenLayer、LogisticRegression是用隨機(jī)數(shù)生成器去隨機(jī)初始化的,我們將它們定義為可以用參數(shù)來初始化的版本,其實(shí)很簡(jiǎn)單,代碼只需要做稍微的改動(dòng),只需要在LogisticRegression、HiddenLayer、LeNetConvPoolLayer這三個(gè)class的__init__()函數(shù)中加兩個(gè)參數(shù)params_W,params_b,然后將params_W,params_b賦值給這三個(gè)class里的W和b:
- self.W = params_W
- self.b = params_b
params_W,params_b就是從params.pkl文件中讀取來的,讀取的函數(shù):- #讀取之前保存的訓(xùn)練參數(shù)
- #layer0_params~layer3_params都是包含W和b的,layer*_params[0]是W,layer*_params[1]是b
- def load_params(params_file):
- f=open(params_file,'rb')
- layer0_params=cPickle.load(f)
- layer1_params=cPickle.load(f)
- layer2_params=cPickle.load(f)
- layer3_params=cPickle.load(f)
- f.close()
- return layer0_params,layer1_params,layer2_params,layer3_params
ok,可以用參數(shù)初始化的CNN定義好了,那現(xiàn)在就將需要測(cè)試的人臉圖輸入該CNN,測(cè)試其類別。同樣的,需要寫一個(gè)讀圖像的函數(shù)load_data(),代碼就不貼了。將圖像數(shù)據(jù)輸入,CNN的輸出便是該圖像的類別,這一部分的代碼在use_CNN()函數(shù)中,代碼很容易看懂。
這一部分還涉及到theano.function的使用,我把一些筆記記在了use_CNN_olivettifaces.py代碼的最后,因?yàn)楦a相關(guān),結(jié)合代碼來看會(huì)比較好,所以下面就不講這部分,有興趣的看看代碼。
最后說說測(cè)試的結(jié)果,我仍然以整副olivettifaces.gif作為輸入,得出其類別后,跟真正的label對(duì)比,程序輸出被錯(cuò)分的那些圖像,運(yùn)行結(jié)果如下:
錯(cuò)了五張,我標(biāo)了三張:
六、一些需要說明的
首先是本文的嚴(yán)謹(jǐn)性:在文章開頭我就說這只是一個(gè)toy implement,我從建立這個(gè)模型到開發(fā)實(shí)現(xiàn),用的時(shí)間比寫這篇文章的時(shí)間還少,所以后來我覺得雖然錯(cuò)分率比較低,但是很可能是模型overfitting了,為什么?樣本太少,可能這個(gè)模型就是在這400張圖片里面跑的效果比較好。你讓這40個(gè)人再拍一些照片來測(cè)這個(gè)模型,可能效果就不好了。這個(gè)是我最后要說明的,以免誤導(dǎo)。
當(dāng)然我寫這篇文章,只是為了總結(jié)一下這個(gè)實(shí)現(xiàn)流程,這一點(diǎn)希望對(duì)讀者也有參考意義。
歡迎留言交流,有任何錯(cuò)誤請(qǐng)不吝指出!