深度卷積神經網絡(AlexNet)
在LeNet提出后的將近20年里,神經網絡一度被其他機器學習方法超越,如支持向量機。雖然LeNet可以在早期的小數據集上取得好的成績,但是在更大的真實數據集上的表現並不盡如人意。一方面,神經網絡計算復雜。雖然20世紀90年代也有過一些針對神經網絡的加速硬件,但並沒有像之后GPU那樣大量普及。因此,訓練一個多通道、多層和有大量參數的卷積神經網絡在當年很難完成。另一方面,當年研究者還沒有大量深入研究參數初始化和非凸優化算法等諸多領域,導致復雜的神經網絡的訓練通常較困難。
省了很多中間步驟。然而,在很長一段時間里更流行的是研究者通過勤勞與智慧所設計並生成的手工特征。這類圖像分類研究的主要流程是:
- 獲取圖像數據集;
- 使用已有的特征提取函數生成圖像的特征;
- 使用機器學習模型對圖像的特征分類。
當時認為的機器學習部分僅限最后這一步。如果那時候跟機器學習研究者交談,他們會認為機器學習既重要又優美。優雅的定理證明了許多分類器的性質。機器學習領域生機勃勃、嚴謹而且極其有用。然而,如果跟計算機視覺研究者交談,則是另外一幅景象。他們會告訴你圖像識別里“不可告人”的現實是:計算機視覺流程中真正重要的是數據和特征。也就是說,使用較干凈的數據集和較有效的特征甚至比機器學習模型的選擇對圖像分類結果的影響更大。
學習特征表示
相當長的時間里,特征都是基於各式各樣手工設計的函數從數據中提取的。事實上,不少研究者通過提出新的特征提取函數不斷改進圖像分類結果。這一度為計算機視覺的發展做出了重要貢獻。
然而,另一些研究者則持異議。他們認為特征本身也應該由學習得來。他們還相信,為了表征足夠復雜的輸入,特征本身應該分級表示。持這一想法的研究者相信,多層神經網絡可能可以學得數據的多級表征,並逐級表示越來越抽象的概念或模式。在多層神經網絡中,圖像的第一級的表示可以是在特定的位置和⻆度是否出現邊緣;而第二級的表示說不定能夠將這些邊緣組合出有趣的模式,如花紋;在第三級的表示中,也許上一級的花紋能進一步匯合成對應物體特定部位的模式。這樣逐級表示下去,最終,模型能夠較容易根據最后一級的表示完成分類任務。需要強調的是,輸入的逐級表示由多層模型中的參數決定,而這些參數都是學出來的。
盡管一直有一群執着的研究者不斷鑽研,試圖學習視覺數據的逐級表征,然而很長一段時間里這些野心都未能實現。這其中有諸多因素值得我們一一分析。
缺失要素一:數據
包含許多特征的深度模型需要大量的有標簽的數據才能表現得比其他經典方法更好。限於早期計算機有限的存儲和90年代有限的研究預算,大部分研究只基於小的公開數據集。例如,不少研究論文基於加州大學歐文分校(UCI)提供的若干個公開數據集,其中許多數據集只有幾百至幾千張圖像。這一狀況在2010年前后興起的大數據浪潮中得到改善。特別是,2009年誕生的ImageNet數據集包含了1,000大類物體,每類有多達數千張不同的圖像。這一規模是當時其他公開數據集無法與之相提並論的。ImageNet數據集同時推動計算機視覺和機器學習研究進入新的階段,使此前的傳統方法不再有優勢。
缺失要素二:硬件
深度學習對計算資源要求很高。早期的硬件計算能力有限,這使訓練較復雜的神經網絡變得很困難。然而,通用GPU的到來改變了這一格局。很久以來,GPU都是為圖像處理和計算機游戲設計的,尤其是針對大吞吐量的矩陣和向量乘法從而服務於基本的圖形變換。值得慶幸的是,這其中的數學表達與深度網絡中的卷積層的表達類似。通用GPU這個概念在2001年開始興起,涌現出諸如OpenCL和CUDA之類的編程框架。這使得GPU也在2010年前后開始被機器學習社區使用。
AlexNet
2012年,AlexNet橫空出世。這個模型的名字來源於論文第一作者的姓名Alex Krizhevsky [1]。AlexNet使用了8層卷積神經網絡,並以很大的優勢贏得了ImageNet 2012圖像識別挑戰賽。它首次證明了學習到的特征可以超越手工設計的特征,從而一舉打破計算機視覺研究的前狀。
AlexNet與LeNet的設計理念非常相似,但也有顯著的區別。
第一,與相對較小的LeNet相比,AlexNet包含8層變換,其中有5層卷積和2層全連接隱藏層,以及1個全連接輸出層。下面我們來詳細描述這些層的設計。
AlexNet第一層中的卷積窗口形狀是11×11。因為ImageNet中絕大多數圖像的高和寬均比MNIST圖像的高和寬大10倍以上,ImageNet圖像的物體占用更多的像素,所以需要更大的卷積窗口來捕獲物體。第二層中的卷積窗口形狀減小到5×55×5,之后全采用3×3。此外,第一、第二和第五個卷積層之后都使用了窗口形狀為3×3、步幅為2的最大池化層。而且,AlexNet使用的卷積通道數也大於LeNet中的卷積通道數數十倍。
緊接着最后一個卷積層的是兩個輸出個數為4096的全連接層。這兩個巨大的全連接層帶來將近1 GB的模型參數。由於早期顯存的限制,最早的AlexNet使用雙數據流的設計使一個GPU只需要處理一半模型。幸運的是,顯存在過去幾年得到了長足的發展,因此通常我們不再需要這樣的特別設計了。
第二,AlexNet將sigmoid激活函數改成了更加簡單的ReLU激活函數。一方面,ReLU激活函數的計算更簡單,例如它並沒有sigmoid激活函數中的求冪運算。另一方面,ReLU激活函數在不同的參數初始化方法下使模型更容易訓練。這是由於當sigmoid激活函數輸出極接近0或1時,這些區域的梯度幾乎為0,從而造成反向傳播無法繼續更新部分模型參數;而ReLU激活函數在正區間的梯度恆為1。因此,若模型參數初始化不當,sigmoid函數可能在正區間得到幾乎為0的梯度,從而令模型無法得到有效訓練。
第三,AlexNet通過丟棄法來控制全連接層的模型復雜度。而LeNet並沒有使用丟棄法。
第四,AlexNet引入了大量的圖像增廣,如翻轉、裁剪和顏色變化,從而進一步擴大數據集來緩解過擬合。
- AlexNet跟LeNet結構類似,但使用了更多的卷積層和更大的參數空間來擬合大規模數據集ImageNet。它是淺層神經網絡和深度神經網絡的分界線。
- 雖然看上去AlexNet的實現比LeNet的實現也就多了幾行代碼而已,但這個觀念上的轉變和真正優秀實驗結果的產生令學術界付出了很多年。
AlexNet代碼實現

