(四)卷积神经网络模型之——GoogLeNet

  • Post author:
  • Post category:其他




模型介绍

2014年,GoogLeNet和VGG是当年ImageNet挑战赛(ILSVRC14)的双雄,GoogLeNet获得了第一名、VGG获得了第二名,这两类模型结构的共同特点是层次更深了。GoogleNet共22层,参数为500万个,AlexNet参数个数是GoogleNet的12倍,VGGNet参数又是AlexNet的3倍,因此在内存或计算资源有限时,GoogleNet是比较好的选择;从模型结果来看,GoogLeNet的性能却更加优越。

GoogLeNet是Christian Szegedy等人在大规模视觉挑战赛(ILSVRC-2014)上使用的一种全新卷积神经网络结构,并以6.65%的错误率力压VGGNet等模型取得了ILSVRC-2014在分类任务上的冠军,于2015年在CVPR发表了论文《Going Deeper with Convolutions》。

在这之前的AlexNet、VGG等结构都是通过增大网络的深度(层数)来获得更好的训练效果,但层数的增加会带来很多负作用,比如overfitting、梯度消失、梯度爆炸等,GoogLeNet则做了更加大胆的网络结构尝试。

Inception的提出则从另一种角度来提升训练结果:能更高效的利用计算资源,在相同的计算量下能提取到更多的特征,从而提升训练结果,采用了Inception结构的GoogLeNet深度只有22层,其参数约为AlexNet的1/12,是同时期VGGNet的1/3。

GoogLeNet是谷歌(Google)提出的深度网络结构,为什么不叫“GoogleNet”,而叫“GoogLeNet”,原因是为了向经典模型“LeNet”致敬



ILSVRC介绍

ILSVRC(ImageNet Large Scale Visual Recognition Challenge)是近年来机器视觉领域最受追捧也是最具权威的学术竞赛之一,代表了图像领域的最高水平。

ImageNet数据集是ILSVRC竞赛使用的是数据集,由斯坦福大学李飞飞教授主导,包含了超过1400万张全尺寸的有标记图片。ILSVRC比赛会每年从ImageNet数据集中抽出部分样本,以2012年为例,比赛的训练集包含1281167张图片,验证集包含50000张图片,测试集为100000张图片。

ILSVRC竞赛的项目主要包括以下几个问题:

(1)图像分类与目标定位(CLS-LOC)

(2)目标检测(DET)

(3)视频目标检测(VID)

(4)场景分类(Scene)



CVPR

CVPR是IEEE Conference on Computer Vision and Pattern Recognition的缩写,即IEEE国际计算机视觉与模式识别会议。该会议是由IEEE举办的计算机视觉和模式识别领域的顶级会议。



GoogLeNet 如何进一步提升性能

一般来说,提升网络性能最直接的办法就是增加网络深度和宽度,深度指网络层次数量、宽度指神经元数量。但这种方式存在以下问题:

(1)参数太多,如果训练数据集有限,很容易产生过拟合;

(2)网络越大、参数越多,计算复杂度越大,难以应用;

(3)网络越深,容易出现梯度弥散问题(梯度越往后穿越容易消失),难以优化模型。

所以,有人调侃 “深度学习” 其实是 “深度调参”。

解决这些问题的方法当然就是在增加网络深度和宽度的同时减少参数,为了减少参数,自然就想到将全连接变成稀疏连接。但是在实现上,全连接变成稀疏连接后实际计算量并不会有质的提升,因为大部分硬件是针对密集矩阵计算优化的,稀疏矩阵虽然数据量少,但是计算所消耗的时间却很难减少。

那么,有没有一种方法既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能。大量的文献表明可以将稀疏矩阵聚类为较为密集的子矩阵来提高计算性能,就如人类的大脑是可以看做是神经元的重复堆积,因此,GoogLeNet 团队提出了 Inception 网络结构,就是构造一种 “基础神经元” 结构,来搭建一个稀疏性、高计算性能的网络结构。



什么是 Inception ?



Inception 基本结构

通过设计一个稀疏网络结构,但是能够产生稠密的数据,既能增加神经网络表现,又能保证计算资源的使用效率。谷歌提出了最原始 Inception 的基本结构:

