1. 程式人生 > >Keras 入門課6 -- 使用Inception V3模型進行遷移學習

Keras 入門課6 -- 使用Inception V3模型進行遷移學習

Keras 入門課6:使用Inception V3模型進行遷移學習

keras 請使用2.1.2版

深度學習可以說是一門資料驅動的學科,各種有名的CNN模型,無一不是在大型的資料庫上進行的訓練。像ImageNet這種規模的資料庫,動輒上百萬張圖片。對於普通的機器學習工作者、學習者來說,面對的任務各不相同,很難拿到如此大規模的資料集。同時也沒有谷歌,Facebook那種大公司驚人的算力支援,想從0訓練一個深度CNN網路,基本是不可能的。但是好在已經訓練好的模型的引數,往往經過簡單的調整和訓練,就可以很好的遷移到其他不同的資料集上,同時也無需大量的算力支撐,便能在短時間內訓練得出滿意的效果。這便是遷移學習。究其根本,就是雖然影象的資料集不同,但是底層的特徵卻是有大部分通用的。

遷移學習主要分為兩種

  • 第一種即所謂的transfer learning,遷移訓練時,移掉最頂層,比如ImageNet訓練任務的頂層就是一個1000輸出的全連線層,換上新的頂層,比如輸出為10的全連線層,然後訓練的時候,只訓練最後兩層,即原網路的倒數第二層和新換的全連線輸出層。可以說transfer learning將底層的網路當做了一個特徵提取器來使用。
  • 第二種叫做fine tune,和transfer learning一樣,換一個新的頂層,但是這一次在訓練的過程中,所有的(或大部分)其它層都會經過訓練。也就是底層的權重也會隨著訓練進行調整。

一個典型的遷移學習過程是這樣的。首先通過transfer learning對新的資料集進行訓練,訓練過一定epoch之後,改用fine tune方法繼續訓練,同時降低學習率。這樣做是因為如果一開始就採用fine tune方法的話,網路還沒有適應新的資料,那麼在進行引數更新的時候,比較大的梯度可能會導致原本訓練的比較好的引數被汙染,反而導致效果下降。

本課,我們將嘗試使用谷歌提出的Inception V3模型來對一個花朵資料集進行遷移學習的訓練。

2018年09月02日更新:
花朵命名按順序命名為flower_A, flower_B, … , flower_Q。

data/
    train/
        class1/
            img1
            img2
            ...
        class2/
            img1
            ...
    validation/
        class1/
            img1
            img2
            ...
        class2/
            img1
            ...
    test/
        class1/
            img1
            img2
            ...
        class2/
            img1
            ...

這個指令碼我將訓練集劃分為800張,驗證集和測試集分別為260張,圖片順序做了隨機打亂

請注意,這裡的花朵識別仍屬於最簡單的單分類任務,樣張如下


這裡寫圖片描述

from keras.preprocessing.image import ImageDataGenerator
from keras.applications.inception_v3 import InceptionV3,preprocess_input
from keras.layers import GlobalAveragePooling2D,Dense
from keras.models import Model
from keras.utils.vis_utils import plot_model
from keras.optimizers import Adagrad
# 資料準備
train_datagen = ImageDataGenerator(
    preprocessing_function=preprocess_input,# ((x/255)-0.5)*2  歸一化到±1之間
    rotation_range=30,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
)
val_datagen = ImageDataGenerator(
    preprocessing_function=preprocess_input,
    rotation_range=30,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
)

這裡用到的資料集和之前都不同,之前用的是一些公共的、Keras內建的資料集,這次用到的是自己準備的資料集。由於資料的影象大小比較大,不適合一次全部載入到記憶體中,所以使用了flow_from_directory方法來按批次從硬碟讀取影象資料,並實時進行影象增強

train_generator = train_datagen.flow_from_directory(directory='./flowers17/train',
                                  target_size=(299,299),#Inception V3規定大小
                                  batch_size=64)
val_generator = val_datagen.flow_from_directory(directory='./flowers17/validation',
                                target_size=(299,299),
                                batch_size=64)
Found 800 images belonging to 17 classes.
Found 280 images belonging to 17 classes.

首先我們需要載入骨架模型,這裡用的InceptionV3模型,其兩個引數比較重要,一個是weights,如果是’imagenet’,Keras就會自動下載已經在ImageNet上訓練好的引數,如果是None,系統會通過隨機的方式初始化引數,目前該引數只有這兩個選擇。另一個引數是include_top,如果是True,輸出是1000個節點的全連線層。如果是False,會去掉頂層,輸出一個8 * 8 * 2048的張量。

ps:在keras.applications裡還有很多其他的預置模型,比如VGG,ResNet,以及適用於移動端的MobileNet等。大家都可以拿來玩玩。

一般我們做遷移訓練,都是要去掉頂層,後面接上各種自定義的其它新層。這已經成為了訓練新任務慣用的套路。
輸出層先用GlobalAveragePooling2D函式將8 * 8 * 2048的輸出轉換成1 * 2048的張量。後面接了一個1024個節點的全連線層,最後是一個17個節點的輸出層,用softmax啟用函式。

# 構建基礎模型
base_model = InceptionV3(weights='imagenet',include_top=False)