1 #alexnet模型 2 import d2lzh as d2l 3 from mxnet import gluon, init, nd 4 from mxnet.gluon import data as gdata, nn 5 import os 6 import sys 7 8 net = nn.Sequential() 9 # 使用較大的11 x 11窗口來捕獲物體。同時使用步幅4來較大幅度減小輸出高和寬。這里使用的輸出通 10 # 道數比LeNet中的也要大很多 11 net.add(nn.Conv2D(96, kernel_size=11, strides=4, activation='relu'), 12 nn.MaxPool2D(pool_size=3, strides=2), 13 # 減小卷積窗口,使用填充為2來使得輸入與輸出的高和寬一致,且增大輸出通道數 14 nn.Conv2D(256, kernel_size=5, padding=2, activation='relu'), 15 nn.MaxPool2D(pool_size=3, strides=2), 16 # 連續3個卷積層,且使用更小的卷積窗口。除了最后的卷積層外,進一步增大了輸出通道數。 17 # 前兩個卷積層后不使用池化層來減小輸入的高和寬 18 nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'), 19 nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'), 20 nn.Conv2D(256, kernel_size=3, padding=1, activation='relu'), 21 nn.MaxPool2D(pool_size=3, strides=2), 22 # 這里全連接層的輸出個數比LeNet中的大數倍。使用丟棄層來緩解過擬合 23 nn.Dense(4096, activation="relu"), nn.Dropout(0.5), 24 nn.Dense(4096, activation="relu"), nn.Dropout(0.5), 25 # 輸出層。由於這里使用Fashion-MNIST,所以用類別數為10,而非論文中的1000 26 nn.Dense(10)) 27 28 X = nd.random.uniform(shape=(1, 1, 224, 224)) 29 net.initialize() 30 for layer in net: 31 X = layer(X) 32 print(layer.name, 'output shape:\t', X.shape) 33 34 # 本函數已保存在d2lzh包中方便以后使用 35 #Fashion-MNIST數據集來演示AlexNet。讀取數據的時候我們額外做了一步將圖像高和寬擴大到AlexNet使用的圖像高和寬224。 36 #這個可以通過Resize實例來實現。也就是說,我們在ToTensor實例前使用Resize實例, 37 #然后使用Compose實例來將這兩個變換串聯以方便調用。 38 39 def load_data_fashion_mnist(batch_size, resize=None, root=os.path.join( 40 '~', '.mxnet', 'datasets', 'fashion-mnist')): 41 root = os.path.expanduser(root) # 展開用戶路徑'~' 42 transformer = [] 43 if resize: 44 transformer += [gdata.vision.transforms.Resize(resize)] 45 transformer += [gdata.vision.transforms.ToTensor()] 46 transformer = gdata.vision.transforms.Compose(transformer) 47 mnist_train = gdata.vision.FashionMNIST(root=root, train=True) 48 mnist_test = gdata.vision.FashionMNIST(root=root, train=False) 49 num_workers = 0 if sys.platform.startswith('win32') else 4 50 train_iter = gdata.DataLoader( 51 mnist_train.transform_first(transformer), batch_size, shuffle=True, 52 num_workers=num_workers) 53 test_iter = gdata.DataLoader( 54 mnist_test.transform_first(transformer), batch_size, shuffle=False, 55 num_workers=num_workers) 56 return train_iter, test_iter 57 58 batch_size = 128 59 # 如出現“out of memory”的報錯信息,可減小batch_size或resize 60 train_iter, test_iter = load_data_fashion_mnist(batch_size, resize=224) 61 62 lr, num_epochs, ctx = 0.01, 5, d2l.try_gpu() 63 net.initialize(force_reinit=True, ctx=ctx, init=init.Xavier()) 64 trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) 65 d2l.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, num_epochs)
使用重復元素的網絡(VGG)
AlexNet在LeNet的基礎上增加了3個卷積層。但AlexNet作者對它們的卷積窗口、輸出通道數和構造順序均做了大量的調整。雖然AlexNet指明了深度卷積神經網絡可以取得出色的結果,但並沒有提供簡單的規則以指導后來的研究者如何設計新的網絡。我們將在本章的后續幾節里介紹幾種不同的深度網絡設計思路。
本節介紹VGG,它的名字來源於論文作者所在的實驗室Visual Geometry Group。VGG提出了可以通過重復使用簡單的基礎塊來構建深度模型的思路。
VGG塊
VGG塊的組成規律是:連續使用數個相同的填充為1、窗口形狀為3×3的卷積層后接上一個步幅為2、窗口形狀為2×2的最大池化層。卷積層保持輸入的高和寬不變,而池化層則對其減半。我們使用vgg_block
函數來實現這個基礎的VGG塊,它可以指定卷積層的數量num_convs
和輸出通道數num_channels
。
VGG網絡
與AlexNet和LeNet一樣,VGG網絡由卷積層模塊后接全連接層模塊構成。卷積層模塊串聯數個vgg_block
,其超參數由變量conv_arch
定義。該變量指定了每個VGG塊里卷積層個數和輸出通道數。全連接模塊則跟AlexNet中的一樣。
- VGG-11通過5個可以重復使用的卷積塊來構造網絡。根據每塊里卷積層個數和輸出通道數的不同可以定義出不同的VGG模型。
VGG模型代碼實現

1 #vgg網絡模型 2 import d2lzh as d2l 3 from mxnet import gluon, init, nd 4 from mxnet.gluon import nn 5 6 def vgg_block(num_convs, num_channels): 7 blk = nn.Sequential() 8 for _ in range(num_convs): 9 blk.add(nn.Conv2D(num_channels, kernel_size=3, 10 padding=1, activation='relu')) 11 blk.add(nn.MaxPool2D(pool_size=2, strides=2)) 12 return blk 13 conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512)) 14 def vgg(conv_arch): 15 net = nn.Sequential() 16 # 卷積層部分 17 for (num_convs, num_channels) in conv_arch: 18 net.add(vgg_block(num_convs, num_channels)) 19 # 全連接層部分 20 net.add(nn.Dense(4096, activation='relu'), nn.Dropout(0.5), 21 nn.Dense(4096, activation='relu'), nn.Dropout(0.5), 22 nn.Dense(10)) 23 return net 24 25 net = vgg(conv_arch) 26 net.initialize() 27 X = nd.random.uniform(shape=(1, 1, 224, 224)) 28 for blk in net: 29 X = blk(X) 30 print(blk.name, 'output shape:\t', X.shape) 31 ratio = 4 32 small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch] 33 net = vgg(small_conv_arch) 34 lr, num_epochs, batch_size, ctx = 0.05, 5, 128, d2l.try_gpu() 35 net.initialize(ctx=ctx, init=init.Xavier()) 36 trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) 37 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224) 38 d2l.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, 39 num_epochs)
網絡中的網絡(NiN)
LeNet、AlexNet和VGG在設計上的共同之處是:先以由卷積層構成的模塊充分抽取空間特征,再以由全連接層構成的模塊來輸出分類結果。其中,AlexNet和VGG對LeNet的改進主要在於如何對這兩個模塊加寬(增加通道數)和加深。本節我們介紹網絡中的網絡(NiN)。它提出了另外一個思路,即串聯多個由卷積層和“全連接”層構成的小網絡來構建一個深層網絡。
卷積層的輸入和輸出通常是四維數組(樣本,通道,高,寬),而全連接層的輸入和輸出則通常是二維數組(樣本,特征)。如果想在全連接層后再接上卷積層,則需要將全連接層的輸出變換為四維。它可以看成全連接層,其中空間維度(高和寬)上的每個元素相當於樣本,通道相當於特征。因此,NiN使用1×1卷積層來替代全連接層,從而使空間信息能夠自然傳遞到后面的層中去。下圖對比了NiN同AlexNet和VGG等網絡在結構上的主要區別。
NiN塊是NiN中的基礎塊。它由一個卷積層加兩個充當全連接層的1×1卷積層串聯而成。其中第一個卷積層的超參數可以自行設置,而第二和第三個卷積層的超參數一般是固定的。
NiN模型
NiN是在AlexNet問世不久后提出的。它們的卷積層設定有類似之處。NiN使用卷積窗口形狀分別為11×11、5×5和3×3的卷積層,相應的輸出通道數也與AlexNet中的一致。每個NiN塊后接一個步幅為2、窗口形狀為3×3的最大池化層。
除使用NiN塊以外,NiN還有一個設計與AlexNet顯著不同:NiN去掉了AlexNet最后的3個全連接層,取而代之地,NiN使用了輸出通道數等於標簽類別數的NiN塊,然后使用全局平均池化層對每個通道中所有元素求平均並直接用於分類。這里的全局平均池化層即窗口形狀等於輸入空間維形狀的平均池化層。NiN的這個設計的好處是可以顯著減小模型參數尺寸,從而緩解過擬合。然而,該設計有時會造成獲得有效模型的訓練時間的增加。
- NiN重復使用由卷積層和代替全連接層的
- 1×1卷積層構成的NiN塊來構建深層網絡。
- NiN去除了容易造成過擬合的全連接輸出層,而是將其替換成輸出通道數等於標簽類別數的NiN塊和全局平均池化層。
- NiN的以上設計思想影響了后面一系列卷積神經網絡的設計。
NiN模型代碼實現

