1. 程式人生 > >深度學習與計算機視覺(PB-10)-Kaggle之貓狗比賽

深度學習與計算機視覺(PB-10)-Kaggle之貓狗比賽

第9節中,我們提到了當資料太大無法載入到記憶體中時,如何使用HDF5儲存大資料集——我們自定義了一個python指令碼將原始影象資料集序列化為高效的HDF5資料集。在HDF5資料集中讀取影象資料集可以避免I/O延遲問題,從而加快訓練過程。

假設我們有N張儲存在磁碟上的影象資料,之前的做法是定義了一個數據生成器,該生成器按順序從磁碟中載入影象,N張影象共需要進行N個讀取操作,每個影象一個讀取操作,這樣會存在I/O延遲問題。如果將影象資料集儲存到HDF5資料集中,我們可以一次性讀取batch大小的影象資料。這樣極大地減少了I/O呼叫的次數,並且可以使用非常大的影象資料集。

在本節中,我們將學習如何為HDF5資料集定義一個影象生成器,從而方便使用Keras訓練卷積神經網路。生成器會不斷地從HDF5資料集中生成用於訓練網路的資料和對應的標籤,直到我們的模型達到足夠低的損失/高精度才會停止。

在訓練模型之前,我們將實現三種新的影象預處理方法——零均值化、patch Preprocessing和隨機裁剪(也稱為10-cropping或過取樣)。之後,我們將利用Krizhevsky等人2012年的論文《ImageNet Classification with Deep Convolutional Neural Networks 》提出的AlexNet網路結構訓練貓狗資料集,並在測試集上評估效能,另外,為了提高準確度,我們將對測試集使用過取樣方法。在下面結果中,我們將看到使用AlexNet網路架構+裁剪方法可以獲得比賽的top50。

預處理

在本節中,我們將實現三個新的影象預處理方法:

  • 1.零均值化(資料歸一化的一種形式):將輸入的影象減去資料集中的紅色、綠色和藍色三個顏色通道的平均值。
  • 2.patch Preprocessing:在訓練過程中,隨機從原始影象中提取MxN大小特徵影象。
  • 3.過取樣:在測試時,對輸入影象的五個區域(四個角+中心區域)進行裁剪並且對剪裁之後的特徵影象進行水平翻轉,總共會得到10張特徵影象。

其中需要注意的是,過取樣方法我們只在測試資料集上使用,通過過取樣方法,我們得到10張特徵影象,然後模型會對每一張影象進行預測,得到10個結果,最後通過投票或者平均計算得到最終的結果,從結果中我們將看到該方法有助於提高分類準確度。

零均值

第9節中,我們在將影象資料集轉換為HDF5格式的過程中,計算了訓練資料集中影象的紅、綠、藍三個顏色通道的平均值,之後,我們對每一張輸入影象都減去對應通道平均值,這個過程我們稱為資料歸一化。即給定輸入影象I及其對應R、G、B通道值,則可以通過:

  • R = R - μ R \mu_{R}
  • G = G - μ G \mu_{G}
  • B = B - μ B \mu_{B}

其中, μ R , μ G , μ B \mu_{R},\mu_{G},\mu_{B} 是三個顏色通道平均值。圖10.1顯示了從輸入影象中減去RGB平均值的視覺化效果。

圖10.1 左:原始影象,右:均值處理之後的影象

在程式碼實現零均值過程之前,我們首先看看整個專案結構,即:

--- pyimagesearch
| |--- __init__.py
| |--- callbacks
| |--- nn
| |--- preprocessing
| | |--- __init__.py
| | |--- aspectawarepreprocessor.py
| | |--- imagetoarraypreprocessor.py
| | |--- meanpreprocessor.py
| | |--- simplepreprocessor.py
| |--- utils

在pyimagesearch專案中的preprocessing子模組中新建一個meanpreprocessor.py,並寫入以下程式碼:

# -*- coding: utf-8 -*-
import cv2
class MeanPreprocessor:
    def __init__(self,rMean,gMean,bMean):
        # 三個顏色通道的平均值
        self.rMean = rMean
        self.gMean = gMean
        self.bMean = bMean

