介紹過去幾年中數個在 ImageNet 競賽(一個著名的計算機視覺競賽)取得優異成績的深度卷積神經網絡。網絡
LeNet 證實了經過梯度降低訓練卷積神經網絡能夠達到手寫數字識別的最早進的結果。這個奠定性的工做第一次將卷積神經網絡推上舞臺,爲世人所知。架構
net = nn.Sequential() net.add( nn.Conv2D(channels=6, kernel_size=5, activation='sigmoid'), nn.MaxPool2D(pool_size=2, strides=2), nn.Conv2D(channels=16, kernel_size=5, activation='sigmoid'), nn.MaxPool2D(pool_size=2, strides=2), # Dense 會默認將(批量大小,通道,高,寬)形狀的輸入轉換成 #(批量大小,通道 x 高 x 寬)形狀的輸入。 nn.Dense(120, activation='sigmoid'), nn.Dense(84, activation='sigmoid'), nn.Dense(10) ) X = nd.random.uniform(shape=(1, 1, 28, 28)) net.initialize() for layer in net: X = layer(X) print(layer.name, 'output shape:\t', X.shape) # output ('conv0', 'output shape:\t', (1L, 6L, 24L, 24L)) ('pool0', 'output shape:\t', (1L, 6L, 12L, 12L)) ('conv1', 'output shape:\t', (1L, 16L, 8L, 8L)) ('pool1', 'output shape:\t', (1L, 16L, 4L, 4L)) ('dense0', 'output shape:\t', (1L, 120L)) ('dense1', 'output shape:\t', (1L, 84L)) ('dense2', 'output shape:\t', (1L, 10L))
訓練:dom
batch_size = 256 train_iter, test_iter = gb.load_data_fashion_mnist(batch_size=batch_size) lr = 0.8 num_epochs = 5 net.initialize(force_reinit=True, ctx=mx.cpu(), init=init.Xavier()) trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr}) loss = gloss.SoftmaxCrossEntropyLoss() # net, train_iter, test_iter, loss, num_epochs, batch_size, params=None, lr=None, trainer=None gb.train_cpu(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, trainer) # output epoch 1, loss 2.3203, train acc 0.103, test acc 0.102 epoch 2, loss 2.3032, train acc 0.108, test acc 0.102 epoch 3, loss 2.2975, train acc 0.121, test acc 0.158 ... epoch 8, loss 0.1189, train acc 0.964, test acc 0.972 epoch 9, loss 0.1023, train acc 0.969, test acc 0.975 epoch 10, loss 0.0906, train acc 0.972, test acc 0.976
2012 年 AlexNet [1],名字來源於論文一做姓名 Alex Krizhevsky,橫空出世,它使用 8 層卷積神經網絡以很大的優點贏得了 ImageNet 2012 圖像識別挑戰賽。它首次證實了學習到的特徵能夠超越手工設計的特徵,從而一舉打破計算機視覺研究的前狀。
AlextNet 與 LeNet 的設計理念很是類似。但也有很是顯著的區別。ide
AlexNet 跟 LeNet 結構相似,但使用了更多的卷積層和更大的參數空間來擬合大規模數據集 ImageNet。它是淺層神經網絡和深度神經網絡的分界線。雖然看上去 AlexNet 的實現比 LeNet 也就就多了幾行而已。但這個觀念上的轉變和真正優秀實驗結果的產生,學術界整整花了 20 年。函數
雖然 AlexNet 指明瞭深度卷積神經網絡能夠取得很高的結果,但並無提供簡單的規則來告訴後來的研究者如何設計新的網絡。
VGG它名字來源於論文做者所在實驗室 Visual Geometry Group。VGG 提出了能夠經過重複使用簡單的基礎塊來構建深層模型。學習
咱們使用 vgg_block 函數來實現這個基礎塊,它能夠指定使用卷積層的數量和其輸出通道數。大數據
def vgg_block(num_convs, num_channels): blk = nn.Sequential() for _ in range(num_convs): blk.add(nn.Conv2D( num_channels, kernel_size=3, padding=1, activation='relu')) blk.add(nn.MaxPool2D(pool_size=2, strides=2)) return blk
咱們根據架構實現 VGG 11: 8 個卷積層和 3 個全鏈接層。google
def vgg(conv_arch): net = nn.Sequential() # 卷積層部分。 for (num_convs, num_channels) in conv_arch: net.add(vgg_block(num_convs, num_channels)) # 全鏈接層部分。 net.add(nn.Dense(4096, activation='relu'), nn.Dropout(0.5), nn.Dense(4096, activation='relu'), nn.Dropout(0.5), nn.Dense(10)) return net conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512)) net = vgg(conv_arch)
它提出了另一個思路,即串聯多個由卷積層和「全鏈接」層(1x1卷積層)構成的小網絡來構建一個深層網絡。.net
NiN 中的一個基礎塊由一個卷積層外加兩個充當全鏈接層的 1×1 卷積層構成。設計
def nin_block(num_channels, kernel_size, strides, padding): blk = nn.Sequential() blk.add(nn.Conv2D(num_channels, kernel_size, strides, padding, activation='relu'), nn.Conv2D(num_channels, kernel_size=1, activation='relu'), nn.Conv2D(num_channels, kernel_size=1, activation='relu')) return blk
除了使用 NiN 塊外,NiN 還有一個重要的跟 AlexNet 不一樣的地方:NiN 去掉了最後的三個全鏈接層,取而代之的是使用輸出通道數等於標籤類數的卷積層,而後使用一個窗口爲輸入高寬的平均池化層來將每一個通道里的數值平均成一個標量直接用於分類。這個設計好處是能夠顯著的減少模型參數大小,從而能很好的避免過擬合,但它也可能會形成訓練時收斂變慢。
net = nn.Sequential() net.add( nin_block(96, kernel_size=11, strides=4, padding=0), nn.MaxPool2D(pool_size=3, strides=2), nin_block(256, kernel_size=5, strides=1, padding=2), nn.MaxPool2D(pool_size=3, strides=2), nin_block(384, kernel_size=3, strides=1, padding=1), nn.MaxPool2D(pool_size=3, strides=2), nn.Dropout(0.5), # 標籤類數是 10。 nin_block(10, kernel_size=3, strides=1, padding=1), # 全局平均池化層將窗口形狀自動設置成輸出的高和寬。 nn.GlobalAvgPool2D(), # 將四維的輸出轉成二維的輸出,其形狀爲(批量大小,10)。 nn.Flatten()) X = nd.random.uniform(shape=(1, 1, 224, 224)) net.initialize() for layer in net: X = layer(X) print(layer.name, 'output shape:\t', X.shape) # output sequential1 output shape: (1, 96, 54, 54) pool0 output shape: (1, 96, 26, 26) sequential2 output shape: (1, 256, 26, 26) pool1 output shape: (1, 256, 12, 12) sequential3 output shape: (1, 384, 12, 12) pool2 output shape: (1, 384, 5, 5) dropout0 output shape: (1, 384, 5, 5) sequential4 output shape: (1, 10, 5, 5) pool3 output shape: (1, 10, 1, 1) flatten0 output shape: (1, 10)
雖然由於精度和收斂速度等問題 NiN 並無像本章中介紹的其餘網絡那麼被普遍使用,但 NiN 的設計思想影響了後面的一系列網絡的設計。
在 2014 年的 Imagenet 競賽中,一個名叫 GoogLeNet [1] 的網絡結構大放光彩。它雖然在名字上是向 LeNet 致敬,但在網絡結構上已經很難看到 LeNet 的影子。
GoogLeNet 吸取了 NiN 的網絡嵌套網絡的想法,並在此基礎上作了很大的改進。
在隨後的幾年裏研究人員對它進行了數次改進,本小節將介紹這個模型系列的第一個版本。
GoogLeNet 中的基礎卷積塊叫作 Inception,得名於同名電影《盜夢空間》(Inception),寓意夢中嵌套夢。
class Inception(nn.Block): # c1 - c4 爲每條線路里的層的輸出通道數。 def __init__(self, c1, c2, c3, c4, **kwargs): super(Inception, self).__init__(**kwargs) # 線路 1,單 1 x 1 卷積層。 self.p1_1 = nn.Conv2D(c1, kernel_size=1, activation='relu') # 線路 2,1 x 1 卷積層後接 3 x 3 卷積層。 self.p2_1 = nn.Conv2D(c2[0], kernel_size=1, activation='relu') self.p2_2 = nn.Conv2D(c2[1], kernel_size=3, padding=1, activation='relu') # 線路 3,1 x 1 卷積層後接 5 x 5 卷積層。 self.p3_1 = nn.Conv2D(c3[0], kernel_size=1, activation='relu') self.p3_2 = nn.Conv2D(c3[1], kernel_size=5, padding=2, activation='relu') # 線路 4,3 x 3 最大池化層後接 1 x 1 卷積層。 self.p4_1 = nn.MaxPool2D(pool_size=3, strides=1, padding=1) self.p4_2 = nn.Conv2D(c4, kernel_size=1, activation='relu') def forward(self, x): p1 = self.p1_1(x) p2 = self.p2_2(self.p2_1(x)) p3 = self.p3_2(self.p3_1(x)) p4 = self.p4_2(self.p4_1(x)) # 在通道維上合併輸出。 return nd.concat(p1, p2, p3, p4, dim=1)
第一個模塊
b1 = nn.Sequential() b1.add( nn.Conv2D(64, kernel_size=7, strides=2, padding=3, activation='relu'), nn.MaxPool2D(pool_size=3, strides=2, padding=1) )
第二個模塊
b2 = nn.Sequential() b2.add( nn.Conv2D(64, kernel_size=1), nn.Conv2D(192, kernel_size=3, padding=1), nn.MaxPool2D(pool_size=3, strides=2, padding=1) )
第三個模塊
b3 = nn.Sequential() b3.add( Inception(64, (96, 128), (16, 32), 32), Inception(128, (128, 192), (32, 96), 64), nn.MaxPool2D(pool_size=3, strides=2, padding=1) )
第四個模塊
b4 = nn.Sequential() b4.add( Inception(192, (96, 208), (16, 48), 64), Inception(160, (112, 224), (24, 64), 64), Inception(128, (128, 256), (24, 64), 64), Inception(112, (144, 288), (32, 64), 64), Inception(256, (160, 320), (32, 128), 128), nn.MaxPool2D(pool_size=3, strides=2, padding=1) )
第五個模塊
b5 = nn.Sequential() b5.add( Inception(256, (160, 320), (32, 128), 128), Inception(384, (192, 384), (48, 128), 128), nn.GlobalAvgPool2D() ) net = nn.Sequential() net.add(b1, b2, b3, b4, b5, nn.Dense(10))
演示各個模塊之間的輸出形狀變化
X = nd.random.uniform(shape=(1, 1, 96, 96)) net.initialize() for layer in net: X = layer(X) print(layer.name, 'output shape:\t', X.shape) # output sequential0 output shape: (1, 64, 24, 24) sequential1 output shape: (1, 192, 12, 12) sequential2 output shape: (1, 480, 6, 6) sequential3 output shape: (1, 832, 3, 3) sequential4 output shape: (1, 1024, 1, 1) dense0 output shape: (1, 10)
Inception 塊至關於一個有四條線路的子網絡,它經過不一樣窗口大小的卷積層和最大池化層來並行抽取信息,並使用 1×1 卷積層減低通道數來減小模型複雜度。
GoogLeNet 將多個精細設計的 Inception 塊和其餘層串聯起來。其通道分配比例是在 ImageNet 數據集上經過大量的實驗得來。
class Residual(nn.Block): def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs): super(Residual, self).__init__(**kwargs) self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1, strides=strides) self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.Conv2D(num_channels, kernel_size=1, strides=strides) else: self.conv3 = None self.bn1 = nn.BatchNorm() self.bn2 = nn.BatchNorm() def forward(self, X): Y = nd.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) return nd.relu(Y + X)
查看輸出形狀
X = nd.random.uniform(shape=(4, 3, 6, 6)) blk = Residual(3) blk.initialize() blk(X).shape # (4,3,6,6) blk = Residual(6, use_1x1conv=True, strides=2) blk.initialize() blk(X).shape #(4,6,3,3)
ResNet 則是使用四個由殘差塊組成的模塊,每一個模塊使用若干個一樣輸出通道的殘差塊。第一個模塊的通道數同輸入一致,同時由於以前已經使用了步幅爲 2 的最大池化層,因此也不減少高寬。以後的每一個模塊在第一個殘差塊裏將上一個模塊的通道數翻倍,並減半高寬。
def resnet_block(num_channels, num_residuals, first_block=False): blk = nn.Sequential() for i in range(num_residuals): if i == 0 and not first_block: blk.add(Residual(num_channels, use_1x1conv=True, strides=2)) else: blk.add(Residual(num_channels)) return blk
ResNet18的網絡結構:
net = nn.Sequential() net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3), nn.BatchNorm(), nn.Activation('relu'), nn.MaxPool2D(pool_size=3, strides=2, padding=1)) net.add(resnet_block(64, 2, first_block=True), resnet_block(128, 2), resnet_block(256, 2), resnet_block(512, 2)) net.add(nn.GlobalAvgPool2D(), nn.Dense(10)) X = nd.random.uniform(shape=(1, 1, 224, 224)) net.initialize() for layer in net: X = layer(X) print(layer.name, 'output shape:\t', X.shape) # output conv5 output shape: (1, 64, 112, 112) batchnorm4 output shape: (1, 64, 112, 112) relu0 output shape: (1, 64, 112, 112) pool0 output shape: (1, 64, 56, 56) sequential1 output shape: (1, 64, 56, 56) sequential2 output shape: (1, 128, 28, 28) sequential3 output shape: (1, 256, 14, 14) sequential4 output shape: (1, 512, 7, 7) pool1 output shape: (1, 512, 1, 1) dense0 output shape: (1, 10)
DenseNet 的主要構建模塊是稠密塊和過渡塊,前者定義了輸入和輸出是如何合併的,後者則用來控制通道數不要過大。
定義Resnet時已經「改良「的卷積塊
def conv_block(num_channels): blk = nn.Sequential() blk.add(nn.BatchNorm(), nn.Activation('relu'), nn.Conv2D(num_channels, kernel_size=3, padding=1)) return blk
稠密塊由多個 conv_block 組成,每塊使用相同的輸出通道數。但在正向傳播時,咱們將每塊的輸出在通道維上同其輸出合併進入下一個塊。
class DenseBlock(nn.Block): def __init__(self, num_convs, num_channels, **kwargs): super(DenseBlock, self).__init__(**kwargs) self.net = nn.Sequential() for _ in range(num_convs): self.net.add(conv_block(num_channels)) def forward(self, X): for blk in self.net: Y = blk(X) # 在通道維上將輸入和輸出合併。 X = nd.concat(X, Y, dim=1) return Y
咱們定義一個有兩個輸出通道數爲 10 的卷積塊,使用通道數爲 3 的輸入時,咱們會獲得通道數爲 3+2×10=23 的輸出。
卷積塊的通道數控制了輸出通道數相對於輸入通道數的增加,所以也被稱爲增加率(growth rate)
blk = DenseBlock(2, 10) blk.initialize() X = nd.random.uniform(shape=(4,3,8,8)) Y = blk(X) Y.shape
過渡塊(transition block)則用來控制模型複雜度。它經過 1×1 卷積層來減少通道數,同時使用步幅爲 2 的平均池化層來將高寬減半來進一步下降複雜度。
def transition_block(num_channels): blk = nn.Sequential() blk.add(nn.BatchNorm(), nn.Activation('relu'), nn.Conv2D(num_channels, kernel_size=1), nn.AvgPool2D(pool_size=2, strides=2)) return blk
DenseNet 21模型
net = nn.Sequential() net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3), nn.BatchNorm(), nn.Activation('relu'), nn.MaxPool2D(pool_size=3, strides=2, padding=1)) num_channels = 64 growth_rate = 32 num_convs_in_dense_blocks = [4, 4, 4, 4] for i, num_convs in enumerate(num_convs_in_dense_blocks): net.add(DenseBlock(num_convs, growth_rate)) # 上一個稠密的輸出通道數。 num_channels += num_convs * growth_rate # 在稠密塊之間加入通道數減半的過渡塊。 if i != len(num_convs_in_dense_blocks) - 1: net.add(transition_block(num_channels // 2)) net.add(nn.BatchNorm(), nn.Activation('relu'), nn.GlobalAvgPool2D(), nn.Dense(10))