閱讀深度學習論文總是很有趣,也很有教育意義,特別是當這些論文和你現在做的項目屬於同一領域時更是如此。但是,這些論文包含的架構和解決方案通常很難訓練,特別是當你想去嘗試他們的方法時,比如說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測試精確度。但是,訓練這個網絡需要好幾周。下圖是這個模型的高層概覽:
GoogLeNet也被稱爲Inception,這是因爲它使用了兩個概念:1x1卷積和Inception模塊。第一個概念中,1x1卷積用於降維的模塊。通過降維,計算量也會減少,這也就意味着網絡的深度和寬度可以增加了。GooLeNet使用了Inception模塊,每個卷積層的大小都不相同。
如圖所示,1x1卷積層、3x3卷積層、5x5卷積層和3x3最大池化層操作組合在了一起,然後這些層的運行結果會在輸出節點處堆疊在一起。GooLeNet總共有22層,看起來像下面這樣:
本文中,我們要使用的最後一個網絡架構是殘差網絡,或者稱作ResNet。前面提到的網絡的問題在於它們太深了,它們有太多層,導致很難訓練(因爲梯度消失)。所以,ResNet使用所謂的“identity shortcut connection”(或者稱作殘差模塊)來解決這個問題。
本質上來講,ResNet沿用了VGG的3x3卷積層的設計,每層卷積後面都有一個Batch Normalization層和ReLu激活函數。但是,差異點在於我們的ResNet在最後一個ReLu前插入了input節點。另一個變種是,輸入值(input value)傳入了1x1卷積層。
數據集
在本文中,我們使用“Cats vs Dogs”的數據集。這個數據集包含了23,262張貓和狗的圖像。
你可能注意到了,這些照片沒有歸一化,它們的大小是不一樣的。但是非常棒的一點是,你可以在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()
以下是輸出結果:
基礎模型 & 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)
訓練過程歷史數據顯示大致如下:
然後我們可以訓練GoogLeNet。
history = googlenet.fit(data_loader.train_batches,
epochs=10,
validation_data=data_loader.validation_batches)
這個網絡訓練過程歷史數據如下:
最後是ResNet的訓練:
history = resnet.fit(data_loader.train_batches,
epochs=10,
validation_data=data_loader.validation_batches)
以下是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/