TensorFlow從1到2(九)遷移學習

遷移學習基本概念

遷移學習是這兩年比較火的一個話題,主要緣由是在當前的機器學習中,樣本數據的獲取是成本最高的一塊。而遷移學習能夠有效的把原有的學習經驗(對於模型就是模型自己及其訓練好的權重值)帶入到新的領域,從而不須要過多的樣本數據,也能達到大批量數據所達成的效果,進一步節省了學習的計算量和時間。python

MobileNet V2是由谷歌在2018年初發布的一個視覺模型,在Keras中已經內置的並使用ImageNet完成了訓練,能夠直接拿來就用,這個咱們在本系列第五篇中已經提過了。MobileNet V2有輕量和高效的特色。經過Inverted residual block結構,能夠用較少的運算量獲得較高的精度,很適合移動端的機器學習需求。論文在這裏
在ImageNet數據集上,MobileNet V2能達到92.5%的識別正確率。本篇中,咱們以此模型爲基礎,介紹一個典型的遷移學習實現方法。並經過調整模型完成優化。bash

問題描述

MobileNet V2本來是識別圖片中主題的名稱。在ImageNet中,有大量的通過標註的照片,標註的信息也很是詳細。好比咱們熟悉的貓貓、狗狗,ImageNet並不簡單的標註爲cat或者dog,而是更詳細的標註爲加菲、德牧這樣精確到具體品種的信息。
咱們這裏使用調整以後的MobileNet V2模型,用於對圖片內容的貓貓和狗狗分類。不去管本來是哪一種具體的品種,只分紅cat/dog兩個大類。
這個問題的描述實際上隱藏了兩個重點:網絡

  • 遷移學習並非無限制、隨意實現的。原有學習數據和數據的場景,同當前的問題,是有共同點、可借鑑可遷移的。
  • 把cat、dog的具體品種忽略,簡單的分紅兩類,並不能認爲就是把問題簡化了。要知道人工智能並非人,舉例來講,其實機器學習模型本身,並不知道「藏獒」跟「狗」之間有什麼關係。在機器學習的模型中,會認爲這是兩個不一樣的分類。

從簡單開始,先展現一下識別

如同本系列第五篇同樣,先使用最簡短的代碼熟悉一下MobileNet V2。
咱們這個例子所使用的數據,是使用tensorflow_datasets模塊來自動下載、解壓、管理的。因此請先安裝這個擴展包:app

$ pip3 install tfds-nightly

程序在第一次運行的時候,會自動下載微軟的實驗數據集。請儘可能使用程序自動下載,由於下載以後會自動解壓。數據集的保存路徑爲:「~/tensorflow_datasets/」,這個是tensorflow_datasets默認的。
數據集中是隨機尺寸的圖片,程序第一步會將圖片統一到224x224的尺寸,這個是預置的MobileNet V2模型所決定的。
咱們從樣本中取頭兩個圖片顯示在屏幕上,而且使用模型預測圖片內容。請參考源代碼中的註釋閱讀程序,也能夠對照一下第五篇的vgg-19/ResNet50模型預測圖片內容的程序,這些模型的使用方法幾乎是相同的。機器學習

#!/usr/bin/env python3

# 引入所使用到的擴展庫
from __future__ import absolute_import, division, print_function
import os
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import tensorflow_datasets as tfds

keras = tf.keras

# 載入訓練數據,載入時按照80%:10%:10%的比例拆分爲訓練、驗證、測試三個數據集
# 本程序只是演示識別圖片,區分爲三類並無直接意義,但下面的程序訓練模型會使用到
SPLIT_WEIGHTS = (8, 1, 1)
splits = tfds.Split.TRAIN.subsplit(weighted=SPLIT_WEIGHTS)
(raw_train, raw_validation, raw_test), metadata = tfds.load(
    'cats_vs_dogs', split=list(splits),
    with_info=True, as_supervised=True)
# 圖片標註
get_label_name = metadata.features['label'].int2str

# 顯示頭兩幅圖片
i = 1
plt.figure('Dog & Cat', figsize=(8, 4))
for image, label in raw_train.take(2):
    plt.subplot(1, 2, i)
    plt.imshow(image)
    plt.title(get_label_name(label))
    i += 1
plt.show()

# 全部圖片轉爲224x224的尺寸,適應模型的要求
IMG_SIZE = 224
def format_example(image, label):
    image = tf.cast(image, tf.float32)
    image = (image/127.5) - 1
    image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))
    return image, label
train = raw_train.map(format_example)

# 載入模型,第一次執行會下載h5文件
# 預測和遷移學習所用的模型並不徹底相同,因此會下載兩次
test_model = tf.keras.applications.MobileNetV2(weights='imagenet')

# 預測頭兩張照片並顯示結果
for image, _ in train.take(2):
    img = np.expand_dims(image, axis=0)
    predict_class = test_model.predict(img)
    desc = tf.keras.applications.mobilenet_v2.decode_predictions(predict_class, top=3)
    print(desc[0])

咱們執行程序看看預測結果:性能

