零基礎入門深度學習(七):圖像分類任務之VGG、GoogLeNet和ResNet

授課講師 | 孫高峯 百度深度學習技術平臺部資深研發工程師python

授課時間 | 每週2、週四晚20:00-21:00web

編輯整理 | 孫高峯算法

內容來源 | 百度飛槳深度學習集訓營網絡

下載安裝命令

## CPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

01 導讀

本課程是百度官方開設的零基礎入門深度學習課程,主要面向沒有深度學習技術基礎或者基礎薄弱的同窗,幫助你們在深度學習領域實現從0到1+的跨越。從本課程中,你將學習到:架構

  1. 深度學習基礎知識app

  2. numpy實現神經網絡構建和梯度降低算法ide

  3. 計算機視覺領域主要方向的原理、實踐函數

  4. 天然語言處理領域主要方向的原理、實踐學習

  5. 個性化推薦算法的原理、實踐測試

本週爲開講第四周,百度深度學習技術平臺部資深研發工程師孫高峯,開始講解計算機視覺中圖像分類任務。在上一節課中,咱們爲你們介紹了經典的LeNet和AlexNet神經網絡結構在眼疾識別任務中的應用,本節將繼續爲你們帶來更多精彩內容。

02 VGG

VGG是當前最流行的CNN模型之一,2014年由Simonyan和Zisserman提出,其命名來源於論文做者所在的實驗室Visual Geometry Group。AlexNet模型經過構造多層網絡,取得了較好的效果,可是並無給出深度神經網絡設計的方向。VGG經過使用一系列大小爲3x3的小尺寸卷積核和pooling層構造深度卷積神經網絡,並取得了較好的效果。VGG模型由於結構簡單、應用性極強而廣受研究者歡迎,尤爲是它的網絡結構設計方法,爲構建深度神經網絡提供了方向。

圖3 是VGG-16的網絡結構示意圖,有13層卷積和3層全鏈接層。VGG網絡的設計嚴格使用的卷積層和池化層來提取特徵,並在網絡的最後面使用三層全鏈接層,將最後一層全鏈接層的輸出做爲分類的預測。在VGG中每層卷積將使用ReLU做爲激活函數,在全鏈接層以後添加dropout來抑制過擬合。使用小的卷積核可以有效地減小參數的個數,使得訓練和測試變得更加有效。好比使用兩層卷積層,能夠獲得感覺野爲5的特徵圖,而比使用的卷積層須要更少的參數。因爲卷積核比較小,能夠堆疊更多的卷積層,加深網絡的深度,這對於圖像分類任務來講是有利的。VGG模型的成功證實了增長網絡的深度,能夠更好的學習圖像中的特徵模式。

圖3:VGG模型網絡結構示意圖

VGG在眼疾識別數據集iChallenge-PM上的具體實現以下代碼所示:

# -*- coding:utf-8 -*-

# VGG模型代碼
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.layer_helper import LayerHelper
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, BatchNorm, FC
from paddle.fluid.dygraph.base import to_variable

# 定義vgg塊,包含多層卷積和1層2x2的最大池化層
class vgg_block(fluid.dygraph.Layer):
    def __init__(self, name_scope, num_convs, num_channels):
        """
        num_convs, 卷積層的數目
        num_channels, 卷積層的輸出通道數,在同一個Incepition塊內,卷積層輸出通道數是同樣的
        """
        super(vgg_block, self).__init__(name_scope)
        self.conv_list = []
        for i in range(num_convs):
            conv_layer = self.add_sublayer('conv_' + str(i), Conv2D(self.full_name(), 
                                        num_filters=num_channels, filter_size=3, padding=1, act='relu'))
            self.conv_list.append(conv_layer)
        self.pool = Pool2D(self.full_name(), pool_stride=2, pool_size = 2, pool_type='max')
    def forward(self, x):
        for item in self.conv_list:
            x = item(x)
        return self.pool(x)

