十三、計算機視覺中的深度學習:使用預訓練網絡


文章代碼來源:《deep learning on keras》,非常好的一本書,大家如果英語好,推薦直接閱讀該書,如果時間不夠,可以看看此係列文章,文章爲我自己翻譯的內容加上自己的一些思考,水平有限,多有不足,請多指正,翻譯版權所有,若有轉載,請先聯繫本人。
個人方向爲數值計算,日後會向深度學習和計算問題的融合方面靠近,若有相近專業人士,歡迎聯繫。


系列文章:
一、搭建屬於你的第一個神經網絡
二、訓練完的網絡去哪裏找
三、【keras實戰】波士頓房價預測
四、keras的function API
五、keras callbacks使用
六、機器學習基礎Ⅰ:機器學習的四個標籤
七、機器學習基礎Ⅱ:評估機器學習模型
八、機器學習基礎Ⅲ:數據預處理、特徵工程和特徵學習
九、機器學習基礎Ⅳ:過擬合和欠擬合
十、機器學習基礎Ⅴ:機器學習的一般流程十一、計算機視覺中的深度學習:卷積神經網絡介紹
十二、計算機視覺中的深度學習:從零開始訓練卷積網絡
十三、計算機視覺中的深度學習:使用預訓練網絡
十四、計算機視覺中的神經網絡:可視化卷積網絡所學到的東西


一個常用、高效的在小圖像數據集上深度學習的方法就是利用預訓練網絡。一個預訓練網絡只是簡單的儲存了之前在大的數據集訓練的結果,通常是大的圖像分類任務。如果原始的數據集已經足夠大,足夠一般,通過預訓練學習到的空間上的特徵層次結構就能有效地在我們的模型中工作,因此這些特徵對許多計算機視覺問題都很有用,儘管這些新問題和原任務相比可能涉及完全不同的類別。例如,一個人或許在ImageNet上訓練了一個網絡(裏面的類別大多是動物和日常物體)然後重新定義這個訓練有素的網絡,讓它去識別圖像裏面的傢俱。這樣學習特徵在不同問題的可移植性是深度學習和其它一些淺學習方法比起來最關鍵的好處,這讓深度學習在小數據問題中十分高效。
在我們的例子中,我們考慮了一個大的在ImageNet數據集上訓練的卷積網絡。圖像網絡包含很多動物,包括不同種的貓和狗,我們可以期望這會在我們的cat vs. dog分類問題中表現得很好。
我們將使用VGG16結構,由Karen Simonyan 和Andrew Zisserman 於2014年改進,簡單且在ImageNet中廣泛使用卷積網絡結構。儘管這是一個有點老的模型,和現在最先進的相比相差不少,且比現在的模型要笨重一些,我們選擇它是因爲它的結構和你熟悉的非常相近,而且不需要引入任何新的內容。這或許是你的第一次遇到這些可愛的模型名字——VGG,ResNet,Inception,Inception-ResNet,Xception...你將會習慣他們,因爲它們會出現的非常頻繁,如果你持續在用深度學習做計算機視覺的話。
這裏有兩種方法去引入一個預訓練的網絡:特徵提取和良好調參。我們將兩個都講,讓我們從特徵提取開始。

特徵提取

特徵提取使用通過之前網絡學習到的表示來從新的樣本中提取有趣的特徵。這些特徵通過新的分類器,這個分類器是從零開始訓練的。
正如我們之前看到的,卷積網絡用來圖像分類包括兩部分:它們從一系列的池化和卷積層開始,以全連接層分類器結束。第一部分叫做模型的“卷積基”。在卷積網絡的情況,“特徵提取”將會簡單的包含拿卷積基作爲預訓練網絡,在這個上面跑一些新數據,在輸出層最上面訓練一個新的分類器。