$ ./cats_dogs_1predict.py
[('n02089078', 'black-and-tan_coonhound', 0.46141574), ('n02105412', 'kelpie', 0.15314996), ('n02106550', 'Rottweiler', 0.092713624)]
[('n02123045', 'tabby', 0.29928064), ('n02123159', 'tiger_cat', 0.08147916), ('n02096177', 'cairn', 0.047330838)]


程序準確的預測出告終果。學習

遷移學習改造

咱們進行貓、狗分類訓練,先了解一下樣本集。樣本集的圖片沒有什麼區別,剛纔咱們見到了。標註則很是簡單,就是1(表明Dog)或者0(表明Cat)。上面的程序中,咱們使用get_label_name(label)將數字轉換爲可讀的字符串。這個標註比ImageNet要簡單的多。
MobileNet V2模型默認是將圖片分類到1000類,每一類都有各自的標註。由於本問題分類只有兩類,因此在代碼上,咱們構建模型的時候增長include_top=False參數,表示咱們不須要原有模型中最後的神經網絡層(分類到1000類),以便咱們增長本身的輸出層。固然這樣在第一次執行程序的時候,須要從新下載另一個不包含top層的h5模型數據文件。
隨後咱們在原有模型的後面增長一個池化層,對數據降維。最後是一個1個節點的輸出層,由於咱們須要的結果只有兩類。
到了遷移學習的重點了,咱們的基礎模型的各項參數變量,咱們並不想改變,由於這樣才能保留原來大規模訓練的優點,從而保留原來的經驗。咱們在程序中使用model.trainable = False,設置在訓練中,基礎模型的各項參數變量不會被新的訓練修改數據。
接着咱們把數據集分爲訓練、驗證、測試三個數據集,用測試集數據和驗證集數據對新的模型進行訓練和過程驗證。隨後對完成訓練的模型,使用測試集數據進行評估。
請看源代碼:測試

#!/usr/bin/env python3

# 引入所使用到的擴展庫
from __future__ import absolute_import, division, print_function
import os
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import tensorflow_datasets as tfds

keras = tf.keras

# 載入訓練數據,載入時按照80%:10%:10%的比例拆分爲訓練、驗證、測試三個數據集
SPLIT_WEIGHTS = (8, 1, 1)
splits = tfds.Split.TRAIN.subsplit(weighted=SPLIT_WEIGHTS)
(raw_train, raw_validation, raw_test), metadata = tfds.load(
    'cats_vs_dogs', split=list(splits),
    with_info=True, as_supervised=True)

# cat/dog兩類圖片,複雜度要低於1000類的圖片,因此分辨率能夠略低,這樣也能節省訓練時間
# 全部圖片從新調整爲160x160點陣
IMG_SIZE = 160
def format_example(image, label):
    image = tf.cast(image, tf.float32)
    # 數據規範化爲-1到+1的浮點數
    image = (image/127.5) - 1
    image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))
    return image, label

train = raw_train.map(format_example)
validation = raw_validation.map(format_example)
test = raw_test.map(format_example)

BATCH_SIZE = 32
SHUFFLE_BUFFER_SIZE = 1000
train_batches = train.shuffle(SHUFFLE_BUFFER_SIZE).batch(BATCH_SIZE)
validation_batches = validation.batch(BATCH_SIZE)
test_batches = test.batch(BATCH_SIZE)

# 輸入形狀就是160x160x3,最後3爲RGB3字節色
IMG_SHAPE = (IMG_SIZE, IMG_SIZE, 3)
base_model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,
                                               include_top=False,  # 使用不包含原有1000類輸出層的模型
                                               weights='imagenet')
# 設置基礎模型:MobileNetV2的各項權重參數不會被訓練所更改
base_model.trainable = False
# 輸出模型彙總信息
base_model.summary()

# 增長輸出池化層
global_average_layer = tf.keras.layers.GlobalAveragePooling2D()

# 輸出層
prediction_layer = keras.layers.Dense(1)
# 定義最終完整的模型
model = tf.keras.Sequential([
    base_model,
    global_average_layer,
    prediction_layer
])
# 學習梯度
base_learning_rate = 0.0001
# 編譯模型
model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
              loss='binary_crossentropy',
              metrics=['accuracy'])

# 各部分數據數量
num_train, num_val, num_test = (
  metadata.splits['train'].num_examples*weight/10
  for weight in SPLIT_WEIGHTS
)
# 迭代次數
initial_epochs = 10
steps_per_epoch = round(num_train)//BATCH_SIZE
# 驗證和評估次數
validation_steps = 20

# 顯示一下未經訓練的初始模型評估結果
loss0, accuracy0 = model.evaluate(test_batches, steps=validation_steps)
print("initial loss: {:.2f}".format(loss0))
print("initial accuracy: {:.2f}".format(accuracy0))

# 訓練
history = model.fit(train_batches.repeat(),
                    epochs=initial_epochs,
                    steps_per_epoch=steps_per_epoch,
                    validation_data=validation_batches.repeat(), 
                    validation_steps=validation_steps)