class VGG(fluid.dygraph.Layer):
    def __init__(self, name_scope, conv_arch=((2, 64), 
                                (2, 128), (3, 256), (3, 512), (3, 512))):
        super(VGG, self).__init__(name_scope)
        self.vgg_blocks=[]
        iter_id = 0
        # 添加vgg_block
        # 這裏一共5個vgg_block,每一個block裏面的卷積層數目和輸出通道數由conv_arch指定
        for (num_convs, num_channels) in conv_arch:
            block = self.add_sublayer('block_' + str(iter_id), 
                    vgg_block(self.full_name(), num_convs, num_channels))
            self.vgg_blocks.append(block)
            iter_id += 1
        self.fc1 = FC(self.full_name(),
                      size=4096,
                      act='relu')
        self.drop1_ratio = 0.5
        self.fc2= FC(self.full_name(),
                      size=4096,
                      act='relu')
        self.drop2_ratio = 0.5
        self.fc3 = FC(self.full_name(),
                      size=1,
                      )
    def forward(self, x):
        for item in self.vgg_blocks:
            x = item(x)
        x = fluid.layers.dropout(self.fc1(x), self.drop1_ratio)
        x = fluid.layers.dropout(self.fc2(x), self.drop2_ratio)
        x = self.fc3(x)
        return x
	
with fluid.dygraph.guard():    model = VGG("VGG")
train(model)

經過運行結果能夠發現,在眼疾篩查數據集iChallenge-PM上使用VGG,loss能有效的降低,通過5個epoch的訓練,在驗證集上的準確率能夠達到94%左右。

03 GoogLeNet

GoogLeNet是2014年ImageNet比賽的冠軍,它的主要特色是網絡不只有深度,還在橫向上具備「寬度」。因爲圖像信息在空間尺寸上的巨大差別,如何選擇合適的卷積核大小來提取特徵就顯得比較困難了。空間分佈範圍更廣的圖像信息適合用較大的卷積核來提取其特徵,而空間分佈範圍較小的圖像信息則適合用較小的卷積核來提取其特徵。爲了解決這個問題,GoogLeNet提出了一種被稱爲Inception模塊的方案。如 圖4 所示:


說明:

  • Google的研究人員爲了向LeNet致敬,特意將模型命名爲GoogLeNet

  • Inception一詞來源於電影《盜夢空間》(Inception)


圖4:Inception模塊結構示意圖

圖4(a)是Inception模塊的設計思想,使用3個不一樣大小的卷積覈對輸入圖片進行卷積操做,並附加最大池化,將這4個操做的輸出沿着通道這一維度進行拼接,構成的輸出特徵圖將會包含通過不一樣大小的卷積核提取出來的特徵。Inception模塊採用多通路(multi-path)的設計形式,每一個支路使用不一樣大小的卷積核,最終輸出特徵圖的通道數是每一個支路輸出通道數的總和,這將會致使輸出通道數變得很大,尤爲是使用多個Inception模塊串聯操做的時候,模型參數量會變得很是巨大。爲了減少參數量,Inception模塊使用了圖(b)中的設計方式,在每一個3x3和5x5的卷積層以前,增長1x1的卷積層來控制輸出通道數;在最大池化層後面增長1x1卷積層減少輸出通道數。基於這一設計思想,造成了上圖(b)中所示的結構。下面這段程序是Inception塊的具體實現方式,能夠對照圖(b)和代碼一塊兒閱讀。


提示:

可能有讀者會問,通過3x3的最大池化以後圖像尺寸不會減少嗎,爲何還能跟另外3個卷積輸出的特徵圖進行拼接?這是由於池化操做能夠指定窗口大小,pool_stride=1和pool_padding=1,輸出特徵圖尺寸能夠保持不變。


Inception模塊的具體實現以下代碼所示:

