我用Python實現了12500張貓狗圖像的精準分類
原創(chuàng)【51CTO.com原創(chuàng)稿件】在這篇文章中,我們將展示如何建立一個深度神經網絡,能做到以 90% 的精度來對圖像進行分類,而在深度神經網絡,特別是卷積神經網絡興起之前,這還是一個非常困難的問題。
深度學習是目前人工智能領域里最讓人興奮的話題之一了,它基于生物學領域的概念發(fā)展而來,現如今是一系列算法的集合。
事實已經證明深度學習在計算機視覺、自然語言處理、語音識別等很多的領域里都可以起到非常好的效果。
在過去的 6 年里,深度學習已經應用到非常廣泛的領域,很多最近的技術突破,都和深度學習相關。
這里僅舉幾個例子:特斯拉的自動駕駛汽車、Facebook 的照片標注系統(tǒng)、像 Siri 或 Cortana 這樣的虛擬助手、聊天機器人、能進行物體識別的相機,這些技術突破都要歸功于深度學習。
在這么多的領域里,深度學習在語言理解、圖像分析這種認知任務上的表現已經達到了我們人類的水平。
如何構建一個在圖像分類任務上能達到 90% 精度的深度神經網絡?
這個問題看似非常簡單,但在深度神經網絡特別是卷積神經網絡(CNN)興起之前,這是一個被計算機科學家們研究了很多年的棘手問題。
本文分為以下三個部分進行講解:
- 展示數據集和用例,并且解釋這個圖像分類任務的復雜度。
- 搭建一個深度學習專用環(huán)境,這個環(huán)境搭建在 AWS 的基于 GPU 的 EC2 服務上。
- 訓練兩個深度學習模型:***個模型是使用 Keras 和 TensorFlow 從頭開始端到端的流程,另一個模型使用是已經在大型數據集上預訓練好的神經網絡。
一個有趣的實例:給貓和狗的圖像分類
有很多的圖像數據集是專門用來給深度學習模型進行基準測試的,我在這篇文章中用到的數據集來自 Cat vs Dogs Kaggle competition,這份數據集包含了大量狗和貓的帶有標簽的圖片。
和每一個 Kaggle 比賽一樣,這份數據集也包含兩個文件夾:
- 訓練文件夾:它包含了 25000 張貓和狗的圖片,每張圖片都含有標簽,這個標簽是作為文件名的一部分。我們將用這個文件夾來訓練和評估我們的模型。
- 測試文件夾:它包含了 12500 張圖片,每張圖片都以數字來命名。對于這份數據集中的每幅圖片來說,我們的模型都要預測這張圖片上是狗還是貓(1= 狗,0= 貓)。事實上,這些數據也被 Kaggle 用來對模型進行打分,然后在排行榜上排名。
我們觀察一下這些圖片的特點,這些圖片各種各樣,分辨率也各不相同。圖片中的貓和狗形狀、所處位置、體表顏色各不一樣。
它們的姿態(tài)不同,有的在坐著而有的則不是,它們的情緒可能是開心的也可能是傷心的,貓可能在睡覺,而狗可能在汪汪地叫著。照片可能以任一焦距從任意角度拍下。
這些圖片有著***種可能,對于我們人類來說在一系列不同種類的照片中識別出一個場景中的寵物自然是毫不費力的事情,然而這對于一臺機器來說可不是一件小事。
實際上,如果要機器實現自動分類,那么我們需要知道如何強有力地描繪出貓和狗的特征,也就是說為什么我們認為這張圖片中的是貓,而那張圖片中的卻是狗。這個需要描繪每個動物的內在特征。
深度神經網絡在圖像分類任務上效果很好的原因是,它們有著能夠自動學習多重抽象層的能力,這些抽象層在給定一個分類任務后又可以對每個類別給出更簡單的特征表示。
深度神經網絡可以識別極端變化的模式,在扭曲的圖像和經過簡單的幾何變換的圖像上也有著很好的魯棒性。讓我們來看看深度神經網絡如何來處理這個問題的。
配置深度學習環(huán)境
深度學習的計算量非常大,當你在自己的電腦上跑一個深度學習模型時,你就能深刻地體會到這一點。
但是如果你使用 GPUs,訓練速度將會大幅加快,因為 GPUs 在處理像矩陣乘法這樣的并行計算任務時非常高效,而神經網絡又幾乎充斥著矩陣乘法運算,所以計算性能會得到令人難以置信的提升。
我自己的電腦上并沒有一個強勁的 GPU,因此我選擇使用一個亞馬遜云服務 (AWS) 上的虛擬機,這個虛擬機名為 p2.xlarge,它是亞馬遜 EC2 的一部分。
這個虛擬機的配置包含一個 12GB 顯存的英偉達GPU、一個 61GB 的 RAM、4 個 vCPU 和 2496 個 CUDA 核。
可以看到這是一臺性能巨獸,讓人高興的是,我們每小時僅需花費 0.9 美元就可以使用它。當然,你還可以選擇其他配置更好的虛擬機,但對于我們現在將要處理的任務來說,一臺 p2.xlarge 虛擬機已經綽綽有余了。
我的虛擬機工作在 Deep Learning AMI CUDA 8 Ubuntu Version 系統(tǒng)上,現在讓我們對這個系統(tǒng)有一個更清楚的了解吧。
這個系統(tǒng)基于一個 Ubuntu 16.04 服務器,已經包裝好了所有的我們需要的深度學習框架(TensorFlow,Theano,Caffe,Keras),并且安裝好了 GPU 驅動(聽說自己安裝驅動是噩夢般的體驗)。
如果你對 AWS 不熟悉的話,你可以參考下面的兩篇文章:
- https://blog.keras.io/running-jupyter-notebooks-on-gpu-on-aws-a-starter-guide.html
- https://hackernoon.com/keras-with-gpu-on-amazon-ec2-a-step-by-step-instruction-4f90364e49ac
這兩篇文章可以讓你知道兩點:
- 建立并連接到一個 EC2 虛擬機。
- 配置網絡以便遠程訪問 jupyter notebook。
用 TensorFlow 和 Keras 建立一個貓/狗圖片分類器
環(huán)境配置好后,我們開始著手建立一個可以將貓狗圖片分類的卷積神經網絡,并使用到深度學習框架 TensorFlow 和 Keras。
先介紹下 Keras:Keras 是一個高層神經網絡 API,它由純 Python 編寫而成并基于Tensorflow、Theano 以及 CNTK 后端,Keras 為支持快速實驗而生,能夠把你的 idea 迅速轉換為結果。
從頭開始搭建一個卷積神經網絡
首先,我們設置一個端到端的 pipeline 訓練 CNN,將經歷如下幾步:數據準備和增強、架構設計、訓練和評估。
我們將繪制訓練集和測試集上的損失和準確度指標圖表,這將使我們能夠更直觀地評估模型在訓練中的改進變化。
數據準備
在開始之前要做的***件事是從 Kaggle 上下載并解壓訓練數據集。
- %matplotlib inline
- from matplotlib import pyplot as plt
- from PIL import Image
- import numpy as np
- import os
- import cv2
- from tqdm import tqdm_notebook
- from random import shuffle
- import shutil
- import pandas as pd
我們必須重新組織數據以便讓 Keras 更容易地處理它們。我們創(chuàng)建一個 data 文件夾,并在其中創(chuàng)建兩個子文件夾:
- train
- validation
在上面的兩個文件夾之下,每個文件夾依然包含兩個子文件夾:
- cats
- dogs
***我們得到下面的文件結構:
data/
train/
dogs/
dog001.jpg
dog002.jpg
...
cats/
cat001.jpg
cat002.jpg
...
validation/
dogs/
dog001.jpg
dog002.jpg
...
cats/
cat001.jpg
cat002.jpg
這個文件結構讓我們的模型知道從哪個文件夾中獲取到圖像和訓練或測試用的標簽。這里提供了一個函數允許你來重新構建這個文件樹,它有 2 個參數:圖像的總數目、測試集 r 的比重。
- def organize_datasets(path_to_data, n=4000, ratio=0.2):
- files = os.listdir(path_to_data)
- files = [os.path.join(path_to_data, f) for f in files]
- shuffle(files)
- files = files[:n]
- n = int(len(files) * ratio)
- val, train = files[:n], files[n:]
- shutil.rmtree('./data/')
- print('/data/ removed')
- for c in ['dogs', 'cats']:
- os.makedirs('./data/train/{0}/'.format(c))
- os.makedirs('./data/validation/{0}/'.format(c))
- print('folders created !')
- for t in tqdm_notebook(train):
- if 'cat' in t:
- shutil.copy2(t, os.path.join('.', 'data', 'train', 'cats'))
- else:
- shutil.copy2(t, os.path.join('.', 'data', 'train', 'dogs'))
- for v in tqdm_notebook(val):
- if 'cat' in v:
- shutil.copy2(v, os.path.join('.', 'data', 'validation', 'cats'))
- else:
- shutil.copy2(v, os.path.join('.', 'data', 'validation', 'dogs'))
- print('Data copied!')
我使用了:
- n:25000(整個數據集的大?。?/strong>
- r:0.2
- ratio = 0.2
- n = 25000
- organize_datasets(path_to_data='./train/',n=n, ratio=ratio)
現在讓我們裝載 Keras 和它的依賴包吧:
- import keras
- from keras.preprocessing.image import ImageDataGenerator
- from keras_tqdm import TQDMNotebookCallback
- from keras.models import Sequential
- from keras.layers import Dense
- from keras.layers import Dropout
- from keras.layers import Flatten
- from keras.constraints import maxnorm
- from keras.optimizers import SGD
- from keras.layers.convolutional import Conv2D
- from keras.layers.convolutional import MaxPooling2D
- from keras.utils import np_utils
- from keras.callbacks import Callback
圖像生成器和數據增強
在訓練模型時,我們不會將整個數據集裝載進內存,因為這種做法并不高效,特別是你使用的還是你自己本地的機器。
我們將用到 ImageDataGenerator 類,這個類可以***制地從訓練集和測試集中批量地引入圖像流。在ImageDataGenerator 類中,我們將在每個批次引入隨機修改。
這個過程我們稱之為數據增強(dataaugmentation)。它可以生成更多的圖片使得我們的模型不會看見兩張完全相同的圖片。這種方法可以防止過擬合,也有助于模型保持更好的泛化性。
我們要創(chuàng)建兩個 ImageDataGenerator 對象。train_datagen 對應訓練集,val_datagen 對應測試集,兩者都會對圖像進行縮放,train_datagen 還將做一些其他的修改。
- batch_size = 32
- train_datagen = ImageDataGenerator(rescale=1/255.,
- shear_range=0.2,
- zoom_range=0.2,
- horizontal_flip=True
- )
- val_datagen = ImageDataGenerator(rescale=1/255.)
基于前面的兩個對象,我們接著創(chuàng)建兩個文件生成器:
- train_generator
- validation_generator
每個生成器在實時數據增強的作用下,在目錄處可以生成批量的圖像數據。這樣,數據將會***制地循環(huán)生成。
- train_generator = train_datagen.flow_from_directory(
- './data/train/',
- target_size=(150, 150),
- batch_size=batch_size,
- class_mode='categorical')
- validation_generator = val_datagen.flow_from_directory(
- './data/validation/',
- target_size=(150, 150),
- batch_size=batch_size,
- class_mode='categorical')
- ##Found 20000 images belonging to 2 classes.
- ##Found 5000 images belonging to 2 classes.
模型結構
我將使用擁有 3 個卷積/池化層和 2 個全連接層的 CNN。3 個卷積層將分別使用 32,32,64 的 3 * 3的濾波器(fiter)。在兩個全連接層,我使用了 dropout 來避免過擬合。
- model = Sequential()
- model.add(Conv2D(32, (3, 3), input_shape=(150, 150, 3), padding='same', activation='relu'))
- model.add(MaxPooling2D(pool_size=(2, 2)))
- model.add(Conv2D(32, (3, 3), padding='same', activation='relu'))
- model.add(MaxPooling2D(pool_size=(2, 2)))
- model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
- model.add(MaxPooling2D(pool_size=(2, 2)))
- model.add(Dropout(0.25))
- model.add(Flatten())
- model.add(Dense(64, activation='relu'))
- model.add(Dropout(0.5))
- model.add(Dense(2, activation='softmax'))
我使用隨機梯度下降法進行優(yōu)化,參數 learning rate 為 0.01,momentum 為 0.9。
- epochs = 50
- lrate = 0.01
- decay = lrate/epochs
- sgd = SGD(lr=lrate, momentum=0.9, decay=decay, nesterov=False)
- model.compile(loss='binary_crossentropy', optimizer=sgd, metrics=['accuracy'])
Keras 提供了一個非常方便的方法來展示模型的全貌。對每一層,我們可以看到輸出的形狀和可訓練參數的個數。在開始擬合模型前,檢查一下是個明智的選擇。
model.summary()
下面讓我們看一下網絡的結構。
結構可視化
在訓練模型前,我定義了兩個將在訓練時調用的回調函數 (callback function):
- 一個用于在損失函數無法改進在測試數據的效果時,提前停止訓練。
- 一個用于存儲每個時期的損失和精確度指標:這可以用來繪制訓練錯誤圖表。
- ## Callback for loss logging per epoch
- class LossHistory(Callback):
- def on_train_begin(self, logs={}):
- self.losses = []
- self.val_losses = []
- def on_epoch_end(self, batch, logs={}):
- self.losses.append(logs.get('loss'))
- self.val_losses.append(logs.get('val_loss'))
- history = LossHistory()
- ## Callback for early stopping the training
- early_stopping = keras.callbacks.EarlyStopping(monitor='val_loss',
- min_delta=0,
- patience=2,
- verbose=0, mode='auto')
我還使用了 keras-tqdm,這是一個和 keras ***整合的非常棒的進度條。它可以讓我們非常容易地監(jiān)視模型的訓練過程。
要想使用它,你僅需要從 keras_tqdm 中加載 TQDMNotebookCallback 類,然后將它作為第三個回調函數傳遞進去。
下面的圖在一個簡單的樣例上展示了 keras-tqdm 的效果。
關于訓練過程,還有幾點要說的:
- 我們使用 fit_generator 方法,它是一個將生成器作為輸入的變體(標準擬合方法)。
- 我們訓練模型的時間超過 50 個 epoch。
- fitted_model = model.fit_generator(
- train_generator,
- steps_per_epoch= int(n * (1-ratio)) // batch_size,
- epochs=50,
- validation_data=validation_generator,
- validation_steps= int(n * ratio) // batch_size,
- callbacks=[TQDMNotebookCallback(leave_inner=True, leave_outer=True), early_stopping, history],
- verbose=0)
這個模型運行時的計算量非常大:
- 如果你在自己的電腦上跑,每個 epoch 會花費 15 分鐘的時間。
- 如果你和我一樣在 EC2 上的 p2.xlarge 虛擬機上跑,每個 epoch 需要花費 2 分鐘的時間。
分類結果
我們在模型運行 34 個 epoch 后達到了 89.4% 的準確率(下文展示訓練/測試錯誤和準確率),考慮到我沒有花費很多時間來設計網絡結構,這已經是一個很好的結果了?,F在我們可以將模型保存,以備以后使用。
model.save(`./models/model4.h5)
下面我們在同一張圖上繪制訓練和測試中的損失指標值:
- losses, val_losses = history.losses, history.val_losses
- fig = plt.figure(figsize=(15, 5))
- plt.plot(fitted_model.history['loss'], 'g', label="train losses")
- plt.plot(fitted_model.history['val_loss'], 'r', label="val losses")
- plt.grid(True)
- plt.title('Training loss vs. Validation loss')
- plt.xlabel('Epochs')
- plt.ylabel('Loss')
- plt.legend()
當在兩個連續(xù)的 epoch 中,測試損失值沒有改善時,我們就中止訓練過程。
下面繪制訓練集和測試集上的準確度。
- losses, val_losses = history.losses, history.val_losses
- fig = plt.figure(figsize=(15, 5))
- plt.plot(fitted_model.history['acc'], 'g', label="accuracy on train set")
- plt.plot(fitted_model.history['val_acc'], 'r', label="accuracy on validation set")
- plt.grid(True)
- plt.title('Training Accuracy vs. Validation Accuracy')
- plt.xlabel('Epochs')
- plt.ylabel('Loss')
- plt.legend()
這兩個指標一直是增長的,直到模型即將開始過擬合的平穩(wěn)期。
裝載預訓練的模型
我們在自己設計的 CNN 上取得了不錯的結果,但還有一種方法能讓我們取得更高的分數:直接載入一個在大型數據集上預訓練過的卷積神經網絡的權重,這個大型數據集包含 1000 個種類的貓和狗的圖片。
這樣的網絡會學習到與我們分類任務相關的特征。
我將加載 VGG16 網絡的權重,具體來說,我要將網絡權重加載到所有的卷積層。這個網絡部分將作為一個特征檢測器來檢測我們將要添加到全連接層的特征。
與 LeNet5 相比,VGG16 是一個非常大的網絡,它有 16 個可以訓練權重的層和 1.4 億個參數。要了解有關 VGG16 的信息,請參閱此篇 pdf 鏈接:https://arxiv.org/pdf/1409.1556.pdf
- from keras import applications
- # include_top: whether to include the 3 fully-connected layers at the top of the network.
- model = applications.VGG16(include_top=False, weights='imagenet')
- datagen = ImageDataGenerator(rescale=1. / 255)
現在我們將圖像傳進網絡來得到特征表示,這些特征表示將會作為神經網絡分類器的輸入。
- generator = datagen.flow_from_directory('./data/train/',
- target_size=(150, 150),
- batch_size=batch_size,
- class_mode=None,
- shuffle=False)
- bottleneck_features_train = model.predict_generator(generator, int(n * (1 - ratio)) // batch_size)
- np.save(open('./features/bottleneck_features_train.npy', 'wb'), bottleneck_features_train)
- ##Found 20000 images belonging to 2 classes.
- generator = datagen.flow_from_directory('./data/validation/',
- target_size=(150, 150),
- batch_size=batch_size,
- class_mode=None,
- shuffle=False)
- bottleneck_features_validation = model.predict_generator(generator, int(n * ratio) // batch_size,)
- np.save('./features/bottleneck_features_validation.npy', bottleneck_features_validation)
- ##Found 5000 images belonging to 2 classes.
圖像在傳遞到網絡中時是有序傳遞的,所以我們可以很容易地為每張圖片關聯上標簽。
- train_data = np.load('./features/bottleneck_features_train.npy')
- train_labels = np.array([0] * (int((1-ratio) * n) // 2) + [1] * (int((1 - ratio) * n) // 2))
- validation_data = np.load('./features/bottleneck_features_validation.npy')
- validation_labels = np.array([0] * (int(ratio * n) // 2) + [1] * (int(ratio * n) // 2))
現在我們設計了一個小型的全連接神經網絡,附加上從 VGG16 中抽取到的特征,我們將它作為 CNN 的分類部分。
- model = Sequential()
- model.add(Flatten(input_shape=train_data.shape[1:]))
- model.add(Dense(512, activation='relu'))
- model.add(Dropout(0.5))
- model.add(Dense(256, activation='relu'))
- model.add(Dropout(0.2))
- model.add(Dense(1, activation='sigmoid'))
- model.compile(optimizer='rmsprop',
- loss='binary_crossentropy', metrics=['accuracy'])
- fitted_model = model.fit(train_data, train_labels,
- epochs=15,
- batch_size=batch_size,
- validation_data=(validation_data, validation_labels[:validation_data.shape[0]]),
- verbose=0,
- callbacks=[TQDMNotebookCallback(leave_inner=True, leave_outer=False), history])
在 15 個 epoch 后,模型就達到了 90.7% 的準確度。這個結果已經很好了,注意現在每個 epoch 在我自己的電腦上跑也僅需 1 分鐘。
- fig = plt.figure(figsize=(15, 5))
- plt.plot(fitted_model.history['loss'], 'g', label="train losses")
- plt.plot(fitted_model.history['val_loss'], 'r', label="val losses")
- plt.grid(True)
- plt.title('Training loss vs. Validation loss - VGG16')
- plt.xlabel('Epochs')
- plt.ylabel('Loss')
- plt.legend()
- fig = plt.figure(figsize=(15, 5))
- plt.plot(fitted_model.history['acc'], 'g', label="accuracy on train set")
- plt.plot(fitted_model.history['val_acc'], 'r', label="accuracy on validation sete")
- plt.grid(True)
- plt.title('Training Accuracy vs. Validation Accuracy - VGG16')
- plt.xlabel('Epochs')
- plt.ylabel('Loss')
- plt.legend()
許多深度學習領域的大牛人物都鼓勵大家在做分類任務時使用預訓練網絡,實際上,預訓練網絡通常使用的是在一個非常大的數據集上生成的非常大的網絡。
而 Keras 可以讓我們很輕易地下載像 VGG16、GoogleNet、ResNet 這樣的預訓練網絡。想要了解更多關于這方面的信息,請參考這里:https://keras.io/applications/
有一句很棒的格言是:不要成為英雄!不要重復發(fā)明輪子!使用預訓練網絡吧!
接下來還可以做什么?
如果你對改進一個傳統(tǒng) CNN 感興趣的話,你可以:
- 在數據集層面上,引入更多增強數據。
- 研究一下網絡超參數(network hyperparameter):卷積層的個數、濾波器的個數和大小,在每種組合后要測試一下效果。
- 改變優(yōu)化方法。
- 嘗試不同的損失函數。
- 使用更多的全連接層。
- 引入更多的 aggressive dropout。
如果你對使用預訓練網絡獲得更好的分類結果感興趣的話,你可以嘗試:
- 使用不同的網絡結構。
- 使用更多包含更多隱藏單元的全連接層。
如果你想知道 CNN 這個深度學習模型到底學習到了什么東西,你可以:
- 將 feature maps 可視化。
- 可以參考:https://arxiv.org/pdf/1311.2901.pdf
如果你想使用訓練過的模型:
- 可以將模型放到 Web APP 上,使用新的貓和狗的圖像來進行測試。這也是一個很好地測試模型泛化性的好方法。
總結
這是一篇手把手教你在 AWS 上搭建深度學習環(huán)境的教程,并且教你怎樣從頭開始建立一個端到端的模型,另外本文也教了你怎樣基于一個預訓練的網絡來搭建一個 CNN 模型。
用 Python 來做深度學習是讓人愉悅的事情,而 Keras 讓數據的預處理和網絡層的搭建變得更加簡單。
如果有一天你需要按自己的想法來搭建一個神經網絡,你可能需要用到其他的深度學習框架。
現在在自然語言處理領域,也有很多人開始使用卷積神經網絡了,下面是一些基于此的工作:
使用了 CNN 的文本分類:
https://chara.cs.illinois.edu/sites/sp16-cs591txt/files/0226-presentation.pdf
自動為圖像生成標題:
https://cs.stanford.edu/people/karpathy/sfmltalk.pdf
字級別的文本分類:
https://papers.nips.cc/paper/5782-character-level-convolutional-networks-fortext-classification.pdf
【51CTO原創(chuàng)稿件,合作站點轉載請注明原文作者和出處為51CTO.com】