介绍过去几年中数个在 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: ', X.shape)
# output
('conv0', 'output shape: ', (1L, 6L, 24L, 24L))
('pool0', 'output shape: ', (1L, 6L, 12L, 12L))
('conv1', 'output shape: ', (1L, 16L, 8L, 8L))
('pool1', 'output shape: ', (1L, 16L, 4L, 4L))
('dense0', 'output shape: ', (1L, 120L))
('dense1', 'output shape: ', (1L, 84L))
('dense2', 'output shape: ', (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: ', 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: ', 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: ', 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))