爲什麼僅僅重用卷積基呢?我們能重用全連接分類器嗎?一般來說,這個應當避免。這個原因是卷積基學習到的表示可能更通用,所以也更能複用:卷積網絡的特徵映射在圖像中間是存在通用的,這就使得忽視手頭的計算機視覺問題很有用了。在另一端,分類器學習到的表示對於我們要訓練的模型的類別是非常特別的——它們將只包含整幅圖在這一類或那一類的概率。此外,在全連接層的表示不再包含任何物體處於輸入圖像的哪個位置的信息:這些層擺脫了空間的概念,儘管物體的位置仍然有卷積特映射來描述。對於物體位置有關的問題,全連接特徵就顯得相當的無用。
注意:某一層表示提取的共性或是說複用能力,取決於模型的深度。早期的層會提取一些局部的,共度通用的特徵比如邊緣、顏色和紋理,同時後期的層會提取一些高度抽象的例如貓眼和狗眼。因此如果你的新數據集和原來的數據集很不一樣,你最好只用前面的幾層模型來做特徵提取,而不是使用整個卷積集。
在我們的例子中,由於ImageNet分類基的確包含大量的貓、狗的類,可能我們直接複用全連接層都會得到一些好處。然而,我們選擇不這樣做,爲了包含一些更加一般的情況,這些情況中需要分類的新問題和原來的模型類別沒有重疊。
讓我們使用VGG16網絡來實踐一下吧,在ImageNet中提取了貓狗的圖像的有趣特徵,最後在cat vs. dog中分類器訓練這些特徵。
VGG16模型,還有一些別的,都被keras提前打包好了。你能直接從keras.applications裏面調用模型。這裏是圖像分類模型的的一個列表,全部都是在ImageNet上面預訓練過了的,作爲kera.applications的一部分:

  • Xception
  • Inception V3
  • ResNet50
  • VGG16
  • VGG19
  • MobileNet
    讓我們實例教學VGG16模型:
from keras.applications import VGG16
conv_base = VGG16(weights='imagenet',
 include_top=False,
 input_shape=(150, 150, 3))

我們給結構傳入了三個參數:

  • weights,用來初始化權重的checkpoint
  • include_top,是否包含最高的一層全連接層。默認的話,全連接分類器將會遵從ImgeNet中的1000類。因爲我們試着使用我們的全連接層(只有兩類,貓和狗)我們不需要把原始的全連接層包含進來。
  • input_shape,我們需要放進網絡中的圖像張量的形狀。這個參數完全可選:如果我們不傳這個進去,網絡就能處理任何形狀的輸入。
    這裏有一些VGG16卷積基結構的細節:這和你熟悉的簡單卷積網絡很相似。
>>> conv_base.summary()
Layer (type) Output Shape Param #
================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
________________________________________________________________
block1_conv1 (Convolution2D) (None, 150, 150, 64) 1792
________________________________________________________________
block1_conv2 (Convolution2D) (None, 150, 150, 64) 36928
________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
________________________________________________________________
block2_conv1 (Convolution2D) (None, 75, 75, 128) 73856
________________________________________________________________
block2_conv2 (Convolution2D) (None, 75, 75, 128) 147584
________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
________________________________________________________________
block3_conv1 (Convolution2D) (None, 37, 37, 256) 295168
________________________________________________________________
block3_conv2 (Convolution2D) (None, 37, 37, 256) 590080
________________________________________________________________
block3_conv3 (Convolution2D) (None, 37, 37, 256) 590080
________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
________________________________________________________________
block4_conv1 (Convolution2D) (None, 18, 18, 512) 1180160
________________________________________________________________
block4_conv2 (Convolution2D) (None, 18, 18, 512) 2359808
________________________________________________________________
block4_conv3 (Convolution2D) (None, 18, 18, 512) 2359808
________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
________________________________________________________________
block5_conv1 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_conv2 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_conv3 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0