其中,三個引數分別是對應的三個顏色通道的均值。

接下來,讓我們定義預處理函式:

    def preprocess(self,image):
        # cv2分割得到的是BGR,而不是RGB
        (B,G,R) = cv2.split(image.astype("float32"))
        # 減去對應通道的均值
        R -= self.rMean
        G -= self.gMean
        B -= self.bMean
        return cv2.merge([B,G,R])

需要注意的是cv2.split函式是把影象劃分成對應的B,G,R,而不是R,G,B。

patch Preprocessing

PatchPreprocessor類主要是在訓練過程中隨機抽取原始影象的MxN大小的特徵影象。當輸入的影象的維度比CNN模型要求的維度大時,我們就需要使用到patch Preprocessing——這是降低過擬合的常用技術,因此也是一種正則化形式。我們不會在訓練過程中使用整個影象,而是隨機裁剪其中的一部分,並將其傳遞給網路(關於裁剪預處理的示例,請參見圖10.2)。

圖10.2 左:原始256x256影象, 右:隨機裁剪之後的227x227影象

使用隨機裁剪方法意味著網路永遠不會看到完全相同的影象(除非隨機發生),類似於資料增強。在第9節中,我們將kaggle上貓狗比賽資料集儲存為HDF5資料集,其中原始資料的大小為256x256。然而,我們將在本章後面實現的AlexNet架構只能接受大小為227x227的影象。

那麼,該如何處理?直接應用simplepreprocessor將每個256x256影象調整到227x227?從圖10.2中,我們看到這樣會損失影象資訊,合理的做法是在訓練過程中將256x256影象中隨機裁剪出一個227x227特徵影象——事實上,一方面類似於資料增強,另一方面這個過程正是Krizhevsky等人在ImageNet資料集中訓練AlexNet的方式。

與其他影象預處理程式一樣,patchpreprocessor預處理程式碼放在pyimagesearch的預處理子模組中,即:

--- pyimagesearch
| |--- __init__.py
| |--- callbacks
| |--- nn
| |--- preprocessing
| | |--- __init__.py
| | |--- aspectawarepreprocessor.py
| | |--- imagetoarraypreprocessor.py
| | |--- meanpreprocessor.py
| | |--- patchpreprocessor.py
| | |--- simplepreprocessor.py
| |--- utils

開啟patchpreprocessor.py,並寫入以下程式碼:

# -*- coding: utf-8 -*-
from sklearn.feature_extraction.image import extract_patches_2d
class PatchPreprocessor:
    def __init__(self,width,height):
        # 目標影象的寬和高
        self.width = width
        self.height = height

其中,width和heigh分別表示裁剪後的影象寬度和高度。

接下來,定義預處理函式:

    def preprocess(self,image):
        # 隨機裁剪出目標大小影象
        return extract_patches_2d(image,(self.height,self.width),
                                  max_patches = 1)[0]

給定需要返回的影象的寬度和高度,我們使用scikit-learn庫的extract_patches_2d函式從原始影象中隨機裁剪出指定大小的影象,其中引數max_patch =1,表明我們只需要輸入影象中的一個隨機patch。

PatchPreprocessor類看起來程式碼並不是很多,但它實際上是一種非常有效的方法,類似於資料增強,一定程度上可以降低過擬合。一般在訓練過程中使用PatchPreprocessor。

隨機裁剪

接下來,我們定義一個CropPreprocessor。在CNN的評估階段,我們對輸入影象的四個角+中心區域進行裁剪,然後進行相應的水平翻轉,最後會產生10個樣本(圖10.3)。

圖10.3 左: 原始256x256影象,右:裁剪得到的10張227x227影象

CNN模型將對這10個測試樣本進行預測,產生10個結果,最後通過投票或者計算平均得到最終結果。利用這種過取樣的方法,往往會增加1- 2%的分類準確率(在某些情況下,甚至更高)。

CropPreprocessor類也將存在於pyimagesearch的preprocessing子模組中:

