深度学习:卷积神经网络(下)【一些经典的神经网络模型】
1、深度卷积神经网络(AlexNet)
📣
AlexNet和LeNet的设计理念非常相似,但也存在显著差异。
- 首先,AlexNet比相对较小的LeNet5要深得多。
- AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。
- 其次,AlexNet使用ReLU而不是sigmoid作为其激活函数。
网络搭建:
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
net = nn.Sequential()
net.add(
# 这里,我们使用一个11*11的更大窗口来捕捉对象。
# 同时,步幅为4,以减少输出的高度和宽度。
# 另外,输出通道的数目远大于LeNet
nn.Conv2D(96, kernel_size=11, strides=4, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2D(256, kernel_size=5, padding=2, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
# 使用三个连续的卷积层和一个较小的卷积窗口。
# 除了最后的卷积层,输出通道的数量进一步增加。
# 前两个卷积层后不使用汇聚层来减小输入的高和宽
nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
nn.Conv2D(256, kernel_size=3, padding=1, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Dense(10))
2、使用块的网络(VGG)
2.1 VGG块
VGG块的组成规律是:连续使用数个相同的填充为1、窗口形状为\(3\times 3\)的卷积层后接上一个步幅为2、窗口形状为\(2\times 2\)的最大池化层。卷积层保持输入的高和宽不变,而池化层则对其减半。我们使用vgg_block
函数来实现这个基础的VGG块,它可以指定卷积层的数量num_convs
和输出通道数num_channels
。
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
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
2.2 VGG网络
与AlexNet、LeNet一样,VGG网络可以分为两部分:第一部分主要由卷积层和汇聚层组成,第二部分由全连接层组成。
网络框架
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
net = vgg(conv_arch)
3、网络中的网络(NiN)
与上面网络的不同之处:串联多个由卷积层和“全连接”层构成的小网络来构建一个深层网络
3.1 NiN块
NiN使用\(1\times 1\)卷积层来替代全连接层,从而使空间信息能够自然传递到后面的层中去。
NiN块是NiN中的基础块。它由一个卷积层加两个充当全连接层的\(1\times 1\)卷积层串联而成。其中第一个卷积层的超参数可以自行设置,而第二和第三个卷积层的超参数一般是固定的。
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
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
3.2 NiN模型
NiN使用卷积窗口形状分别为\(11\times 11\)、\(5\times 5\)和\(3\times 3\)的卷积层,相应的输出通道数也与AlexNet中的一致。
- 每个NiN块后接一个步幅为2、窗口形状为\(3\times 3\)的最大池化层。
除使用NiN块以外,NiN还有一个设计与AlexNet显著不同:
- NiN去掉了AlexNet最后的3个全连接层,取而代之地,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())
4、含并行连结的网络(GoogLeNet)
GoogLeNet吸收了NiN中网络串联网络的思想,并在此基础上做了很大改进。
4.1 Inception 块
在GoogLeNet中,基本的卷积块被称为Inception块(Inception block)。
Inception块里有4条并行的线路。前3条线路使用窗口大小分别是\(1\times 1\)、\(3\times 3\)和\(5\times 5\)的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做\(1\times 1\)卷积来减少输入通道数,以降低模型复杂度。第四条线路则使用\(3\times 3\)最大池化层,后接\(1\times 1\)卷积层来改变通道数。4条线路都使用了合适的填充来使输入与输出的高和宽一致。最后我们将每条线路的输出在通道维上连结,并输入接下来的层中去。
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
class Inception(nn.Block):
# c1--c4是每条路径的输出通道数
def __init__(self, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# 线路1,单1x1卷积层
self.p1_1 = nn.Conv2D(c1, kernel_size=1, activation='relu')
# 线路2,1x1卷积层后接3x3卷积层
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,1x1卷积层后接5x5卷积层
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,3x3最大汇聚层后接1x1卷积层
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 np.concatenate((p1, p2, p3, p4), axis=1)
4.2 GoogLeNet模型
太复杂了。。。
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, activation='relu'),
nn.Conv2D(192, kernel_size=3, padding=1, activation='relu'),
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))
5、残差网络(ResNet)
5.1 残差块
ResNet沿用了VGG全\(3\times 3\)卷积层的设计。
残差块里首先有2个有相同输出通道数的\(3\times 3\)卷积层。
- 每个卷积层后接一个批量归一化层和ReLU激活函数。
然后我们将输入跳过这2个卷积运算后直接加在最后的ReLU激活函数前。
这样的设计要求2个卷积层的输出与输入形状一样,从而可以相加。
如果想改变通道数,就需要引入一个额外的\(1\times 1\)卷积层来将输入变换成需要的形状后再做相加运算。
5.2 ResNet模型
ResNet的前两层跟之前介绍的GoogLeNet中的一样:在输出通道数为64、步幅为2的\(7\times 7\)卷积层后接步幅为2的\(3\times 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))
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
#为ResNet加入所有残差块。这里每个模块使用2个残差块
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))
📣
这里每个模块里有4个卷积层(不计算\(1\times 1\)卷积层),加上最开始的卷积层和最后的全连接层,共计18层。
这个模型通常也被称为ResNet-18。
通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。
虽然ResNet的主体架构跟GoogLeNet的类似,但ResNet结构更简单,修改也更方便。
6、稠密连接网络(DenseNet)
图中将部分前后相邻的运算抽象为模块\(A\)和模块\(B\)。
与ResNet的主要区别在于:
- DenseNet里模块\(B\)的输出不是像ResNet那样和模块\(A\)的输出相加,而是在通道维上连结。
- 这样模块\(A\)的输出可以直接传入模块\(B\)后面的层。
- 在这个设计里,模块\(A\)直接跟模块\(B\)后面的所有层连接在了一起。这也是它被称为“稠密连接”的原因。
DenseNet的主要构建模块是稠密块(dense block)和过渡层(transition layer)。前者定义了输入和输出是如何连结的,后者则用来控制通道数,使之不过大。
6.1 稠密块
DenseNet使用了ResNet改良版的“批量归一化、激活和卷积”结构
from mxnet import np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
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
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
6.2 过渡层
由于每个稠密块都会带来通道数的增加,使用过多则会带来过于复杂的模型。过渡层用来控制模型复杂度。它通过\(1\times1\)卷积层来减小通道数,并使用步幅为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
6.3 DenseNet模型
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为当前的通道数
num_channels, growth_rate = 64, 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:
num_channels //= 2
net.add(transition_block(num_channels))
net.add(nn.BatchNorm(),
nn.Activation('relu'),
nn.GlobalAvgPool2D(),
nn.Dense(10))
参考文献
《动手学深度学习》