最終的特徵的形狀是(4,4,512)。這就是我們要接入全連接層的特徵。
在這個時候,我們有兩種方法來進行:

  • 通過我們的數據集來跑卷積基,記錄其輸出爲數組類型,然後使用這些數據作爲輸入來訓練單獨一個全連接分類器,就像我們第一次舉得那個例子一樣。這個方法很快很容易去運行,因爲這對於每一個輸入的圖像只需要運行卷積基一次,卷積基目前來說是管道中最貴的。然而,同樣的原因,這種方法不允許我們利用數據增加。
  • 給我們有的卷積基增加一層全連接層,然後端到端的運行整個輸入數據。這允許我們使用數據增加,因爲每個輸入圖像都通過了整個卷積基。然而,相同的原因,這種方法也比較昂貴。
    我們將包含兩種方法。讓我們第一個看看所需的代碼:在我們的數據中記錄輸出層conv_base並使用這些輸出作爲新模型的輸入。
    我們將會從之前介紹的ImageDataGenerator實例來通過數組的形式提取圖像和它們的標籤。我們將會從這些圖像中使用predict方法來提取特徵。
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator
base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')
datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20
def extract_features(directory, sample_count):
 features = np.zeros(shape=(sample_count, 4, 4, 512))
 labels = np.zeros(shape=(sample_count))
 generator = datagen.flow_from_directory(
 directory,
 target_size=(150, 150),
 batch_size=batch_size,
 class_mode='binary')
 i = 0
 for inputs_batch, labels_batch in generator:
 features_batch = conv_base.predict(inputs_batch)
 features[i * batch_size : (i + 1) * batch_size] = features_batch
 labels[i * batch_size : (i + 1) * batch_size] = labels_batch
 i += 1
 if i * batch_size >= sample_count:
 # Note that since generators yield data indefinitely in a loop,
 # we must `break` after every image has been seen once.
 break
 return features, labels
train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)

在把之前的輸出丟進全連接分類器之前,我們需要先把它們拉平:

train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))

此時,我們能夠定義我們的全連接分類器了(使用dropout正則化),並在我們所記錄的數據和標籤上進行訓練:

from keras import models
from keras import layers
from keras import optimizers
model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
 loss='binary_crossentropy',
 metrics=['acc'])
history = model.fit(train_features, train_labels,
 epochs=30,
 batch_size=20,
 validation_data=(validation_features, validation_labels))

訓練很快,我們可以解決兩個全連接層,每一批次在CPU上都只需要不到一秒的時間。
讓我們看一下訓練的損失和準確率曲線:

import matplotlib.pyplot as plt
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()


我們在驗證集上面的準確率到了約90%,比我們之前從零開始訓練的小模型要好得多。然而,我們的圖同樣揭示了我們幾乎從一開始就過擬合,儘管對dropout用了很高的rate。這是因爲這些方法都沒有利用數據增多,這對於小樣本來說預防過擬合相當的重要。
現在,讓我們回顧第二種我們提到的做特徵提取的方法,這將慢得多,貴得多,但這允許我們在訓練的時候使用數據增多的方式:擴展卷積基模型,然後端到端的運行輸入。注意到這個技術實際上非常貴,所以你只能在GPU上面試:這在CPU上跑幾乎僵化。如果你無法在GPU上跑代碼,那麼你就用第一種方法吧。
因爲模型表現得像個層,你能在sequential模型上加模型:

from keras import models
from keras import layers
model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

讓我們看一下最後長啥樣

>>> model.summary()
Layer (type) Output Shape Param #
================================================================
vgg16 (Model) (None, 4, 4, 512) 14714688
________________________________________________________________
flatten_1 (Flatten) (None, 8192) 0
________________________________________________________________
dense_1 (Dense) (None, 256) 2097408
________________________________________________________________
dense_2 (Dense) (None, 1) 257
================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0

正如你看到的,VGG16的卷積基有14714688個參數,非常大。我們在頂上加的分類器有2000個參數。
在我們編譯和訓練模型之前,一個很重要的事情就是凍結卷積基。凍結一層或多層,意味着阻止其權重在訓練的時候發生變化。如果你不這樣做,之前訓練好的卷積基將會在訓練的過程中發生改變。因爲頂上的全連接層隨機初始化,很多的權重升級就會通過網絡傳播,有效的毀壞之前學習到的表示。
在keras裏面,凍結網絡是通過將trainable屬性設置爲False來做到的:

>>> print('This is the number of trainable weights '
 'before freezing the conv base:', len(model.trainable_weights))
This is the number of trainable weights before freezing the conv base: 30
>>> conv_base.trainable = False
>>> print('This is the number of trainable weights '
 'after freezing the conv base:', len(model.trainable_weights))
This is the number of trainable weights after freezing the conv base: 4

通過這個設置,只有兩個加進去的全連接層能夠被訓練。總共只有兩個權重張量:每層兩個(主要的權重矩陣和偏置向量)。注意爲了讓這些改變更有效,我們必須首先將模型編譯。如果你曾經修改了權重的可訓練性,在編譯之後,你應當重新編譯模型,不然這些改變會被忽略。
現在我們開始訓練我們的模型,使用之前我們用過的數據增多方法。

from keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator(
 rescale=1./255,
 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')
# Note that the validation data should not be augmented!
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
 # This is the target directory
 train_dir,
 # All images will be resized to 150x150
 target_size=(150, 150),
 batch_size=20,
 # Since we use binary_crossentropy loss, we need binary labels
 class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
 validation_dir,
 target_size=(150, 150),
 batch_size=20,
 class_mode='binary')
model.compile(loss='binary_crossentropy',
 optimizer=optimizers.RMSprop(lr=2e-5),
 metrics=['acc'])
history = model.fit_generator(
 train_generator,
 steps_per_epoch=100,
 epochs=30,
 validation_data=validation_generator,
 validation_steps=50)

再次畫出結果:




如你所見,我們在驗證集的準確率到達了大約96%。這比我們之前在小卷積網絡上從零訓練的結果要好的多。

好的調參

另一個在模型複用中廣泛應用的和特徵提取互補,是好調參。好調參是最後幾層都不凍結,使得可以在最後比較抽象的層次也可以複用調參,使得它們對於手頭的問題來說更加相關。



我們曾經聲明過爲了能夠訓練頂層隨機初始化的分類器,凍結VGG16的卷積基非常重要。相同的原因,只有當卷積基中的分類器被訓練過以後,纔有可能調參。如果分類器還沒有被訓練過,傳給網絡的錯誤的信號就太大了,這個表示之前學到的將會被毀掉。因此調參的步驟如下:

  • 1)在已經訓練的基礎網絡頂層添加你自定義的網絡
  • 2)凍結基礎網絡
  • 3)訓練你添加的部分
  • 4)解凍基礎網絡的某些層
  • 5)將這些原來的層和你加的層一起訓練

在做特徵提取的時候,我們已經結束了前三步。讓我們接下來做第四步:我們將會解凍我們的conv_base層,然後凍結其中的一些單獨的層。
作爲提醒,下面是我們的卷積基的樣子:

>>> conv_base.summary()
Layer (type) Output Shape Param #
================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
________________________________________________________________
block1_conv1 (Convolution2D) (None, 150, 150, 64) 1792
________________________________________________________________
block1_conv2 (Convolution2D) (None, 150, 150, 64) 36928
________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
________________________________________________________________
block2_conv1 (Convolution2D) (None, 75, 75, 128) 73856
________________________________________________________________
block2_conv2 (Convolution2D) (None, 75, 75, 128) 147584
________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
________________________________________________________________
block3_conv1 (Convolution2D) (None, 37, 37, 256) 295168
________________________________________________________________
block3_conv2 (Convolution2D) (None, 37, 37, 256) 590080
________________________________________________________________
block3_conv3 (Convolution2D) (None, 37, 37, 256) 590080
________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
________________________________________________________________
block4_conv1 (Convolution2D) (None, 18, 18, 512) 1180160
________________________________________________________________
block4_conv2 (Convolution2D) (None, 18, 18, 512) 2359808
________________________________________________________________
block4_conv3 (Convolution2D) (None, 18, 18, 512) 2359808
________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
________________________________________________________________
block5_conv1 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_conv2 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_conv3 (Convolution2D) (None, 9, 9, 512) 2359808
________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
================================================================
Total params: 14714688

