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

前言

在前一篇文章介紹VGG時,我提到2014年對於計算機視覺領域是一個豐收的一年,在這一年的ImageNet圖像識別挑戰賽(ILSVRC,ImageNet Large Scale Visual Recognition Challenge)中出現了兩個經典、影響至深的卷積神經網絡模型,

其中第一名是GoogLeNet、第二名是VGG。

沒錯,本文的主角就是2014年ILSVRC的第一名--GoogLeNet(Going Deeper with Convolutions),要注意的是,這個網絡模型的名稱是"GoogLeNet",而不是"GoogleNet",雖然只有一個大小寫字母的卻別,含義卻不同,GoogLeNet之所以叫做這個名字,主要是為了想LeNet致敬。

GoogLeNet與VGG出現在同一年,二者自然有一些相似之處,但是兩個模型更多的是差異性。

首先說一下GoogLeNet與VGG的相同之處:

都提出了基礎塊的思想均是為了克服網絡逐漸變深帶來的問題

首先,說一下第一點--都提出了基礎塊的思想

前文已經介紹了,VGG使用代替的思想,這使得VGG在遷移性方面表現非常好,也因此得到了廣泛的應用。而GoogLeNet也使用了基礎塊的思想,它引入了Inception塊,想必說到這裡應該接觸過深度計算機視覺的同學應該恍然大悟,也許對GoogLeNet的概念已經變的模糊,但是Inception卻如雷貫耳,目前在很多CNN模型中同樣作為基礎模塊使用。

其次,說一下第二點--均是為了克服網絡逐漸變深帶來的問題

隨著卷積神經網絡模型的更新換代,我們發現網絡層數逐漸變多,模型變的越來越深,這是因為提升模型效果最為直接有效的方法就是增加網絡深度和寬度,但是,隨著網絡層數的加深、加寬,它也會帶來很多負面影響,

參數數量增加梯度消失和梯度爆炸計算複雜度增加

因此,從VGG、GoogLeNet開始,包括後面會講到的ResNet,研究者逐漸把目光聚焦在"如何在增加網絡深度和寬度的同時,避免上述這些弊端?"

不同的網絡模型所採取的方式不同,這也就引出了VGG與GoogLe的不同之處,

輸出層不同克服網絡加深弊端的方式不同

首先,說一下第一點--輸出層不同

VGG是在LeNet、AlexNet的基礎上引入了基礎塊的思想,但是在網絡架構、輸出等放並沒有進行太多的改變,在輸出層方面同樣是採用連續三個全連接層,全連接層的輸入是前面卷積層的輸出經過reshape得到。

雖然GoogLeNet是向LeNet致敬,但是在GoogLeNet的身上卻很難看到LeNet和AlexNet的影子,它的輸出更是採用NiN的思想(Network in Network),它把全連接層編程了1*1的卷積層。

其次,說一下第二點--克服網絡加深弊端的方式不同

VGG在克服網絡加深帶來的問題方面採用的是引入基礎塊的思想,但是整體上還是偏向於"更深",而GoogLeNet更加偏重於"更寬",它引入了並行網絡結構的思想,每一層有4個不同的線路對輸入進行處理,然後再塊的輸出部分在沿著通道維進行連接。

GoogLeNet通過對模型的大幅度改進,使得它在參數數量計算資源方面要明顯優於VGG,但是GoogLeNet的模型複雜度相對於VGG也要高一些,因此,在遷移性方面VGG要優於GoogLeNet。

GoogLeNet模型

Inception塊是GoogLeNet模型中一個非常重要的組成部分,因此,在介紹完整的GoogLeNet模型之前,我先來講解一下Inception塊的結構。

Inception塊

上圖就是就是Inception的結構,Inception分為兩個版本:

簡化版降維版

二者主要的區別就在於1*1的卷積層,降維版在第2、3、4條線路添加了1*1的卷積層來減少通道維度,以減小模型複雜度,本文就以降維版為例來講解GoogLeNet。

現在來看一下Inception的結構,可以很清楚的看出,它包含4條並行線路,其中,第1、2、3條線路分別採用了1*13*35*5,不同的卷積核大小來對輸入圖像進行特徵提取,使用不同大小卷積核能夠充分提取圖像特徵。其中,第2、3兩條線路都加入了1*1的卷積層,這裡要明確一點,第2、3兩條線路的1*1與第1條線路

1*1的卷積層的功能不同,第1條線路是用於特徵提取,而第2、3條線路的目的是降低模型複雜度。第4條線路採用的不是卷積層,而是3*3的池化層。最後,4條線路通過適當的填充,使得每一條線路輸出的寬和高一致,然後經過Filter Concatenation把4條線路的輸出在通道維進行連接

上述就是Inception塊的介紹,在GoogLeNet模型中,Inception塊會被多次用到,下面就開始介紹GoogLeNet的完整模型結構。

GoogLeNet

GoogLeNet在網絡模型方面與AlexNet、VGG還是有一些相通之處的,它們的主要相通之處就體現在卷積部分

AlexNet採用5個卷積層VGG把5個卷積層替換成5個卷積塊GoogLeNet採用5個不同的模塊組成主體卷積部分

上述就是GoogLeNet的結構,可以看出,和AlexNet統一使用5個卷積層、VGG統一使用5個卷積塊不同,GoogLeNet在主體卷積部分是

卷積層Inception塊混合使用。另外,需要注意一下,在輸出層GoogleNet採用全局平均池化,得到的是高和寬均為1的卷積層,而不是通過reshape得到的全連接層。

下面就來詳細介紹一下GoogLeNet的模型結構。

模塊1

第一個模塊採用的是一個單純的卷積層緊跟一個最大池化層。

卷積層:卷積核大小7*7,步長為2,輸出通道數64

池化層:窗口大小3*3,步長為2,輸出通道數64

模塊2

第二個模塊採用

2個卷積層,後面跟一個最大池化層。

卷積層:卷積核大小3*3,步長為1,輸出通道數192

池化層:窗口大小3*3,步長為2,輸出通道數192

模塊3

第三個模塊採用的是2個串聯的Inception塊,後面跟一個最大池化層。

第一個Inception的4條線路輸出的通道數分別是641283232,輸出的總通道數是4條線路的加和

,為256

第二個Inception的4條線路輸出的通道數分別是1281929664,輸出的總通道數為480

池化層:窗口大小3*3,步長為2,輸出通道數480

模塊4

第4個模塊採用的是5個串聯的Inception塊,後面跟一個最大池化層。

第一個Inception的4條線路輸出的通道數分別是19220848

64,輸出的總通道數為512

第二個Inception的4條線路輸出的通道數分別是1602246464,輸出的總通道數為512

第三個Inception的4條線路輸出的通道數分別是1282566464,輸出的總通道數為512

第四個Inception的4條線路輸出的通道數分別是1122886464,輸出的總通道數為528

第五個Inception的4條線路輸出的通道數分別是256320128128,輸出的總通道數為832

池化層:窗口大小3*3,步長為2,輸出通道數832

模塊5

第五個模塊採用的是2個串聯的Inception塊

第一個Inception的4條線路輸出的通道數分別是256320128128,輸出的總通道數為832

第二個Inception的4條線路輸出的通道數分別是384384128128,輸出的總通道數為1024

輸出層

前面已經多次提到,在輸出層GoogLeNet與AlexNet、VGG採用3個連續的全連接層不同,GoogLeNet採用的是全局平均池化層,得到的是高和寬均為1的卷積層,然後添加丟棄概率為40%的Dropout,輸出層激活函數採用的是softmax

激活函數

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

編程實踐

當我們拿到一個需求的時候,應該先對它進行一下分析、分解,針對GoogLeNet,我們通過分析可以把它分解成如下幾個模塊,

Inception塊卷積層池化層線性層

通過上述分解,我們逐個來實現上述每個模塊。

Inception塊

前面講解過程中已經詳細介紹Inception塊的結構,它包括4條線路,而對於Inception塊最重要的參數就是每個線路輸出的通道數,由於其中步長、填充方式、卷積核大小都是固定的,因此不需要我們進行傳參。我們把4條線路中每層的輸出通道數作為Inception塊的入參,具體實現過程如下,

def inception_block(X, c1, c2, c3, c4, name):
in_channels = int(X.get_shape()[-1])
# 線路1
with tf.variable_scope('conv1X1_{}'.format(name)) as scope:
weight = tf.get_variable("weight", [1, 1, in_channels, c1])
bias = tf.get_variable("bias", [c1])
p1_1 = tf.nn.conv2d(X, weight, strides=[1, 1, 1, 1], padding="SAME")
p1_1 = tf.nn.relu(tf.nn.bias_add(p1_1, bias))

# 線路2
with tf.variable_scope('conv2X1_{}'.format(name)) as scope:
weight = tf.get_variable("weight", [1, 1, in_channels, c2[0]])
bias = tf.get_variable("bias", [c2[0]])
p2_1 = tf.nn.conv2d(X, weight, strides=[1, 1, 1, 1], padding="SAME")
p2_1 = tf.nn.relu(tf.nn.bias_add(p2_1, bias))
p2_shape = int(p2_1.get_shape()[-1])
with tf.variable_scope('conv2X2_{}'.format(name)) as scope:
weight = tf.get_variable("weight", [3, 3, p2_shape, c2[1]])
bias = tf.get_variable("bias", [c2[1]])
p2_2 = tf.nn.conv2d(p2_1, weight, strides=[1, 1, 1, 1], padding="SAME")
p2_2 = tf.nn.relu(tf.nn.bias_add(p2_2, bias))

卷積及池化

在GoogLeNet中多處用到了卷積層和最大池化層,這些結構在AlexNet中都已經實現過,我們直接拿過來使用即可,

