Categories
程式開發

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南


遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 1

閱讀深度學習論文總是很有趣,也很有教育意義,特別是當這些論文和你現在做的項目屬於同一領域時更是如此。但是,這些論文包含的架構和解決方案通常很難訓練,特別是當你想去嘗試他們的方法時,比如說ILSCVR(ImageNet Large Scale Visual Recognition)競賽中的一些獲獎者的方法。我記得我在讀VGG16的論文時就在想“這個方法很酷,但是我的GPU跑這個網絡時都快掛了。”為了能輕鬆使用這些網絡,Tensorflow 2提供了大量的預訓練模型,你可以很快用上它們。而本文,我們將介紹怎樣通過一些有名的CNN(Convolutional Neural Network)架構來訓練這些論文裡介紹的新的神經網絡模型。

這時你可能會問“預訓練模型是什麼?”。本質上來說,預訓練模型是之前在大數據集上已經訓練好並保存下來的模型,比如說在ImageNet數據集上訓練的模型。這些模型可以在tensorflow.keras.applications模塊裡找到。有兩種方式使用這些預訓練模型,你可以直接使用它們,或者通過遷移學習使用它們。由於大數據集通常用於某種全局解,所以你可以讓預訓練模型定制化,使其特別針對性地解決某個特定的問題。通過這個方式,你可以在訓練時利用一些最有名的神經網絡,不會損失太多的訓練時間和計算資源。另外,你可以選定網絡裡的一些層,修改這些層的行為,實現這些模型的微調。我們在後面的文章裡會講到這一點。

架構

在本文中,我們使用3個預訓練模型來解決分類問題的一個例子:VGG16、GoogLeNet(Inception)和ResNet。這每一個架構都贏得了當年的ILSCVR競賽。 2014年,VGG16與GooLeNet有著相同的最好成績,而ResNet贏得了2015年的競賽。這些模型是Tensorflow 2中tensorflow.keras.applications模塊的一部分。讓我們深入探究一下這幾個模型。

我們首先看一下VGG16這個架構。它是一個大型的卷積神經網絡,由K. Simonyan和A. Zisserman在“Very Deep Convolutional Networks for Large-Scale Image Recognition”這篇論文裡提出。這個網絡在ImageNet數據集上達到了92.7%的top-5測試精確度。但是,訓練這個網絡需要好幾週。下圖是這個模型的高層概覽:

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 2

VGG16架構

GoogLeNet也被稱為Inception,這是因為它使用了兩個概念:1×1卷積和Inception模塊。第一個概念中,1×1卷積用於降維的模塊。通過降維,計算量也會減少,這也就意味著網絡的深度和寬度可以增加了。 GooLeNet使用了Inception模塊,每個卷積層的大小都不相同。

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 3

帶有降維功能的Inception模塊

如圖所示,1×1卷積層、3×3卷積層、5×5卷積層和3×3最大池化層操作組合在了一起,然後這些層的運行結果會在輸出節點處堆疊在一起。 GooLeNet總共有22層,看起來像下面這樣:

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 4

本文中,我們要使用的最後一個網絡架構是殘差網絡,或者稱作ResNet。前面提到的網絡的問題在於它們太深了,它們有太多層,導致很難訓練(因為梯度消失)。所以,ResNet使用所謂的“identity shortcut connection”(或者稱作殘差模塊)來解決這個問題。

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 5

帶有降維和不帶降維的殘差模塊

本質上來講,ResNet沿用了VGG的3×3卷積層的設計,每層卷積後面都有一個Batch Normalization層和ReLu激活函數。但是,差異點在於我們的ResNet在最後一個ReLu前插入了input節點。另一個變種是,輸入值(input value)傳入了1×1卷積層。

數據集

在本文中,我們使用“Cats vs Dogs”的數據集。這個數據集包含了23,262張貓和狗的圖像。

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 6

你可能注意到了,這些照片沒有歸一化,它們的大小是不一樣的。但是非常棒的一點是,你可以在Tensorflow Datasets中獲取這個數據集。所以,確保你的環境里安裝了Tensorflow Dataset。