在这里插入图片描述

该结构将 CNN 中常用的卷积(1×1,3×3,5×5)、池化操作(3×3)堆叠在一起(卷积、池化后的尺寸相同,将通道相加),一方面增加了网络的宽度,另一方面也增加了网络对尺度的适应性。

网络卷积层中的网络能够提取输入的每一个细节信息,同时 5×5 的滤波器也能够覆盖大部分接受层的的输入。还可以进行一个池化操作,以减少空间大小,降低过度拟合。在这些层之上,在每一个卷积层后都要做一个 ReLU 操作,以增加网络的非线性特征。



Inception v1 网络结构

然而那个 Inception 原始版本,所有的卷积核都在上一层的所有输出上来做,而那个 5×5 的卷积核所需的计算量就太大了,造成了特征图的厚度很大,为了避免这种情况,在 3×3 前、5×5 前、max pooling 后分别加上了 1×1 的卷积核,以起到了降低特征图厚度的作用,这也就形成了 Inception v1 的网络结构,如下图所示:

在这里插入图片描述

1×1 卷积的主要目的是为了减少维度,还用于修正线性激活(ReLU)。比如,上一层的输出为 100x100x128,经过具有 256 个通道的 5×5 卷积层之后 (stride=1,pad=2),输出数据为 100x100x256,其中,卷积层的参数为 128x5x5x256= 819200。而假如上一层输出先经过具有 32 个通道的 1×1 卷积层,再经过具有 256 个输出的 5×5 卷积层,那么输出数据仍为为 100x100x256,但卷积参数量已经减少为 128x1x1x32 + 32x5x5x256= 204800,大约减少了 4 倍。



GoogLeNet模型详解

下面给出了GoogLeNet架构的缩略图,更详细以及带标注的图放在文末。相比于以前的卷积神经网络结构,除了在深度上进行了延伸,还对网络的宽度进行了扩展,整个网络由许多块状子网络的堆叠而成,这个子网络即Inception模块。

在这里插入图片描述

模型的亮点:

1、采用了模块化的设计(stem, stacked inception module, axuiliary function和classifier),方便层的添加与修改。

(1)Stem部分:论文指出Inception module要在网络中间使用的效果比较好,因此网络前半部分依旧使用传统的卷积层代替。

(2)辅助函数(Axuiliary Function):从信息流动的角度看梯度消失,因为是梯度信息在BP过程中能量衰减,无法到达浅层区域,因此在中间开个口子,加个辅助损失函数直接为浅层。

(3)Classifier部分:从VGGNet以及NIN的论文中可知,fc层具有大量层数,因此用average pooling替代fc,减少参数数量防止过拟合。在softmax前的fc之间加入dropout,p=0.7,进一步防止过拟合。

2、使用1×1的卷积核进行降维以及映射处理(虽然VGG网络中也有,但该论文介绍的更详细)。

3、引入了Inception结构(融合不同尺度的特征信息)。

4、丢弃全连接层,使用平均池化(average pooling)层,大大减少模型参数。

5、为了避免梯度消失,网络额外增加了2个辅助的softmax用于向前传导梯度(辅助分类器)。辅助分类器是将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中,这样相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化,对于整个网络的训练很有裨益。而在实际测试的时候,这两个额外的softmax会被去掉。



Inception模块

GoogLeNet中使用的Inception模块被命名为Inception v1,实际上在2014-2016年间,Google团队不断地对GoogLeNet进行改进的过程中形成了Inception v1-v4和Xception结构。

在这里插入图片描述

左图是GoogleNet作者设计的初始inception结构(native inception),其想法是用多个不同类型的卷积核(1 × 1 ,3 × 3 ,5 × 5,3 × 3 pool)堆叠在一起(卷积、池化后的尺寸相同,将通道相加)代替一个3×3的小卷积核,好处是可以使提取出来的特征具有多样化,并且特征之间的co-relationship不会很大,最后用把feature map都concatenate起来使网络做得很宽,然后堆叠Inception Module将网络变深。但仅仅简单这么做会使一层的计算量爆炸式增长。

