「動手學計算機視覺」第十七講:卷積神經網絡之VGG

更多精彩內容,請關注公眾號【平凡而詩意】,或者收藏我的個人主頁https://jackpopc.github.io/  

前言

「動手學計算機視覺」第十七講:卷積神經網絡之VGG

2014年對於計算機視覺領域是一個豐收的一年,在這一年的ImageNet圖像識別挑戰賽(ILSVRC,ImageNet Large Scale Visual Recognition Challenge)中出現了兩個經典、響至深的卷積神經網絡模型,其中第一名是GoogLeNet、第二名是VGG,都可以稱得上是深度計算機視覺發展過程中的經典之作。

雖然在名次上GoogLeNet蓋過了VGG,但是在可遷移性方面GoogLeNet對比於VGG卻有很大的差距,而且在模型構建思想方面對比於它之前的AlexNet、LeNet做出了很大的改進,因此,VGG後來常作為後續卷積神經網絡模型的基礎模塊,用於特徵提取。直到5年後的今天,依然可以在很多新穎的CNN模型中可以見到VGG的身影,本文就來詳細介紹一下這個經典的卷積神經網絡模型。

VGG模型

「動手學計算機視覺」第十七講:卷積神經網絡之VGG

VGG(VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION),是由牛津大學的研究者提出,它的名稱也是以作者所在實驗室而命名(Visual Geometry Group)。

前一篇文章介紹了經典的AlexNet,雖然它在識別效果方面非常令人驚豔,但是這些都是建立在對超參數進行大量的調整的基礎上,而它並沒有提出一種明確的模型設計規則以便指導後續的新網絡模型設計,這也限制了它的遷移能力。因此,雖然它很知名,但是在近幾年的模型基礎框架卻很少出現AlexNet的身影,反觀VGG則成為了很多新模型基礎框架的必選項之一,這也是它相對於AlexNet的優勢之一:VGG提出用基礎塊代替網絡層的思想,這使得它在構建深度網絡模型時可以重複使用這些基礎塊。

正如前面所說,VGG使用了塊代替層的思想,具體的來說,它提出了構建基礎的卷積塊和全連接塊來替代卷積層和全連接層,而這裡的塊是由多個輸出通道相同的層組成。

VGG和AlexNet指代單一的模型不同,VGG其實包含多個不同的模型,從上圖可以看出,它主要包括下列模型,

  • VGG-11
  • VGG-13
  • VGG-16
  • VGG-19

其中,後面的數字11、13、16、19是網絡層數。

從圖中可以看出,VGG的特點是每個卷積塊(由1個或多個卷積層組成)後面跟隨一個最大池化層,整體架構和AlexNet非常類似,主要區別就是把層替換成了塊。

從圖中紅框標記可以看出,每個卷積塊中輸出通道數相同,另外從橫向維度來看,不同模型在相同卷積塊中輸出通道也相同。

下面就以比較常用的VGG-16這個模型為例來介紹一下VGG的模型架構。

VGG-16是由5個卷積塊和3個全連接層共8部分組成(回想一下,AlexNet也是由8個部分組成,只不過AlexNet是由5個卷積層和3個全連接層組成),下面詳細介紹每一個部門的詳細情況。

注意:前兩篇文章我們在搭建LeNet和AlexNet時會發現,不同層的卷積核、步長均有差別,這也是遷移過程中比較困難的一點,而在VGG中就沒有這樣的困擾,VGG卷積塊中統一採用的是3*3的卷積核,卷積層的步長均為1,而在池化層窗口大小統一採用

2*2,步長為2。因為每個卷積層、池化層窗口大小、步長都是確定的,因此要搭建VGG我們只需要關注每一層輸入輸出的通道數即可。

卷積塊1

包含2個卷積層,輸入是224*224*3的圖像,輸入通道數為3,輸出通道數為64。

卷積塊2

包含2個卷積層,輸入是上一個卷積塊的輸出,輸入通道數為64,輸出通道數為128。

卷積塊3

包含3個卷積層,輸入是上一個卷積塊的輸出,輸入通道數為128,輸出通道數為256。

卷積塊4

包含3個卷積層,輸入是上一個卷積塊的輸出,輸入通道數為256,輸出通道數為512。

卷積塊5

包含3個卷積層,輸入是上一個卷積塊的輸出,輸入通道數為512,輸出通道數為512。

全連接層1

輸入為上一層的輸出,輸入通道數為前一卷積塊輸出reshape成一維的長度,輸出通道數為4096。

全連接層2

輸入為上一層的輸出,輸入通道數為4096,輸出通道數為4096。

全連接層3

輸入為上一層的輸出,輸入通道數為4096,輸出通道數為1000。

激活函數

VGG中每層使用的激活函數為ReLU激活函數。

由於VGG非常經典,所以,網絡上有關於VGG-16、VGG-19預訓練的權重,為了為了展示一下每一層的架構,讀取VGG-16預訓練權重看一下,

import numpy as np

path = "vgg16.npy"
layers = ["conv1_1", "conv1_2",
"conv2_1", "conv2_2",
"conv3_1", "conv3_2", "conv3_3",
"conv4_1", "conv4_2", "conv4_3",
"conv5_1", "conv5_2", "conv5_3",
"fc6", "fc7", "fc8"]

data_dict = np.load(path, encoding='latin1').item()

for layer in layers:
print(data_dict[layer][0].shape)

# 輸出
(3, 3, 3, 64)
(3, 3, 64, 64)

(3, 3, 64, 128)
(3, 3, 128, 128)
(3, 3, 128, 256)
(3, 3, 256, 256)
(3, 3, 256, 256)
(3, 3, 256, 512)
(3, 3, 512, 512)
(3, 3, 512, 512)
(3, 3, 512, 512)
(3, 3, 512, 512)
(3, 3, 512, 512)
(25088, 4096)
(4096, 4096)
(4096, 1000)

網絡共16層,卷積層部分為1*4維的,其中從前到後分別是卷積核高度、卷積核寬度、輸入數據通道數、輸出數據通道數。

到此為止,應該已經瞭解了VGG的模型結構,下面就開始使用tensorflow編程實現一下 VGG。

編程實踐

因為 VGG非常經典,所以網絡上有VGG的預訓練權重,我們可以直接讀取預訓練的權重去搭建模型,這樣就可以忽略對輸入和輸出通道數的感知,要簡單很多,但是為了更加清楚的理解網絡模型,在這裡還是從最基本的部分開始搭建,自己初始化權重和偏差,這樣能夠更加清楚每層輸入和輸出的結構。

卷積塊

經過前面的介紹應該瞭解,VGG的主要特點就在於卷積塊的使用,因此,我們首先來完成卷積塊部分的編寫。在完成一段代碼的編寫之前,我們應該首先弄明白兩點:輸入和輸出。

輸出當然很明確,就是經過每個卷積塊(多個卷積層)卷積、激活後的tensor,我們要明確的就是應該輸入哪些參數?

最重要的3個輸入:要進行運算的tensor、每個卷積塊內卷積層的個數、輸出通道數。

當然,我們為了更加規範的搭建模型,也需要對每一層規定一個命名空間,這樣還需要輸入每一層的名稱。至於輸入通道數,我們可以通過tensorflow的get_shape函數獲取,

def conv_block(self, X, num_layers, block_index, num_channels):
in_channels = int(X.get_shape()[-1])
for i in range(num_layers):
name = "conv{}_{}".format(block_index, i)
with tf.variable_scope(name) as scope:
weight = tf.get_variable("weight", [3, 3, in_channels, num_channels])
bias = tf.get_variable("bias", [num_channels])
conv = tf.nn.conv2d(X, weight, strides=[1, 1, 1, 1], padding="SAME")
X = tf.nn.relu(tf.nn.bias_add(conv, bias))
in_channels = num_channels
print(X.get_shape())
return X

從代碼中可以看出,有幾個參數是固定的:

  • 卷積窗口大小
  • 步長
  • 填充方式
  • 激活函數

到此為止,我們就完成了VGG最核心一部分的搭建。

池化層

之前看過前兩篇關於AlexNet、LeNet的同學應該記得,池化層有兩個重要的參數:窗口大小、步長。由於在VGG中這兩個超參數是固定的,因此,不用再作為函數的入參,直接寫在代碼中即可。

def max_pool(self, X):
return tf.nn.max_pool(X, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")

全連接層

至於全連接層,和前面介紹的兩個模型沒有什麼區別,我們只需要知道輸出通道數即可,每一層的輸出為上一層的輸出,

def full_connect_layer(self, X, out_filters, name):
in_filters = X.get_shape()[-1]
with tf.variable_scope(name) as scope:
w_fc = tf.get_variable("weight", shape=[in_filters, out_filters])
b_fc = tf.get_variable("bias", shape=[out_filters], trainable=True)
fc = tf.nn.xw_plus_b(X, w_fc, b_fc)
return tf.nn.relu(fc)

由於不同網絡模型之前主要的不同之處就在於模型的結構,至於訓練和驗證過程中需要的準確率、損失函數、優化函數等都大同小異,在前兩篇文章中已經實現了訓練和驗證部分,所以這裡就不再贅述。在本文裡,我使用numpy生成一個隨機的測試集測試一下網絡模型是否搭建成功即可。

測試

首先使用numpy生成符合正態分佈的隨機數,形狀為(5, 224, 224, 3),5為批量數據的大小,244為輸入圖像的尺寸,3為輸入圖像的通道數,設定輸出類別數為1000,

def main():
X = np.random.normal(size=(5, 224, 224, 3))
images = tf.placeholder("float", [5, 224, 224, 3])
vgg = VGG(1000)
writer = tf.summary.FileWriter("logs")
with tf.Session() as sess:
model = vgg.create(images)
sess.run(tf.global_variables_initializer())
writer.add_graph(sess.graph)
prob = sess.run(model, feed_dict={images: X})
print(sess.run(tf.argmax(prob, 1)))

# 輸出
(5, 224, 224, 64)
(5, 224, 224, 64)
(5, 112, 112, 128)
(5, 112, 112, 128)
(5, 56, 56, 256)
(5, 56, 56, 256)
(5, 56, 56, 256)
(5, 28, 28, 512)
(5, 28, 28, 512)
(5, 28, 28, 512)
(5, 14, 14, 512)
(5, 14, 14, 512)
(5, 14, 14, 512)
(5, 4096)
(5, 4096)
(5, 1000)
[862 862 862 862 862]

可以對比看出,每層網絡的尺寸和前面加載的預訓練模型是匹配的,下面在看一下tensorboard的結果,

$ tensorboard --logdir="logs" 

結果,

「動手學計算機視覺」第十七講:卷積神經網絡之VGG

完整代碼

完整代碼請查看github項目aiLearnNotes,也可以直接訪問下面鏈接,

https://github.com/Jackpopc/aiLearnNotes/blob/master/computer_vision/VGG-16.py


分享到:


相關文章: