人臉檢測及識別python實現系列(5)——利用keras庫訓練人臉識別模型
經過前面稍顯羅嗦的准備工作,現在,我們終於可以嘗試訓練我們自己的卷積神經網絡模型了。CNN擅長圖像處理,keras庫的tensorflow版亦支持此種網絡模型,萬事俱備,就放開手做吧。前面說過,我們需要通過大量的訓練數據訓練我們的模型,因此首先要做的就是把訓練數據准備好,並將其輸入給CNN。前面我們已經准備好了2000張臉部圖像,但沒有進行標注,並且還需要將數據加載到內存,以方便輸入給CNN。因此,第一步工作就是加載並標注數據到內存。
首先我們建立一個空白的python文件,文件名為:load_face_dataset.py,代碼如下:
1 # -*- coding: utf-8 -*- 2 3 import os 4 import sys 5 import numpy as np 6 import cv2 7 8 IMAGE_SIZE = 64 9 10 #按照指定圖像大小調整尺寸 11 def resize_image(image, height = IMAGE_SIZE, width = IMAGE_SIZE): 12 top, bottom, left, right = (0, 0, 0, 0) 13 14 #獲取圖像尺寸 15 h, w, _ = image.shape 16 17 #對於長寬不相等的圖片,找到最長的一邊 18 longest_edge = max(h, w) 19 20 #計算短邊需要增加多上像素寬度使其與長邊等長 21 if h < longest_edge: 22 dh = longest_edge - h 23 top = dh // 2 24 bottom = dh - top 25 elif w < longest_edge: 26 dw = longest_edge - w 27 left = dw // 2 28 right = dw - left 29 else: 30 pass 31 32 #RGB顏色 33 BLACK = [0, 0, 0] 34 35 #給圖像增加邊界,是圖片長、寬等長,cv2.BORDER_CONSTANT指定邊界顏色由value指定 36 constant = cv2.copyMakeBorder(image, top , bottom, left, right, cv2.BORDER_CONSTANT, value = BLACK) 37 38 #調整圖像大小並返回 39 return cv2.resize(constant, (height, width)) 40 41 #讀取訓練數據 42 images = [] 43 labels = [] 44 def read_path(path_name): 45 for dir_item in os.listdir(path_name): 46 #從初始路徑開始疊加,合並成可識別的操作路徑 47 full_path = os.path.abspath(os.path.join(path_name, dir_item)) 48 49 if os.path.isdir(full_path): #如果是文件夾,繼續遞歸調用 50 read_path(full_path) 51 else: #文件 52 if dir_item.endswith('.jpg'): 53 image = cv2.imread(full_path) 54 image = resize_image(image, IMAGE_SIZE, IMAGE_SIZE) 55 56 #放開這個代碼,可以看到resize_image()函數的實際調用效果 57 #cv2.imwrite('1.jpg', image) 58 59 images.append(image) 60 labels.append(path_name) 61 62 return images,labels 63 64 65 #從指定路徑讀取訓練數據 66 def load_dataset(path_name): 67 images,labels = read_path(path_name) 68 69 #將輸入的所有圖片轉成四維數組,尺寸為(圖片數量*IMAGE_SIZE*IMAGE_SIZE*3) 70 #我和閨女兩個人共1200張圖片,IMAGE_SIZE為64,故對我來說尺寸為1200 * 64 * 64 * 3 71 #圖片為64 * 64像素,一個像素3個顏色值(RGB) 72 images = np.array(images) 73 print(images.shape) 74 75 #標注數據,'me'文件夾下都是我的臉部圖像,全部指定為0,另外一個文件夾下是閨女的,全部指定為1 76 labels = np.array([0 if label.endswith('me') else 1 for label in labels]) 77 78 return images, labels 79 80 if __name__ == '__main__': 81 if len(sys.argv) != 2: 82 print("Usage:%s path_name\r\n" % (sys.argv[0])) 83 else: 84 images, labels = load_dataset(sys.argv[1]) 85
上面給出的代碼主函數就是load_dataset(),它將圖片數據進行標注並以多維數組的形式加載到內存中。我實際用於訓練的臉部數據共1200張,我去掉了一些模糊的或者表情基本一致的頭像,留下了清晰、臉部表情有些區別的,我和閨女各留了600張,所以訓練數據變成了1200。上述代碼注釋很清楚,不多講,唯一一個理解起來稍微有點難度的就是resize_image()函數。這個函數其實就做了一件事情,判斷圖片是不是四邊等長,也就是圖片是不是正方形。如果不是,則短的那兩邊增加兩條黑色的邊框,使圖像變成正方形,這樣再調用cv2.resize()函數就可以實現等比例縮放了。因為我們指定縮放的比例就是64 x 64,只有縮放之前圖像為正方形才能確保圖像不失真。resize_image()函數的執行結果如下所示:
上圖為200 x 300的圖片,寬度小於高度,因此,需要增加寬度,正常應該是兩邊各增加寬50像素的黑邊:
如我們所願,成了一個300 x 300的正方形圖片,這時我們再縮放到64 x 64就可以了:
上圖就是我們將要輸入到CNN中的圖片,之所以縮放到這么小,主要是為了減少計算量及內存占用,提升訓練速度。執行程序之前,請把圖片組織一下,結構參見下圖:
load_face_dataset.py所在文件夾下建立一個data文件夾,在data下再建立me和other兩個文件夾,me放本人的圖像,other放其他人的,對我來說就是閨女的。我各放了600張圖片。
這些工作做完之后,我們就可以開始構建訓練代碼了。
同樣,在load_face_dataset.py所在文件夾下新建一個python空白文件face_train_use_keras.py,然后我們先把需要的庫文件添加到代碼中:
#-*- coding: utf-8 -*- import random import numpy as np from sklearn.cross_validation import train_test_split from keras.preprocessing.image import ImageDataGenerator from keras.models import Sequential from keras.layers import Dense, Dropout, Activation, Flatten from keras.layers import Convolution2D, MaxPooling2D from keras.optimizers import SGD from keras.utils import np_utils from keras.models import load_model from keras import backend as K from load_face_dataset import load_dataset, resize_image, IMAGE_SIZE
我們先不管導入的這些庫是干啥的,你只要知道接下來的代碼要用到這些庫就夠了,用到了我們再講。到目前為止,數據加載的工作已經完成,我們只需調用這個接口即可。關於訓練集的使用,我們需要拿出一部分用於訓練網絡,建立識別模型;另一部分用於驗證模型。同時我們還有一些其它的比如數據歸一化等預處理的工作要做,因此,我們把這些工作封裝成一個dataset類來完成:
1 class Dataset: 2 def __init__(self, path_name): 3 #訓練集 4 self.train_images = None 5 self.train_labels = None 6 7 #驗證集 8 self.valid_images = None 9 self.valid_labels = None 10 11 #測試集 12 self.test_images = None 13 self.test_labels = None 14 15 #數據集加載路徑 16 self.path_name = path_name 17 18 #當前庫采用的維度順序 19 self.input_shape = None 20 21 #加載數據集並按照交叉驗證的原則划分數據集並進行相關預處理工作 22 def load(self, img_rows = IMAGE_SIZE, img_cols = IMAGE_SIZE, 23 img_channels = 3, nb_classes = 2): 24 #加載數據集到內存 25 images, labels = load_dataset(self.path_name) 26 27 train_images, valid_images, train_labels, valid_labels = train_test_split(images, labels, test_size = 0.3, random_state = random.randint(0, 100)) 28 _, test_images, _, test_labels = train_test_split(images, labels, test_size = 0.5, random_state = random.randint(0, 100)) 29 30 #當前的維度順序如果為'th',則輸入圖片數據時的順序為:channels,rows,cols,否則:rows,cols,channels 31 #這部分代碼就是根據keras庫要求的維度順序重組訓練數據集 32 if K.image_dim_ordering() == 'th': 33 train_images = train_images.reshape(train_images.shape[0], img_channels, img_rows, img_cols) 34 valid_images = valid_images.reshape(valid_images.shape[0], img_channels, img_rows, img_cols) 35 test_images = test_images.reshape(test_images.shape[0], img_channels, img_rows, img_cols) 36 self.input_shape = (img_channels, img_rows, img_cols) 37 else: 38 train_images = train_images.reshape(train_images.shape[0], img_rows, img_cols, img_channels) 39 valid_images = valid_images.reshape(valid_images.shape[0], img_rows, img_cols, img_channels) 40 test_images = test_images.reshape(test_images.shape[0], img_rows, img_cols, img_channels) 41 self.input_shape = (img_rows, img_cols, img_channels) 42 43 #輸出訓練集、驗證集、測試集的數量 44 print(train_images.shape[0], 'train samples') 45 print(valid_images.shape[0], 'valid samples') 46 print(test_images.shape[0], 'test samples') 47 48 #我們的模型使用categorical_crossentropy作為損失函數,因此需要根據類別數量nb_classes將 49 #類別標簽進行one-hot編碼使其向量化,在這里我們的類別只有兩種,經過轉化后標簽數據變為二維 50 train_labels = np_utils.to_categorical(train_labels, nb_classes) 51 valid_labels = np_utils.to_categorical(valid_labels, nb_classes) 52 test_labels = np_utils.to_categorical(test_labels, nb_classes) 53 54 #像素數據浮點化以便歸一化 55 train_images = train_images.astype('float32') 56 valid_images = valid_images.astype('float32') 57 test_images = test_images.astype('float32') 58 59 #將其歸一化,圖像的各像素值歸一化到0~1區間 60 train_images /= 255 61 valid_images /= 255 62 test_images /= 255 63 64 self.train_images = train_images 65 self.valid_images = valid_images 66 self.test_images = test_images 67 self.train_labels = train_labels 68 self.valid_labels = valid_labels 69 self.test_labels = test_labels
我們構建了一個Dataset類,用於數據加載及預處理。其中,__init__()為類的初始化函數,load()則完成實際的數據加載及預處理工作。加載前面已經說過很多了,就不多說了。關於預處理,我們做了幾項工作:
1)按照交叉驗證的原則將數據集划分成三部分:訓練集、驗證集、測試集;
2)按照keras庫運行的后端系統要求改變圖像數據的維度順序;
3)將數據標簽進行one-hot編碼,使其向量化
4)歸一化圖像數據
關於第一項工作,先簡單說說什么是交叉驗證?交叉驗證屬於機器學習中常用的精度測試方法,它的目的是提升模型的可靠和穩定性。我們會拿出大部分數據用於模型訓練,小部分數據用於對訓練后的模型驗證,驗證結果會與驗證集真實值(即標簽值)比較並計算出差平方和,此項工作重復進行,直至所有驗證結果與真實值相同,交叉驗證結束,模型交付使用。在這里我們導入了sklearn庫的交叉驗證模塊,利用函數train_test_split()來划分訓練集和驗證集,具體語句如下:
train_images, valid_images, train_labels, valid_labels = train_test_split(images, labels, test_size = 0.2,
random_state = random.randint(0, 100))
train_test_split()會根據test_size參數按比例划分數據集(不要被test_size的外表所迷惑,它只是用來指定數據集划分比例的,本質上與測試無關,划分完了你愛咋用就咋用),在這里我們划分出了30%的數據用於驗證,70%用於訓練模型。參數random_state用於指定一個隨機數種子,從全部數據中隨機選取數據建立訓練集和驗證集,所以你將會看到每次訓練的結果都會稍有不同。當然,為了省事,測試集我也調用了這個函數:
_, test_images, _, test_labels = train_test_split(images, labels, test_size = 0.5,
random_state = random.randint(0, 100))
在這里,測試集我選擇的比例為0.5,所以前面的“_, test_images, _, test_labels”語句你調個順序也成,即“test_images, _, test_labels, _”,但是如果你改成其它數值,就必須嚴格按照代碼給出的順序才能得到你想要的結果。train_test_split()函數會按照訓練集特征數據(這里就是圖像數據)、測試集特征數據、訓練集標簽、測試集標簽的順序返回各數據集。所以,看你的選擇了。
關於第二項工作,我們前面不止一次說過keras建立在tensorflow或theano基礎上,換句話說,keras的后端系統可以是tensorflow也可以是theano。后端系統決定了圖像數據輸入CNN網絡時的維度順序,tensorflow的維度順序為行數(rows)、列數(cols)、通道數(顏色通道,channels);theano則是通道數、行數、列數。所以,我們通過調用image_dim_ordering()函數來確定后端系統的類型(‘th’代表theano,'tf'代表tensorflow),然后我們再通過numpy提供的reshape()函數重新調整數組維度。
關於第三項工作,對標簽集進行one-hot編碼的原因是我們的訓練模型采用categorical_crossentropy作為損失函數(多分類問題的常用函數,后面會詳解),這個函數要求標簽集必須采用one-hot編碼形式。所以,我們對訓練集、驗證集和測試集標簽均做了編碼轉換。那么什么是one-hot編碼呢?one-hot有的翻譯成獨熱,有的翻譯成一位有效,個人感覺一位有效更直白一些。因為one-hot編碼采用狀態寄存器的組織方式對狀態進行編碼,每個狀態值對應一個寄存器位,且任意時刻,只有一位有效。對於我們的程序來說,我們類別狀態只有兩種(nb_classes = 2):0和1,0代表我,1代表閨女。one-hot編碼會提供兩個寄存器位保存這兩個狀態,如果標簽值為0,則編碼后值為[1 0],代表第一位有效;如果為1,則編碼后值為[0 1],代表第2為有效。換句話說,one-hot編碼將數值變成了位置信息,使其向量化,這樣更方便CNN操作。
關於第四項工作,數據集先浮點后歸一化的目的是提升網絡收斂速度,減少訓練時間,同時適應值域在(0,1)之間的激活函數,增大區分度。其實歸一化有一個特別重要的原因是確保特征值權重一致。舉個例子,我們使用mse這樣的均方誤差函數時,大的特征數值比如(5000-1000)2與小的特征值(3-1)2相加再求平均得到的誤差值,顯然大值對誤差值的影響最大,但大部分情況下,特征值的權重應該是一樣的,只是因為單位不同才導致數值相差甚大。因此,我們提前對特征數據做歸一化處理,以解決此類問題。關於歸一化的詳細介紹有興趣的請參考如下鏈接:
數據准備工作到此完成,接下來就要進入整個系列最關鍵的一個節點——建立我們自己的卷積神經網絡模型,激動吧;)?與數據集加載及預處理模塊一樣,我們依然將模型構建成一個類來使用,新建的這個模型類添加在Dataset類的下面:
1 #CNN網絡模型類 2 class Model: 3 def __init__(self): 4 self.model = None 5 6 #建立模型 7 def build_model(self, dataset, nb_classes = 2): 8 #構建一個空的網絡模型,它是一個線性堆疊模型,各神經網絡層會被順序添加,專業名稱為序貫模型或線性堆疊模型 9 self.model = Sequential() 10 11 #以下代碼將順序添加CNN網絡需要的各層,一個add就是一個網絡層 12 self.model.add(Convolution2D(32, 3, 3, border_mode='same', 13 input_shape = dataset.input_shape)) #1 2維卷積層 14 self.model.add(Activation('relu')) #2 激活函數層 15 16 self.model.add(Convolution2D(32, 3, 3)) #3 2維卷積層 17 self.model.add(Activation('relu')) #4 激活函數層 18 19 self.model.add(MaxPooling2D(pool_size=(2, 2))) #5 池化層 20 self.model.add(Dropout(0.25)) #6 Dropout層 21 22 self.model.add(Convolution2D(64, 3, 3, border_mode='same')) #7 2維卷積層 23 self.model.add(Activation('relu')) #8 激活函數層 24 25 self.model.add(Convolution2D(64, 3, 3)) #9 2維卷積層 26 self.model.add(Activation('relu')) #10 激活函數層 27 28 self.model.add(MaxPooling2D(pool_size=(2, 2))) #11 池化層 29 self.model.add(Dropout(0.25)) #12 Dropout層 30 31 self.model.add(Flatten()) #13 Flatten層 32 self.model.add(Dense(512)) #14 Dense層,又被稱作全連接層 33 self.model.add(Activation('relu')) #15 激活函數層 34 self.model.add(Dropout(0.5)) #16 Dropout層 35 self.model.add(Dense(nb_classes)) #17 Dense層 36 self.model.add(Activation('softmax')) #18 分類層,輸出最終結果 37 38 #輸出模型概況 39 self.model.summary()
先不解釋代碼,咱先看看上述代碼的運行情況,接着再添加幾行測試代碼:
if __name__ == '__main__': dataset = Dataset('./data/') dataset.load() model = Model() model.build_model(dataset)
然后在控制台輸入:
python3 face_train_use_keras.py
如果你沒敲錯代碼,一切順利的話,你應該看到類似下面這樣的輸出內容:
我們通過調用self.model.summary()函數將網絡模型基本結構信息展示在我們面前,包括層類型、維度、參數個數、層連接等信息,一目了然,簡潔、清晰。通過上圖我們可以看出,這個網絡模型共18層,包括4個卷積層、5個激活函數層、2個池化層(pooling layer)、3個Dropout層、2個全連接層、1個Flatten層、1個分類層,訓練參數為6,489,634個,還是很可觀的。
你看,這個實際運作的網絡比我們上次給出的那個3層卷積的網絡復雜多了,多了池化、Dropout、Dense、Flatten以及最終的分類層,這些都是些什么鬼東西,需要我們逐個理一理:
卷積層(convolution layer):這一層前面講了太多,這里重點講講Convolution2D()函數。根據keras官方文檔描述,2D代表這是一個2維卷積,其功能為對2維輸入進行滑窗卷積計算。我們的臉部圖像尺寸為64*64,擁有長、寬兩維,所以在這里我們使用2維卷積函數計算卷積。所謂的滑窗計算,其實就是利用卷積核逐個像素、順序進行計算,如下圖:
上圖選擇了最簡單的均值卷積核,3x3大小,我們用這個卷積核作為掩模對前面4x4大小的圖像逐個像素作卷積運算。首先我們將卷積核中心對准圖像第一個像素,在這里就是像素值為237的那個像素。卷積核覆蓋的區域(掩模之稱即由此來),其下所有像素取均值然后相加:
C(1) = 0 * 0.5 + 0 * 0.5 + 0 * 0.5 + 0 * 0.5 + 237 * 0.5 + 203 * 0.5 + 0 * 0.5 + 123 * 0.5 + 112 * 0.5
結果直接替換卷積核中心覆蓋的像素值,接着是第二個像素、然后第三個,從左至右,由上到下……以此類推,卷積核逐個覆蓋所有像素。整個操作過程就像一個滑動的窗口逐個滑過所有像素,最終生成一副尺寸相同但已經過卷積處理的圖像。上圖我們采用的是均值卷積核,實際效果就是將圖像變模糊了。顯然,卷積核覆蓋圖像邊界像素時,會有部分區域越界,越界的部分我們以0填充,如上圖。對於此種情況,還有一種處理方法,就是丟掉邊界像素,從覆蓋區域不越界的像素開始計算。像上圖,如果采用丟掉邊界像素的方法,3x3的卷積核就應該從第2行第2列的像素(值為112)開始,到第3行第3列結束,最終我們會得到一個2x2的圖像。這種處理方式會丟掉圖像的邊界特征;而第一種方式則保留了圖像的邊界特征。在我們建立的模型中,卷積層采用哪種方式處理圖像邊界,卷積核尺寸有多大等參數都可以通過Convolution2D()函數來指定:
self.model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape = dataset.input_shape))
第一個卷積層包含32個卷積核,每個卷積核大小為3x3,border_mode值為“same”意味着我們采用保留邊界特征的方式滑窗,而值“valid”則指定丟掉邊界像素。根據keras開發文檔的說明,當我們將卷積層作為網絡的第一層時,我們還應指定input_shape參數,顯式地告知輸入數據的形狀,對我們的程序來說,input_shape的值為(64,64,3),來自Dataset類,代表64x64的彩色RGB圖像。
激活函數層:它的作用前面已經說了,這里講一下代碼中采用的relu(Rectified Linear Units,修正線性單元)函數,它的數學形式如下:
ƒ(x) = max(0, x)
這個函數非常簡單,其輸出一目了然,小於0的輸入,輸出全部為0,大於0的則輸入與輸出相等。該函數的優點是收斂速度快,除了它,keras庫還支持其它幾種激活函數,如下:
- softplus
- softsign
- tanh
- sigmoid
- hard_sigmoid
- linear
它們的函數式、優缺點度娘會告訴你,不多說。對於不同的需求,我們可以選擇不同的激活函數,這也是模型訓練可調整的一部分,運用之妙,存乎一心,請自忖之。另外再交代一句,其實激活函數層按照我們前文所講,其屬於人工神經元的一部分,所以我們亦可以在構造層對象時通過傳遞activation參數設置,如下:
self.model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape = dataset.input_shape)) self.model.add(Activation('relu')) #設置為單獨的激活層 #通過傳遞activation參數設置,與上兩行代碼的作用相同 self.model.add(Convolution2D(32, 3, 3, border_mode='same', input_shape = dataset.input_shape, activation='relu'))
池化層(pooling layer):池化層存在的目的是縮小輸入的特征圖,簡化網絡計算復雜度;同時進行特征壓縮,突出主要特征。我們通過調用MaxPooling2D()函數建立了池化層,這個函數采用了最大值池化法,這個方法選取覆蓋區域的最大值作為區域主要特征組成新的縮小后的特征圖:
顯然,池化層與卷積層覆蓋區域的方法不同,前者按照池化尺寸逐塊覆蓋特征圖,卷積層則是逐個像素滑動覆蓋。對於我們輸入的64x64的臉部特征圖來說,經過2x2池化后,圖像變為32x32大小。
Dropout層:隨機斷開一定百分比的輸入神經元鏈接,以防止過擬合。那么什么是過擬合呢?一句話解釋就是訓練數據預測准確率很高,測試數據預測准確率很低,用圖形表示就是擬合曲線較尖,不平滑。導致這種現象的原因是模型的參數很多,但訓練樣本太少,導致模型擬合過度。為了解決這個問題,Dropout層將有意識的隨機減少模型參數,讓模型變得簡單,而越簡單的模型越不容易產生過擬合。代碼中Dropout()函數只有一個輸入參數——指定拋棄比率,范圍為0~1之間的浮點數,其實就是百分比。這個參數亦是一個可調參數,我們可以根據訓練結果調整它以達到更好的模型成熟度。
Flatten層:截止到Flatten層之前,在網絡中流動的數據還是多維的(對於我們的程序就是2維的),經過多次的卷積、池化、Dropout之后,到了這里就可以進入全連接層做最后的處理了。全連接層要求輸入的數據必須是一維的,因此,我們必須把輸入數據“壓扁”成一維后才能進入全連接層,Flatten層的作用即在於此。該層的作用如此純粹,因此反映到代碼上我們看到它不需要任何輸入參數。
全連接層(dense layer):全連接層的作用就是用於分類或回歸,對於我們來說就是分類。keras將全連接層定義為Dense層,其含義就是這里的神經元連接非常“稠密”。我們通過Dense()函數定義全連接層。這個函數的一個必填參數就是神經元個數,其實就是指定該層有多少個輸出。在我們的代碼中,第一個全連接層(#14 Dense層)指定了512個神經元,也就是保留了512個特征輸出到下一層。這個參數可以根據實際訓練情況進行調整,依然是沒有可參考的調整標准,自調之。
分類層:全連接層最終的目的就是完成我們的分類要求:0或者1,模型構建代碼的最后兩行完成此項工作:
self.model.add(Dense(nb_classes)) #17 Dense層 self.model.add(Activation('softmax')) #18 分類層,輸出最終結果
第17層我們按照實際的分類要求指定神經元個數,對我們來說就是2,18層我們通過softmax函數完成最終分類。關於softmax函數,其函數式如下:
代表第L層第j個神經元的輸出,
代表第L層第j個神經元的輸入,我們用單個神經元的輸入結合自然常數e做指數運算,運算結果除以所有L層神經元輸入的指數運算之和,就得到了一個介於0~1之間的浮點值
。顯然,從上述公式很容易看出,所有神經元輸出之和肯定為1:
這個值其實就是第j個神經元在所有神經元輸出中所占的百分比。從分類的角度來說,該神經元的輸出值越大,其對應的類別為真實類別的可能性就越大。因此,經過softmax函數,上層的N個輸入被映射成N個概率分布,概率之和為1,概率值最大者即為模型預測的類別。
好了,模型構建完畢,接下來構建訓練代碼,在build_model()函數下面繼續添加如下代碼:
1 #訓練模型 2 def train(self, dataset, batch_size = 20, nb_epoch = 10, data_augmentation = True): 3 sgd = SGD(lr = 0.01, decay = 1e-6, 4 momentum = 0.9, nesterov = True) #采用SGD+momentum的優化器進行訓練,首先生成一個優化器對象 5 self.model.compile(loss='categorical_crossentropy', 6 optimizer=sgd, 7 metrics=['accuracy']) #完成實際的模型配置工作 8 9 #不使用數據提升,所謂的提升就是從我們提供的訓練數據中利用旋轉、翻轉、加噪聲等方法創造新的 10 #訓練數據,有意識的提升訓練數據規模,增加模型訓練量 11 if not data_augmentation: 12 self.model.fit(dataset.train_images, 13 dataset.train_labels, 14 batch_size = batch_size, 15 nb_epoch = nb_epoch, 16 validation_data = (dataset.valid_images, dataset.valid_labels), 17 shuffle = True) 18 #使用實時數據提升 19 else: 20 #定義數據生成器用於數據提升,其返回一個生成器對象datagen,datagen每被調用一 21 #次其生成一組數據(順序生成),節省內存,其實就是python的數據生成器 22 datagen = ImageDataGenerator( 23 featurewise_center = False, #是否使輸入數據去中心化(均值為0), 24 samplewise_center = False, #是否使輸入數據的每個樣本均值為0 25 featurewise_std_normalization = False, #是否數據標准化(輸入數據除以數據集的標准差) 26 samplewise_std_normalization = False, #是否將每個樣本數據除以自身的標准差 27 zca_whitening = False, #是否對輸入數據施以ZCA白化 28 rotation_range = 20, #數據提升時圖片隨機轉動的角度(范圍為0~180) 29 width_shift_range = 0.2, #數據提升時圖片水平偏移的幅度(單位為圖片寬度的占比,0~1之間的浮點數) 30 height_shift_range = 0.2, #同上,只不過這里是垂直 31 horizontal_flip = True, #是否進行隨機水平翻轉 32 vertical_flip = False) #是否進行隨機垂直翻轉 33 34 #計算整個訓練樣本集的數量以用於特征值歸一化、ZCA白化等處理 35 datagen.fit(dataset.train_images) 36 37 #利用生成器開始訓練模型 38 self.model.fit_generator(datagen.flow(dataset.train_images, dataset.train_labels, 39 batch_size = batch_size), 40 samples_per_epoch = dataset.train_images.shape[0], 41 nb_epoch = nb_epoch, 42 validation_data = (dataset.valid_images, dataset.valid_labels))
按照我們的習慣,依然先不解釋代碼,先看執行結果,程序執行前添加如下一行代碼:
#先前添加的測試build_model()函數的代碼 model.build_model(dataset) #測試訓練函數的代碼 model.train(dataset)
保存,控制台輸入:
python3 face_train_use_keras.py
訓練結果如下:
我們共進行了10輪次訓練(nb_epoch = 10),每輪42次迭代(840 / 20,訓練集1200 x (1-0.3) = 840),每次迭代訓練使用20個樣本(batch_size = 20),得到的訓練結果還不錯(以第10輪次訓練結果為例):
訓練誤差(loss):0.0529
訓練准確率(acc):0.9893
驗證誤差(val_loass):0.0377
驗證准確率(val_acc):0.9917
驗證集准確率高達99%,至少從驗證結果上看模型已達實用化要求,下一步可以用測試數據集對其進行測試了。添加測試代碼之前,我們需要對訓練代碼中幾個關鍵函數交代一下。首先是優化器函數,優化器用於訓練模型,它的作用就是調整訓練參數(權重和偏置值)使其最優,確保e值最小(參見系列4——CNN入門)。keras提供了很多優化器,我們在這里采用的SGD就是其中一種,它就是機器學習領域最著名的隨機梯度下降法。函數第一個參數lr用於指定學習效率(lr,Learning Rate,參見系列4),其值為大於0的浮點數。decay指定每次更新后學習效率的衰減值,這個值一定很小(1e-6,0.000 001),否則速率會衰減很快。momentum指定動量值。SGD方法有一個明顯的缺點就是,它的下降方向完全依賴當前的訓練樣本(batch),因此其優化十分不穩定。為了解決這個問題,大牛們引進了動量(momentum),用它來模擬物體運動時的慣性,讓優化器在一定程度上保留之前的優化方向,同時利用當前樣本微調最終的優化方向,這樣即能增加穩定性,提高學習速度,又在一定程度上避免了陷入局部最優陷阱。參數momentum即用於指定在多大程度上保留原有方向,其值為0~1之間的浮點數。一般來說,選擇一個在0.5~0.9之間的數即可。代碼中SGD函數的最后一個參數nesterov則用於指定是否采用nesterov動量方法,nesterov momentum是對傳統動量法的一個改進方法,其效率更高,關於它的詳細介紹可參考如下鏈接:
http://www.360doc.com/content/16/1010/08/36492363_597225745.shtml
對於compile()函數,其作用就是編譯模型以完成實際的配置工作,為接下來的模型訓練做好准備。換句話說,compile之后模型就可以開始訓練了。這個函數有一個很重要的參數:loss,它用於指定一個損失函數。所謂損失函數,通俗地說,它是統計學中衡量損失和錯誤程度的函數,顯然,其值越小,模型就越好。如果你仔細閱讀了系列4——CNN入門,那么,你肯定能猜到這個函數其實就是我們的優化對象。代碼中loss的值為“categorical_crossentropy”,常用於多分類問題,其與激活函數softmax配對使用(我們的類別只有兩種,也可采用‘binary_crossentropy’二值分類函數,該函數與sigmoid配對使用,注意如果采用它就不需要one-hot編碼)。參數metrics用於指定模型評價指標,參數值”accuracy“表示用准確率來評價(keras官方文檔目前沒有查到第2種評價指標,有知道的請告知)。
接着就是數據提升,我們可以選擇不提升,也就是采用原始訓練集和驗證集,這時我們直接調用model.fit()函數即可開始模型訓練。該函數shuffle參數用於指定是否隨機打亂數據集。一般來說選擇數據提升要比不提升好,這樣可以讓我們利用有限數量的圖片獲得無限數量的訓練圖片。因為我們一旦選擇數據提升,ImageDataGenerator()函數返回的生成器會在模型訓練時無限生成訓練數據,直至所有訓練輪次(epoch)結束(對我們的代碼來說就是840 x 10,生成了8400張圖片)。model.fit_generator()函數使用生成器開始模型訓練。
在這里需要重點交代一下batch_size和nb_epoch兩個參數。nb_epoch指定模型需要訓練多少輪次,使用訓練集全部樣本訓練一次為一個訓練輪次。根據模型成熟度,我們可以適當調整該值以增加或減少訓練次數。batch_size則是一個影響模型訓練結果的重要參數。我們知道,一個訓練輪次要經過多次迭代訓練才能讓模型逐漸趨向本輪最優,這是因為理論上每次迭代訓練結束后,模型都應該朝着梯度下降的方向前進一步,直至全部樣本訓練完畢,模型梯度到達本輪最小點。之所以說理論上,是因為決定梯度方向的是訓練樣本,每次迭代訓練選取的樣本——其決定的下降方向能否很好的代表樣本全體,直接決定了模型能否到達正確的極值點。對於小的訓練集,我們完全可以采用全數據集的方式進行訓練,因為,全數據集確定的方向肯定能代表正確方向。但這樣做對大的訓練集就很不現實,因為內存有限,無法一次載入全部數據。於是,批梯度下降法(Mini-batches Learning)應運而生。我們一次選取適當數量的訓練樣本(視內存大小,可多可少),逐批次迭代,直至本輪全部樣本訓練完畢。參數batch_size的作用即在於此,其指定每次迭代訓練樣本的數量。該值的選取非常講究,不能盲目的增大或減小,因為batch_size太大或太小都會讓模型訓練效率變慢。顯然,batch_size肯定存在一個局部最優值,這需要我們慢慢調試,調試時可從一個小值開始,慢慢加大,直至到達一個合理值(建議編碼實現該參數調優)。
現在模型訓練的工作已經完成,接下來我們就要考慮模型使用的問題了。要想使用模型,我們必須能夠把模型保存下來,因此,我們繼續為Model類添加兩個函數:
1 MODEL_PATH = './me.face.model.h5' 2 def save_model(self, file_path = MODEL_PATH): 3 self.model.save(file_path) 4 5 def load_model(self, file_path = MODEL_PATH): 6 self.model = load_model(file_path)
一個函數用於保存模型,一個函數用於加載模型。keras庫利用了壓縮效率更高的HDF5保存模型,所以我們用“.h5”作為文件后綴。上述代碼添加完畢后,我們接着在文件尾部添加測試代碼,把模型訓練好並把模型保存下來:
1 if __name__ == '__main__': 2 dataset = Dataset('./data/') 3 dataset.load() 4 5 model = Model() 6 model.build_model(dataset) 7 model.train(dataset) 8 model.save_model(file_path = './model/me.face.model.h5')
執行上述代碼,順利的話,我們應當看到模型保存文件出現在model文件夾下了:
好了,接下來我們就要用前面Dataset類提供的測試集測試模型了。首先,我們為Model類添加一個模型評估函數:
1 def evaluate(self, dataset): 2 score = self.model.evaluate(dataset.test_images, dataset.test_labels, verbose = 1) 3 print("%s: %.2f%%" % (self.model.metrics_names[1], score[1] * 100))
然后,繼續添加測試代碼:
1 if __name__ == '__main__': 2 dataset = Dataset('./data/') 3 dataset.load() 4 5 ''' 6 #訓練模型,這段代碼不用,注釋掉 7 model = Model() 8 model.build_model(dataset) 9 model.train(dataset) 10 model.save_model(file_path = './model/me.face.model.h5') 11 ''' 12 13 #評估模型 14 model = Model() 15 model.load_model(file_path = './model/me.face.model.h5') 16 model.evaluate(dataset)
執行結果如下:
准確率99.5%,相當高的評估結果了。
至此,我們完成了模型建立工作,下一篇博文討論如何用它識別出“我”了。