卷積神經(jīng)網(wǎng)絡之GoogLeNet(附完整代碼)

2014年,GoogLeNet獲得了第一名、VGG獲得了第二名

(1)參數(shù)太多,如果訓練數(shù)據(jù)集有限,很容易產(chǎn)生過擬合;--比如VGG
(2)網(wǎng)絡越大、參數(shù)越多,計算復雜度越大,難以應用;
(3)網(wǎng)絡越深,容易出現(xiàn)梯度彌散問題(梯度越往后穿越容易消失),難以優(yōu)化模型。

為了減少參數(shù),自然就想到將全連接變成稀疏連接,但是在實現(xiàn)上,全連接變成稀疏連接后實際計算量并不會有質(zhì)的提升,,因為大部分硬件是針對密集矩陣計算優(yōu)化的,稀疏矩陣雖然數(shù)據(jù)量少,但是計算所消耗的時間卻很難減少。

【問題來了】什么是Inception呢?

一、Inception V1 原始結構

image

該結構將CNN中常用的卷積(1x1,3x3,5x5)、池化操作(3x3)堆疊在一起(卷積、池化后的尺寸相同,將通道相加),一方面增加了網(wǎng)絡的寬度,另一方面也增加了網(wǎng)絡對尺度的適應性。
網(wǎng)絡卷積層中的網(wǎng)絡能夠提取輸入的每一個細節(jié)信息,同時5x5的濾波器也能夠覆蓋大部分接受層的的輸入。還可以進行一個池化操作,以減少空間大小,降低過度擬合。在這些層之上,在每一個卷積層后都要做一個ReLU操作,以增加網(wǎng)絡的非線性特征。然而這個Inception原始版本,所有的卷積核都在上一層的所有輸出上來做,而那個5x5的卷積核所需的計算量就太大了,造成了特征圖的厚度很大,為了避免這種情況,在3x3前、5x5前、max pooling后分別加上了1x1的卷積核,以起到了降低特征圖厚度的作用,這也就形成了Inception v1的網(wǎng)絡結構,如下圖所示:

image

1x1的卷積核有什么用呢?
1x1卷積的主要目的是為了減少維度,還用于修正線性激活(ReLU)。比如,上一層的輸出為100x100x128,經(jīng)過具有256個通道的5x5卷積層之后(stride=1,pad=2),輸出數(shù)據(jù)為100x100x256,其中,卷積層的參數(shù)為128x5x5x256= 819200。而假如上一層輸出先經(jīng)過具有32個通道的1x1卷積層,再經(jīng)過具有256個輸出的5x5卷積層,那么輸出數(shù)據(jù)仍為為100x100x256,但卷積參數(shù)量已經(jīng)減少為128x1x1x32 + 32x5x5x256= 204800,大約減少了4倍。

基于Inception構建了GoogLeNet的網(wǎng)絡結構如下(共22層):


image

對上圖說明如下:
(1)GoogLeNet采用了模塊化的結構(Inception結構),方便增添和修改;
(2)網(wǎng)絡最后采用了average pooling(平均池化)來代替全連接層,該想法來自NIN(Network in Network),事實證明這樣可以將準確率提高0.6%。但是,實際在最后還是加了一個全連接層,主要是為了方便對輸出進行靈活調(diào)整;
(3)雖然移除了全連接,但是網(wǎng)絡中依然使用了Dropout ;
(4)為了避免梯度消失,網(wǎng)絡額外增加了2個輔助的softmax用于向前傳導梯度(輔助分類器)。輔助分類器是將中間某一層的輸出用作分類,并按一個較小的權重(0.3)加到最終分類結果中,這樣相當于做了模型融合,同時給網(wǎng)絡增加了反向傳播的梯度信號,也提供了額外的正則化,對于整個網(wǎng)絡的訓練很有裨益。而在實際測試的時候,這兩個額外的softmax會被去掉。

GoogLeNet的網(wǎng)絡結構圖細節(jié)如下:


image

注:上表中的“#3x3 reduce”,“#5x5 reduce”表示在3x3,5x5卷積操作之前使用了1x1卷積的數(shù)量。

