在本教程中,我們將介紹一些簡單而有效的方法,可以使用這些方法構建一個功能強大的圖像分類器,只使用很少的訓練數據 —— 每類幾百或幾千張圖片。

將介紹以下內容:

  • 從頭開始訓練小型網路(作為基線)
  • 使用預先訓練的網路的瓶頸功能
  • 微調預訓練網路的top layers

我們將使用到以下Keras的features:

  • fit_generator 使用Python數據生成器,訓練Keras模型
  • ImageDataGenerator 用於實時數據增強
  • 層凍結(layer freezing)和模型fine-tuning

注意:需要Keras 2.0.0或更高版本方可運行。


開工:2000個訓練樣本(每類1000個)

我們將從以下設置開始:

  • 安裝了Keras,SciPy,PIL的電腦(當然如果有Nvidia 的GPU最好了)。
  • 訓練數據集和驗證數據集,目錄如下:

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

本實驗中,我們從Kaggle獲得了1000隻貓和1000隻狗的圖像(原始數據集有12,500隻貓和12,500隻狗,各取了前1000張)作為訓練集,額外的400張作為驗證集。

對於深度學習來說,這是一個很小的數據集。用小樣本訓練深度學習模型是一個非常有挑戰性的問題,但它也是一個現實的問題:在許多現實世界的使用案例中,即使是小規模的數據收集也可能非常昂貴或有時幾乎不可能(例如在醫學成像中)。能夠充分利用非常少的數據是有能力的數據科學家的關鍵技能。

這個問題有多難?兩年前的Kaggle貓狗比賽(共計25,000張訓練圖像),有以下聲明:

「在多年前進行的非正式民意調查中,計算機視覺專家認為,如果沒有現有技術的重大進步,精度高於60%的分類器將很難實現。目前的文獻表明,機器分類器在此任務上的準確度可以達到80%以上[參考]。「

在最終的比賽中,頂級參賽者通過使用現代深度學習技術獲得了超過98%的準確率。在我們的例子中,因為我們僅將自己限制在數據集的8%,所以問題要困難得多。

深度學習與小數據

我們經常聽到的一條信息是「深度學習只有在擁有大量數據時纔有意義」。當然,深度學習需要能夠從數據中自動學習特徵,這通常只有在有大量訓練數據可用時纔有可能 ,特別是對於輸入樣本非常高維的問題,如圖像。然而,卷積神經網路 —— 深度學習的支柱演算法 —— 是大多數「感知」問題(例如圖像分類)的最佳模型之一,即使只有很少的訓練數據,依然可以訓練一個不錯的模型,而不需要任何自定義特徵工程。

更重要的是,深度學習模型本質上是高度可再利用的:例如,您可以採用在大規模數據集上訓練的圖像分類或語音到文本模型,然後在一個不同的任務重複使用它,只需進行微小的更改,如我們將在這篇文章中看到。特別是在計算機視覺的任務中,許多預先訓練的模型(通常在ImageNet數據集上訓練)現在可以公開下載,並且可以用於從非常少的數據中推導強大的視覺模型。


數據預處理和數據增強

為了充分利用訓練樣本,我們將通過一系列隨機變換來「擴充」它們,這樣模型就不會看到完全相同的兩次圖像。這有助於防止過擬合,並增強模型的泛化性能。