--- pyimagesearch
| |--- __init__.py
| |--- callbacks
| |--- nn
| |--- preprocessing
| | |--- __init__.py
| | |--- aspectawarepreprocessor.py
| | |--- croppreprocessor.py
| | |--- imagetoarraypreprocessor.py
| | |--- meanpreprocessor.py
| | |--- patchpreprocessor.py
| | |--- simplepreprocessor.py
| |--- utils

開啟croppreprocessor.py檔案,寫入以下程式碼:

# -*- coding: utf-8 -*-
import numpy as np
import cv2
class GropPreprocessor:
    def __init__(self,width,height,horiz = True,inter=cv2.INTER_AREA):
        # 儲存目標引數
        self.width  = width
        self.heiggt = height
        self.horiz  = horiz
        self.inter  = inter

其中,引數分別為:

  • width: 輸出的影象寬度
  • heigh:輸出的影象高度
  • horiz:是否進行水平翻轉,預設為True
  • inter:openCV中用於調整大小的插值演算法

定義預處理方法:

    def preprocess(self,image):
        crops = []
        # 原始影象的高跟寬
        (h,w) = image.shape[:2]
        #四個角
        coords = [
                [0,0,self.width,self.height],
                [w - self.width,0,w,self.height],
                [w - self.width,h - self.height,w,h],
                [0,h - self.height,self.width,h]
                ]
        # 計算中心區域
        dW = int(0.5 * (w - self.width))
        dH = int(0.5 * (h - self.height))
        coords.append([dW,dH,w - dW,h - dH])

其中預處理主函式只有一個引數,就是輸入的影象,然後我們計算四個角(左上角、右上角、右下角、左下角)的座標(x,y)以及中心座標。並提取對應的部分影象:

        for (startX,startY,endX,endY) in coords:
            # 裁剪
            crop = image[startY:endY,startX:endX]
            # 由於裁剪過程,可能會造成大小相差1左右,所以進行插值
            crop = cv2.resize(crop,(self.width,self.height),
                              interpolation = self.inter)
            crops.append(crop)
        if self.horiz:
            # 水平翻轉
            mirrors = [cv2.flip(x,1) for x in crops]
            crops.extend(mirrors)
        return np.array(crops)

由於提取的影象大小會與目標大小存在1左右的差別,因此,我們進行調整大小。通過水平翻轉,我們可以得到10張特徵影象。

HDF5資料集生成器

在訓練模型之前,我們首先需要定義一個類,用於從HDF5資料集中生成batch大小的影象資料和對應的標籤。在第9節中討論瞭如何將儲存在磁碟上的一組影象轉換為HDF5資料集——但是我們如何將它們重新返回?

在pyimagesearch的io子模組中定義一個HDF5DatasetGenerator類:

--- pyimagesearch
| |--- __init__.py
| |--- callbacks
| |--- io
| | |--- __init__.py
| | |--- hdf5datasetgenerator.py
| | |--- hdf5datasetwriter.py
| |--- nn
| |--- preprocessing
| |--- utils

之前訓練模型時,所有影象資料集都可以載入到記憶體中,這樣我們就可以依賴Keras生成器工具來生成batch大小的影象和相應的標籤。但是,由於我們的資料集太大,無法全部載入到記憶體,所以我們需要自己實現一個針對HDF5資料集的生成器。

開啟hdf5datasetgenerator.py檔案,寫入以下程式碼:

# -*- coding: utf-8 -*-
from keras.utils import np_utils
import numpy as np
import h5py

class HDF5DatasetGenerator:
    def __inti__(self,dbPath,batchSize,preprocessors = None,
                 aug = None,binarize=True,classes=2):
        # 儲存引數列表
        self.batchSize = batchSize
        self.preprocessors = preprocessors
        self.aug = aug
        self.binarize = binarize
        self.classes = classes
        # hdf5資料集
        self.db = h5py.File(dbPath)
        self.numImages = self.db['labels'].shape[0]

其中,引數分別為:

  • dbPath: HDF5資料集的路徑
  • batchSize: batch資料量的大小
  • preprocessors:資料預處理列表,比如(MeanPreprocessor,ImageToArrayPreprocessor等等)。
  • aug: 預設為None,可以使用Keras中的ImageDataGenerator模組來進行資料增強。
  • binarize: 通常我們將類標籤作為一個整數儲存在HDF5資料集中,但是,如果我們使用categorical cross-entropy或者binary cross-entropy作為損失函式,那麼我們需要把標籤進行one-hot編碼向量化——該引數主要說明是否進行二值化處理(預設為True)。
  • classes:類別個數,該數值決定了one-hot向量的shape

接下來,我們需要定義一個生成器函式,它負責在訓練網路時將batch大小的影象和類標籤傳遞給keras.fit_generator函式。

    def generator(self,passes=np.inf):
        epochs = 0
        # 預設是無限迴圈遍歷
        while epochs < passes:
            # 遍歷資料
            for i in np.arange(0,self.numImages,self.batchSize):
                # 從hdf5中提取資料集
                images = self.db['images'][i: i+self.batchSize]
                labels = self.db['labels'][i: i+self.batchSize]

其中,passes為epochs的總數,一般而言,我們不用關心passes數值大小,因為在訓練過程中,往往我們會指定訓練的epoch個數,或者使用early stopping等等。所以預設為np.inf,即該迴圈會一直進行,直到:

  • 1.模型訓練達到終止條件。
  • 2.手動停止(例如,ctrl + c)。

從hdf5資料集中提取資料之後,我們需要對資料進行預處理和標籤one-hot編碼:

                if self.binarize:
                    labels = np_utils.to_categorical(labels,self.calsses)
                # 預處理
                if self.preprocessors is not None:
                    proImages = []
                    for image in images:
                        for p in self.preprocessors:
                            image = p.preprocess(image)
                        proImages.append(image)
                    images = np.array(proImages)

同樣也可以設定是否進行資料增強處理:

                if self.aug is not None:
                    (images,labels) = next(self.aug.flow(images,
                        labels,batch_size = self.batchSize))

最後,我們將返回由影象和標籤組成的二元祖資料,

                # 返回
                yield (images,labels)
            epochs += 1
    def close(self):
        # 關閉db
        self.db.close()

整個HDF5資料集的生成器已經完成,在實際應用中,我們往往需要額外的工具來幫助我們快速地處理資料集,尤其是那些太大而無法載入到記憶體的資料集。後面,我們在開發深度學習專案或者實驗時,HDF5DatasetGenerator將會加速我們處理資料的速度。

AlexNet

這部分,我們將實現Krizhevsky等人提出的具有開創性的AlexNet架構。完整的AlexNet網路架構如下圖所示:

圖10.4 AlexNet結構

為什麼我們將輸入影象的大小調整為227x227x3——這實際上是AlexNet架構要求的正確輸入大小。實際上,Krizhevsky等人發表的原論文中使用的影象維度是224x224x3,但是,使用大小為11x1的卷積核遍歷影象,會發現邊界填充的結果是小數,這個顯然是不對的,即(224-11) /4+1的結果是小數,通過推導實際上應該是227x227。

說明:輸出資料體在空間上的尺寸可以通過輸入資料尺寸(W),卷積層中神經元的感受野尺寸(F),步長(S)和零填充的數量(P)的函式來計算,即資料體的空間尺寸為(W-F+2P)/s+1.

接下來,在pyimagesearch中nn的conv子模組中建立一個名為alexnet.py檔案,如下目錄結構:

--- pyimagesearch
| |--- __init__.py
| |--- callbacks
| |--- io
| |--- nn
| | |--- __init__.py
| | |--- conv
| | | |--- __init__.py
| | | |--- alexnet.py
...
| |--- preprocessing
| |--- utils

開啟alexnet.py,並寫入以下程式碼:

# -*- coding: utf-8 -*-
# 載入所需模組
from keras.models import Sequential
from keras.layers import BatchNormalization
from keras.layers import Conv2D
from keras.layers import MaxPooling2D
from keras.layers import Activation
from keras.layers import Flatten
from keras.layers import Dropout
from keras.layers import Dense
from keras.regularizers import l2
from keras import backend as K