GoogLeNet網(wǎng)絡結構明細表解析如下:
0、輸入
原始輸入圖像為224x224x3,且都進行了零均值化的預處理操作(圖像每個像素減去均值)。
1、第一層(卷積層)
使用7x7的卷積核(滑動步長2,padding為3),64通道,輸出為112x112x64,卷積后進行ReLU操作
經(jīng)過3x3的max pooling(步長為2),輸出為((112 - 3+1)/2)+1=56,即56x56x64,再進行ReLU操作
2、第二層(卷積層)
使用3x3的卷積核(滑動步長為1,padding為1),192通道,輸出為56x56x192,卷積后進行ReLU操作
經(jīng)過3x3的max pooling(步長為2),輸出為((56 - 3+1)/2)+1=28,即28x28x192,再進行ReLU操作
3a、第三層(Inception 3a層)
分為四個分支,采用不同尺度的卷積核來進行處理
(1)64個1x1的卷積核,然后RuLU,輸出28x28x64
(2)96個1x1的卷積核,作為3x3卷積核之前的降維,變成28x28x96,然后進行ReLU計算,再進行128個3x3的卷積(padding為1),輸出28x28x128
(3)16個1x1的卷積核,作為5x5卷積核之前的降維,變成28x28x16,進行ReLU計算后,再進行32個5x5的卷積(padding為2),輸出28x28x32
(4)pool層,使用3x3的核(padding為1),輸出28x28x192,然后進行32個1x1的卷積,輸出28x28x32。
將四個結果進行連接,對這四部分輸出結果的第三維并聯(lián),即64+128+32+32=256,最終輸出28x28x256
3b、第三層(Inception 3b層)
(1)128個1x1的卷積核,然后RuLU,輸出28x28x128
(2)128個1x1的卷積核,作為3x3卷積核之前的降維,變成28x28x128,進行ReLU,再進行192個3x3的卷積(padding為1),輸出28x28x192
(3)32個1x1的卷積核,作為5x5卷積核之前的降維,變成28x28x32,進行ReLU計算后,再進行96個5x5的卷積(padding為2),輸出28x28x96
(4)pool層,使用3x3的核(padding為1),輸出28x28x256,然后進行64個1x1的卷積,輸出28x28x64。
將四個結果進行連接,對這四部分輸出結果的第三維并聯(lián),即128+192+96+64=480,最終輸出輸出為28x28x480

第四層(4a,4b,4c,4d,4e)、第五層(5a,5b)……,與3a、3b類似,在此就不再重復。

從GoogLeNet的實驗結果來看,效果很明顯,差錯率比MSRA、VGG等模型都要低,對比結果如下表所示:
image
# -- encoding:utf-8 --
"""
Create on 19/6/26 20:12
"""

import tensorflow as tf


class GoogleNet(object):
    def __init__(self):
        with tf.variable_scope("net", initializer=tf.random_normal_initializer(0.0, 0.001)):
            with tf.variable_scope("Input"):
                input_x = tf.placeholder(dtype=tf.float32, shape=[None, 28, 28, 1])
                # 由于GoogleNet網(wǎng)絡要求輸入大小為224 * 224 *  3的圖像,所以做一個轉(zhuǎn)換。 channel不做轉(zhuǎn)換了。
                net = tf.image.resize_images(images=input_x, size=(224, 224))

            net = self.__conv2d('conv1', net, 64, 7, 2)
            net = self.__max_pool('pool2', net, 3, 2)
            net = self.__conv2d('conv3', net, 192, 3, 1)
            net = self.__conv2d('conv4', net, 192, 3, 1)
            net = self.__max_pool('pool5', net, 3, 2)
            net = self.__inception('inception6', net, 64, 96, 128, 16, 32, 32)
            net = self.__inception('inception7', net, 64, 96, 128, 16, 32, 32)
            net = self.__inception('inception8', net, 128, 128, 192, 32, 96, 64)
            net = self.__inception('inception9', net, 128, 128, 192, 32, 96, 64)
            net = self.__max_pool('pool10', net, 3, 2)
            net = self.__inception('inception11', net, 192, 96, 208, 16, 48, 64)
            net = self.__inception('inception12', net, 192, 96, 208, 16, 48, 64)
            net = self.__inception('inception13', net, 160, 112, 224, 24, 64, 64)
            net = self.__inception('inception14', net, 160, 112, 224, 24, 64, 64)
            net = self.__inception('inception15', net, 128, 128, 256, 24, 64, 64)
            net = self.__inception('inception16', net, 128, 128, 256, 24, 64, 64)
            net = self.__inception('inception17', net, 112, 144, 288, 32, 64, 64)
            net = self.__inception('inception18', net, 112, 144, 288, 32, 64, 64)
            net = self.__inception('inception19', net, 256, 160, 320, 32, 128, 128)
            net = self.__inception('inception20', net, 256, 160, 320, 32, 128, 128)
            net = self.__max_pool('pool21', net, 3, 2)
            net = self.__inception('inception22', net, 256, 160, 320, 32, 128, 128)
            net = self.__inception('inception23', net, 256, 160, 320, 32, 128, 128)
            net = self.__inception('inception24', net, 384, 192, 384, 48, 128, 128)
            net = self.__inception('inception25', net, 384, 192, 384, 48, 128, 128)
            net = self.__avg_pool('pool26', net, 7, 1, padding='VALID')
            net = tf.nn.dropout(net, keep_prob=0.4)
            shape = net.get_shape()
            net = tf.reshape(net, shape=[-1, shape[1] * shape[2] * shape[3]])
            net = self.__fc('fc27', net, 10)
            logits = tf.nn.softmax(net)

            self.logits = logits

    def __fc(self, name, net, units, with_activation=True):
        with tf.variable_scope(name):
            input_units = net.get_shape()[-1]
            w = tf.get_variable('w', shape=[input_units, units])
            b = tf.get_variable('b', shape=[units])
            net = tf.add(tf.matmul(net, w), b)
            if with_activation:
                net = tf.nn.relu(net)
            return net

    def __conv2d(self, name, net, output_channels, window_size, stride_size, padding='SAME', with_activation=True):
        with tf.variable_scope(name):
            input_channels = net.get_shape()[-1]
            filter = tf.get_variable('w', shape=[window_size, window_size, input_channels, output_channels])
            bias = tf.get_variable('b', shape=[output_channels])
            net = tf.nn.bias_add(tf.nn.conv2d(input=net, filter=filter,
                                              strides=[1, stride_size, stride_size, 1],
                                              padding=padding), bias)
            if with_activation:
                net = tf.nn.relu(net)
            return net

    def __max_pool(self, name, net, window_size, stride_size, padding='SAME'):
        with tf.variable_scope(name):
            net = tf.nn.max_pool(net, ksize=[1, window_size, window_size, 1],
                                 strides=[1, stride_size, stride_size, 1],
                                 padding=padding)
            return net

    def __avg_pool(self, name, net, window_size, stride_size, padding='SAME'):
        with tf.variable_scope(name):
            net = tf.nn.avg_pool(net, ksize=[1, window_size, window_size, 1],
                                 strides=[1, stride_size, stride_size, 1],
                                 padding=padding)
            return net

    def __inception(self, name, net,
                    branch1_output_channels,
                    branch2_reduce_output_channels, branch2_output_channels,
                    branch3_reduce_output_channels, branch3_output_channels,
                    branch4_output_channels):
        with tf.variable_scope(name):
            with tf.variable_scope("branch_1"):
                # 1. 第一個分支
                net1 = self.__conv2d('conv1', net, branch1_output_channels, 1, 1)
            with tf.variable_scope("branch_2"):
                # 2. 第二個分支
                tmp_net = self.__conv2d('conv1', net, branch2_reduce_output_channels, 1, 1)
                net2 = self.__conv2d('conv2', tmp_net, branch2_output_channels, 3, 1)
            with tf.variable_scope("branch_3"):
                # 3. 第三個分支
                tmp_net = self.__conv2d('conv1', net, branch3_reduce_output_channels, 1, 1)
                net3 = self.__conv2d('conv2', tmp_net, branch3_output_channels, 5, 1)
            with tf.variable_scope("branch_4"):
                # 4. 第四個分支
                tmp_net = self.__max_pool('pool1', net, 3, 1)
                net4 = self.__conv2d('conv1', tmp_net, branch4_output_channels, 1, 1)
            with tf.variable_scope("Concat"):
                net = tf.concat([net1, net2, net3, net4], axis=-1)
            return net


def train():
    # TODO: 損失函數(shù)、圖的運行過程等代碼,自己完善,我不寫了。
    with tf.Graph().as_default():
        net = GoogleNet()
        writer = tf.summary.FileWriter(logdir='./models/graph/01', graph=tf.get_default_graph())
        writer.close()


if __name__ == '__main__':
    train()

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容