pip install tensorflow-dataset

和這個庫中的其他數據集不同,這個數據集沒有劃分成訓練集和測試集,所以我們需要自己對這兩類數據集做個區分。你可以在這裡找到這個數據集的更多信息。

實現

這個實現分成了幾個部分。首先,我們實現了一個類,其負責載入數據和準備數據。然後,我們導入預訓練模型,構建一個用於修改最頂端的幾層網絡。最後,我們把訓練過程運行起來,並進行評估。當然,在這之前,我們必須導入一些代碼庫,定義一些全局常量:

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf
import tensorflow_datasets as tfds

IMG_SIZE = 160
BATCH_SIZE = 32
SHUFFLE_SIZE = 1000
IMG_SHAPE = (IMG_SIZE, IMG_SIZE, 3)

好,讓我們仔細來看下實現!

數據載入器

這個類負責載入數據和準備數據,用於後續的數據處理。以下是這個類的實現:

class DataLoader(object):
    def __init__(self, image_size, batch_size):
        
        self.image_size = image_size
        self.batch_size = batch_size
        
        # 80% train data, 10% validation data, 10% test data
        split_weights = (8, 1, 1)
        splits = tfds.Split.TRAIN.subsplit(weighted=split_weights)
        
        (self.train_data_raw, self.validation_data_raw, self.test_data_raw), self.metadata = tfds.load(
            'cats_vs_dogs', split=list(splits),
            with_info=True, as_supervised=True)
        
        # Get the number of train examples
        self.num_train_examples = self.metadata.splits('train').num_examples*80/100
        self.get_label_name = self.metadata.features('label').int2str
        
        # Pre-process data
        self._prepare_data()
        self._prepare_batches()
        
    # Resize all images to image_size x image_size
    def _prepare_data(self):
        self.train_data = self.train_data_raw.map(self._resize_sample)
        self.validation_data = self.validation_data_raw.map(self._resize_sample)
        self.test_data = self.test_data_raw.map(self._resize_sample)
    
    # Resize one image to image_size x image_size
    def _resize_sample(self, image, label):
        image = tf.cast(image, tf.float32)
        image = (image/127.5) - 1
        image = tf.image.resize(image, (self.image_size, self.image_size))
        return image, label
    
    def _prepare_batches(self):
        self.train_batches = self.train_data.shuffle(1000).batch(self.batch_size)
        self.validation_batches = self.validation_data.batch(self.batch_size)
        self.test_batches = self.test_data.batch(self.batch_size)
   
    # Get defined number of  not processed images
    def get_random_raw_images(self, num_of_images):
        random_train_raw_data = self.train_data_raw.shuffle(1000)
        return random_train_raw_data.take(num_of_images)

這個類實現了很多功能,它實現了很多“public”方法

  • _prepare_data:內部方法,用於縮放和歸一化數據集裡的圖像。構造函數需要用到該函數。
  • _resize_sample:內部方法,用於縮放單張圖像。
  • _prepare_batches:內部方法,用於將圖像打包創建為batches。創建train_batches、validation_batches和test_batches,分別用於訓練、評估過程。
  • get_random_raw_images:這個方法用於從原始的、沒有經過處理的數據中隨機獲取固定數量的圖像。

但是,這個類的主要功能還是在構造函數中完成的。讓我們仔細看看這個類的構造函數。

def __init__(self, image_size, batch_size):

    self.image_size = image_size
    self.batch_size = batch_size

    # 80% train data, 10% validation data, 10% test data
    split_weights = (8, 1, 1)
    splits = tfds.Split.TRAIN.subsplit(weighted=split_weights)

    (self.train_data_raw, self.validation_data_raw, self.test_data_raw), self.metadata = tfds.load(
        'cats_vs_dogs', split=list(splits),
        with_info=True, as_supervised=True)

    # Get the number of train examples
    self.num_train_examples = self.metadata.splits('train').num_examples*80/100
    self.get_label_name = self.metadata.features('label').int2str

    # Pre-process data
    self._prepare_data()
    self._prepare_batches()