native inception中所有的卷积核都在上一层的所有输出上来做,而那个5×5的卷积核所需的计算量就太大了,造成了特征图的厚度很大,为了避免这种情况,在3×3前、5×5前、max pooling后分别加上了1×1的卷积核,以起到了降低特征图厚度的作用,这也就形成了Inception v1的网络结构(右图)。

假设input feature map的size为28 × 28 × 256 ,output feature map的size为28 × 28 × 480,则native Inception Module的计算量有854M。计算过程如下

在这里插入图片描述

从上图可以看出,计算量主要来自高维卷积核的卷积操作,因而在每一个卷积前先使用1 × 1的卷积核将输入图片的feature map维度先降低,进行信息压缩,在使用3×3卷积核进行特征提取运算,相同情况下,Inception v1的计算量仅为358M。

在这里插入图片描述

Inception结构总共有4个分支,输入的feature map并行的通过这四个分支得到四个输出,然后再将这四个输出在深度维度(channel维度)进行拼接(concate)得到我们的最终输出(注意,为了让四个分支的输出能够在深度方向进行拼接,必须保证四个分支输出的特征矩阵高度和宽度都相同),因此inception结构的参数为:

branch1:Conv1×1, stride=1

branch2:Conv3×3, stride=1, padding=1

branch3:Conv5×5, stride=1, padding=2

branch4:MaxPool3×3, stride=1, padding=1

GoogLeNet中使用了9个Inception v1 module,分别被命名为inception(3a)、inception(3b)、inception(4a)、inception(4b)、inception(4c)、inception(4d)、inception(4e)、inception(5a)、inception(5b)。



辅助分类器

GoogLeNet网络结构中有深层和浅层2个分类器,两个辅助分类器结构是一模一样的,其组成如下图所示,这两个辅助分类器的输入分别来自Inception(4a)和Inception(4d)

在这里插入图片描述

辅助分类器的第一层是一个平均池化下采样层,池化核大小为5×5,stride=3;第二层是卷积层,卷积核大小为1×1,stride=1,卷积核个数是128;第三层是全连接层,节点个数是1024;第四层是全连接层,节点个数是1000(对应分类的类别个数)。

在模型训练时的损失函数按照:

在这里插入图片描述

L0是最后的分类损失。在测试阶段则去掉辅助分类器,只记最终的分类损失。



GoogLeNet网络结构

基于 Inception 构建了 GoogLeNet 的网络结构如下(共 22 层):

在这里插入图片描述

对上图说明如下:

(1)GoogLeNet 采用了模块化的结构(Inception 结构),方便增添和修改;

(2)网络最后采用了 average pooling(平均池化)来代替全连接层,该想法来自 NIN(Network in Network),事实证明这样可以将准确率提高 0.6%。但是,实际在最后还是加了一个全连接层,主要是为了方便对输出进行灵活调整;

(3)虽然移除了全连接,但是网络中依然使用了 Dropout ;

(4)为了避免梯度消失,网络额外增加了 2 个辅助的 softmax 用于向前传导梯度(辅助分类器)。辅助分类器是将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中,这样相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化,对于整个网络的训练很有裨益。而在实际测试的时候,这两个额外的 softmax 会被去掉。



GoogLeNet 网络结构图细节

每个卷积层的卷积核个数如何确定呢,下面是原论文中给出的参数列表,对于我们搭建的Inception模块,所需要使用到参数有#1×1, #3x3reduce, #3×3, #5x5reduce, #5×5, poolproj,这6个参数,分别对应着所使用的卷积核个数。

在这里插入图片描述

注:上表中的“#3×3 reduce”,“#5×5 reduce”表示在3×3,5×5卷积操作之前使用了1×1卷积的数量。



GoogLeNet 网络结构明细表解析

0、输入

原始输入图像为 224x224x3,且都进行了零均值化的预处理操作(图像每个像素减去均值)。

1、第一层(卷积层)

使用 7×7 的卷积核(滑动步长 2,padding 为 3),64 通道,输出为 112x112x64,卷积后进行 ReLU 操作经过 3×3 的 max pooling(步长为 2),输出为 ((112 – 3+1) / 2) +1 = 56,即 56x56x64,再进行 ReLU 操作。

2、第二层(卷积层)

使用 3×3 的卷积核(滑动步长为 1,padding 为 1),192 通道,输出为 56x56x192,卷积后进行 ReLU 操作经过 3×3 的 max pooling(步长为 2),输出为 ((56 – 3+1)/2)+1=28,即 28x28x192,再进行 ReLU 操作。

3a、第三层(Inception 3a 层)

分为四个分支,采用不同尺度的卷积核来进行处理

(1)64 个 1×1 的卷积核,然后 RuLU,输出 28x28x64

(2)96 个 1×1 的卷积核,作为 3×3 卷积核之前的降维,变成 28x28x96,然后进行 ReLU 计算,再进行 128 个 3×3 的卷积(padding 为 1),输出 28x28x128

(3)16 个 1×1 的卷积核,作为 5×5 卷积核之前的降维,变成 28x28x16,进行 ReLU 计算后,再进行 32 个 5×5 的卷积(padding 为 2),输出 28x28x32

(4)pool 层,使用 3×3 的核(padding 为 1),输出 28x28x192,然后进行 32 个 1×1 的卷积,输出 28x28x32。

将四个结果进行连接,对这四部分输出结果的第三维并联,即 64+128+32+32=256,最终输出 28x28x256

3b、第三层(Inception 3b 层)

(1)128 个 1×1 的卷积核,然后 RuLU,输出 28x28x128

(2)128 个 1×1 的卷积核,作为 3×3 卷积核之前的降维,变成 28x28x128,进行 ReLU,再进行 192 个 3×3 的卷积(padding 为 1),输出 28x28x192

(3)32 个 1×1 的卷积核,作为 5×5 卷积核之前的降维,变成 28x28x32,进行 ReLU 计算后,再进行 96 个 5×5 的卷积(padding 为 2),输出 28x28x96

(4)pool 层,使用 3×3 的核(padding 为 1),输出 28x28x256,然后进行 64 个 1×1 的卷积,输出 28x28x64。

将四个结果进行连接,对这四部分输出结果的第三维并联,即 128+192+96+64=480,最终输出输出为 28x28x480

第四层(4a,4b,4c,4d,4e)、第五层(5a,5b)……,与 3a、3b 类似,在此就不再重复。



GoogLeNet 实验结果对比

从 GoogLeNet 的实验结果来看,效果很明显,差错率比 MSRA、VGG 等模型都要低,对比结果如下表所示:

在这里插入图片描述



pytorch构建的代码

import torch
import torch.nn as nn
import torch.nn.functional as F