1 #nin模型實現 2 import d2lzh as d2l 3 from mxnet import gluon, init, nd 4 from mxnet.gluon import nn 5 6 def nin_block(num_channels, kernel_size, strides, padding): 7 blk = nn.Sequential() 8 blk.add(nn.Conv2D(num_channels, kernel_size, 9 strides, padding, activation='relu'), 10 nn.Conv2D(num_channels, kernel_size=1, activation='relu'), 11 nn.Conv2D(num_channels, kernel_size=1, activation='relu')) 12 return blk 13 net = nn.Sequential() 14 net.add(nin_block(96, kernel_size=11, strides=4, padding=0), 15 nn.MaxPool2D(pool_size=3, strides=2), 16 nin_block(256, kernel_size=5, strides=1, padding=2), 17 nn.MaxPool2D(pool_size=3, strides=2), 18 nin_block(384, kernel_size=3, strides=1, padding=1), 19 nn.MaxPool2D(pool_size=3, strides=2), nn.Dropout(0.5), 20 # 標簽類別數是10 21 nin_block(10, kernel_size=3, strides=1, padding=1), 22 # 全局平均池化層將窗口形狀自動設置成輸入的高和寬 23 nn.GlobalAvgPool2D(), 24 # 將四維的輸出轉成二維的輸出,其形狀為(批量大小, 10) 25 nn.Flatten()) 26 X = nd.random.uniform(shape=(1, 1, 224, 224)) 27 net.initialize() 28 for layer in net: 29 X = layer(X) 30 print(layer.name, 'output shape:\t', X.shape) 31 lr, num_epochs, batch_size, ctx = 0.1, 5, 128, d2l.try_gpu() 32 net.initialize(force_reinit=True, ctx=ctx, init=init.Xavier()) 33 trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) 34 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224) 35 d2l.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, 36 num_epochs)
含並行連結的網絡(GoogLeNet)
在2014年的ImageNet圖像識別挑戰賽中,一個名叫GoogLeNet的網絡結構大放異彩 。它雖然在名字上向LeNet致敬,但在網絡結構上已經很難看到LeNet的影子。GoogLeNet吸收了NiN中網絡串聯網絡的思想,並在此基礎上做了很大改進。在隨后的幾年里,研究人員對GoogLeNet進行了數次改進,本節將介紹這個模型系列的第一個版本。
Inception 塊
GoogLeNet中的基礎卷積塊叫作Inception塊,得名於同名電影《盜夢空間》(Inception)。與上一節介紹的NiN塊相比,這個基礎塊在結構上更加復雜。
可以看出,Inception塊里有4條並行的線路。前3條線路使用窗口大小分別是1×1、3×3和5×5的卷積層來抽取不同空間尺寸下的信息,其中中間2個線路會對輸入先做1×1卷積來減少輸入通道數,以降低模型復雜度。第四條線路則使用3×3最大池化層,后接1×1卷積層來改變通道數。4條線路都使用了合適的填充來使輸入與輸出的高和寬一致。最后我們將每條線路的輸出在通道維上連結,並輸入接下來的層中去。
Inception塊中可以自定義的超參數是每個層的輸出通道數,我們以此來控制模型復雜度。
GoogLeNet模型
GoogLeNet跟VGG一樣,在主體卷積部分中使用5個模塊(block),每個模塊之間使用步幅為2的3×3最大池化層來減小輸出高寬。第一模塊使用一個64通道的7×7卷積層。
- inception塊相當於一個有4條線路的子網絡。它通過不同窗口形狀的卷積層和最大池化層來並行抽取信息,並使用
- 1×1卷積層減少通道數從而降低模型復雜度。
- GoogLeNet將多個設計精細的Inception塊和其他層串聯起來。其中Inception塊的通道數分配之比是在ImageNet數據集上通過大量的實驗得來的。
- GoogLeNet和它的后繼者們一度是ImageNet上最高效的模型之一:在類似的測試精度下,它們的計算復雜度往往更低。
GoogLeNet模型代碼實現