class Inception(fluid.dygraph.Layer):
    def __init__(self, name_scope, c1, c2, c3, c4, **kwargs):
        '''
        Inception模塊的實現代碼,
        name_scope, 模塊名稱,數據類型爲string
        c1,  圖(b)中第一條支路1x1卷積的輸出通道數,數據類型是整數
        c2,圖(b)中第二條支路卷積的輸出通道數,數據類型是tuple或list, 
               其中c2[0]是1x1卷積的輸出通道數,c2[1]是3x3
        c3,圖(b)中第三條支路卷積的輸出通道數,數據類型是tuple或list, 
               其中c3[0]是1x1卷積的輸出通道數,c3[1]是3x3
        c4,  圖(b)中第一條支路1x1卷積的輸出通道數,數據類型是整數
        '''
        super(Inception, self).__init__(name_scope)
        # 依次建立Inception塊每條支路上使用到的操做
        self.p1_1 = Conv2D(self.full_name(), num_filters=c1, 
                           filter_size=1, act='relu')
        self.p2_1 = Conv2D(self.full_name(), num_filters=c2[0], 
                           filter_size=1, act='relu')
        self.p2_2 = Conv2D(self.full_name(), num_filters=c2[1], 
                           filter_size=3, padding=1, act='relu')
        self.p3_1 = Conv2D(self.full_name(), num_filters=c3[0], 
                           filter_size=1, act='relu')
        self.p3_2 = Conv2D(self.full_name(), num_filters=c3[1], 
                           filter_size=5, padding=2, act='relu')
        self.p4_1 = Pool2D(self.full_name(), pool_size=3, 
                           pool_stride=1,  pool_padding=1, 
                           pool_type='max')
        self.p4_2 = Conv2D(self.full_name(), num_filters=c4, 
                           filter_size=1, act='relu')


    def forward(self, x):
        # 支路1只包含一個1x1卷積
        p1 = self.p1_1(x)
        # 支路2包含 1x1卷積 + 3x3卷積
        p2 = self.p2_2(self.p2_1(x))
        # 支路3包含 1x1卷積 + 5x5卷積
        p3 = self.p3_2(self.p3_1(x))
        # 支路4包含 最大池化和1x1卷積
        p4 = self.p4_2(self.p4_1(x))
        # 將每一個支路的輸出特徵圖拼接在一塊兒做爲最終的輸出結果
        return fluid.layers.concat([p1, p2, p3, p4], axis=1)

GoogLeNet的架構如 圖5 所示,在主體卷積部分中使用5個模塊(block),每一個模塊之間使用步幅爲2的3 ×3最大池化層來減少輸出高寬。

  • 第一模塊使用一個64通道的7 × 7卷積層。

  • 第二模塊使用2個卷積層:首先是64通道的1 × 1卷積層,而後是將通道增大3倍的3 × 3卷積層。

  • 第三模塊串聯2個完整的Inception塊。

  • 第四模塊串聯了5個Inception塊。

  • 第五模塊串聯了2 個Inception塊。

  • 第五模塊的後面緊跟輸出層,使用全局平均池化 層來將每一個通道的高和寬變成1,最後接上一個輸出個數爲標籤類別數的全鏈接層。


說明:在原做者的論文中添加了圖中所示的softmax1和softmax2兩個輔助分類器,以下圖所示,訓練時將三個分類器的損失函數進行加權求和,以緩解梯度消失現象。這裏的程序做了簡化,沒有加入輔助分類器。


圖5:GoogLeNet模型網絡結構示意圖

GoogLeNet的具體實現以下代碼所示:

# -*- coding:utf-8 -*-

# GoogLeNet模型代碼
import numpy as np
import paddle
import paddle.fluid as fluid
from paddle.fluid.layer_helper import LayerHelper
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, BatchNorm, FC
from paddle.fluid.dygraph.base import to_variable

# 定義Inception塊
class Inception(fluid.dygraph.Layer):
    def __init__(self, name_scope, c1, c2, c3, c4, **kwargs):
        '''
        Inception模塊的實現代碼,
        name_scope, 模塊名稱,數據類型爲string
        c1,  圖(b)中第一條支路1x1卷積的輸出通道數,數據類型是整數
        c2,圖(b)中第二條支路卷積的輸出通道數,數據類型是tuple或list, 
               其中c2[0]是1x1卷積的輸出通道數,c2[1]是3x3
        c3,圖(b)中第三條支路卷積的輸出通道數,數據類型是tuple或list, 
               其中c3[0]是1x1卷積的輸出通道數,c3[1]是3x3
        c4,  圖(b)中第一條支路1x1卷積的輸出通道數,數據類型是整數
        '''
        super(Inception, self).__init__(name_scope)
        # 依次建立Inception塊每條支路上使用到的操做
        self.p1_1 = Conv2D(self.full_name(), num_filters=c1, 
                           filter_size=1, act='relu')
        self.p2_1 = Conv2D(self.full_name(), num_filters=c2[0], 
                           filter_size=1, act='relu')
        self.p2_2 = Conv2D(self.full_name(), num_filters=c2[1], 
                           filter_size=3, padding=1, act='relu')
        self.p3_1 = Conv2D(self.full_name(), num_filters=c3[0], 
                           filter_size=1, act='relu')
        self.p3_2 = Conv2D(self.full_name(), num_filters=c3[1], 
                           filter_size=5, padding=2, act='relu')
        self.p4_1 = Pool2D(self.full_name(), pool_size=3, 
                           pool_stride=1,  pool_padding=1, 
                           pool_type='max')
        self.p4_2 = Conv2D(self.full_name(), num_filters=c4, 
                           filter_size=1, act='relu')

    def forward(self, x):
        # 支路1只包含一個1x1卷積
        p1 = self.p1_1(x)
        # 支路2包含 1x1卷積 + 3x3卷積
        p2 = self.p2_2(self.p2_1(x))
        # 支路3包含 1x1卷積 + 5x5卷積
        p3 = self.p3_2(self.p3_1(x))
        # 支路4包含 最大池化和1x1卷積
        p4 = self.p4_2(self.p4_1(x))
        # 將每一個支路的輸出特徵圖拼接在一塊兒做爲最終的輸出結果
        return fluid.layers.concat([p1, p2, p3, p4], axis=1)  

class GoogLeNet(fluid.dygraph.Layer):
    def __init__(self, name_scope):
        super(GoogLeNet, self).__init__(name_scope)
        # GoogLeNet包含五個模塊,每一個模塊後面緊跟一個池化層
        # 第一個模塊包含1個卷積層
        self.conv1 = Conv2D(self.full_name(), num_filters=64, filter_size=7, 
                            padding=3, act='relu')
        # 3x3最大池化
        self.pool1 = Pool2D(self.full_name(), pool_size=3, pool_stride=2,  
                            pool_padding=1, pool_type='max')
        # 第二個模塊包含2個卷積層
        self.conv2_1 = Conv2D(self.full_name(), num_filters=64, 
                              filter_size=1, act='relu')
        self.conv2_2 = Conv2D(self.full_name(), num_filters=192, 
                              filter_size=3, padding=1, act='relu')
        # 3x3最大池化
        self.pool2 = Pool2D(self.full_name(), pool_size=3, pool_stride=2,  
                            pool_padding=1, pool_type='max')
        # 第三個模塊包含2個Inception塊
        self.block3_1 = Inception(self.full_name(), 64, (96, 128), (16, 32), 32)
        self.block3_2 = Inception(self.full_name(), 128, (128, 192), (32, 96), 64)
        # 3x3最大池化
        self.pool3 = Pool2D(self.full_name(), pool_size=3, pool_stride=2,  
                               pool_padding=1, pool_type='max')
        # 第四個模塊包含5個Inception塊
        self.block4_1 = Inception(self.full_name(), 192, (96, 208), (16, 48), 64)
        self.block4_2 = Inception(self.full_name(), 160, (112, 224), (24, 64), 64)
        self.block4_3 = Inception(self.full_name(), 128, (128, 256), (24, 64), 64)
        self.block4_4 = Inception(self.full_name(), 112, (144, 288), (32, 64), 64)
        self.block4_5 = Inception(self.full_name(), 256, (160, 320), (32, 128), 128)
        # 3x3最大池化
        self.pool4 = Pool2D(self.full_name(), pool_size=3, pool_stride=2,  
                               pool_padding=1, pool_type='max')
        # 第五個模塊包含2個Inception塊
        self.block5_1 = Inception(self.full_name(), 256, (160, 320), (32, 128), 128)
        self.block5_2 = Inception(self.full_name(), 384, (192, 384), (48, 128), 128)
        # 全局池化,尺寸用的是global_pooling,pool_stride不起做用
        self.pool5 = Pool2D(self.full_name(), pool_stride=1, 
                               global_pooling=True, pool_type='avg')
        self.fc = FC(self.full_name(),  size=1)

    def forward(self, x):
        x = self.pool1(self.conv1(x))
        x = self.pool2(self.conv2_2(self.conv2_1(x)))
        x = self.pool3(self.block3_2(self.block3_1(x)))
        x = self.block4_3(self.block4_2(self.block4_1(x)))
        x = self.pool4(self.block4_5(self.block4_4(x)))
        x = self.pool5(self.block5_2(self.block5_1(x)))
        x = self.fc(x)
        return x

	with fluid.dygraph.guard():
    model = GoogLeNet("GoogLeNet")