class GoogLeNet(nn.Module):
    def __init__(self, num_classes=1000, aux_logits=True, init_weights=False):
        super().__init__()
        self.aux_logits = aux_logits

        self.conv1 = BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3)
        self.pool1 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)
        self.conv2 = BasicConv2d(64, 64, kernel_size=1)
        self.conv3 = BasicConv2d(64, 192, kernel_size=3, padding=1)
        self.pool2 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)

        self.inception3a = Inception(192, 64, 96, 128, 16, 32, 32) 
        self.inception3b = Inception(256, 128, 128, 192, 32, 96, 64)
        self.pool3 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)

        self.inception4a = Inception(480, 192, 96, 208, 16, 48, 64) 
        self.inception4b = Inception(512, 160, 112, 224, 24, 64, 64)
        self.inception4c = Inception(512, 128, 128, 256, 24, 64, 64)
        self.inception4d = Inception(512, 112, 144, 288, 32, 64, 64) 
        self.inception4e = Inception(528, 256, 160, 320, 32, 128, 128)
        self.pool4 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True) 

        self.inception5a = Inception(832, 256, 160, 320, 32, 128, 128)
        self.inception5b = Inception(832, 384, 192, 384, 48, 128, 128)

        if aux_logits:
            self.aux1 = InceptionAux(512, num_classes)
            self.aux2 = InceptionAux(528, num_classes)

        self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1, 1))
        self.dropout = nn.Dropout(p=0.2)
        self.fc = nn.Linear(in_features=1024, out_features=num_classes)

        if init_weights:
            self._init_weights()

    def forward(self, x):
        x = self.conv1(x)  # [None, 3, 224, 224] -> [None, 64, 112, 112]
        x = self.pool1(x)  # [None, 64, 112, 112] -> [None, 64, 56, 56]
        x = self.conv2(x)
        x = self.conv3(x)  # [None, 64, 112, 112] -> [None, 192, 56, 56]
        x = self.pool2(x)  # [None, 192, 56, 56] -> [None, 192, 28, 28]

        x = self.inception3a(x) # [None, 192, 28, 28] -> [None, 256, 28, 28]
        x = self.inception3b(x)  # [None, 256, 28, 28] -> [None, 480, 28, 28]
        x = self.pool3(x)  # [None, 480, 28, 28] -> [None, 480, 14, 14]
        x = self.inception4a(x) # [None, 480, 14, 14] -> [None, 512, 14, 14]
        if self.training and self.aux_logits:  # eval mode discards this layer
            aux1 = self.aux1(x)

        x = self.inception4b(x)
        x = self.inception4c(x)
        x = self.inception4d(x) # [None, 512, 14, 14] -> [None, 528, 14, 14]
        if self.training and self.aux_logits:
            aux2 = self.aux2(x)

        x = self.inception4e(x)  # [None, 528, 14, 14] -> [None, 832, 14, 14]
        x = self.pool4(x) # [None, 832, 14, 14] -> [None, 832, 7, 7]
        x = self.inception5a(x)
        x = self.inception5b(x)  # [None, 832, 7, 7] -> [None, 1024, 7, 7]

        x = self.avgpool(x)
        x = torch.flatten(x, start_dim=1)
        x = self.dropout(x)
        x = self.fc(x)
        if self.training and self.aux_logits:
            return x, aux2, aux1
        return x

    def _init_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_uniform_(m.weight, mode='fan_out', nonlinearity='leaky_relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.constant_(m.weight, 0.01)
                nn.init.constant_(m.bias, 0)


class BasicConv2d(nn.Module):
    def __init__(self, in_channels, out_channels, **kwargs):
        super().__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, bias=False, **kwargs)
        self.bn = nn.BatchNorm2d(num_features=out_channels, eps=0.001)

    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        return F.relu(x, inplace=True)


class Inception(nn.Module):
    def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
        super().__init__()

        self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)

        self.branch2 = nn.Sequential(
            BasicConv2d(in_channels, ch3x3red, kernel_size=1),
            BasicConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1)
        )

        self.branch3 = nn.Sequential(
            BasicConv2d(in_channels, ch5x5red, kernel_size=1),
            BasicConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2)
        )

        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            BasicConv2d(in_channels, pool_proj, kernel_size=1)
        )

    def forward(self, x):
        branch1 = self.branch1(x)
        branch2 = self.branch2(x)
        branch3 = self.branch3(x)
        branch4 = self.branch4(x)

        outputs = [branch1, branch2, branch3, branch4]
        return torch.cat(outputs, dim=1)


class InceptionAux(nn.Module):
    def __init__(self, in_channels, num_classes):
        super().__init__()
        # self.avgpool = nn.AvgPool2d(kernel_size=5, stride=3)
        self.avgpool = nn.AdaptiveAvgPool2d(output_size=(4, 4))
        self.conv = BasicConv2d(in_channels, 128, kernel_size=1)  # output size [batch, 128, 4, 4]

        self.fc1 = nn.Linear(2048, 1024)
        self.fc2 = nn.Linear(1024, num_classes)

    def forward(self, x):
        # aux1: N x 512 x 14 x 14, aux2: N x 528 x 14 x 14
        x = self.avgpool(x)
        # aux1: N x 512 x 4 x 4, aux2: N x 528 x 4 x 4
        x = self.conv(x)
        # N x 128 x 4 x 4
        x = torch.flatten(x, start_dim=1)
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.fc1(x)
        x = F.relu(x, inplace=True)
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.fc2(x)
        return x



参考

https://my.oschina.net/u/876354/blog/1637819

https://blog.csdn.net/weixin_44772440/article/details/122943095

https://blog.csdn.net/L888666Q/article/details/124496381



版权声明:本文为ximu__l原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。