def conv_layer(self, X, ksize, out_filters, stride, name):
in_filters = int(X.get_shape()[-1])
with tf.variable_scope(name) as scope:
weight = tf.get_variable("weight", [ksize, ksize, in_filters, out_filters])
bias = tf.get_variable("bias", [out_filters])
conv = tf.nn.conv2d(X, weight, strides=[1, stride, stride, 1], padding="SAME")
activation = tf.nn.relu(tf.nn.bias_add(conv, bias))
return activation

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

線性層

GoogLeNet與AlexNet、VGG在輸出層不同,AlexNet和VGG是通過連續的全連接層處理,然後輸入到激活函數即可,而GoogLeNet需要進行全局平均池化後進行一次線性映射,對於這一點實現過程如下,

def linear(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)

搭建模型

上面幾步已經把GoogLeNet主要使用的組件已經搭建完成,接下來要做的就是把它們組合到一起即可。這裡需要注意一點,全局平均池化層的填充方式和前面卷積層、池化層使用的不同,這裡需要使用VALID填充方式,

def create(self, X):
# 模塊1
module1_1 = self.conv_layer(X, 7, 64, 2, "module1_1")


pool_layer1 = self.pool_layer(module1_1, 3, 2)

# 模塊2
module2_1 = self.conv_layer(pool_layer1, 1, 64, 1, "modul2_1")
module2_2 = self.conv_layer(module2_1, 3, 192, 1, "module2_2")
pool_layer2 = self.pool_layer(module2_2, 3, 2)

# 模塊3
module3a = self.inception_block(pool_layer2, 64, (96, 128), (16, 32), 32, "3a")
module3b = self.inception_block(module3a, 128, (128, 192), (32, 96), 64, "3b")
pool_layer3 = self.pool_layer(module3b, 3, 2)

# 模塊4
module4a = self.inception_block(pool_layer3, 192, (96, 208), (16, 48), 64, "4a")
module4b = self.inception_block(module4a, 160, (112, 224), (24, 64), 64, "4b")
module4c = self.inception_block(module4b, 128, (128, 256), (24, 64), 64, "4c")
module4d = self.inception_block(module4c, 112, (144, 288), (32, 64), 64, "4d")
module4e = self.inception_block(module4d, 256, (160, 320), (32, 128), 128, "4e")
pool_layer4 = self.pool_layer(module4e, 3, 2)

# 模塊5
module5a = self.inception_block(pool_layer4, 256, (160, 320), (32, 128), 128, "5a")
module5b = self.inception_block(module5a, 384, (192, 384), (48, 128), 128, "5b")

pool_layer5 = tf.nn.avg_pool(module5b, ksize=[1, 7, 7, 1], strides=[1, 1, 1, 1], padding="VALID")
flatten = tf.reshape(pool_layer5, [-1, 1024])
dropout = tf.nn.dropout(flatten, keep_prob=self.keep_prob)
linear = self.linear(dropout, self.num_classes, 'linear')
return tf.nn.softmax(linear)

驗證

為了驗證每一個模塊輸出的形狀和原文中給出的是否一致,我使用numpy,生成了樣本數為5的隨機樣本,看一下每一層的輸出結果,

def main():
X = np.random.normal(size=(5, 224, 224, 3))
images = tf.placeholder("float", [5, 224, 224, 3])
googlenet = GoogLeNet(1000, 0.4)
writer = tf.summary.FileWriter("logs")
with tf.Session() as sess:
model = googlenet.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)))

# 輸出
module1_1: (5, 112, 112, 64)
pool_layer1: (5, 56, 56, 64)
module2_1: (5, 56, 56, 64)
module2_2: (5, 56, 56, 192)
pool_layer2: (5, 28, 28, 192)
module3a: (5, 28, 28, 256)
module3b: (5, 28, 28, 480)
pool_layer3: (5, 14, 14, 480)
module4a: (5, 14, 14, 512)
module4b: (5, 14, 14, 512)
module4c: (5, 14, 14, 512)
module4d: (5, 14, 14, 528)
module4e: (5, 14, 14, 832)
pool_layer4: (5, 7, 7, 832)
module5a: (5, 7, 7, 832)
module5b: (5, 7, 7, 1024)
pool_layer5: (5, 1, 1, 1024)
flatten: (5, 1024)
linear: (5, 1000)

可以從上述輸出可以看出,每一層的輸出形狀和原文中給出的一致,至於在不同場景、不同數據集下的表現效果,這需要針對性的進行調優。

鏈接

本文完整代碼

https://github.com/Jackpopc/aiLearnNotes/blob/master/computer_vision/GoogLeNet.py

Going Deeper with Convolutions

http://www.arxiv.org/pdf/1409.4842.pdf

Network In Network

http://arxiv.org/pdf/1312.4400

我把本講文檔已經上傳到github,如果需要可以搜索github項目aiLearnNotes查看。