從載入的模組中可以看出,我們使用了BN和l2模組。AlexNet原文中使用的標準化是LRN(區域性響應歸一化),在實現中,我們使用更高階的BN演算法,另外為了防止過擬合,我們將會卷積層和FC使用l2正則化。

下面,定義AlexNet結構:

class AlexNet:
    @staticmethod
    def build(width,height,depth,classes,reg=0.0002):
        # 初始化序列模型
        model = Sequential()
        inputShape = (height,width,depth)
        chanDim = -1
        # 主要識別keras的後端是thensorflow還是theano[目前預設都是thensorflow]
        if K.image_data_format() == "channels_first":
            inputShape = (depth,height,width)
            chanDim = 1

其中:

  • width:輸入影象的寬度
  • height:輸入影象的高度
  • depth:輸入影象的通道數,彩色為3,灰色為1
  • classes:類別總數
  • reg:正則化係數,對於較大、較深的網路,應用正則化有助於降低過擬合,同時提高準確度

接下來,我們構建模型的主體部分。首先,定義網路中的第一個CONV => RELU =>POOL 模組:

        # Block # 1 first CONV => RELU => POOL layer set
        model.add(Conv2D(96,(11,11),strides=(4,4),input_shape=inputShape,
                         padding='same',kernel_regularizer=l2(reg)))
        model.add(Activation('relu'))
        model.add(BatchNormalization(axis = chanDim))
        model.add(MaxPooling2D(pool_size = (3,3),strides=(2,2)))
        model.add(Dropout(0.25))

第一個CONV層有96個過濾器,每個過濾器大小為11x11,平移步長為4,啟用函式為RELU,並且對卷積層使用了l2正則化(l2正則化後面會一直應用在CONV層和FC層)。

從圖10.4,可以看出卷積層之後,是標準化操作,原文論文中使用的標準化為LRN,這裡我們使用更加高階的BN層。標準化之後,是MaxPooling2D層,pool層可以降低特徵影象維度,並減少引數量。最後,我們還增加了dropout層,進一步降低過擬合。

下面,我們定義第二個block,主要是CONV => RELU => POOL,類似於與第一個block,一共有256個過濾器,每一個過濾器的大小為5x5,平移步長為1:

        # Block #2: second CONV => RELU => POOL layer set
        model.add(Conv2D(256,(5,5),padding='same',
                         kernel_regularizer=l2(reg)))
        model.add(Activation('relu'))
        model.add(BatchNormalization(axis=chanDim))
        model.add(MaxPooling2D(pool_size=(3,3),strides(2,2)))
        model.add(Dropout(0.25))

定義第三個block,第三個block中疊加了多個CONV層,將學習到更深層次且豐富的特徵:

        # Block #3: CONV => RELU => CONV => RELU => CONV => RELU
        model.add(Conv2D(384,(3,3),padding='same',
                         kernel_regularizer=l2(reg)))
        model.add(Activation('relu'))
        model.add(BatchNormalization(axis = chanDim))
        model.add(Conv2D(384,(3,3),padding='same',
                         kernel_regularizer=l2(reg)))
        model.add(Activation('relu'))
        model.add(BatchNormalization(axis=chanDim))
        model.add(Conv2D(256,(3,3),padding='same',
                         kernel_regularizer=l2(reg)))
        model.add(Activation('relu'))
        model.add(BatchNormalization(axis = chanDim))
        model.add(MaxPooling2D(pool_size=(3,3),strides=(2,2))
        model.add(Dropout(0.25))

前兩個CONV層主要是由384個大小為3x3的過濾器組成,而第三個CONV由256個大小為3x3的過濾器組成,步長全部為1。

接著,我們拼接兩個全連線層,每個全連線層都由4096神經元組成:

        # Block #4: first set of FC => RELU layers
        model.add(Flatten()) # 拉平
        # 全連線層
        model.add(Dense(4096,kernel_regularizer=l2(reg)))
        model.add(Activation('relu'))
        model.add(BatchNormalization(axis = chanDim))
        model.add(Dropout(0.5))

        # Block #5: second set of FC => RELU layers
        model.add(Dense(4096,kernel_regularizer=l2(reg)<