CNN人臉識別(python實現)
本文主要講解將CNN應用於人臉識別的流程,程式基於Python+numpy+theano+PIL開發,採用類似LeNet5的CNN模型,應用於olivettifaces人臉資料庫,實現人臉識別的功能,模型的誤差降到了5%以下。本程式只是個人學習過程的一個toy
implement,樣本很小,模型隨時都會過擬合。
但是,本文意在理清程式開發CNN模型的具體步驟,特別是針對影象識別,從拿到影象資料庫,到實現一個針對這個影象資料庫的CNN模型,我覺得本文對這些流程的實現具有參考意義。
《本文目錄》
一、olivettifaces人臉資料庫介紹
二、CNN的基本“構件”(LogisticRegression、HiddenLayer、LeNetConvPoolLayer)
三、組建CNN模型,設定優化演算法,應用於Olivetti Faces進行人臉識別
四、訓練結果以及引數設定的討論
五、利用訓練好的引數初始化模型
六、一些需要說明的
一、olivettifaces人臉資料庫介紹
由40個人的400張圖片構成,即每個人的人臉圖片為10張。每張圖片的灰度級為8位,每個畫素的灰度大小位於0-255之間,每張圖片大小為64×64。如下圖,這個圖片大小是1190*942,一共有20*20張人臉,故每張人臉大小是(1190/20)*(942/20)即57*47=2679:
本文所用的訓練資料就是這張圖片,400個樣本,40個類別,乍一看樣本好像比較小,用CNN效果會好嗎?先別下結論,請往下看。
要執行CNN演算法,這張圖片必須先轉化為陣列(或者說矩陣),這個用到python的影象庫PIL,幾行程式碼就可以搞定,具體的方法我之前剛好寫過一篇文章,也是用這張圖,考慮到文章冗長,就不復制過來了,連結在此:《利用Python PIL、cPickle讀取和儲存影象資料庫》。
訓練機器學習演算法,我們一般將原始資料分成訓練資料(training_set)、驗證資料(validation_set)、測試資料(testing_set)。本程式將training_set、validation_set、testing_set分別設定為320、40、40個樣本。它們的label為0~39,對應40個不同的人。這部分的程式碼如下:
- """
- 載入影象資料的函式,dataset_path即影象olivettifaces的路徑
- 載入olivettifaces後,劃分為train_data,valid_data,test_data三個資料集
- 函式返回train_data,valid_data,test_data以及對應的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)
- #分成訓練集、驗證集、測試集,大小如下
- 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]
- #將資料集定義成shared型別,才能將資料複製進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的基本“構件”(LogisticRegression、HiddenLayer、LeNetConvPoolLayer)
卷積神經網路(CNN)的基本結構就是輸入層、卷積層(conv)、子取樣層(pooling)、全連線層、輸出層(分類器)。 卷積層+子取樣層一般都會有若干個,本程式實現的CNN模型參考LeNet5,有兩個“卷積+子取樣層”LeNetConvPoolLayer。全連線層相當於MLP(多層感知機)中的隱含層HiddenLayer。輸出層即分類器,一般採用softmax迴歸(也有人直接叫邏輯迴歸,其實就是多類別的logistics regression),本程式也直接用LogisticRegression表示。 總結起來,要組建CNN模型,必須先定義LeNetConvPoolLayer、HiddenLayer、LogisticRegression這三種layer,這一點在我上一篇文章介紹CNN演算法時講得很詳細,包括程式碼註解,因為太冗長,這裡給出連結:《DeepLearning tutorial(4)CNN卷積神經網路原理簡介+程式碼詳解》。程式碼太長,就不貼具體的了,只給出框架,具體可以下載我的程式碼看看:
- #分類器,即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模型,設定優化演算法,應用於Olivetti Faces進行人臉識別
上面定義好了CNN的幾個基本“構件”,現在我們使用這些構件來組建CNN模型,本程式的CNN模型參考LeNet5,具體為:input+layer0(LeNetConvPoolLayer)+layer1(LeNetConvPoolLayer)+layer2(HiddenLayer)+layer3(LogisticRegression)
這是一個串聯結構,程式碼也很好寫,直接用第二部分定義好的各種layer去組建就行了,上一layer的輸出接下一layer的輸入,具體可以看看程式碼evaluate_olivettifaces函式中的“建立CNN模型”部分。
CNN模型組建好了,就剩下用優化演算法求解了,優化演算法採用批量隨機梯度下降演算法(MSGD),所以要先定義MSGD的一些要素,主要包括:代價函式,訓練、驗證、測試model、引數更新規則(即梯度下降)。這部分的程式碼在evaluate_olivettifaces函式中的“定義優化演算法的一些基本要素”部分。
優化演算法的基本要素也定義好了,接下來就要運用人臉影象資料集來訓練這個模型了,訓練過程有訓練步數(n_epoch)的設定,每個epoch會遍歷所有的訓練資料(training_set),本程式中也就是320個人臉圖。還有迭代次數iter,一次迭代遍歷一個batch裡的所有樣本,具體為多少要看所設定的batch_size。關於引數的設定我在下面會討論。這一部分的程式碼在evaluate_olivettifaces函式中的“訓練CNN階段”部分。
程式碼很長,只貼框架,具體可以下載我的程式碼看看:
- def evaluate_olivettifaces(learning_rate=0.05, n_epochs=200,
- dataset='olivettifaces.gif',
- nkerns=[5, 10], batch_size=40):
- #隨機數生成器,用於初始化引數....
- #載入資料.....
- #計算各資料集的batch個數....
- #定義幾個變數,x代表人臉資料,作為layer0的輸入......
- ######################
- #建立CNN模型:
- #input+layer0(LeNetConvPoolLayer)+layer1(LeNetConvPoolLayer)+layer2(HiddenLayer)+layer3(LogisticRegression)
- ######################
- ...
- ....
- ......
- #########################
- # 定義優化演算法的一些基本要素:代價函式,訓練、驗證、測試model、引數更新規則(即梯度下降)
- #########################
- ...
- ....
- ......
- #########################
- # 訓練CNN階段,尋找最優的引數。
- ########################
- ...
- .....
- .......
另外,值得一提的是,在訓練CNN階段,我們必須定時地儲存模型的引數,這是在訓練機器學習演算法時一個經常會做的事情,這一部分的詳細介紹我之前寫過一篇文章《DeepLearning tutorial(2)機器學習演算法在訓練過程中儲存引數》。簡單來說,我們要儲存CNN模型中layer0、layer1、layer2、layer3的引數,所以在“訓練CNN階段”這部分下面,有一句程式碼:
- save_params(layer0.params,layer1.params,layer2.params,layer3.params)
這個函式具體定義為:
- #儲存訓練引數的函式
- 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()
如果在其他演算法中,你要儲存的引數有五個六個甚至更多,那麼改一下這個函式的引數就行啦。
四、訓練結果以及引數設定的討論
ok,上面基本介紹完了CNN模型的構建,以及模型的訓練,我將它們的程式碼都放在train_CNN_olivettifaces.py這個原始檔中,將Olivetti Faces這張圖片跟這個程式碼檔案放在同個目錄下,執行這個檔案,幾分鐘就可以訓練完模型,並且在同個目錄下得到一個params.pkl檔案,這個檔案儲存的就是最後的模型的引數,方便你以後直接使用這個模型。 好了,現在討論一下怎麼設定引數,具體來說,程式中可以設定的引數包括:學習速率learning_rate、batch_size、n_epochs、nkerns、poolsize。下面逐一討論調節它們時對模型的影響。- 調節learning_rate
(2)nkerns=[20, 50], batch_size=40,poolsize=(2,2),learning_rate=0.01時,訓練到epoch 60多時,validation-error降到5%,test-error降到15%
(3)nkerns=[20, 50], batch_size=40,poolsize=(2,2),learning_rate=0.05時,訓練到epoch 36時,validation-error降到2.5%,test-error降到5%
注意,驗證集和測試集都只有40張圖片,也就是說只有一兩張識別錯了,還是不錯的,資料集再大點,誤差率可以降到更小。最後我將learning_rate設定為0.05。
PS:學習速率應該自適應地減小,是有專門的一些演算法的,本程式沒有實現這個功能,有時間再研究一下。
- 調節batch_size
回到本文的模型,首先因為我們train_dataset是320,valid_dataset和test_dataset都是40,所以batch_size最好都是40的因子,也就是能讓40整除,比如40、20、10、5、2、1,否則會浪費一些樣本,比如設定為30,則320/30=10,餘數時20,這20個樣本是沒被利用的。並且,如果batch_size設定為30,則得出的validation-error和test-error只是30個樣本的錯誤率,並不是全部40個樣本的錯誤率。這是設定batch_size要注意的。特別是樣本比較少的時候。
下面是我實驗時的記錄,固定其他引數,改變batch_size:
batch_size=1、2、5、10、20時,validation-error一直是97.5%,沒降下來。我覺得可能是樣本類別覆蓋率過小,因為我們的資料是按類別排的,每個類別10個樣本是連續排在一起的,batch_size等於20時其實只包含了兩個類別,這樣優化會很慢。
因此最後我將batch_size設為40,也就是valid_dataset和test_dataset的大小了,沒辦法,原始資料集樣本太少了。一般我們都不會讓batch_size達到valid_dataset和test_dataset的大小的。
- 關於n_epochs
n_epochs也就是最大的訓練步數,比如設為200,那訓練過程最多遍歷你的資料集200遍,當遍歷了200遍你的dataset時,程式會停止。n_epochs就相當於一個停止程式的控制引數,並不會影響CNN模型的優化程度和速度,只是一個控制程式結束的引數。
- nkerns=[20, 50]
20表示第一個卷積層的卷積核的個數,50表示第二個卷積層的卷積核的個數。這個我也是瞎調的,暫時沒什麼經驗可以總結。
不過從理論上來說,卷積核的個數其實就代表了特徵的個數,你提取的特徵越多,可能最後分類就越準。但是,特徵太多(卷積核太多),會增加引數的規模,加大了計算複雜度,而且有時候卷積核也不是越多越好,應根據具體的應用物件來確定。所以我覺得,CNN雖號稱自動提取特徵,免去複雜的特徵工程,但是很多引數比如這裡的nkerns還是需要去調節的,還是需要一些“人工”的。
下面是我的實驗記錄,固定batch_size=40,learning_rate=0.05,poolsize=(2,2):
(1)nkerns=[20, 50]時,訓練到epoch 36時,validation-error降到2.5%,test-error降到5%
(2)nkerns=[10, 30]時,訓練到epoch 46時,validation-error降到5%,test-error降到5%
(3)nkerns=[5, 10]時,訓練到epoch 38時,validation-error降到5%,test-error降到7.5%
- poolsize=(2, 2)
poolzize在本程式中是設定為(2,2),即從一個2*2的區域裡maxpooling出1個畫素,說白了就算4和畫素保留成1個畫素。本例程中人臉影象大小是57*47,對這種小影象來說,(2,2)時比較合理的。如果你用的影象比較大,可以把poolsize設的大一點。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++分割線+++++++++++++++++++++++++++++++++++++++++++
上面部分介紹完了CNN模型構建以及模型訓練的過程,程式碼都在train_CNN_olivettifaces.py裡面,訓練完可以得到一個params.pkl檔案,這個檔案儲存的就是最後的模型的引數,方便你以後直接使用這個模型。以後只需利用這些儲存下來的引數來初始化CNN模型,就得到一個可以使用的CNN系統,將人臉圖輸入這個CNN系統,預測人臉圖的類別。
接下來就介紹怎麼使用訓練好的引數的方法,這部分的程式碼放在use_CNN_olivettifaces.py檔案中。
五、利用訓練好的引數初始化模型
在train_CNN_olivettifaces.py中的LeNetConvPoolLayer、HiddenLayer、LogisticRegression是用隨機數生成器去隨機初始化的,我們將它們定義為可以用引數來初始化的版本,其實很簡單,程式碼只需要做稍微的改動,只需要在LogisticRegression、HiddenLayer、LeNetConvPoolLayer這三個class的__init__()函式中加兩個引數params_W,params_b,然後將params_W,params_b賦值給這三個class裡的W和b:- self.W = params_W
- self.b = params_b
params_W,params_b就是從params.pkl檔案中讀取來的,讀取的函式:
- #讀取之前儲存的訓練引數
- #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,可以用引數初始化的CNN定義好了,那現在就將需要測試的人臉圖輸入該CNN,測試其類別。同樣的,需要寫一個讀影象的函式load_data(),程式碼就不貼了。將影象資料輸入,CNN的輸出便是該影象的類別,這一部分的程式碼在use_CNN()函式中,程式碼很容易看懂。 這一部分還涉及到theano.function的使用,我把一些筆記記在了use_CNN_olivettifaces.py程式碼的最後,因為跟程式碼相關,結合程式碼來看會比較好,所以下面就不講這部分,有興趣的看看程式碼。 最後說說測試的結果,我仍然以整副olivettifaces.gif作為輸入,得出其類別後,跟真正的label對比,程式輸出被錯分的那些影象,執行結果如下:
錯了五張,我標了三張: