携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情
常见卷积神经网络
介绍一些常见卷积神经网络,认识以及编写卷积神经网络。
LeNet
LeNet 网络结构图如下:
该模型由两部分组成:
- 卷积编码器:由两个卷积块组成;每个卷积块中的基本单元是一个卷积层、一个
sigmoid激活函数和平均池化层。每个卷积层使用卷积核和一个sigmoid激活函数。第一卷积层有6个输出通道,而第二个卷积层有16个输出通道。卷积的输出形状由批量大小、通道数、高度、宽度决定。 - 全连接层密集块:由三个全连接层组成。LeNet的稠密块有三个全连接层,分别有120、84和 n 个输出。输出层的 n 维对应于最后输出结果的数量。
利用 Fashion-MNIST 作为数据集,当一个大小为 的单通道图像,进入模型,整体过程阐述:当输入形状 的单通道图像,经过第一个卷积层,卷积层使用的是 2 像素作为填充,输出形状是 ,第一个卷积层输出 6 个通道,第一层的输出形状:。接下来是平均池化层,大小 ,步幅为 2,经过这一层的输出形状为。第二个卷积层卷积核为,没填充,输出形状为 ,输出通道数为 16,最后输出形状为,经过平均池化层,大小 ,步幅为 2,,最终形状为.
卷积层块的输出形状为(batch_size, output_channels, height, width),所以卷积层块输出形状为。当进入全连接层时,需要将卷积层输出的结果变平,展平成一个向量,形状就是,输入进入全连接层。
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
# 输入 1 * 28 * 28
self.conv = nn.Sequential(
# 卷积层1
# 在输入基础上增加了padding,28 * 28 -> 32 * 32
# 1 * 32 * 32 -> 6 * 28 * 28
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), nn.Sigmoid(),
# 6 * 28 * 28 -> 6 * 14 * 14
nn.AvgPool2d(kernel_size=2, stride=2), # kernel_size, stride
# 卷积层2
# 6 * 14 * 14 -> 16 * 10 * 10
nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5), nn.Sigmoid(),
# 16 * 10 * 10 -> 16 * 5 * 5
nn.AvgPool2d(kernel_size=2, stride=2)
nn.Flatten(),
# 全连接层1
nn.Linear(in_features=16 * 5 * 5, out_features=120), nn.Sigmoid(),
# 全连接层2
nn.Linear(in_features=120, out_features=84), nn.Sigmoid(),
nn.Linear(in_features=84, out_features=10)
)
AlexNet
网络结构图如下:
AlexNet 与 LeNet 设计理念比较类似,但是有着显著的区别。
- AlexNet的第一层,卷积窗口的形状是,可以处理更大的图像,而不是的单通道图像,捕获目标更多。
- 第二层中的卷积窗口形状被缩减为,接下来的三层都是是。
- 在第一层、第二层和第五层卷积层之后,加入窗口形状为、步幅为2的最大池化层。
- 卷积层结束后,有两个全连接层,分别有4096个输出。
- AlexNet 将 sigmoid 激活函数改为更简单的 ReLU 激活函数。
- AlexNet 通过暂退法控制全连接层的模型复杂度,而 LeNet 只使用了权重衰减。
- AlexNet 引入了大量的图像增广,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
# convolution layer will change input shape into: floor((input_shape - kernel_size + padding + stride) / stride)
# input shape: 1 * 224 * 224
# convolution part
self.conv = nn.Sequential(
# conv layer 1
# floor((224 - 11 + 2 + 4) / 4) = floor(54.75) = 54
# conv: 1 * 224 * 224 -> 96 * 54 * 54
nn.Conv2d(in_channels=1, out_channels=96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
# floor((54 - 3 + 2) / 2) = floor(26.5) = 26
# 96 * 54 * 54 -> 96 * 26 * 26
nn.MaxPool2d(kernel_size=3, stride=2),
# conv layer 2: decrease kernel size, add padding to keep input and output size same, increase channel number
# floor((26 - 5 + 4 + 1) / 1) = 26
# 96 * 26 * 26 -> 256 * 26 * 26
nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2), nn.ReLU(),
# floor((26 - 3 + 2) / 2) = 12
# 256 * 26 * 26 -> 256 * 12 * 12
nn.MaxPool2d(kernel_size=3, stride=2),
# 3 consecutive conv layer, smaller kernel size
# floor((12 - 3 + 2 + 1) / 1) = 12
# 256 * 12 * 12 -> 384 * 12 * 12
nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1), nn.ReLU(),
# 384 * 12 * 12 -> 384 * 12 * 12
nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1), nn.ReLU(),
# 384 * 12 * 12 -> 256 * 12 * 12
nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1), nn.ReLU(),
# floor((12 - 3 + 2) / 2) = 5
# 256 * 5 * 5
nn.MaxPool2d(kernel_size=3, stride=2)
)
# fully connect part
self.fc = nn.Sequential(
nn.Linear(256 * 5 * 5, 4096),
nn.ReLU(),
# Use the dropout layer to mitigate overfitting
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Dropout(p=0.5),
# Output layer.
# the number of classes in Fashion-MNIST is 10
nn.Linear(4096, 10),
)
def forward(self, img):
feature = self.conv(img)
#当卷积层输出结果进入全连接层时候,需要将卷积层结果展平。
output = self.fc(feature.view(img.shape[0], -1))
return output
VGG
VGG与前面模型最大的不同就是通过重复使用基础块(Block)思想来构造深度模型。
VGG块基本组成部分序列:
- 带填充卷积层;
- 非线性激活函数,如ReLU;
- 池化层,如最大池化层。
VGG论文中提出的方法是:连续使用数个相同的填充为1、窗口形状为 3 × 3 的卷积层后紧接一个步幅为2、窗口形状为 2 × 2 的最大池化层。卷积层保持输入和输出的高和宽不变,而池化层对输入的尺寸减半。
为了方便模型搭建,会构建一个 Vgg_block 函数,来实现 VGG 块构成。
# 该函数有三个参数,分别对应于卷积层的数量num_convs、输入通道的数量in_channels 和输出通道的数量out_channels.
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
VGG 网络分为两部分,一个是VGG 模块,另一个就是全连接层模块。
论文中 VGG 模块搭建结构图如下:
从上到下是网络搭建过程,第一行是方案的名称,接着是说明网络的层数。
conv3-64:conv 是卷积层,3 是卷积核(正方形)64 是输出的维度。
FC-4096:是全连接层,4096 指输出维度
在VGG模块中,主要是卷积核 3x3 卷积层为主导。常用的 VGG 模型就是 VGG-A11。
def vgg(conv_arch):
conv_blks = []
in_channels = 1
# 卷积层部分
for (num_convs, out_channels) in conv_arch:
conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
in_channels = out_channels
return nn.Sequential(
*conv_blks, nn.Flatten(),
# 全连接层部分
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 10))
NiN
假设卷积核的形状是 的话,计算过程如下:
如上图所示:假设:()作为输出某一个通道的卷积核参数,()是输入通道的一组参数,根据互相关的运算如下:
这个运算过程与全连接层运算几乎一致, 个输入通道的向量可以理解成 MLP 网络的特征, 个输出通道结果,需要的卷积核维度为 ,然后加上偏置,也可以完成全连接层的工作。
如果你还没有理解,你可以换一种角度看待:
如果卷积层输入通道数是 3,但是输入值是一个宫格,而不是九宫格,把宫格里面写入数字,整个过程就是全连接层。
我理解 9 个宫格代表 9 组参数。
经过卷积层后,高和宽是不变的,变化的是通道的数目。
NiN 模型与LeNet/AlexNet/VGG 等网络最大的改变,就是利用 卷积层代替全连接层。
NiN 块以一个普通卷积层开始,后面是两个的卷积层。这两个卷积层充当带有ReLU激活函数的全连接层。由于 1x1 卷积层输出结果宽高不变,那么需要设置的就是第一层普通卷积层。
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())
根据上诉图中,NiN 使用窗口形状为、和的卷积层,最后接上平均池化层。
NiN 的设计可以显著减小模型参数尺寸,然而,在实践中,该设计的有效模型的训练时间会增加。
# Fashion-MNIST 1 * 28 * 28, resize into the input into 1 * 224 * 224
# input shape: 1 * 224 * 224
net = nn.Sequential(
# n*1*244*244->n*96*54*54
nin_block(1, 96, kernel_size=11, strides=4, padding=0),
# n*96*54*54->n*96*26*26
nn.MaxPool2d(3, stride=2),
# n*96*26*26->n*256*26*26
nin_block(96, 256, kernel_size=5, strides=1, padding=2),
# n*256*26*26->n*256*12*12
nn.MaxPool2d(3, stride=2),
# n*256*12*12->n*384*12*12
nin_block(256, 384, kernel_size=3, strides=1, padding=1),
# n*384*12*12->n*384*5*5
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
# n*384*5*5->n*10*5*5
nin_block(384, 10, kernel_size=3, strides=1, padding=1),
# n*10*5*5->n*10*1*1
nn.AdaptiveAvgPool2d((1, 1)),
# 将四维的输出转成二维的输出,其形状为(批量大小,10)
nn.Flatten())
GoogLeNet
GoogLeNet 中的基础卷积块叫作 Inception 块,结构上会更加复杂,如图所示:
Inception 块由四条并行路径组成:
- 卷积层
- 卷积层+卷积层
- 卷积层+卷积层
- 最大池化层+卷积层
这四条路径都使用合适的填充来保证输入与输出的高和宽一致,最后我们将每条线路的输出在通道维度上连结,并构成 Inception 块的输出。
class Inception(nn.Module):
# c1--c4是每条路径的输出通道数
def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# 线路1,单1x1卷积层
self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
# 线路2,1x1卷积层后接3x3卷积层
self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1x1卷积层后接5x5卷积层
self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3x3最大汇聚层后接1x1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)
def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
# 在通道维度上连结输出
return torch.cat((p1, p2, p3, p4), dim=1)
GoogLeNet 一共使用9个 Inception 块和全局平均汇聚层的堆叠来生成其估计值,如下图所示
开始实现 GoogLeNet的每个模块,用96×96大小的输入来演示各个步骤后数据形状的变化。
第一个模块使用卷积层。
# n * 1 * 96 * 96 ->n * 64 * 24 * 24
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
第二个模块使用卷积层+卷积层。
b2 = nn.Sequential(
# n * 64 * 24 * 24-> n * 64 * 24 * 24
nn.Conv2d(64, 64, kernel_size=1),
nn.ReLU(),
# n * 64 * 24 * 24-> n * 192 * 24 * 24
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.ReLU(),
# n * 192 * 24 * 24-> n * 192 * 12 * 12
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
第三个模块:串联了两个完整的 Inception 块。第一个Inception块的输出通道数为,第二个Inception块的输出通道数增加到
b3 = nn.Sequential(
# n * 192 * 12 * 12->n * 256 * 12 * 12
Inception(192, 64, (96, 128), (16, 32), 32),
# n * 256 * 12 * 12->n * 480 * 12 * 12
Inception(256, 128, (128, 192), (32, 96), 64),
# n * 480 * 12 * 12->n * 480 * 6 * 6
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
第四个模块:串联了5个Inception块,其输出通道数分别是、、、和。
b4 = nn.Sequential(
# n * 480 * 6 * 6->n * 512 * 6 * 6
Inception(480, 192, (96, 208), (16, 48), 64),
# n * 512 * 6 * 6->n * 512 * 6 * 6
Inception(512, 160, (112, 224), (24, 64), 64),
# n * 512 * 6 * 6->n * 512 * 6 * 6
Inception(512, 128, (128, 256), (24, 64), 64),
# n * 512 * 6 * 6->n * 528 * 6 * 6
Inception(512, 112, (144, 288), (32, 64), 64),
# n * 528 * 6 * 6->n * 832 * 6 * 6
Inception(528, 256, (160, 320), (32, 128), 128),
# n * 832 * 6 * 6->n * 832 * 3 * 3
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
第五个模块:串联两个Inception块: 和 。后面紧跟输出层,使用全局平均池化层。
b5 = nn.Sequential(
# n * 832 * 3 * 3->n * 832 * 3 * 3
Inception(832, 256, (160, 320), (32, 128), 128),
# n * 832 * 3 * 3->n * 1024 * 3 * 3
Inception(832, 384, (192, 384), (48, 128), 128),
# n * 1024 * 3 * 3->n * 1024 * 1
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten())
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))