常用卷积神经网络介绍

1,050 阅读10分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情

常见卷积神经网络

介绍一些常见卷积神经网络,认识以及编写卷积神经网络。

LeNet

LeNet 网络结构图如下:

LetNet网络结构

该模型由两部分组成:

  • 卷积编码器:由两个卷积块组成;每个卷积块中的基本单元是一个卷积层、一个 sigmoid 激活函数和平均池化层。每个卷积层使用5×55\times 5卷积核和一个 sigmoid 激活函数。第一卷积层有6个输出通道,而第二个卷积层有16个输出通道。卷积的输出形状由批量大小、通道数、高度、宽度决定。
  • 全连接层密集块:由三个全连接层组成。LeNet的稠密块有三个全连接层,分别有120、84和 n 个输出。输出层的 n 维对应于最后输出结果的数量。

image.png

利用 Fashion-MNIST 作为数据集,当一个大小为 28×2828\times28的单通道图像,进入模型,整体过程阐述:当输入形状 28×2828\times28的单通道图像,经过第一个卷积层,卷积层使用的是 2 像素作为填充,输出形状是 (285+4+1)×(285+4+1)=28×28(28 - 5 + 4 + 1) × (28 - 5 + 4 + 1) = 28 × 28 ,第一个卷积层输出 6 个通道,第一层的输出形状:6×28×286\times28\times28。接下来是平均池化层,大小 2×22\times2,步幅为 2,经过这一层的输出形状为6×14×146\times14\times14。第二个卷积层卷积核为5×55\times5,没填充,输出形状为 (145+1)×(145+1)=10×10(14 - 5 + 1) × (14 - 5 + 1) = 10 × 10,输出通道数为 16,最后输出形状为16×10×1016\times10\times10,经过平均池化层,大小 2×22\times2,步幅为 2,,最终形状为16×5×516\times5\times5.

卷积层块的输出形状为(batch_size, output_channels, height, width),所以卷积层块输出形状为1×16×5×51\times16\times5\times5。当进入全连接层时,需要将卷积层输出的结果变平,展平成一个向量,形状就是1×4001\times400,输入进入全连接层。

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

网络结构图如下:

img

AlexNet 与 LeNet 设计理念比较类似,但是有着显著的区别。

  • AlexNet的第一层,卷积窗口的形状是11×1111\times11,可以处理更大的图像,而不是28×2828\times28的单通道图像,捕获目标更多。
  • 第二层中的卷积窗口形状被缩减为5×55\times5,接下来的三层都是是3×33\times3
  • 在第一层、第二层和第五层卷积层之后,加入窗口形状为3×33\times3、步幅为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 模块,另一个就是全连接层模块。

img

论文中 VGG 模块搭建结构图如下:

image.png

从上到下是网络搭建过程,第一行是方案的名称,接着是说明网络的层数。

conv3-64conv 是卷积层,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

假设卷积核的形状是 1×11\times1的话,计算过程如下:

img

如上图所示:假设:(K0,0,K0,1,K0,2K_{0,0},K_{0,1},K_{0,2})作为输出某一个通道的卷积核参数,(I0,i,j,I1,i,j,I2,i,jI_{0,i,j},I_{1,i,j},I_{2,i,j})是输入通道的一组参数,根据互相关的运算如下:

I0,i,j×K0,0+I1,i,j×K0,1+I2,i,j×K0,2I_{0, i, j} \times K_{0,0}+I_{1, i, j} \times K_{0,1}+I_{2, i, j} \times K_{0,2}

这个运算过程与全连接层运算几乎一致,cic_i 个输入通道的向量可以理解成 MLP 网络的特征,coc_o 个输出通道结果,需要的1×11\times 1卷积核维度为 co×cic_o\times c_i,然后加上偏置,也可以完成全连接层的工作。

如果你还没有理解,你可以换一种角度看待:

如果卷积层输入通道数是 3,但是输入值是一个宫格,而不是九宫格,把宫格里面写入数字,整个过程就是全连接层。

我理解 9 个宫格代表 9 组参数。

经过1×11\times 1卷积层后,高和宽是不变的,变化的是通道的数目。

NiN 模型与LeNet/AlexNet/VGG 等网络最大的改变,就是利用 1×11\times 1 卷积层代替全连接层。

../_images/nin.svg

NiN 块以一个普通卷积层开始,后面是两个1×11 \times 1的卷积层。这两个1×11 \times 1卷积层充当带有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 使用窗口形状为11×1111\times 115×55\times 53×33\times 3的卷积层,最后接上平均池化层。

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 块,结构上会更加复杂,如图所示:

../_images/inception.svg

Inception 块由四条并行路径组成:

  • 1×11\times1卷积层
  • 1×11\times1卷积层+3×33\times3卷积层
  • 1×11\times1卷积层+5×55\times5卷积层
  • 3×33\times3 最大池化层+1×11\times1卷积层

这四条路径都使用合适的填充来保证输入与输出的高和宽一致,最后我们将每条线路的输出在通道维度上连结,并构成 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 块和全局平均汇聚层的堆叠来生成其估计值,如下图所示

../_images/inception-full.svg

开始实现 GoogLeNet的每个模块,用96×96大小的输入来演示各个步骤后数据形状的变化。

第一个模块使用7×77\times 7卷积层。

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

第二个模块使用1×11\times 1卷积层+3×33\times 3卷积层。

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块的输出通道数为64+128+32+32=25664+128+32+32=256,第二个Inception块的输出通道数增加到128+192+96+64=480128+192+96+64=480

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块,其输出通道数分别是192+208+48+64=512192+208+48+64=512160+224+64+64=512160+224+64+64=512128+256+64+64=512128+256+64+64=512112+288+64+64=528112+288+64+64=528256+320+128+128=832256+320+128+128=832

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块:256+320+128+128=832256+320+128+128=832384+384+128+128=1024384+384+128+128=1024。后面紧跟输出层,使用全局平均池化层。

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