1. 程式人生 > >keras面向小資料集的影象分類(VGG-16基礎上fine-tune)實現(附程式碼)

keras面向小資料集的影象分類(VGG-16基礎上fine-tune)實現(附程式碼)

參考譯文地址:http://keras-cn.readthedocs.io/en/latest/blog/image_classification_using_very_little_data/

本文作者:Francois Chollet

概述

在本文中,將使用VGG-16模型提供一種面向小資料集(幾百張到幾千張圖片)構造高效、實用的影象分類器的方法並給出試驗結果。

本文將探討如下幾種方法:

  • 從圖片中直接訓練一個小網路(作為基準方法)

  • 利用預訓練網路的bottleneck(瓶頸)特徵

  • fine-tune預訓練網路的高層

本文需要使用的Keras模組有:

  • fit_generator:用於從Python生成器中訓練網路

  • ImageDataGenerator:用於實時資料提升

  • 層引數凍結和模型fine-tune

配置情況

我們的實驗基於下面的配置

  • 2000張訓練圖片構成的資料集,一共兩個類別,每類1000張

  • 安裝有Keras,SciPy,PIL的機器,如果有NVIDIA GPU那就更好了,但因為我們面對的是小資料集,沒有也可以。

  • 資料集按照下面的形式存放(圖片名可不遵循以下規則)

data/
    train/
        dogs/
            dog001.jpg
            dog002.jpg
            ...
        cats/
            cat001/jpg
            cat002.jpg
            ...
    validation/
        dogs/
            dog001.jpg
            dog002.jpg
            ...
        cats/
            cat001/jpg
            cat002.jpg
            ...

這份資料集來源於Kaggle,可以使用百度網盤下載。原資料集有12500只貓和12500只狗,我們只取了各個類的前1000張圖片。另外我們還從各個類中取了400張額外圖片用於測試。