我們將會調最後三層的參數,意味着直到block4_pool的層都應該被凍結,最後幾層,block5_conv1,block5_conv2和block5_conv3應當被訓練。
爲什麼不調更多層的參數?爲什麼不調整個卷積基的參數?我們可以。然而我們需要考慮下面這些:

  • 卷積基中早期的層編碼了一些通用的特徵,後期的層編碼了一些比較專業的特徵,這些纔是我們在新的問題中需要重新設計的。在早期層數裏面調參會導致結果急劇下降。
  • 我們訓練的參數愈多,我們過擬合的風險越大。卷積基有15M個參數,所以在我們小樣本上訓練有很大的風險。

因此,在我們的情況下,好的策略是訓練最後一兩層卷積基。
讓我們結束之前剩下的小尾巴:

conv_base.trainable = True
set_trainable = False
for layer in conv_base.layers:
 if layer.name == 'block5_conv1':
 set_trainable = True
 if set_trainable:
 layer.trainable = True
 else:
 layer.trainable = False

很巧妙的把之前的層全凍了,現在我們能夠開始給我們的網絡調參了。我們將會用RMSprop優化器,使用很低的學習率。我們選擇使用低學習率的原因是我們想要限制我們對這三層的修改的大小。改動太大會損害原有的表示。
現在讓我們開始訓練吧:

model.compile(loss='binary_crossentropy',
 optimizer=optimizers.RMSprop(lr=1e-5),
 metrics=['acc'])
history = model.fit_generator(
 train_generator,
 steps_per_epoch=100,
 epochs=100,
 validation_data=validation_generator,
 validation_steps=50)

用之前的代碼畫出圖:




這個曲線看起來噪聲很大。爲了讓它們更加具有可讀性,我們可以光滑它們通過滑動平均:

def smooth_curve(points, factor=0.8):
 smoothed_points = []
 for point in points:
 if smoothed_points:
 previous = smoothed_points[-1]
 smoothed_points.append(previous * factor + point * (1 - factor))
 else:
 smoothed_points.append(point)
 return smoothed_points
plt.plot(epochs,
 smooth_curve(acc), 'bo', label='Smoothed training acc')
plt.plot(epochs,
 smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs,
 smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs,
 smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()


現在曲線看起來更加乾淨穩定了。我們看到了1%的提升。
注意到損失曲線並沒有展現出真實的提升,實際上它惡化了。你或許想知道,準確率是如何提升的,如果損失沒有下降的化?答案很簡單:當我們看平均的損失值時,實際上影響準確率的是損失值,不是平均,因爲準確率是模型分類預測的二進閾值的結果。模型或許仍然在在變好,只不過沒有在平均損失中體現出來。
我們最後能在測試集上評估模型:

test_generator = test_datagen.flow_from_directory(
 test_dir,
 target_size=(150, 150),
 batch_size=20,
 class_mode='binary')
test_loss, test_acc = model.evaluate_generator(test_generator, steps=50)
print('test acc:', test_acc)

到這裏爲止,我們得到了測試準確率爲97%。在原來的Kaggle競賽中關於這個數據集,這是最頂尖的幾個結果之一。然而,使用現代深度學習技術,我們可以使用只用很少的訓練數據(大約百分之十)來達到這個結果。在20000個樣本和2000個樣本之間有着巨大的區別。

外賣:在小的數據集上使用卷積網絡

這裏有一些你需要打包帶走的東西,通過之前兩部分的學習:

  • 卷積網絡對於計算機視覺任務來說是最好的機器學習模型。使用少量數據訓練得到體面的結果是可能的。
  • 在小數據集上,過擬合將會是一個主要問題。數據增加是一個有力的對抗過擬合的方法,當我們面臨圖像數據時。
  • 這很容易在存在的卷積網絡上覆用數據集,通過特徵提取。這對於在小數據集上工作十分有價值。
  • 作爲特徵提取的補充,使用調參,適應存在的模型表示的一些新問題。這將表現推得更遠。

現在你已經有了一個有力的工具來解決圖像分類問題,特別是小的數據集。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章