1 import d2lzh as d2l 2 from mxnet import gluon, init, nd 3 from mxnet.gluon import nn 4 5 class Inception(nn.Block): 6 # c1 - c4為每條線路里的層的輸出通道數 7 def __init__(self, c1, c2, c3, c4, **kwargs): 8 super(Inception, self).__init__(**kwargs) 9 # 線路1,單1 x 1卷積層 10 self.p1_1 = nn.Conv2D(c1, kernel_size=1, activation='relu') 11 # 線路2,1 x 1卷積層后接3 x 3卷積層 12 self.p2_1 = nn.Conv2D(c2[0], kernel_size=1, activation='relu') 13 self.p2_2 = nn.Conv2D(c2[1], kernel_size=3, padding=1, 14 activation='relu') 15 # 線路3,1 x 1卷積層后接5 x 5卷積層 16 self.p3_1 = nn.Conv2D(c3[0], kernel_size=1, activation='relu') 17 self.p3_2 = nn.Conv2D(c3[1], kernel_size=5, padding=2, 18 activation='relu') 19 # 線路4,3 x 3最大池化層后接1 x 1卷積層 20 self.p4_1 = nn.MaxPool2D(pool_size=3, strides=1, padding=1) 21 self.p4_2 = nn.Conv2D(c4, kernel_size=1, activation='relu') 22 23 def forward(self, x): 24 p1 = self.p1_1(x) 25 p2 = self.p2_2(self.p2_1(x)) 26 p3 = self.p3_2(self.p3_1(x)) 27 p4 = self.p4_2(self.p4_1(x)) 28 return nd.concat(p1, p2, p3, p4, dim=1) # 在通道維上連結輸出 29 #googlenet模型 30 b1 = nn.Sequential() 31 b1.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3, activation='relu'), 32 nn.MaxPool2D(pool_size=3, strides=2, padding=1)) 33 #第二模塊使用2個卷積層:首先是64通道的 1×1 卷積層,然后是將通道增大3倍的 3×3 卷積層。它對應Inception塊中的第二條線路。 34 b2 = nn.Sequential() 35 b2.add(nn.Conv2D(64, kernel_size=1, activation='relu'), 36 nn.Conv2D(192, kernel_size=3, padding=1, activation='relu'), 37 nn.MaxPool2D(pool_size=3, strides=2, padding=1)) 38 b3 = nn.Sequential() 39 b3.add(Inception(64, (96, 128), (16, 32), 32), 40 Inception(128, (128, 192), (32, 96), 64), 41 nn.MaxPool2D(pool_size=3, strides=2, padding=1)) 42 b4 = nn.Sequential() 43 b4.add(Inception(192, (96, 208), (16, 48), 64), 44 Inception(160, (112, 224), (24, 64), 64), 45 Inception(128, (128, 256), (24, 64), 64), 46 Inception(112, (144, 288), (32, 64), 64), 47 Inception(256, (160, 320), (32, 128), 128), 48 nn.MaxPool2D(pool_size=3, strides=2, padding=1)) 49 b5 = nn.Sequential() 50 b5.add(Inception(256, (160, 320), (32, 128), 128), 51 Inception(384, (192, 384), (48, 128), 128), 52 nn.GlobalAvgPool2D()) 53 54 net = nn.Sequential() 55 net.add(b1, b2, b3, b4, b5, nn.Dense(10)) 56 X = nd.random.uniform(shape=(1, 1, 96, 96)) 57 net.initialize() 58 for layer in net: 59 X = layer(X) 60 print(layer.name, 'output shape:\t', X.shape) 61 lr, num_epochs, batch_size, ctx = 0.1, 5, 128, d2l.try_gpu() 62 net.initialize(force_reinit=True, ctx=ctx, init=init.Xavier()) 63 trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) 64 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96) 65 d2l.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, 66 num_epochs)