下面是資料集的一些示例圖片,圖片的數量非常少,這對於影象分類來說是個大麻煩。但現實是,很多真實世界圖片獲取是很困難的,我們能得到的樣本數目確實很有限(比如醫學影象,每張正樣本都意味著一個承受痛苦的病人。對資料科學家而言,我們應該有能夠榨取少量資料的全部價值的能力,而不是簡單的伸手要更多的資料。

cats_and_dogs

在Kaggle的貓狗大戰競賽種,參賽者通過使用現代的深度學習技術達到了98%的正確率,本文方法只使用了全部資料的8%,因此這個問題對我們來說更難。

針對小資料集的深度學習

我經常聽到的一種說法是,深度學習只有在你擁有海量資料時才有意義。雖然這種說法並不是完全不對,但卻具有較強的誤導性。當然,深度學習強調從資料中自動學習特徵的能力,沒有足夠的訓練樣本,這幾乎是不可能的。尤其是當輸入的資料維度很高(如圖片)時。然而,卷積神經網路作為深度學習的支柱,被設計為針對“感知”問題最好的模型之一(如影象分類問題),即使只有很少的資料,網路也能把特徵學的不錯。針對小資料集的神經網路依然能夠得到合理的結果,並不需要任何手工的特徵工程。一言以蔽之,卷積神經網路大法好!

動。尤其在計算機視覺領域,許多預訓練的模型現在都被公開下載,並被重用在其他問題上以提升在小資料集上的效能。另一方面,深度學習模型天然就具有可重用的特性:比方說,你可以把一個在大規模資料上訓練好的影象分類或語音識別的模型重用在另一個很不一樣的問題上,而只需要做有限的一點改

資料預處理與資料提升

為了儘量利用我們有限的訓練資料,我們將通過一系列隨機變換堆資料進行提升,這樣我們的模型將看不到任何兩張完全相同的圖片,這有利於我們抑制過擬合,使得模型的泛化能力更好。

在Keras中,這個步驟可以通過keras.preprocessing.image.ImageGenerator來實現,這個類使你可以:

  • 在訓練過程中,設定要施行的隨機變換

  • 通過.flow.flow_from_directory(directory)方法例項化一個針對影象batch的生成器,這些生成器可以被用作keras模型相關方法的輸入,如fit_generatorevaluate_generatorpredict_generator

現在讓我們看個例子:

from keras.preprocessing.image import ImageDataGenerator

datagen = ImageDataGenerator(
        rotation_range=40,
        width_shift_range=0.2,
        height_shift_range=0.2,
        rescale=1./255,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True,
        fill_mode='nearest')

上面顯示的只是一部分選項,請閱讀文件的相關部分來檢視全部可用的選項。我們來快速的瀏覽一下這些選項的含義:

  • rotation_range是一個0~180的度數,用來指定隨機選擇圖片的角度。

  • width_shiftheight_shift用來指定水平和豎直方向隨機移動的程度,這是兩個0~1之間的比例。

  • rescale值將在執行其他處理前乘到整個影象上,我們的影象在RGB通道都是0~255的整數,這樣的操作可能使影象的值過高或過低,所以我們將這個值定為0~1之間的數。

  • shear_range是用來進行剪下變換的程度,參考剪下變換

  • zoom_range用來進行隨機的放大

  • horizontal_flip隨機的對圖片進行水平翻轉,這個引數適用於水平翻轉不影響圖片語義的時候

  • fill_mode用來指定當需要進行畫素填充,如旋轉,水平和豎直位移時,如何填充新出現的畫素

下面我們使用這個工具來生成圖片,並將它們儲存在一個臨時資料夾中,這樣我們可以感覺一下資料提升究竟做了什麼事。為了使圖片能夠展示出來,這裡沒有使用rescaling

from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img

datagen = ImageDataGenerator(
        rotation_range=40,
        width_shift_range=0.2,
        height_shift_range=0.2,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True,
        fill_mode='nearest')

img = load_img('data/train/cats/cat.0.jpg')  # this is a PIL image
x = img_to_array(img)  # this is a Numpy array with shape (3, 150, 150)
x = x.reshape((1,) + x.shape)  # this is a Numpy array with shape (1, 3, 150, 150)

# the .flow() command below generates batches of randomly transformed images
# and saves the results to the `preview/` directory
i = 0
for batch in datagen.flow(x, batch_size=1,
                          save_to_dir='preview', save_prefix='cat', save_format='jpeg'):
    i += 1
    if i > 20:
        break  # otherwise the generator would loop indefinitely

下面是一張圖片被提升以後得到的多個結果:

cat_data_augmentation

在小資料集上訓練神經網路:40行程式碼達到80%的準確率

進行影象分類的正確工具是卷積網路,所以我們來試試用卷積神經網路搭建一個初級的模型。因為我們的樣本數很少,所以我們應該對過擬合的問題多加註意。當一個模型從很少的樣本中學習到不能推廣到新資料的模式時,我們稱為出現了過擬合的問題。過擬合發生時,模型試圖使用不相關的特徵來進行預測。例如,你有三張伐木工人的照片,有三張水手的照片。六張照片中只有一個伐木工人戴了帽子,如果你認為戴帽子是能將伐木工人與水手區別開的特徵,那麼此時你就是一個差勁的分類器。

資料提升是對抗過擬合問題的一個武器,但還不夠,因為提升過的資料仍然是高度相關的。對抗過擬合的你應該主要關注的是模型的“熵容量”——模型允許儲存的資訊量。能夠儲存更多資訊的模型能夠利用更多的特徵取得更好的效能,但也有儲存不相關特徵的風險。另一方面,只能儲存少量資訊的模型會將儲存的特徵主要集中在真正相關的特徵上,並有更好的泛化效能。

有很多不同的方法來調整模型的“熵容量”,常見的一種選擇是調整模型的引數數目,即模型的層數和每層的規模。另一種方法是對權重進行正則化約束,如L1或L2.這種約束會使模型的權重偏向較小的值。

在我們的模型裡,我們使用了很小的卷積網路,只有很少的幾層,每層的濾波器數目也不多。再加上資料提升和Dropout,就差不多了。Dropout通過防止一層看到兩次完全一樣的模式來防止過擬合,相當於也是一種資料提升的方法。(你可以說dropout和資料提升都在隨機擾亂資料的相關性)

下面展示的程式碼是我們的第一個模型,一個很簡單的3層卷積加上ReLU啟用函式,再接max-pooling層。這個結構和Yann LeCun在1990年釋出的影象分類器很相似(除了ReLU)

這個實驗的全部程式碼在這裡

from keras.models import Sequential
from keras.layers import Convolution2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense

model = Sequential()
model.add(Convolution2D(32, 3, 3, input_shape=(3, 150, 150)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Convolution2D(32, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Convolution2D(64, 3, 3))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

# the model so far outputs 3D feature maps (height, width, features)

然後我們接了兩個全連線網路,並以單個神經元和sigmoid啟用結束模型。這種選擇會產生二分類的結果,與這種配置相適應,我們使用binary_crossentropy作為損失函式。

model.add(Flatten())  # this converts our 3D feature maps to 1D feature vectors
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy',
              optimizer='rmsprop',
              metrics=['accuracy'])

然後我們開始準備資料,使用.flow_from_directory()來從我們的jpgs圖片中直接產生資料和標籤。

# this is the augmentation configuration we will use for training
train_datagen = ImageDataGenerator(
        rescale=1./255,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True)

# this is the augmentation configuration we will use for testing:
# only rescaling
test_datagen = ImageDataGenerator(rescale=1./255)

# this is a generator that will read pictures found in
# subfolers of 'data/train', and indefinitely generate
# batches of augmented image data
train_generator = train_datagen.flow_from_directory(
        'data/train',  # this is the target directory
        target_size=(150, 150),  # all images will be resized to 150x150
        batch_size=32,
        class_mode='binary')  # since we use binary_crossentropy loss, we need binary labels

# this is a similar generator, for validation data
validation_generator = test_datagen.flow_from_directory(
        'data/validation',
        target_size=(150, 150),
        batch_size=32,
        class_mode='binary')

然後我們可以用這個生成器來訓練網路了,在GPU上每個epoch耗時20~30秒,在CPU上耗時300~400秒,所以如果你不是很著急,在CPU上跑這個模型也是完全可以的。

model.fit_generator(
        train_generator,
        samples_per_epoch=2000,
        nb_epoch=50,
        validation_data=validation_generator,
        nb_val_samples=800)
model.save_weights('first_try.h5')  # always save your weights after training or during training

這個模型在50個epoch後的準確率為79%~81%,別忘了我們只用了8%的資料,也沒有花時間來做模型和超引數的優化。在Kaggle中,這個模型已經可以進前100名了(一共215隊參與),估計剩下的115隊都沒有用深度學習:)

注意這個準確率的變化可能會比較大,因為準確率本來就是一個變化較高的評估引數,而且我們只有800個樣本用來測試。比較好的驗證方法是使用K折交叉驗證,但每輪驗證中我們都要訓練一個模型。

使用預訓練網路的bottleneck特徵:一分鐘達到90%的正確率

一個稍微講究一點的辦法是,利用在大規模資料集上預訓練好的網路。這樣的網路在多數的計算機視覺問題上都能取得不錯的特徵,利用這樣的特徵可以讓我們獲得更高的準確率。

我們將使用vgg-16網路,該網路在ImageNet資料集上進行訓練,這個模型我們之前提到過了。因為ImageNet資料集包含多種“貓”類和多種“狗”類,這個模型已經能夠學習與我們這個資料集相關的特徵了。事實上,簡單的記錄原來網路的輸出而不用bottleneck特徵就已經足夠把我們的問題解決的不錯了。不過我們這裡講的方法對其他的類似問題有更好的推廣性,包括在ImageNet中沒有出現的類別的分類問題。

VGG-16的網路結構如下:

vgg_16

我們的方法是這樣的,我們將利用網路的卷積層部分,把全連線以上的部分拋掉。然後在我們的訓練集和測試集上跑一遍,將得到的輸出(即“bottleneck feature”,網路在全連線之前的最後一層啟用的feature map)記錄在兩個numpy array裡。然後我們基於記錄下來的特徵訓練一個全連線網路。

我們將這些特徵儲存為離線形式,而不是將我們的全連線模型直接加到網路上並凍結之前的層引數進行訓練的原因是處於計算效率的考慮。執行VGG網路的代價是非常高昂的,尤其是在CPU上執行,所以我們只想執行一次。這也是我們不進行資料提升的原因。

我們不再贅述如何搭建vgg-16網路了,這件事之前已經說過,在keras的example裡也可以找到。但讓我們看看如何記錄bottleneck特徵。

generator = datagen.flow_from_directory(
        'data/train',
        target_size=(150, 150),
        batch_size=32,
        class_mode=None,  # this means our generator will only yield batches of data, no labels
        shuffle=False)  # our data will be in order, so all first 1000 images will be cats, then 1000 dogs
# the predict_generator method returns the output of a model, given
# a generator that yields batches of numpy data
bottleneck_features_train = model.predict_generator(generator, 2000)
# save the output as a Numpy array
np.save(open('bottleneck_features_train.npy', 'w'), bottleneck_features_train)

generator = datagen.flow_from_directory(
        'data/validation',
        target_size=(150, 150),
        batch_size=32,
        class_mode=None,
        shuffle=False)
bottleneck_features_validation = model.predict_generator(generator, 800)
np.save(open('bottleneck_features_validation.npy', 'w'), bottleneck_features_validation)

記錄完畢後我們可以將資料載入,用於訓練我們的全連線網路:

train_data = np.load(open('bottleneck_features_train.npy'))
# the features were saved in order, so recreating the labels is easy
train_labels = np.array([0] * 1000 + [1] * 1000)

validation_data = np.load(open('bottleneck_features_validation.npy'))
validation_labels = np.array([0] * 400 + [1] * 400)

model = Sequential()
model.add(Flatten(input_shape=train_data.shape[1:]))
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(train_data, train_labels,
          nb_epoch=50, batch_size=32,
          validation_data=(validation_data, validation_labels))
model.save_weights('bottleneck_fc_model.h5')

因為特徵的size很小,模型在CPU上跑的也會很快,大概1s一個epoch,最後我們的準確率是90%~91%,這麼好的結果多半歸功於預訓練的vgg網路幫助我們提取特徵。

在預訓練的網路上fine-tune

為了進一步提高之前的結果,我們可以試著fine-tune網路的後面幾層。Fine-tune以一個預訓練好的網路為基礎,在新的資料集上重新訓練一小部分權重。在這個實驗中,fine-tune分三個步驟

  • 搭建vgg-16並載入權重

  • 將之前定義的全連線網路加在模型的頂部,並載入權重

  • 凍結vgg16網路的一部分引數

vgg16_modified

注意:

  • 為了進行fine-tune,所有的層都應該以訓練好的權重為初始值,例如,你不能將隨機初始的全連線放在預訓練的卷積層之上,這是因為由隨機權重產生的大地圖將會破壞卷積層預訓練的權重。在我們的情形中,這就是為什麼我們首先訓練頂層分類器,然後再基於它進行fine-tune的原因

  • 我們選擇只fine-tune最後的卷積塊,而不是整個網路,這是為了防止過擬合。整個網路具有巨大的熵容量,因此具有很高的過擬合傾向。由底層卷積模組學習到的特徵更加一般,更加不具有抽象性,因此我們要保持前兩個卷積塊(學習一般特徵)不動,只fine-tune後面的卷積塊(學習特別的特徵)。

  • fine-tune應該在很低的學習率下進行,通常使用SGD優化而不是其他自適應學習率的優化演算法,如RMSProp。這是為了保證更新的幅度保持在較低的程度,以免毀壞預訓練的特徵。

程式碼如下,首先在初始化好的vgg網路上新增我們預訓練好的模型:

# build a classifier model to put on top of the convolutional model
top_model = Sequential()
top_model.add(Flatten(input_shape=model.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(1, activation='sigmoid'))

# note that it is necessary to start with a fully-trained
# classifier, including the top classifier,
# in order to successfully do fine-tuning
top_model.load_weights(top_model_weights_path)

# add the model on top of the convolutional base
model.add(top_model)

然後將最後一個卷積塊前的卷積層引數凍結:

# set the first 25 layers (up to the last conv block)
# to non-trainable (weights will not be updated)
for layer in model.layers[:25]:
    layer.trainable = False

# compile the model with a SGD/momentum optimizer
# and a very slow learning rate.
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])

然後以很低的學習率進行訓練:

# prepare data augmentation configuration
train_datagen = ImageDataGenerator(
        rescale=1./255,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
        train_data_dir,
        target_size=(img_height, img_width),
        batch_size=32,
        class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
        validation_data_dir,
        target_size=(img_height, img_width),
        batch_size=32,
        class_mode='binary')

# fine-tune the model
model.fit_generator(
        train_generator,
        samples_per_epoch=nb_train_samples,
        nb_epoch=nb_epoch,
        validation_data=validation_generator,
        nb_val_samples=nb_validation_samples)

在50個epoch之後該方法的準確率為94%,非常成功

通過下面的方法你可以達到95%以上的正確率:

  • 更加強烈的資料提升

  • 更加強烈的dropout

  • 使用L1和L2正則項(也稱為權重衰減)

  • fine-tune更多的卷積塊(配合更大的正則)