实战|手把手教你训练一个基于Keras的多标签图像分类器
圖片來自?pexels,作者:Jean-Christophe André
2019 年第 63 篇文章,總第 87 篇文章
原文鏈接:https://www.pyimagesearch.com/2018/05/07/multi-label-classification-with-keras/
作者:Adrian Rosebrock
今天介紹的是基于 Keras 實現多標簽圖像分類,主要分為四個部分:
介紹采用的多標簽數據集
簡單介紹使用的網絡模型?SmallerVGGNet,一個簡化版的?VGGNet
實現?SmallerVGGNet?模型并訓練
利用訓練好的模型,對測試樣例進行分類測試
接下來就開始本文的內容。
1. 多標簽圖像數據集
我們將采用如下所示的多標簽圖像數據集,一個服飾圖片數據集,總共是 2167 張圖片,六大類別:
黑色牛仔褲(Black Jeans, 344張)
藍色連衣裙(Blue Dress,386張)
藍色牛仔褲(Blue Jeans, 356張)
藍色襯衫(Blue Shirt, 369張)
紅色連衣裙(Red Dress,380張)
紅色襯衫(Red Shirt,332張)
因此我們的 CNN 網絡模型的目標就是同時預測衣服的顏色以及類型。
關于如何收集和建立這個數據集,可以參考這篇文章:
https://www.pyimagesearch.com/2018/04/09/how-to-quickly-build-a-deep-learning-image-dataset/
這篇文章會介紹如何采用微軟的 Bing 服務接口進行圖片下載,然后刪除不相關的圖片。
2. 多標簽分類項目結構
整個多標簽分類的項目結構如下所示:
├──?classify.py ├──?dataset │???├──?black_jeans?[344?entries │???├──?blue_dress?[386?entries] │???├──?blue_jeans?[356?entries] │???├──?blue_shirt?[369?entries] │???├──?red_dress?[380?entries] │???└──?red_shirt?[332?entries] ├──?examples │???├──?example_01.jpg │???├──?example_02.jpg │???├──?example_03.jpg │???├──?example_04.jpg │???├──?example_05.jpg │???├──?example_06.jpg │???└──?example_07.jpg ├──?fashion.model ├──?mlb.pickle ├──?plot.png ├──?pyimagesearch │???├──?__init__.py │???└──?smallervggnet.py ├──?search_bing_api.py └──?train.py簡單介紹每份代碼和每個文件夾的功能作用:
search_bing_api.py?:主要是圖片下載,但本文會提供好數據集,所以可以不需要運行該代碼;
train.py?:最主要的代碼,處理和加載數據以及訓練模型;
fashion.model?:保存的模型文件,用于?classify.py?進行對測試圖片的分類;
mlb.pickle:由?scikit-learn?模塊的?MultiLabelBinarizer?序列化的文件,將所有類別名字保存為一個序列化的數據結構形式
plot.png?:繪制訓練過程的準確率、損失隨訓練時間變化的圖
classify.py?:對新的圖片進行測試
三個文件夾:
dataset:數據集文件夾,包含六個子文件夾,分別對應六個類別
pyimagesearch?:主要包含建立 Keras 的模型代碼文件--smallervggnet.py
examples:7張測試圖片
3. 基于 Keras 建立的網絡結構
本文采用的是一個簡化版本的?VGGNet,VGGNet?是 2014 年由 Simonyan 和 Zisserman 提出的,論文--Very Deep Convolutional Networks for Large Scale Image Recognition。
這里先來展示下?SmallerVGGNet?的實現代碼,首先是加載需要的 Keras 的模塊和方法:
#?import?the?necessary?packages from?keras.models?import?Sequential from?keras.layers.normalization?import?BatchNormalization from?keras.layers.convolutional?import?Conv2D from?keras.layers.convolutional?import?MaxPooling2D from?keras.layers.core?import?Activation from?keras.layers.core?import?Flatten from?keras.layers.core?import?Dropout from?keras.layers.core?import?Dense from?keras?import?backend?as?K接著開始定義網絡模型--SmallerVGGNet?類,它包含?build?方法用于建立網絡,接收 5 個參數,width, height, depth?就是圖片的寬、高和通道數量,然后?classes?是數據集的類別數量,最后一個參數?finalAct?表示輸出層的激活函數,注意一般的圖像分類采用的是?softmax?激活函數,但是多標簽圖像分類需要采用?sigmoid?。
class?SmallerVGGNet:@staticmethoddef?build(width,?height,?depth,?classes,?finalAct="softmax"):#?initialize?the?model?along?with?the?input?shape?to?be#?"channels?last"?and?the?channels?dimension?itselfmodel?=?Sequential()inputShape?=?(height,?width,?depth)chanDim?=?-1#?if?we?are?using?"channels?first",?update?the?input?shape#?and?channels?dimensionif?K.image_data_format()?==?"channels_first":inputShape?=?(depth,?height,?width)chanDim?=?1接著,就開始建立網絡模型了,總共是 5 層的卷積層,最后加上一個全連接層和輸出層,其中卷積層部分可以說是分為三個部分,每一部分都是基礎的卷積層、RELU 層、BatchNormalization 層,最后是一個最大池化層(MaxPoolingLayer)以及 Dropout 層。
????????#?CONV?=>?RELU?=>?POOLmodel.add(Conv2D(32,?(3,?3),?padding="same",input_shape=inputShape))model.add(Activation("relu"))model.add(BatchNormalization(axis=chanDim))model.add(MaxPooling2D(pool_size=(3,?3)))model.add(Dropout(0.25))#?(CONV?=>?RELU)?*?2?=>?POOLmodel.add(Conv2D(64,?(3,?3),?padding="same"))model.add(Activation("relu"))model.add(BatchNormalization(axis=chanDim))model.add(Conv2D(64,?(3,?3),?padding="same"))model.add(Activation("relu"))model.add(BatchNormalization(axis=chanDim))model.add(MaxPooling2D(pool_size=(2,?2)))model.add(Dropout(0.25))#?(CONV?=>?RELU)?*?2?=>?POOLmodel.add(Conv2D(128,?(3,?3),?padding="same"))model.add(Activation("relu"))model.add(BatchNormalization(axis=chanDim))model.add(Conv2D(128,?(3,?3),?padding="same"))model.add(Activation("relu"))model.add(BatchNormalization(axis=chanDim))model.add(MaxPooling2D(pool_size=(2,?2)))model.add(Dropout(0.25))#?first?(and?only)?set?of?FC?=>?RELU?layersmodel.add(Flatten())model.add(Dense(1024))model.add(Activation("relu"))model.add(BatchNormalization())model.add(Dropout(0.5))#?use?a?*softmax*?activation?for?single-label?classification#?and?*sigmoid*?activation?for?multi-label?classificationmodel.add(Dense(classes))model.add(Activation(finalAct))#?return?the?constructed?network?architecturereturn?model4. 實現網絡模型以及訓練
現在已經搭建好我們的網絡模型SmallerVGGNet?了,接下來就是?train.py?這份代碼,也就是實現訓練模型的代碼。
首先,同樣是導入必須的模塊,主要是?keras?,其次還有繪圖相關的?matplotlib、cv2,處理數據和標簽的?sklearn?、pickle?等。
#?set?the?matplotlib?backend?so?figures?can?be?saved?in?the?background import?matplotlib matplotlib.use("Agg")#?import?the?necessary?packages from?keras.preprocessing.image?import?ImageDataGenerator from?keras.optimizers?import?Adam from?keras.preprocessing.image?import?img_to_array from?sklearn.preprocessing?import?MultiLabelBinarizer from?sklearn.model_selection?import?train_test_split from?pyimagesearch.smallervggnet?import?SmallerVGGNet import?matplotlib.pyplot?as?plt from?imutils?import?paths import?numpy?as?np import?argparse import?random import?pickle import?cv2 import?os注意,這里需要提前安裝的第三方模塊包括?Keras, scikit-learn, matplotlib, imutils, OpenCV,安裝命令如下:
pip?install?keras,?scikit-learn,?matplotlib,?imutils,?opencv-python當然,還需要安裝?tensorflow?,如果僅僅采用 CPU 版本,可以直接?pip install tensorflow?,而如果希望采用 GPU ,那就需要安裝 CUDA,具體教程可以看看如下教程:
https://www.pyimagesearch.com/2017/09/27/setting-up-ubuntu-16-04-cuda-gpu-for-deep-learning-with-python/
接著,繼續設置命令行參數:
#?construct?the?argument?parse?and?parse?the?arguments ap?=?argparse.ArgumentParser() ap.add_argument("-d",?"--dataset",?required=True,help="path?to?input?dataset?(i.e.,?directory?of?images)") ap.add_argument("-m",?"--model",?required=True,help="path?to?output?model") ap.add_argument("-l",?"--labelbin",?required=True,help="path?to?output?label?binarizer") ap.add_argument("-p",?"--plot",?type=str,?default="plot.png",help="path?to?output?accuracy/loss?plot") args?=?vars(ap.parse_args())這里主要是四個參數:
--dataset: 數據集路徑
--model?: 保存的模型路徑
--labelbin?: 保存的多標簽二進制對象路徑
--plot?: 保存繪制的訓練準確率和損失圖
然后,設置一些重要的參數,包括訓練的總次數?EPOCHS?、初始學習率INIT_LR、批大小?BS、輸入圖片大小?IMAGE_DIMS?:
#?initialize?the?number?of?epochs?to?train?for,?initial?learning?rate, #?batch?size,?and?image?dimensions EPOCHS?=?75 INIT_LR?=?1e-3 BS?=?32 IMAGE_DIMS?=?(96,?96,?3)然后就開始數據處理部分的代碼,首先是加載數據的代碼:
#?grab?the?image?paths?and?randomly?shuffle?them print("[INFO]?loading?images...") imagePaths?=?sorted(list(paths.list_images(args["dataset"]))) random.seed(42) random.shuffle(imagePaths)#?initialize?the?data?and?labels data?=?[] labels?=?[] #?loop?over?the?input?images for?imagePath?in?imagePaths:#?load?the?image,?pre-process?it,?and?store?it?in?the?data?listimage?=?cv2.imread(imagePath)image?=?cv2.resize(image,?(IMAGE_DIMS[1],?IMAGE_DIMS[0]))image?=?img_to_array(image)data.append(image)#?extract?set?of?class?labels?from?the?image?path?and?update?the#?labels?listl?=?label?=?imagePath.split(os.path.sep)[-2].split("_")labels.append(l)這部分代碼,首先是將所有數據集的路徑都保存到?imagePaths?中,接著進行?shuffle?隨機打亂操作,然后循環讀取圖片,對圖片做尺寸調整操作,并處理標簽,得到?data?和?labels?兩個列表,其中處理標簽部分的實現結果如下所示:
$?python >>>?import?os >>>?labels?=?[] >>>?imagePath?=?"dataset/red_dress/long_dress_from_macys_red.png" >>>?l?=?label?=?imagePath.split(os.path.sep)[-2].split("_") >>>?l ['red',?'dress'] >>>?labels.append(l) >>> >>>?imagePath?=?"dataset/blue_jeans/stylish_blue_jeans_from_your_favorite_store.png" >>>?l?=?label?=?imagePath.split(os.path.sep)[-2].split("_") >>>?labels.append(l) >>> >>>?imagePath?=?"dataset/red_shirt/red_shirt_from_target.png" >>>?l?=?label?=?imagePath.split(os.path.sep)[-2].split("_") >>>?labels.append(l) >>> >>>?labels [['red',?'dress'],?['blue',?'jeans'],?['red',?'shirt']]因此,labels?就是一個嵌套列表的列表,每個子列表都包含兩個元素。
然后就是數據的預處理,包括轉換為?numpy?的數組,對數據進行歸一化操作,以及采用?scikit-learn?的方法?MultiLabelBinarizer?將標簽進行?One-hot?編碼操作:
#?scale?the?raw?pixel?intensities?to?the?range?[0,?1] data?=?np.array(data,?dtype="float")?/?255.0 labels?=?np.array(labels) print("[INFO]?data?matrix:?{}?images?({:.2f}MB)".format(len(imagePaths),?data.nbytes?/?(1024?*?1000.0)))#?binarize?the?labels?using?scikit-learn's?special?multi-label #?binarizer?implementation print("[INFO]?class?labels:") mlb?=?MultiLabelBinarizer() labels?=?mlb.fit_transform(labels)#?loop?over?each?of?the?possible?class?labels?and?show?them for?(i,?label)?in?enumerate(mlb.classes_):print("{}.?{}".format(i?+?1,?label))同樣,這里也看看對標簽進行?One-hot?編碼操作的結果是怎樣的:
$?python >>>?from?sklearn.preprocessing?import?MultiLabelBinarizer >>>?labels?=?[ ...?????("blue",?"jeans"), ...?????("blue",?"dress"), ...?????("red",?"dress"), ...?????("red",?"shirt"), ...?????("blue",?"shirt"), ...?????("black",?"jeans") ...?] >>>?mlb?=?MultiLabelBinarizer() >>>?mlb.fit(labels) MultiLabelBinarizer(classes=None,?sparse_output=False) >>>?mlb.classes_ array(['black',?'blue',?'dress',?'jeans',?'red',?'shirt'],?dtype=object) >>>?mlb.transform([("red",?"dress")]) array([[0,?0,?1,?0,?1,?0]])MultiLabelBinarizer?會先統計所有類別的數量,然后按順序排列,即對每個標簽分配好其位置,然后進行映射得到?One-hot?變量,如上所示,總管六個類別,依次是?'black', 'blue', 'dress', 'jeans', 'red', 'shirt',而?red?和?dress?分別是第 5 和 3 個位置,所以得到的?One-hot?變量是?[0, 0, 1, 0, 1, 0]
數據處理最后一步,劃分訓練集和測試集,以及采用?keras?的數據增強方法?ImageDataGenerator?:
#?partition?the?data?into?training?and?testing?splits?using?80%?of #?the?data?for?training?and?the?remaining?20%?for?testing (trainX,?testX,?trainY,?testY)?=?train_test_split(data,labels,?test_size=0.2,?random_state=42)#?construct?the?image?generator?for?data?augmentation aug?=?ImageDataGenerator(rotation_range=25,?width_shift_range=0.1,height_shift_range=0.1,?shear_range=0.2,?zoom_range=0.2,horizontal_flip=True,?fill_mode="nearest")訓練集和測試集采用scikit-learn?的方法?train_test_split?,按照比例 8:2 劃分。
然后就是初始化模型對象、優化方法,開始訓練:
#?initialize?the?model?using?a?sigmoid?activation?as?the?final?layer #?in?the?network?so?we?can?perform?multi-label?classification print("[INFO]?compiling?model...") model?=?SmallerVGGNet.build(width=IMAGE_DIMS[1],?height=IMAGE_DIMS[0],depth=IMAGE_DIMS[2],?classes=len(mlb.classes_),finalAct="sigmoid")#?initialize?the?optimizer opt?=?Adam(lr=INIT_LR,?decay=INIT_LR?/?EPOCHS) #?compile?the?model?using?binary?cross-entropy?rather?than #?categorical?cross-entropy?--?this?may?seem?counterintuitive?for #?multi-label?classification,?but?keep?in?mind?that?the?goal?here #?is?to?treat?each?output?label?as?an?independent?Bernoulli #?distribution model.compile(loss="binary_crossentropy",?optimizer=opt,metrics=["accuracy"])#?train?the?network print("[INFO]?training?network...") H?=?model.fit_generator(aug.flow(trainX,?trainY,?batch_size=BS),validation_data=(testX,?testY),steps_per_epoch=len(trainX)?//?BS,epochs=EPOCHS,?verbose=1)這里采用的是?Adam?優化方法,損失函數是?binary cross-entropy?而非圖像分類常用的?categorical cross-entropy,原因主要是多標簽分類的目標是將每個輸出的標簽作為一個獨立的伯努利分布,并且希望單獨懲罰每一個輸出節點。
最后就是保存模型,繪制曲線圖的代碼了:
#?save?the?model?to?disk print("[INFO]?serializing?network...") model.save(args["model"])#?save?the?multi-label?binarizer?to?disk print("[INFO]?serializing?label?binarizer...") f?=?open(args["labelbin"],?"wb") f.write(pickle.dumps(mlb)) f.close()#?plot?the?training?loss?and?accuracy plt.style.use("ggplot") plt.figure() N?=?EPOCHS plt.plot(np.arange(0,?N),?H.history["loss"],?label="train_loss") plt.plot(np.arange(0,?N),?H.history["val_loss"],?label="val_loss") plt.plot(np.arange(0,?N),?H.history["acc"],?label="train_acc") plt.plot(np.arange(0,?N),?H.history["val_acc"],?label="val_acc") plt.title("Training?Loss?and?Accuracy") plt.xlabel("Epoch?#") plt.ylabel("Loss/Accuracy") plt.legend(loc="upper?left") plt.savefig(args["plot"])訓練代碼寫好了,那么就可以開始進行訓練了,訓練的命令如下:
$?python?train.py?--dataset?dataset?--model?fashion.model?\--labelbin?mlb.pickle訓練的部分記錄如下所示:
Using?TensorFlow?backend. [INFO]?loading?images... [INFO]?data?matrix:?2165?images?(467.64MB) [INFO]?class?labels: 1.?black 2.?blue 3.?dress 4.?jeans 5.?red 6.?shirt [INFO]?compiling?model... [INFO]?training?network... Epoch?1/75 name:?GeForce?GTX?TITAN?X 54/54?[==============================]?-?4s?-?loss:?0.3503?-?acc:?0.8682?-?val_loss:?0.9417?-?val_acc:?0.6520 Epoch?2/75 54/54?[==============================]?-?2s?-?loss:?0.1833?-?acc:?0.9324?-?val_loss:?0.7770?-?val_acc:?0.5377 Epoch?3/75 54/54?[==============================]?-?2s?-?loss:?0.1736?-?acc:?0.9378?-?val_loss:?1.1532?-?val_acc:?0.6436 ... Epoch?73/75 54/54?[==============================]?-?2s?-?loss:?0.0534?-?acc:?0.9813?-?val_loss:?0.0324?-?val_acc:?0.9888 Epoch?74/75 54/54?[==============================]?-?2s?-?loss:?0.0518?-?acc:?0.9833?-?val_loss:?0.0645?-?val_acc:?0.9784 Epoch?75/75 54/54?[==============================]?-?2s?-?loss:?0.0405?-?acc:?0.9857?-?val_loss:?0.0429?-?val_acc:?0.9842 [INFO]?serializing?network... [INFO]?serializing?label?binarizer...在訓練結束后,訓練集和測試集上的準確率分別是?98.57%?和?98.42?,繪制的訓練損失和準確率折線圖圖如下所示,上方是訓練集和測試集的準確率變化曲線,下方則是訓練集和測試集的損失圖,從這看出,訓練的網絡模型并沒有遭遇明顯的過擬合或者欠擬合問題。
5. 測試網絡模型
訓練好模型后,就是測試新的圖片了,首先先完成代碼?classify.py?,代碼如下:
#?import?the?necessary?packages from?keras.preprocessing.image?import?img_to_array from?keras.models?import?load_model import?numpy?as?np import?argparse import?imutils import?pickle import?cv2 import?os#?construct?the?argument?parse?and?parse?the?arguments ap?=?argparse.ArgumentParser() ap.add_argument("-m",?"--model",?required=True,help="path?to?trained?model?model") ap.add_argument("-l",?"--labelbin",?required=True,help="path?to?label?binarizer") ap.add_argument("-i",?"--image",?required=True,help="path?to?input?image") args?=?vars(ap.parse_args())#?load?the?image image?=?cv2.imread(args["image"]) output?=?imutils.resize(image,?width=400)#?pre-process?the?image?for?classification image?=?cv2.resize(image,?(96,?96)) image?=?image.astype("float")?/?255.0 image?=?img_to_array(image) image?=?np.expand_dims(image,?axis=0)#?load?the?trained?convolutional?neural?network?and?the?multi-label #?binarizer print("[INFO]?loading?network...") model?=?load_model(args["model"]) mlb?=?pickle.loads(open(args["labelbin"],?"rb").read())#?classify?the?input?image?then?find?the?indexes?of?the?two?class #?labels?with?the?*largest*?probability print("[INFO]?classifying?image...") proba?=?model.predict(image)[0] idxs?=?np.argsort(proba)[::-1][:2]#?loop?over?the?indexes?of?the?high?confidence?class?labels for?(i,?j)?in?enumerate(idxs):#?build?the?label?and?draw?the?label?on?the?imagelabel?=?"{}:?{:.2f}%".format(mlb.classes_[j],?proba[j]?*?100)cv2.putText(output,?label,?(10,?(i?*?30)?+?25),?cv2.FONT_HERSHEY_SIMPLEX,?0.7,?(0,?255,?0),?2)#?show?the?probabilities?for?each?of?the?individual?labels for?(label,?p)?in?zip(mlb.classes_,?proba):print("{}:?{:.2f}%".format(label,?p?*?100))#?show?the?output?image cv2.imshow("Output",?output) cv2.waitKey(0)這部分代碼也不難,主要是加載圖片和模型,然后進行預測,得到結果會輸出前兩個概率最大的結果,然后用?OpenCV?展示出來,調用的命令如下:
$?python?classify.py?--model?fashion.model?--labelbin?mlb.pickle?\--image?examples/example_01.jpg實驗結果如下,給出的預測結果是紅色連衣裙,展示出來的也的確是紅色連衣裙的圖片。
Using?TensorFlow?backend. [INFO]?loading?network... [INFO]?classifying?image... black:?0.00% blue:?3.58% dress:?95.14% jeans:?0.00% red:?100.00% shirt:?64.02%其他的樣例圖片都可以通過相同的命令,只需要修改輸入圖片的名字即可,然后就是其中最后一張圖片,是比較特殊的,輸入命令如下所示:
$?python?classify.py?--model?fashion.model?--labelbin?mlb.pickle?\--image?examples/example_07.jpg Using?TensorFlow?backend. [INFO]?loading?network... [INFO]?classifying?image... black:?91.28% blue:?7.70% dress:?5.48% jeans:?71.87% red:?0.00% shirt:?5.92%展示的結果,這是一條黑色連衣裙,但預測結果給出黑色牛仔褲的結果。
這里的主要原因就是黑色連衣裙并不在我們的訓練集類別中。這其實也是目前圖像分類的一個問題,無法預測未知的類別,因為訓練集并不包含這個類別,因此 CNN 沒有見過,也就預測不出來。
6. 小結
本文介紹了如何采用 Keras 實現多標簽圖像分類,主要的兩個關鍵點:
輸出層采用?sigmoid?激活函數,而非?softmax?激活函數;
損失函數采用?binary cross-entropy?,而非?categorical cross-entropy。
如果想了解更多關于多標簽圖像分類的理論知識,可以查看下面這篇綜述:
【技術綜述】多標簽圖像分類綜述
此外,本文的代碼和數據集獲取方式如下所示:
關注公眾號“算法猿的成長”
在公眾號會話界面回復“多標簽”,即可獲取
歡迎關注我的微信公眾號--算法猿的成長,或者掃描下方的二維碼,大家一起交流,學習和進步!
如果覺得不錯,在看、轉發就是對小編的一個支持!
總結
以上是生活随笔為你收集整理的实战|手把手教你训练一个基于Keras的多标签图像分类器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java NumberFormat,De
- 下一篇: 爬虫实战 -- QQ空间自动点赞