首先我們通過傳入參數定義了圖像大小和batch大小。然後,由於該數據集本身沒有區分訓練集和測試集,我們通過劃分權值對數據進行劃分。這真是Tensorflow Dataset引入的非常棒的功能,因為我們可以留在Tensorflow生態系統中做這件事,我們不用引入其他的庫(比如Pandas或者Scikit Learn)。一旦我們執行了數據劃分,我們就開始計算訓練樣本數量,然後調用輔助函數來為訓練準備數據。在這之後,我們需要做的僅僅是實例化這個類的對象,然後載入數據即可。

data_loader = DataLoader(IMG_SIZE, BATCH_SIZE)

plt.figure(figsize=(10, 8))
i = 0
for img, label in data_loader.get_random_raw_images(20):
    plt.subplot(4, 5, i+1)
    plt.imshow(img)
    plt.title("{} - {}".format(data_loader.get_label_name(label), img.shape))
    plt.xticks(())
    plt.yticks(())
    i += 1
plt.tight_layout()
plt.show()

以下是輸出結果:

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 6

基礎模型 & Wrapper

下一個步驟就是載入預訓練模型了。我們前面提到過,這些模型位於tensorflow.kearas.applications。我們可以用下面的語句直接載入它們:

vgg16_base = tf.keras.applications.VGG16(input_shape=IMG_SHAPE, include_top=False, weights='imagenet')
googlenet_base = tf.keras.applications.InceptionV3(input_shape=IMG_SHAPE, include_top=False, weights='imagenet')
resnet_base = tf.keras.applications.ResNet101V2(input_shape=IMG_SHAPE, include_top=False, weights='imagenet')

這段代碼就是我們創建上述三種網絡結構基礎模型的方式。注意,每個模型構造函數的include_top參數傳入的是false。這意味著這些模型是用於提取特徵的。我們一旦創建了這些模型,我們就需要修改這些模型頂部的網絡層,使之適用於我們的具體問題。我們使用Wrapper類來完成這個步驟。這個類接收預訓練模型,然後添加一個Global Average Polling Layer和一個Dense Layer。本質上,這最後的Dense Layer會用於我們的二分類問題(貓或狗)。 Wrapper類把所有這些元素都放到了一起,放在了同一個模型中。

class Wrapper(tf.keras.Model):
    def __init__(self, base_model):
        super(Wrapper, self).__init__()
        
        self.base_model = base_model
        self.average_pooling_layer = tf.keras.layers.GlobalAveragePooling2D()
        self.output_layer = tf.keras.layers.Dense(1)
        
    def call(self, inputs):
        x = self.base_model(inputs)
        x = self.average_pooling_layer(x)
        output = self.output_layer(x)
        return output

然後我們就可以創建Cats vs Dogs分類問題的模型了,並且編譯這個模型。

base_learning_rate = 0.0001

vgg16_base.trainable = False
vgg16 = Wrapper(vgg16_base)
vgg16.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
              loss='binary_crossentropy',
              metrics=('accuracy'))

googlenet_base.trainable = False
googlenet = Wrapper(googlenet_base)
googlenet.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
              loss='binary_crossentropy',
              metrics=('accuracy'))

resnet_base.trainable = False
resnet = Wrapper(resnet_base)
resnet.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
              loss='binary_crossentropy',
              metrics=('accuracy'))

注意,我們標記了基礎模型是不參與訓練的,這意味著在訓練過程中,我們只會訓練新添加到頂部的網絡層,而在網絡底部的權重值不會發生變化。

訓練

在我們開始整個訓練過程之前,讓我們思考一下,這些模型的大部頭其實已經被訓練過了。所以,我們可以執行評估過程來看看評估結果如何:

steps_per_epoch = round(data_loader.num_train_examples)//BATCH_SIZE
validation_steps = 20

loss1, accuracy1 = vgg16.evaluate(data_loader.validation_batches, steps = 20)
loss2, accuracy2 = googlenet.evaluate(data_loader.validation_batches, steps = 20)
loss3, accuracy3 = resnet.evaluate(data_loader.validation_batches, steps = 20)

print("--------VGG16---------")
print("Initial loss: {:.2f}".format(loss1))
print("Initial accuracy: {:.2f}".format(accuracy1))
print("---------------------------")

print("--------GoogLeNet---------")
print("Initial loss: {:.2f}".format(loss2))
print("Initial accuracy: {:.2f}".format(accuracy2))
print("---------------------------")

print("--------ResNet---------")
print("Initial loss: {:.2f}".format(loss3))
print("Initial accuracy: {:.2f}".format(accuracy3))
print("---------------------------")

有意思的是,這些模型在沒有預先訓練的情況下,我們得到的結果也還過得去(50%的精確度):

———VGG16———
Initial loss: 5.30
Initial accuracy: 0.51
—————————-

——GoogLeNet—–
Initial loss: 7.21
Initial accuracy: 0.51
—————————-

——–ResNet———
Initial loss: 6.01
Initial accuracy: 0.51
—————————-

把50%作為訓練的起點已經挺好的了。所以,就讓我們把訓練過程跑起來吧,看看我們是否能得到更好的結果。首先,我們訓練VGG16:

history = vgg16.fit(data_loader.train_batches,
                    epochs=10,
                    validation_data=data_loader.validation_batches)

訓練過程歷史數據顯示大致如下:

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 8

VGG16的訓練過程歷史數據

然後我們可以訓練GoogLeNet。

history = googlenet.fit(data_loader.train_batches,
                    epochs=10,
                    validation_data=data_loader.validation_batches)

這個網絡訓練過程歷史數據如下:

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 9

GoogLeNet的訓練過程歷史數據

最後是ResNet的訓練:

history = resnet.fit(data_loader.train_batches,
                    epochs=10,
                    validation_data=data_loader.validation_batches)

以下是ResNet訓練過程歷史數據如下:

遷移學習沒有那麼難:TensorFlow 2.0預訓練模型實踐指南 10

ResNet的訓練過程歷史數據

由於我們只訓練了頂部的幾層網絡,而不是整個網絡,所以訓練這三個模型只用了幾個小時,而不是幾個星期。

評估

我們看到在訓練開始前,我們已經有了50%左右的精確度。讓我們來看下訓練後是什麼情況:

loss1, accuracy1 = vgg16.evaluate(data_loader.test_batches, steps = 20)
loss2, accuracy2 = googlenet.evaluate(data_loader.test_batches, steps = 20)
loss3, accuracy3 = resnet.evaluate(data_loader.test_batches, steps = 20)

print("--------VGG16---------")
print("Loss: {:.2f}".format(loss1))
print("Accuracy: {:.2f}".format(accuracy1))
print("---------------------------")

print("--------GoogLeNet---------")
print("Loss: {:.2f}".format(loss2))
print("Accuracy: {:.2f}".format(accuracy2))
print("---------------------------")

print("--------ResNet---------")
print("Loss: {:.2f}".format(loss3))
print("Accuracy: {:.2f}".format(accuracy3))
print("---------------------------")

結果如下:

——–VGG16———
Loss: 0.25
Accuracy: 0.93
—————————

——–GoogLeNet———
Loss: 0.54
Accuracy: 0.95
—————————
——–ResNet———
Loss: 0.40
Accuracy: 0.97
—————————

我們可以看到這三個模型的結果都相當好,其中ResNet效果最好,精確度高達97%。

結論

在本文中,我們演示了怎樣使用Tensorflow進行遷移學習。我們創建了一個試驗場,在其中可以嘗試不同的數據預訓練架構,並且在幾個小時內就能得到較好的結果。在我們的例子裡,我們使用了三個很有名的捲積架構,快速將其修改用於具體的問題。在下篇文章中,我們將微調這些模型,來看看我們是否能得到更好的結果。

原文鏈接:

https://rubikscode.net/2019/11/11/transfer-learning-with-tensorflow-2/