在Keras中可以通過keras.preprocessing.image.ImageDataGenerator來完成。這個類允許我們:

  • 配置訓練過程中圖像的各種變換和歸一化操作
  • 通過調用.flow(data, labels)或者.flow_from_directory(directory)方法,返回上述操作的圖像batch生成器。這個生成器可以和Keras的模型方法(fit_generator,evaluate_generator和predict_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_shift和height_shift是在垂直或水平方向上隨機平移圖片的範圍(作為總寬度或高度的一部分)
  • rescale是一個值,對圖像的亮度值進行縮放,這個操作在所有操作之前,例如rescale為1/255時,表示把RGB的值從0-255轉換到0-1之間。
  • shear_range用於隨機應用剪切變換
  • zoom_range 用於隨機縮放
  • horizontal_flip 50%的隨機概率對圖像進行水平翻轉
  • fill_mode 像素填充策略,在進行旋轉或平移之後,需要對圖像像素進行填充。

現在讓我們開始使用這個工具生成一些圖片並將它們保存到臨時目錄中,這樣我們就可以瞭解我們的增強策略正在做什麼 —— 禁用rescale以保持圖像可顯示:

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

下圖是我們的數據增強策略的樣子。


從頭開始訓練一個小卷積神經網路:40行代碼,準確率達到80%

卷積神經網路是圖像分類任務的不二選擇,所以讓我們從訓練一個小網路開始,作為初始基線。由於只有少量訓練數據,我們的頭號問題應該是過擬合。當暴露於太少示例的模型學習不能推廣到新數據的模式時,即當模型開始使用不相關的特徵進行預測時,就會發生過度擬合。例如,如果你作為一個人,只能看到三個伐木工人的圖像,三個是水手人的圖像,其中只有一個伐木工人戴著帽子,你可能會開始認為戴帽子是一個作為一名伐木工人的標誌,而不是一名水手。然後你會做一個非常糟糕的伐木工/水手分類器。

數據增加是對抗過度擬合的一種方法,但這還不夠,因為我們的增強樣本仍然是高度相關的。過度擬合的主要焦點應該是模型的熵能力 - 我們的模型可以存儲多少信息。通過利用更多功能,可以存儲大量信息的模型可能更加準確,但開始存儲不相關的功能也存在風險。同時,只能存儲一些功能的模型必須關注數據中發現的最重要的功能,這些功能更有可能真正相關並更好地推廣。

調製熵容量有不同的方法。主要的是選擇模型中的參數數量,即層數和每層的大小。另一種方法是使用權重正則化,例如L1或L2正則化,其包括迫使模型權重接受較小的值。

在我們的例子中,我們將使用一個非常小的convnet,每層有少量層和少量過濾器,以及數據增加和dropout。Dropout還有助於減少過度擬合,防止圖層看到完全相同模式的兩倍,從而以類似於數據增強的方式運行(您可以說丟失和數據增加都會破壞數據中出現的隨機關聯)。

下面的代碼片段是我們的第一個模型,一個包含3個卷積層的簡單堆棧,其中包含ReLU激活,然後是最大池化層。這與Yann LeCun在20世紀90年代提倡的用於圖像分類的架構(ReLU除外)非常相似。

可以在此處找到此實驗的完整代碼。

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

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

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

model.add(Conv2D(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生成批量圖像數據(及其標籤)。

batch_size = 16

# 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=batch_size,
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=batch_size,
class_mode=binary)

我們現在可以使用這些發電機來訓練我們的模型。每個紀元在GPU上需要20-30秒,在CPU上需要300-400秒。因此,如果您不趕時間,在CPU上運行此模型絕對可行。

model.fit_generator(
train_generator,
steps_per_epoch=2000 // batch_size,
epochs=50,
validation_data=validation_generator,
validation_steps=800 // batch_size)
model.save_weights(first_try.h5) # always save your weights after training or during training

這種方法使我們在50個epoch之後達到0.79-0.81的驗證準確度(這個數字是任意選擇的 - 因為模型很小並且使用了非常激進的dropout,到那時它似乎不會過度擬合)。因此,在推出Kaggle比賽時,我們已經成為「最先進的」 - 擁有8%的數據,並且沒有努力優化我們的架構或超參數。事實上,在Kaggle比賽中,這個模型將進入前100名(215名參賽者中)。我想至少有115名參賽者沒有使用深度學習;)

請注意,驗證準確度的方差相當高,因為準確度是一個高方差度量,因為我們只使用800個驗證樣本。在這種情況下,一個很好的驗證策略是進行k折交叉驗證,但這需要在每輪評估中訓練k個模型。


使用預先訓練的網路的瓶頸功能:一分鐘內準確率達到90%

更精確的方法是利用在大型數據集上預先訓練的網路。這樣的網路已經學習了對大多數計算機視覺問題有用的特徵,並且利用這些特徵將使我們能夠比僅依賴於可用數據的任何方法獲得更好的準確性。

我們將使用VGG16架構,該架構在ImageNet數據集上進行了預訓練 - 這是此博客之前的模型。由於ImageNet數據集在其總共1000個類中包含多個「貓」類(波斯貓,暹羅貓......)和許多「狗」類,因此該模型已經學習了與我們的分類問題相關的特徵。實際上,僅僅記錄模型的softmax預測而不是瓶頸特徵就足以解決我們的狗與貓的分類問題。然而,我們在這裡提出的方法更有可能很好地推廣到更廣泛的問題,包括ImageNet中缺少類的問題。

這就是VGG16架構的樣子:

我們的策略如下:我們只會實例化模型的卷積部分,直到完全連接的層。然後,我們將在訓練和驗證數據上運行此模型一次,在兩個numpy陣列中記錄輸出(來自VGG16模型的「瓶頸特徵」:完全連接的層之前的最後一個激活映射)。然後,我們將在存儲的功能之上訓練一個小的完全連接模型。

我們之所以離線存儲這些功能而不是直接在凍結的卷積基礎上添加我們的完全連接模型並運行整個功能,是因為計算效率。運行VGG16很昂貴,特別是如果你正在使用CPU,我們只想做一次。請注意,這會阻止我們使用數據擴充。

您可以在此處找到此實驗的完整代碼。你可以從Github獲得權重文件。我們不會檢查模型的構建和載入方式 - 這已經在多個Keras示例中介紹過了。但是讓我們來看看如何使用圖像數據生成器記錄瓶頸特徵:

batch_size = 16

generator = datagen.flow_from_directory(
data/train,
target_size=(150, 150),
batch_size=batch_size,
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=batch_size,
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,
epochs=50,
batch_size=batch_size,
validation_data=(validation_data, validation_labels))
model.save_weights(bottleneck_fc_model.h5)

由於它的體積小,這種型號甚至可以在CPU(每個epoch 1秒)上快速訓練:

Train on 2000 samples, validate on 800 samples
Epoch 1/50
2000/2000 [==============================] - 1s - loss: 0.8932 - acc: 0.7345 - val_loss: 0.2664 - val_acc: 0.8862
Epoch 2/50
2000/2000 [==============================] - 1s - loss: 0.3556 - acc: 0.8460 - val_loss: 0.4704 - val_acc: 0.7725
...
Epoch 47/50
2000/2000 [==============================] - 1s - loss: 0.0063 - acc: 0.9990 - val_loss: 0.8230 - val_acc: 0.9125
Epoch 48/50
2000/2000 [==============================] - 1s - loss: 0.0144 - acc: 0.9960 - val_loss: 0.8204 - val_acc: 0.9075
Epoch 49/50
2000/2000 [==============================] - 1s - loss: 0.0102 - acc: 0.9960 - val_loss: 0.8334 - val_acc: 0.9038
Epoch 50/50
2000/2000 [==============================] - 1s - loss: 0.0040 - acc: 0.9985 - val_loss: 0.8556 - val_acc: 0.9075

我們達到0.90-0.91的驗證準確度:一點也不差。這在一定程度上部分原因在於基礎模型是在已經具有狗和貓(其他數百個類別)的數據集上進行訓練的。


微調預訓練網路的top layers

為了進一步改進之前的結果,我們可以對VGG16模型的最後一個卷積塊和最終的全連接層進行Fine-tuning。分以下3步完成:

  • 實例化VGG16卷積網路,並載入預訓練權重
  • 在頂部添加我們先前定義的完全連接模型,並載入其權重
  • 凍結VGG16模型的層到最後一個卷積塊

注意:

  • 為了進行微調,所有層都應該從訓練有素的權重開始:例如,你不應該在預先訓練好的卷積基礎上打一個隨機初始化的全連接網路。這是因為由隨機初始化的權重觸發的大梯度更新將破壞卷積基礎中的學習權重。在我們的例子中,這就是為什麼我們首先訓練頂級分類器,然後才開始微調卷積權重。
  • 我們選擇僅微調最後的卷積塊而不是整個網路以防止過度擬合,因為整個網路將具有非常大的熵容量並因此具有過度擬合的強烈傾向。低級卷積塊學習的特徵比較高級的卷積塊更加通用,不那麼抽象,所以保持前幾個塊固定(更一般的特徵)並且只調整最後一個塊(更專業的特徵)是明智的 )。
  • 微調應該以非常慢的學習速率完成,通常使用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])

最後,我們開始訓練整個事情,學習速度非常慢:

batch_size = 16

# 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=batch_size,
class_mode=binary)

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

# fine-tune the model
model.fit_generator(
train_generator,
steps_per_epoch=nb_train_samples // batch_size,
epochs=epochs,
validation_data=validation_generator,
validation_steps=nb_validation_samples // batch_size)

這種方法使我們在50個epoch之後達到0.94的驗證準確度。巨大的成功!

以下是一些您可以嘗試達到0.95以上的方法:

  • 更具侵略性的數據擴充
  • 更積極的輟學
  • 使用L1和L2正則化(也稱為「重量衰減」)
  • 微調一個卷積塊(同時更大的正則化)

這篇文章在這裡結束!回顧一下,在這裡您可以找到我們三個實驗的代碼:

  • Convnet從頭開始訓練
  • 瓶頸功能
  • 微調

註:本文章翻譯自:

Building powerful image classification models using very little data?

blog.keras.io圖標
推薦閱讀:

相關文章