辛普森一家動畫人物識別(CNN)
來自專欄機器學習的打怪之路
前言:
- 此文是在學習機器學習過程中對實踐項目的一點總結,若有錯漏之處,請指正,敬請諒解
- 使用工具為Python3,Keras
該項目是運用卷積神經網路(CNN)來實現臉部識別,知乎里已經有一篇相關項目介紹
量子學園:刷劇不忘學CNN:TF+Keras識別辛普森一家人物 | 教程+代碼+數據集但本文講的更詳細,並且有部分代碼改進,更偏重基礎並增加了實現過程中的心得體會,例如代碼的注釋與講解,調參策略等,對於想拿此項目練手的同學更具有指導性,同樣本文將從以下幾個方面進行介紹:
- 數據預處理
- 構建模型
- 模型訓練
- 模型評估
- 模型優化
- 超參數選擇
- 調參策略
數據集的介紹可以參閱上面我發的鏈接,下載鏈接:https://pan.baidu.com/s/14hUSlqipz8yTWGdgJCXkaw 密碼:hrmc,目前我這裡的人物類別有47類,項目選取了其中18類作為樣本數據集,樣本圖片大小不一,樣式千奇百怪,背景也不盡相同,隨便貼幾張圖大家感受下
看完後有什麼感想?是不是覺得想要達到100%的識別準確率是真的難- -|||
1.數據預處理
從文件夾中選取樣本,每個人物選取的訓練集樣本佔比為0.85,1000個樣本,測試集佔比0.15,如果選取的圖片數目小於該人物的總圖片數,則從中隨機選取,否則選取該人物所有的圖片作為樣本數據集,然後通過OpenCV來讀取圖片,因為OpenCV默認通道為BGR,所以需要對圖片轉換為咱們熟悉的RGB圖像。
a = cv2.imread(pic)a = cv2.cvtColor(a, cv2.COLOR_BGR2RGB)
注意,對於深度學習來說,輸入的圖片大小必須是一致的,因為只有這樣才會統一編碼,所以每張圖片必須resize成統一大小。
a = cv2.resize(a, (pic_size,pic_size))
然後將讀取的label轉為one-hot編碼。
y = keras.utils.to_categorical(y, num_classes)
完成上述步驟後,將選取的樣本save下來,下次直接load就可以,否則調參失去意義,最後也是非常重要的數據歸一化。
map_characters = {0: abraham_grampa_simpson, 1: apu_nahasapeemapetilon, 2: bart_simpson, 3: charles_montgomery_burns, 4: chief_wiggum, 5: comic_book_guy, 6: edna_krabappel, 7: homer_simpson, 8: kent_brockman, 9: krusty_the_clown, 10: lisa_simpson, 11: marge_simpson, 12: milhouse_van_houten, 13: moe_szyslak, 14: ned_flanders, 15: nelson_muntz, 16: principal_skinner, 17: sideshow_bob}pic_size = 64#設定圖片大小batch_size = 128epochs = 200num_classes = len(map_characters)pictures_per_class = 1000test_size = 0.15def load_pictures(BGR): pics = [] labels = [] for k, char in map_characters.items(): pictures = [k for k in glob.glob(./characters/%s/* % char)]#從每類人物的文件夾里返回所有圖片名字 #print(pictures) #從pictures中選樣本集,如果樣本數目<pictures數目,則返回樣本數目;如果大於,則返回pictures數目 nb_pic = round(pictures_per_class/(1-test_size)) if round(pictures_per_class/(1-test_size))<len(pictures) else len(pictures) # nb_pic = len(pictures) for pic in np.random.choice(pictures, nb_pic):#從每類pictures中隨機選np_pic張圖片作為樣本數據集 a = cv2.imread(pic)#讀取圖片 if BGR: a = cv2.cvtColor(a, cv2.COLOR_BGR2RGB)#色彩空間BGR轉為RGB a = cv2.resize(a, (pic_size,pic_size))#按比例縮放為pic_size * pic_size大小 pics.append(a) labels.append(k) return np.array(pics), np.array(labels) def get_dataset(save=False, load=False, BGR=False): if load: h5f = h5py.File(dataset.h5,r) X_train = h5f[X_train][:] X_test = h5f[X_test][:] h5f.close() h5f = h5py.File(labels.h5,r) y_train = h5f[y_train][:] y_test = h5f[y_test][:] h5f.close() else: X, y = load_pictures(BGR)#讀取並獲得圖片信息 #print(X.shape,y.shape) y = keras.utils.to_categorical(y, num_classes)#轉換為one-hot編碼 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size)#拆分數據集 if save: h5f = h5py.File(dataset.h5, w) h5f.create_dataset(X_train, data=X_train) h5f.create_dataset(X_test, data=X_test) h5f.close() h5f = h5py.File(labels.h5, w) h5f.create_dataset(y_train, data=y_train) h5f.create_dataset(y_test, data=y_test) h5f.close() X_train = X_train.astype(float32) / 255.#歸一化 X_test = X_test.astype(float32) / 255. print("Train", X_train.shape, y_train.shape) print("Test", X_test.shape, y_test.shape)###把每類的訓練集和測試集數目列印出來 if not load: dist = {k:tuple(d[k] for d in [dict(Counter(np.where(y_train==1)[1])), dict(Counter(np.where(y_test==1)[1]))]) for k in range(num_classes)} print(
.join(["%s : %d train pictures & %d test pictures" % (map_characters[k], v[0], v[1]) for k,v in sorted(dist.items(), key=lambda x:x[1][0], reverse=True)])) return X_train, X_test, y_train, y_test
2.構建模型
構建卷積神經網路,帶有6個ReLU激活函數的卷積層及3個池化層和1個全連接層,之所以構造6個卷積層,首先是因為辛普森一家裡的人物確實長得都太像了,稍不留神就成了臉盲,其次是人物的形態不一,人臉分布在不同的區域,所以要更加細緻的提取特徵,以達到區分的能力。卷積池化層還增加了Dropout,可以有效防止過擬合,輸出層採用softmax函數來輸出各類概率,優化器optimizer選用隨機梯度下降SGD,學習率lr=0.01,學習率衰減decay=1e-6,動量momentum=0.9,動量是為了越過平坦區域或泥石流區域(左右震蕩)。
def create_model_six_conv(input_shape): model = Sequential() model.add(Conv2D(32, (3, 3), padding=same, input_shape=input_shape))#padding=same 輸出與原始圖像大小相同 model.add(Activation(relu)) model.add(Conv2D(32, (3, 3))) model.add(Activation(relu)) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.2)) model.add(Conv2D(64, (3, 3), padding=same)) model.add(Activation(relu)) model.add(Conv2D(64, (3, 3))) model.add(Activation(relu)) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.2)) model.add(Conv2D(256, (3, 3), padding=same)) model.add(Activation(relu)) model.add(Conv2D(256, (3, 3))) model.add(Activation(relu)) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.2)) model.add(Flatten()) model.add(Dense(1024)) model.add(Activation(relu)) model.add(Dropout(0.5)) model.add(Dense(num_classes, activation=softmax)) opt = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)#nesterov=True使用動量 return model, opt
3.模型訓練
先對原始數據集進行訓練,epochs為200,通常情況下,頭幾次訓練epochs儘可能的大,以便觀察模型訓練情況。另外我還用到回調函數(callbaks)中的ModelCheckpoint,實現的功能是將val_acc最高的模型保存下來,然後測試時直接載入使用。callbaks概念摘自官方中文文檔,解釋的很明白,說白了就是在訓練時想干點別的,可以通過callbaks來完成。
回調函數 Callbacks - Keras 中文文檔
回調函數是一個函數的合集,會在訓練的階段中所使用。你可以使用回調函數來查看訓練模型的內在狀態和統計。你可以傳遞一個列表的回調函數(作為 callbacks 關鍵字參數)到 Sequential 或 Model 類型的 .fit() 方法。在訓練時,相應的回調函數的方法就會被在各自的階段被調用。
###每當val_acc有提升就保存checkpoint#save_best_only=True被監測數據的最佳模型就不會被覆蓋,mode=max保存的是準確率最大值filepath="weights_8conv_%s.hdf5" % time.strftime("%Y%m%d") checkpoint = ModelCheckpoint(filepath, monitor=val_acc, verbose=0, save_best_only=True, mode=max)history = model.fit(X_train, y_train, batch_size=batch_size, epochs=epochs, validation_split=0.1,#用作驗證集的訓練數據的比例 verbose=1,#日誌顯示進度條 shuffle=True,#是否在每輪迭代之前進行數據混洗 callbacks=checkpoint)score = model.evaluate(X_test, y_test, verbose=1)print(Test loss:, score[0])print(Test accuracy:, score[1])Train (14317, 64, 64, 3) (14317, 18)Test (2527, 64, 64, 3) (2527, 18)Train on 12885 samples, validate on 1432 samplesEpoch 1/20012885/12885 [==============================] - 4s 323us/step - loss: 2.8252 - acc: 0.0869 - val_loss: 2.7111 - val_acc: 0.1648Epoch 2/20012885/12885 [==============================] - 3s 261us/step - loss: 2.5845 - acc: 0.1918 - val_loss: 2.4444 - val_acc: 0.2479...Epoch 199/20012885/12885 [==============================] - 3s 265us/step - loss: 0.0063 - acc: 0.9981 - val_loss: 0.5669 - val_acc: 0.9148Epoch 200/20012885/12885 [==============================] - 3s 264us/step - loss: 0.0050 - acc: 0.9983 - val_loss: 0.5910 - val_acc: 0.92252527/2527 [==============================] - 0s 138us/stepTest loss: 0.5129188157600574Test accuracy: 0.9283735651624594
分別畫出訓練集和測試集上的accuracy和loss變化曲線,注意,模型訓練完後返回的是一個History
對象。其History.history
屬性是連續 epoch 訓練損失和評估值,以及驗證集損失和評估值的記錄。
#acc和loss可視化# accuracyplt.plot(history.history[acc])plt.plot(history.history[val_acc])plt.title(model accuracy)plt.ylabel(accuracy)plt.xlabel(epoch)plt.legend([train], loc=upper left)plt.legend([train, test], loc=upper left)plt.show()# lossplt.plot(history.history[loss])plt.plot(history.history[val_loss])plt.title(model loss)plt.ylabel(loss)plt.xlabel(epoch)plt.legend([train], loc=upper left)plt.legend([train, test], loc=upper left)plt.show()
4.模型評估
驗證集的Test accuracy為0.928,感覺還不錯,但是根據上面的曲線可以看出,網路發生過擬合,再看看分類報告
#查看分類報告,返回每類的精確率,召回率,F1值#P=TP/(TP+FP),R=TP/(TP+FN),F1=2PR/(P+R)score = model.evaluate(X_test,y_test,verbose=0)print(Test loss:, score[0])print(Test accuracy:, score[1])y_pred = model.predict(X_test)print(
, sklearn.metrics.classification_report(np.where(y_test > 0)[1], np.argmax(y_pred, axis=1), target_names=list(map_characters.values())), sep=) precision recall f1-score support abraham_grampa_simpson 0.95 0.90 0.93 167 apu_nahasapeemapetilon 0.96 0.99 0.97 73 bart_simpson 0.89 0.91 0.90 161charles_montgomery_burns 0.94 0.90 0.92 165 chief_wiggum 0.95 0.96 0.95 165 comic_book_guy 0.93 0.92 0.93 74 edna_krabappel 0.92 0.89 0.90 73 homer_simpson 0.89 0.86 0.87 174 kent_brockman 0.91 0.97 0.94 87 krusty_the_clown 0.92 0.97 0.94 170 lisa_simpson 0.91 0.85 0.88 178 marge_simpson 0.98 0.99 0.98 169 milhouse_van_houten 0.97 0.99 0.98 145 moe_szyslak 0.93 0.88 0.90 172 ned_flanders 0.95 0.95 0.95 184 nelson_muntz 0.88 0.83 0.85 42 principal_skinner 0.93 0.95 0.94 197 sideshow_bob 0.89 0.97 0.93 131 avg / total 0.93 0.93 0.93 2527
畫出各類別交叉關係圖
#畫出各類別交叉關係圖plt.figure(figsize = (10,10))cnf_matrix = sklearn.metrics.confusion_matrix(np.where(y_test > 0)[1],np.argmax(y_pred, axis=1))classes = list(map_characters.values())thresh = cnf_matrix.max() / 2.#閾值for i, j in itertools.product(range(cnf_matrix.shape[0]), range(cnf_matrix.shape[1])): plt.text(j, i, cnf_matrix[i, j],#在圖形中添加文本注釋 horizontalalignment="center",#水平對齊 color="white" if cnf_matrix[i, j] > thresh else "black")plt.imshow(cnf_matrix,interpolation=nearest,cmap=plt.cm.Blues)#cmap顏色圖譜,默認RGB(A)plt.colorbar()#顯示顏色條plt.title(confusion_matrix)#標題tick_marks = np.arange(len(classes))plt.xticks(tick_marks,classes,rotation=90)plt.yticks(tick_marks,classes)plt.ylabel(True label)plt.xlabel(Predicted label)
可以看出,Lisa的準確率偏低,很大部分誤分成Bart,因為什麼呢?先看看他倆長啥樣再說,你會發現,他倆除了頭型其他確實很像!這下分錯也情有可原了- -||
5.模型優化
接著分析過擬合發生的原因,可能是訓練樣本數目太少,也可能是模型複雜等,那麼就分別改進方法來驗證什麼原因導致,先從模型複雜度入手,可能Dropout力度不夠,改為0.5試試,跑完測試結果如下:
Test loss: 0.31535795541368883Test accuracy: 0.9271863867135495
可以看出,模型仍然過擬合,驗證集準確率不但沒有提升反而下降,並且Lisa的準確率仍然很低,基本可以排除是模型複雜導致的過擬合,那麼很有可能是樣本數量太少導致,接下來試下數據增強(data augmentation),這裡用到Keras的ImageDataGenerator
datagen = ImageDataGenerator( featurewise_center=False, # 將輸入數據的均值設置為 0,逐特徵進行 samplewise_center=False, # 將每個樣本的均值設置為 0 featurewise_std_normalization=False, # 將輸入除以數據標準差,逐特徵進行 samplewise_std_normalization=False, # 將每個輸入除以其標準差 zca_whitening=False, # 應用 ZCA 白化 rotation_range=10, # 隨機旋轉的度數範圍(degrees, 0 to 180),旋轉角度 width_shift_range=0.1, # 隨機水平移動的範圍,比例 height_shift_range=0.1, # 隨機垂直移動的範圍,比例 horizontal_flip=True, # 隨機水平翻轉,相當於鏡像 vertical_flip=False) # 隨機垂直翻轉,相當於鏡像
還有一些其他參數可以參考這篇文章介紹
圖片數據集太少?看我七十二變,Keras Image Data Augmentation 各參數詳解這裡主要針對圖像處理,主要用到rotation_range(人物歪著頭),width_shift_range(臉部不完整,請看文章開頭的圖片),height_shift_range,horizontal_flip(臉部朝左朝右)四個功能,當然其他的你也可以自己嘗試,然後訓練,flow()返回一個生成器,用來擴充數據,每次生成batch_size個樣本
history = model.fit_generator(datagen.flow(X_train, y_train, batch_size=batch_size), steps_per_epoch=X_train.shape[0] // batch_size, epochs=epochs, validation_data=(X_test, y_test), verbose=1, callbacks=callbacks_list)#調用一些列回調函數Test loss: 0.10107234056333621Test accuracy: 0.9790265136761396
由圖看出,過擬合問題解決了,根源就在於數據集量太少,而且準確率提升很明顯!所以針對小數據集,data augmentation真的很重要!到這裡模型訓練結果基本上不會有較大的提升了,剩下的就是繼續不斷地嘗試,對模型優化,調參,讓結果無限接近100%。
6.超參數選擇
超參數選取的好,可以給模型訓練起到錦上添花的作用,當然深度學習還是要「唯結果論」。
- 可變學習率(LearningRateScheduler)
def lr_schedule(epoch): initial_lrate = 0.03#初始學習率 drop = 0.5#衰減為原來的多少倍 epochs_drop = 12.0#每隔多久改變學習率 lrate = initial_lrate * math.pow(drop, math.floor((1+epoch)/epochs_drop)) return lratelrate = LearningRateScheduler(lr_schedule,verbose=1)
這個實現的功能跟本文前面用到的學習率衰減(lr decay)類似,在優化的過程中,學習率應該不斷的減小,保證在山坡上大步邁,接近山谷時小步走,我寫了一個函數,模型每訓練12次將學習率減半。但是訓練結果並沒有多少提升,可能是因為沒調到最優參數。
- EarlyStopping
early_stopping = EarlyStopping(monitor=val_loss, patience=10, verbose=1, mode=min)
實現的功能是提前停止訓練,可以避免過擬合,當val_loss不再下降時,模型會自動停止訓練。patience是設定val_loss沒有下降的次數,超過這個次數則停止訓練,開始的時候可以先不用EarlyStopping跑幾次看看抖動的次數,然後再設定比該次數稍大就行。mode有『auto』, 『min』, 『max』三種可能,根據monitor設定,如果是val_loss就設定min,val_acc設定max,要是不清楚該選哪個就選auto,不過要是知道的話還是建議設置下,確保沒問題。
7.調參策略
- 剛開始可以先構建簡單的網路結構模型,如果是大數據集可以先選取小量樣本訓練,如果泛化能力表現可以,再訓練更深更大的網路
- 卷積層激活函數一般選用ReLU,全連接輸出層一般用softmax
- 出現過擬合時,如果數據集小,那麼可以嘗試採用data augmentation,如果數據集大,可以用Dropout來降低模型複雜度或者調節learning rate,採用lr decay或可變學習率根據結果取捨,並配合EarlyStopping提高效率
- 嘗試用RMSprop,Adam,Adadelta來優化函數,效果通常也不錯,但我更喜歡SGD+momentum,因為操作可控且效果好
- 採用Batch Normalization,絕對的神器,每一層輸出進入激活函數前,將數據統一分布成均值為0,方差為1的標準正態分布,相當於將原本映射到飽和區域拉到中間區域,可以大大的提高收斂速度,epoch可以減小一半以上
- 增加網路深度,同時要增加epoch,層數多提取特徵多,模型複雜意味著訓練時間更長
- 調整卷積核數目,卷積核數目不宜過大,否則容易過擬合
- 要更多關注val_loss,因為下一輪epoch的val_loss上升,val_acc也跟著提高的情況也不是沒有,畢竟loss是優化目標
總結
- 數據集應當儘可能的大,可以採用data augmentation來擴充數據集
- 優先調learning rate,對模型表現影響很大,過大無法收斂到最優值,會產生「抖動」,過小收斂速度太慢,設置合理lr很重要
- Dropout一定要用,為了防止過擬合,測試集可以去掉
- Batch Normalization也很好用,加快收斂速度,提高效率
- 可視化可以更直觀的觀察數據分類情況
- 最好在GPU上跑模型,否則你會哭的
最後附上完整代碼
https://github.com/lpdsdx/Simpson-Recognition
註:轉載、翻譯請直接私聊本人,經本人同意後方可進行轉載。
推薦閱讀:
TAG:深度學習DeepLearning | 卷積神經網路CNN | 人臉識別 |