MXNET:卷積神經網絡


介紹過去幾年中數個在 ImageNet 競賽(一個著名的計算機視覺競賽)取得優異成績的深度卷積神經網絡。

LeNet

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))

訓練:

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

AlexNet

2012 年 AlexNet [1],名字來源於論文一作姓名 Alex Krizhevsky,橫空出世,它使用 8 層卷積神經網絡以很大的優勢贏得了 ImageNet 2012 圖像識別挑戰賽。它首次證明了學習到的特征可以超越手工設計的特征,從而一舉打破計算機視覺研究的前狀。
AlextNet 與 LeNet 的設計理念非常相似。但也有非常顯著的區別。

  • 與相對較小的 LeNet 相比,AlexNet 包含 8 層變換,其中有五層卷積和兩層全連接隱含層,以及一個輸出層。
  • 將 sigmoid 激活函數改成了更加簡單的 relu 函數 f(x)=max(x,0)。它計算上更簡單,同時在不同的參數初始化方法下收斂更加穩定。
  • 通過丟棄法來控制全連接層的模型復雜度。
  • 引入了大量的圖片增廣,例如翻轉、裁剪和顏色變化,進一步擴大數據集來減小過擬合。

AlexNet 跟 LeNet 結構類似,但使用了更多的卷積層和更大的參數空間來擬合大規模數據集 ImageNet。它是淺層神經網絡和深度神經網絡的分界線。雖然看上去 AlexNet 的實現比 LeNet 也就就多了幾行而已。但這個觀念上的轉變和真正優秀實驗結果的產生,學術界整整花了 20 年

VGG

雖然 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 個全連接層。

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)

NiN

它提出了另外一個思路,即串聯多個由卷積層和“全連接”層(1x1卷積層)構成的小網絡來構建一個深層網絡。

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 的設計思想影響了后面的一系列網絡的設計。

GoogleLeNet

在 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 數據集上通過大量的實驗得來。

ResNet

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

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))


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM