總結一些CNN裏的經典網絡架構:
一、批量歸一化(batch normalization)
batch_norm是一種可以使較深的神經網絡能容易訓練的一種方法。一般放在仿射變換和激活函數之間。
𝜙(BN(𝑥))
他的實現代碼如下:
import utils as d2l
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import nn
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通過autograd來判斷當前模式是訓練模式還是預測模式
if not autograd.is_training():
# 如果是在預測模式下,直接使用傳入的移動平均所得的均值和方差
X_hat = (X - moving_mean) / nd.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全連接層的情況,計算特徵維上的均值和方差
mean = X.mean(axis=0)
var = ((X - mean) ** 2).mean(axis=0)
else:
# 使用二維卷積層的情況,計算通道維上(axis=1)的均值和方差。這裏我們需要保持
# X的形狀以便後面可以做廣播運算
mean = X.mean(axis=(0, 2, 3), keepdims=True)
var = ((X - mean) ** 2).mean(axis=(0, 2, 3), keepdims=True)
# 訓練模式下用當前的均值和方差做標準化
X_hat = (X - mean) / nd.sqrt(var + eps)
# 更新移動平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 拉伸和偏移
return Y, moving_mean, moving_var
然後,自定義一個batch_norm層:
class BatchNorm(nn.Block):
def __init__(self, num_features, num_dims, **kwargs):
super(BatchNorm, self).__init__(**kwargs)
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)
# 參與求梯度和迭代的拉伸和偏移參數,分別初始化成1和0
self.gamma = self.params.get('gamma', shape=shape, init=init.One())
self.beta = self.params.get('beta', shape=shape, init=init.Zero())
# 不參與求梯度和迭代的變量,全在內存上初始化成0
self.moving_mean = nd.zeros(shape)
self.moving_var = nd.zeros(shape)
def forward(self, X):
# 如果X不在內存上,將moving_mean和moving_var複製到X所在顯存上
if self.moving_mean.context != X.context:
self.moving_mean = self.moving_mean.copyto(X.context)
self.moving_var = self.moving_var.copyto(X.context)
# 保存更新過的moving_mean和moving_var
Y, self.moving_mean, self.moving_var = batch_norm(
X, self.gamma.data(), self.beta.data(), self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y
然後,可以構建一個使用批量歸一化的LeNet
net = nn.Sequential()
net.add(nn.Conv2D(6, kernel_size=5),
BatchNorm(6, num_dims=4),
nn.Activation('sigmoid'),
nn.MaxPool2D(pool_size=2, strides=2),
nn.Conv2D(16, kernel_size=5),
BatchNorm(16, num_dims=4),
nn.Activation('sigmoid'),
nn.MaxPool2D(pool_size=2, strides=2),
nn.Dense(120),
BatchNorm(120, num_dims=2),
nn.Activation('sigmoid'),
nn.Dense(84),
BatchNorm(84, num_dims=2),
nn.Activation('sigmoid'),
nn.Dense(10))
二、NIN
LeNet、AlexNet和VGG在設計上的共同之處是:先以由卷積層構成的模塊充分抽取空間特徵,再以由全連接層構成的模塊來輸出分類結果。其中,AlexNet和VGG對LeNet的改進主要在於如何對這兩個模塊加寬(增加通道數)和加深。
NIN的一個特點是使用1×1卷積層來替代全連接層,從而使空間信息能夠自然傳遞到後面的層中去。
nin_block是一個基礎塊,它由一個卷積層加兩個充當全連接層的1×11×1卷積層串聯而成:
import utils as d2l
from mxnet import gluon, init, nd
from mxnet.gluon import nn
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是在AlexNet問世不久後提出的。它們的卷積層設定有類似之處。NiN使用卷積窗口形狀分別爲11×1111×11、5×55×5和3×33×3的卷積層,相應的輸出通道數也與AlexNet中的一致。每個NiN塊後接一個步幅爲2、窗口形狀爲3×33×3的最大池化層。
除使用NiN塊以外,NiN還有一個設計與AlexNet顯著不同:NiN去掉了AlexNet最後的3個全連接層,取而代之地,NiN使用了輸出通道數等於標籤類別數的NiN塊,然後使用全局平均池化層對每個通道中所有元素求平均並直接用於分類。這裏的全局平均池化層即窗口形狀等於輸入空間維形狀的平均池化層。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())
三、GoogleNet
GoogLeNet中的基礎卷積塊叫作Inception塊。Inception塊裏有4條並行的線路。前3條線路使用窗口大小分別1×1、3×3和5×5的卷積層來抽取不同空間尺寸下的信息,其中中間2個線路會對輸入先做1×1卷積來減少輸入通道數,以降低模型複雜度。第四條線路則使用3×3最大池化層,後接1×1卷積層來改變通道數。4條線路都使用了合適的填充來使輸入與輸出的高和寬一致。最後我們將每條線路的輸出在通道維上連結,並輸入接下來的層中去。Inception塊中可以自定義的超參數是每個層的輸出通道數,我們以此來控制模型複雜度。
import utils as d2l
from mxnet import gluon, init, nd
from mxnet.gluon import nn
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) # 在通道維上連結輸出
GoogleNet由五個模塊構成,如下:
#第一模塊使用一個64通道的 7×7 卷積層。
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))
#第二模塊使用2個卷積層:首先是64通道的 1×1 卷積層,然後是將通道增大3倍的 3×3 卷積層。它對應Inception塊中的第二條線路。
b2 = nn.Sequential()
b2.add(nn.Conv2D(64, kernel_size=1, activation='relu'),
nn.Conv2D(192, kernel_size=3, padding=1, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2, padding=1))
#第三模塊串聯2個完整的Inception塊。第一個Inception塊的輸出通道數爲 64+128+32+32=256 ,其中4條線路的輸出通道數比例爲 64:128:32:32=2:4:1:1 。其中第二、第三條線路先分別將輸入通道數減小至 96/192=1/2 和 16/192=1/12 後,再接上第二層卷積層。第二個Inception塊輸出通道數增至 128+192+96+64=480 ,每條線路的輸出通道數之比爲 128:192:96:64=4:6:3:2 。其中第二、第三條線路先分別將輸入通道數減小至 128/256=1/2 和 32/256=1/8 。
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))
#第四模塊更加複雜。它串聯了5個Inception塊,其輸出通道數分別是 192+208+48+64=512 、 160+224+64+64=512 、 128+256+64+64=512 、 112+288+64+64=528 和 256+320+128+128=832 。這些線路的通道數分配和第三模塊中的類似,首先是含 3×3 卷積層的第二條線路輸出最多通道,其次是僅含 1×1 卷積層的第一條線路,之後是含 5×5 卷積層的第三條線路和含 3×3 最大池化層的第四條線路。其中第二、第三條線路都會先按比例減小通道數。這些比例在各個Inception塊中都略有不同。
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))
#第五模塊有輸出通道數爲 256+320+128+128=832 和 384+384+128+128=1024 的兩個Inception塊。其中每條線路的通道數的分配思路和第三、第四模塊中的一致,只是在具體數值上有所不同。需要注意的是,第五模塊的後面緊跟輸出層,該模塊同NiN一樣使用全局平均池化層來將每個通道的高和寬變成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))
四、ResNet
ResNet沿用了VGG全3×3卷積層的設計。殘差塊裏首先有2個有相同輸出通道數的3×3卷積層。每個卷積層後接一個批量歸一化層和ReLU激活函數。然後我們將輸入跳過這2個卷積運算後直接加在最後的ReLU激活函數前。這樣的設計要求2個卷積層的輸出與輸入形狀一樣,從而可以相加。如果想改變通道數,就需要引入一個額外的1×1卷積層來將輸入變換成需要的形狀後再做相加運算。
import d2lzh as d2l
from mxnet import gluon, init, nd
from mxnet.gluon import nn
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)
ResNet模型如下:
#ResNet的前兩層跟之前介紹的GoogLeNet中的一樣:在輸出通道數爲64、步幅爲2的 7×7 卷積層後接步幅爲2的 3×3 的最大池化層。不同之處在於ResNet每個卷積層後增加的批量歸一化層。
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))
#GoogLeNet在後面接了4個由Inception塊組成的模塊。ResNet則使用4個由殘差塊組成的模塊,每個模塊使用若干個同樣輸出通道數的殘差塊。第一個模塊的通道數同輸入通道數一致。由於之前已經使用了步幅爲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
net.add(resnet_block(64, 2, first_block=True),
resnet_block(128, 2),
resnet_block(256, 2),
resnet_block(512, 2))
#最後,與GoogLeNet一樣,加入全局平均池化層後接上全連接層輸出。
net.add(nn.GlobalAvgPool2D(), nn.Dense(10))
五、DesNet
Resnet和DesNet最大的區別在於他們的連接方式,前者是用+連接,後者是用concat方式連接。
DenseNet的主要構建模塊是稠密塊(dense block)和過渡層(transition layer)。前者定義了輸入和輸出是如何連結的,後者則用來控制通道數,使之不過大。
DenseNet使用了ResNet改良版的“批量歸一化、激活和卷積”結構。他是先做了batch_norm和relu再進行卷積:
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 X
由於每個稠密塊都會帶來通道數的增加,使用過多則會帶來過於複雜的模型。過渡層用來控制模型複雜度。它通過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
下面是DesNet的網絡架構:
#DenseNet首先使用同ResNet一樣的單卷積層和最大池化層。
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))
#類似於ResNet接下來使用的4個殘差塊,DenseNet使用的是4個稠密塊。同ResNet一樣,我們可以設置每個稠密塊使用多少個卷積層。這裏我們設成4,從而與上一節的ResNet-18保持一致。稠密塊裏的卷積層通道數(即增長率)設爲32,所以每個稠密塊將增加128個通道。
#ResNet裏通過步幅爲2的殘差塊在每個模塊之間減小高和寬。這裏我們則使用過渡層來減半高和寬,並減半通道數。
num_channels, growth_rate = 64, 32 # num_channels爲當前的通道數
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:
num_channels //= 2
net.add(transition_block(num_channels))
#同ResNet一樣,最後接上全局池化層和全連接層來輸出。
net.add(nn.BatchNorm(), nn.Activation('relu'), nn.GlobalAvgPool2D(),
nn.Dense(10))