train(model)

經過運行結果能夠發現,使用GoogLeNet在眼疾篩查數據集iChallenge-PM上,loss能有效的降低,通過5個epoch的訓練,在驗證集上的準確率能夠達到95%左右。

04 ResNet

ResNet是2015年ImageNet比賽的冠軍,將識別錯誤率下降到了3.6%,這個結果甚至超出了正常人眼識別的精度。

經過前面幾個經典模型學習,咱們能夠發現隨着深度學習的不斷髮展,模型的層數愈來愈多,網絡結構也愈來愈複雜。那麼是否加深網絡結構,就必定會獲得更好的效果呢?從理論上來講,假設新增長的層都是恆等映射,只要原有的層學出跟原模型同樣的參數,那麼深模型結構就能達到原模型結構的效果。換句話說,原模型的解只是新模型的解的子空間,在新模型解的空間裏應該能找到比原模型解對應的子空間更好的結果。可是實踐代表,增長網絡的層數以後,訓練偏差每每不降反升。

Kaiming He等人提出了殘差網絡ResNet來解決上述問題,其基本思想如 圖6所示。

  • 圖6(a):表示增長網絡的時候,將x映射成輸出。

  • 圖6(b):對圖6(a)做了改進,輸出。這時不是直接學習輸出特徵y的表示,而是學習。

    • 若是想學習出原模型的表示,只需將F(x)的參數所有設置爲0,則是恆等映射。

    • 也叫作殘差項,若是的映射接近恆等映射,圖6(b)中經過學習殘差項也比圖6(a)學習完整映射形式更加容易。

圖6:殘差塊設計思想

圖6(b)的結構是殘差網絡的基礎,這種結構也叫作殘差塊(residual block)。輸入x經過跨層鏈接,能更快的向前傳播數據,或者向後傳播梯度。殘差塊的具體設計方案如 7 所示,這種設計方案也成稱做瓶頸結構(BottleNeck)。

圖7:殘差塊結構示意圖

下圖表示出了ResNet-50的結構,一共包含49層卷積和1層全鏈接,因此被稱爲ResNet-50。

圖8:ResNet-50模型網絡結構示意圖

 

經過運行結果能夠發現,使用ResNet在眼疾篩查數據集iChallenge-PM上,loss能有效的降低,通過5個epoch的訓練,在驗證集上的準確率能夠達到95%左右。

05 總結

本週課程中孫老師主要爲你們講解了計算機視覺中分類任務的主要內容,以眼疾識別任務爲例,講解了經典卷積神經網絡VGG、GoogLeNet和ResNet。在後期課程中,將繼續爲你們帶來內容更豐富的課程,幫助學員快速掌握深度學習方法。

【如何學習】

  1. 如何觀看配套視頻?如何代碼實踐?

視頻+代碼已經發布在AI Studio實踐平臺上,視頻支持PC端/手機端同步觀看,也鼓勵你們親手體驗運行代碼哦。打開如下連接:

https://aistudio.baidu.com/aistudio/course/introduce/888

2. 學習過程當中,有疑問怎麼辦?

加入深度學習集訓營QQ羣:726887660,班主任與飛槳研發會在羣裏進行答疑與學習資料發放。

下載安裝命令

## CPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

>> 訪問 PaddlePaddle 官網,瞭解更多相關內容

相關文章
相關標籤/搜索