# 增加新的輸出層
x = base_model.output
x = GlobalAveragePooling2D()(x) # GlobalAveragePooling2D 將 MxNxC 的張量轉換成 1xC 張量,C是通道數
x = Dense(1024,activation='relu')(x)
predictions = Dense(17,activation='softmax')(x)
model = Model(inputs=base_model.input,outputs=predictions)
# plot_model(model,'tlmodel.png')

構建完新模型後需要進行模型的配置。下面的兩個函式分別對transfer learning和fine tune兩種方法分別進行了配置。每個函式有兩個引數,分別是model和base_model。這裡可能會有同學有疑問,上面定義了model,這裡又將base_model一起做配置,對base_model的更改會對model產生影響麼?
答案是會的。如果你debug追進去看的話,可以看到model的第一層和base_model的第一層是指向同一個記憶體地址的。這裡將base_model作為引數,只是為了方便對骨架模型進行設定。

setup_to_transfer_learning: 這個函式將骨架模型的所有層都設定為不可訓練
setup_to_fine_tune:這個函式將骨架模型中的前幾層設定為不可訓練,後面的所有Inception模組都設定為可訓練。
這裡面的GAP_LAYER需要配合列印圖和除錯的方法確認正確的值,感興趣具體怎麼操作的同學,可以私信我,以後看有沒有必要把這個點寫成教程。GAP_LAYER的確定方法已在文末更新。

'''
這裡的base_model和model裡面的iv3都指向同一個地址
'''
def setup_to_transfer_learning(model,base_model):#base_model
    for layer in base_model.layers:
        layer.trainable = False
    model.compile(optimizer='adam',loss='categorical_crossentropy',metrics=['accuracy'])

def setup_to_fine_tune(model,base_model):
    GAP_LAYER = 17 # max_pooling_2d_2
    for layer in base_model.layers[:GAP_LAYER+1]:
        layer.trainable = False
    for layer in base_model.layers[GAP_LAYER+1:]:
        layer.trainable = True
    model.compile(optimizer=Adagrad(lr=0.0001),loss='categorical_crossentropy',metrics=['accuracy'])

下面開始訓練,這段程式碼也演示瞭如何在全部訓練過程中改變模型。

setup_to_transfer_learning(model,base_model)
history_tl = model.fit_generator(generator=train_generator,
                    steps_per_epoch=800,#800
                    epochs=2,#2
                    validation_data=val_generator,
                    validation_steps=12,#12
                    class_weight='auto'
                    )
model.save('./flowers17_iv3_tl.h5')
setup_to_fine_tune(model,base_model)
history_ft = model.fit_generator(generator=train_generator,
                                 steps_per_epoch=800,
                                 epochs=2,
                                 validation_data=val_generator,
                                 validation_steps=1,
                                 class_weight='auto')
model.save('./flowers17_iv3_ft.h5')
Epoch 1/2
800/800 [==============================] - 2249s 3s/step - loss: 0.1890 - acc: 0.9436 - val_loss: 0.4528 - val_acc: 0.8750
Epoch 2/2
800/800 [==============================] - 2243s 3s/step - loss: 0.0586 - acc: 0.9814 - val_loss: 0.4786 - val_acc: 0.8910
Epoch 1/2
800/800 [==============================] - 2377s 3s/step - loss: 0.0043 - acc: 0.9989 - val_loss: 0.1643 - val_acc: 0.9531
Epoch 2/2
800/800 [==============================] - 2317s 3s/step - loss: 0.0018 - acc: 0.9995 - val_loss: 0.1179 - val_acc: 0.9688

可以看到經過兩個epoch的transfer learning後,驗證集準確率達到89.1%。再經過兩個epoch的fine tune後驗證集準確率達96.88%。可以看到遷移學習的效果還是很好的。

============
2018年08月26日 更新
很多朋友都在留言區問怎麼樣來找GAP_LAYER,我就簡單的來說一下。

首先使用plot_model函式來列印一份整個網路的結構圖,方便觀察。
這裡寫圖片描述
這張圖是Keras內建Inception網路的起始部分。
我們在第二部分做fine tune的時候想訓練所有的Inception模組,之前的一些基礎層引數不去動。所以我們要保證max_pooling_2d_2這一層以及之前層的引數都是不可訓練的。也就是max_pooling_2d_2這一層我們稱之為GAP_LAYER。那麼接下來的任務就很簡單了,只要找到base_model.layers中max_pooling_2d_2的索引即可。

為了方便檢視,我們直接用debug模式進行除錯,檢視base_model.layers中的值。找到MaxPooling2D物件進行檢視name的值是否是max_pooling_2d_2。
這裡寫圖片描述
很容易的查到了017號元素就是我們想要的層。
這裡寫圖片描述
至此GAP_LAYER值就找到了。
以此類推,任何一個模型的任何一個你想要的GAP_LAYER層都可以用這個方法找到。

文中debug模式用的是Pycharm IDE

============

總結

  1. 學習了兩種常用遷移學習方法(tranfer learning,fine tune)及訓練技巧
  2. 學習了使用自己的資料樣本進行訓練
  3. 學習了載入Keras預置的經典模型
  4. 學習瞭如何在預置模型頂部新增新的層
  5. 學習瞭如何設定層的引數為不可訓練

本節課也是用Keras做CNN的一個完結,基本上涵蓋了從定義最簡單CNN到使用現成模型進行自己資料的遷移學習。下一階段看情況可能會出一個RNN系列的入門課。

參考