# 評估
loss0, accuracy0 = model.evaluate(test_batches, steps=validation_steps)
print("Train1ed loss: {:.2f}".format(loss0))
print("Train1ed accuracy: {:.2f}".format(accuracy0))

由於數據集比較大,模型也比較複雜,因此程序執行起來時間很長。最終的評估結果相似爲:優化

Train1ed loss: 0.38
Train1ed accuracy: 0.95

若是不對比來看,這個結果還不錯啦。不過準確率比原來對更爲複雜的ImageNet數據集的評估值還要低不少,顯然還有很大的優化空間。人工智能

模型優化

在整個模型中,咱們本身增長的部分不多,優化的餘地並很少。考慮到原有ImageNet圖片庫的樣本,大多並不是貓和狗。因此徹底保留原有的模型參數可能對MobileNet V2來說也是資源上的浪費。
所以咱們採起的優化策略就是開放一部分模型的網絡層,容許在進一步的訓練中,調整其權重值,從而指望更好的結果。
在MobileNet V2模型中,一共有155層卷積或者神經網絡。這個值可使用len(model.layers)來查看。咱們仍然鎖定前面的100層,把後面的網絡層打開,容許訓練修改其參數。
在隨後新模型的訓練中,也不須要所有重頭開始訓練,model.fit方法中,能夠指定initial_epoch參數,接着前面的訓練繼續進行。
請看源代碼:

...繼續前面的代碼最後輸入...
# 打開容許修改基礎模型的權重參數
base_model.trainable = True
# 仍然鎖死前100層的權重不容許修改
fine_tune_at = 100
# 凍結fine_tune_at以前的層
for layer in base_model.layers[:fine_tune_at]:
    layer.trainable = False
# 從新編譯模型
model.compile(loss='binary_crossentropy',
              optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate/10),
              metrics=['accuracy'])
# 輸出模型的概況,注意觀察有些層被鎖定,有些層能夠更改
model.summary()
# 在前次訓練的基礎上再訓練10次
fine_tune_epochs = 10
total_epochs = initial_epochs + fine_tune_epochs
# 訓練模型
history_fine = model.fit(train_batches.repeat(), 
                         steps_per_epoch=steps_per_epoch,
                         epochs=total_epochs, 
                         initial_epoch=initial_epochs,
                         validation_data=validation_batches.repeat(), 
                         validation_steps=validation_steps)
# 評估新模型
loss0, accuracy0 = model.evaluate(test_batches, steps=validation_steps)
print("final loss: {:.2f}".format(loss0))
print("final accuracy: {:.2f}".format(accuracy0))

執行優化後的程序,其中model.summary()的輸出信息:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
mobilenetv2_1.00_160 (Model) (None, 5, 5, 1280)        2257984
_________________________________________________________________
global_average_pooling2d (Gl (None, 1280)              0
_________________________________________________________________
dense (Dense)                (None, 1)                 1281
=================================================================
Total params: 2,259,265
Trainable params: 1,863,873
Non-trainable params: 395,392

Non-trainable params這一部分,就是指咱們鎖定不參與新數據訓練的參數數量。若是你注意看前面一部分base_model.summary()的輸出,全部的參數都被鎖定不參與訓練。
最後的評估結果爲:

final loss: 0.23
final accuracy: 0.97

看起來雖然略好,但彷佛優化效果並不明顯。
其實這主要是觀察數值不夠直觀形成的,咱們繼續爲程序增長繪圖功能:

...繼續前面的代碼最後輸入...
# 優化前訓練迭代數據
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']

# 優化後訓練迭代數據
acc += history_fine.history['accuracy']
val_acc += history_fine.history['val_accuracy']

loss += history_fine.history['loss']
val_loss += history_fine.history['val_loss']

# 使用訓練數據繪圖
plt.figure(figsize=(8, 8))
plt.subplot(2, 1, 1)
plt.plot(acc, label='Training Accuracy')
plt.plot(val_acc, label='Validation Accuracy')
plt.ylim([0.8, 1])
plt.plot([initial_epochs-1,initial_epochs-1],
          plt.ylim(), label='Start Fine Tuning')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')

plt.subplot(2, 1, 2)
plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
plt.ylim([0, 1.0])
plt.plot([initial_epochs-1,initial_epochs-1],
         plt.ylim(), label='Start Fine Tuning')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.xlabel('epoch')
plt.show()

咱們爲了講解方便,把最終的程序分紅了幾個部分,實際上爲了節省時間,是能夠合併在一塊兒執行的,這樣不須要重複訓練不少次。
從繪圖結果看,優化的效果仍是很明顯的:
兩張圖,中間都有一條綠線分隔開優化前和優化後的訓練數據。在前半段,正確率和損失值的優化過程是明顯比較慢的,並且訓練集和驗證集兩條線的分離也說明有過擬合的現象。在後半段,有一個明顯的階梯表現出來模型性能明顯改善,訓練集和驗證集也更接近。說明各項指標都有效改善了。 (待續...)

相關文章
相關標籤/搜索