深入解读GoogLeNet网络结构(附代码实现)

前言

七夕了,看着你们秀恩爱,单身狗的我还是做俺该做的事吧!

在上一篇文章中介绍了VGG网络结构,VGG在2014年ImageNet 中获得了定位任务第1名和分类任务第2名的好成绩,而同年分类任务的第一名则是GoogleNet 。GoogleNet是Google研发的深度网络结构,之所以叫“GoogLeNet”,是为了向“LeNet”致敬,有兴趣的同学可以看下原文Going Deeper with Convolutions

与VGGNet模型相比较,GoogleNet模型的网络深度已经达到了22层( 如果只计算有参数的层,GoogleNet网络有22层深 ,算上池化层有27层),而且在网络架构中引入了Inception单元,从而进一步提升模型整体的性能。虽然深度达到了22层,但大小却比AlexNet和VGG小很多,GoogleNet参数为500万个( 5M ),VGG16参数是138M,是GoogleNet的27倍多,而VGG16参数量则是AlexNet的两倍多。

Inception单元结构

我们先来看一下模型中的Inception单元结构,然后在此基础上详细分析GoogleNet网络结构,这里推荐看一下我的这篇博客从Inception到Xception,卷积方式的成长之路,可以对下面的内容有更好的理解。

Inception 最初提出的版本主要思想是利用不同大小的卷积核实现不同尺度的感知,网络结构图如下:
在这里插入图片描述
Inception Module基本组成结构有四个成分。1*1卷积,3*3卷积,5*5卷积,3*3最大池化。最后对四个成分运算结果进行通道上组合,这就是Naive Inception的核心思想:利用不同大小的卷积核实现不同尺度的感知,最后进行融合,可以得到图像更好的表征。

下面通过一个具体的实例来看看整个Naive Inception单元的详细工作过程,假设在上图中Naive Inception单元的前一层输入的数据是一个32×32×256的特征图,该特征图先被复制成4份并分别被传至接下来的4个部分。我们假设这4个部分对应的滑动窗口的步长均为1,其中,1×1卷积层的Padding为0,滑动窗口维度为1×1×256,要求输出的特征图深度为128;3×3卷积层的Padding为1,滑动窗口维度为3×3×256,要求输出的特征图深度为192;5×5卷积层的Padding为2,滑动窗口维度为5×5×256,要求输出的特征图深度为96;3×3最大池化层的 Padding为1,滑动窗口维度为3×3×256。这里对每个卷积层要求输出的特征图深度没有特殊意义,仅仅举例用,之后通过计算,分别得到这4部分输出的特征图为32×32×128、32×32×192、32×32×96 和 32×32×256,最后在合并层进行合并,得到32×32×672的特征图,合并的方法是将各个部分输出的特征图相加,最后这个Naive Inception单元输出的特征图维度是32×32×672,总的参数量就是1*1*256*128+3*3*256*192+5*5*256*96=1089536

但是Naive Inception有两个非常严重的问题:首先,所有卷积层直接和前一层输入的数据对接,所以卷积层中的计算量会很大;其次,在这个单元中使用的最大池化层保留了输入数据的特征图的深度,所以在最后进行合并时,总的输出的特征图的深度只会增加,这样增加了该单元之后的网络结构的计算量。于是人们就要想办法减少参数量来减少计算量,在受到了模型 “Network in Network”的启发,开发出了在GoogleNet模型中使用的Inception单元(Inception V1),这种方法可以看做是一个额外的1*1卷积层再加上一个ReLU层。如下所示:

在这里插入图片描述

这里使用1x1 卷积核主要目的是进行压缩降维,减少参数量,从而让网络更深、更宽,更好的提取特征,这种思想也称为Pointwise Conv,简称PW。

举个例子来论证下吧。假设新增加的 1×1 的卷积的输出深度为64,步长为1,Padding为0,其他卷积和池化的输出深度、步长都和之前在Naive Inception单元中定义的一样(即上面例子中定义的一样),前一层输入的数据仍然使用同之前一样的维度为32×32×256的特征图,通过计算,分别得到这 4 部分输出的特征图维度为32×32×128、32×32×192、32×32×96 和32×32×64,将其合并后得到维度为32×32×480的特征图,将这4部分输出的特征图进行相加,最后Inception单元输出的特征图维度是32×32×480。新增加的3个 1×1 的卷积参数量是3*1*1*256*64=49152,原来的卷积核参数量是1*1*256*128+3*3*64*192+5*5*64*96=296960,总的参数量就是49152+296960=346112

在输出的结果中,32×32×128、32×32×192、32×32×96 和之前的Naive Inception 单元是一样的,但其实这三部分因为1×1卷积层的加入,总的卷积参数数量已经大大低于之前的Naive Inception单元而且因为在最大池化层之前也加入了1×1的卷积层,所以最终输出的特征图的深度也降低了,这样也降低了该单元之后的网络结构的计算量

GoogLeNet模型解读

GoogleNet网络结构(Inception V1)的网络结构如下:

在这里插入图片描述

GoogLeNet网络有22层深(包括pool层,有27层深),在分类器之前,采用Network in Network中用Averagepool(平均池化)来代替全连接层的思想,而在avg pool之后,还是添加了一个全连接层,是为了大家做finetune(微调)。而无论是VGG还是LeNet、AlexNet,在输出层方面均是采用连续三个全连接层,全连接层的输入是前面卷积层的输出经过reshape得到。据发现,GoogLeNet将fully-connected layer用avg pooling layer代替后,top-1 accuracy 提高了大约0.6%;然而即使在去除了fully-connected layer后,依然必须dropout。

由于全连接网络参数多,计算量大,容易过拟合,所以GoogLeNet没有采用VGG、LeNet、AlexNet三层全连接结构,直接在Inception模块之后使用Average Pool和Dropout方法,不仅起到降维作用,还在一定程度上防止过拟合。

在Dropout层之前添加了一个7×7的Average Pool,一方面是降维,另一方面也是对低层特征的组合。我们希望网络在高层可以抽象出图像全局的特征,那么应该在网络的高层增加卷积核的大小或者增加池化区域的大小,GoogLeNet将这种操作放到了最后的池化过程,前面的Inception模块中卷积核大小都是固定的,而且比较小,主要是为了卷积时的计算方便。

GoogLeNet在网络模型方面与AlexNet、VGG还是有一些相通之处的,它们的主要相通之处就体现在卷积部分,

  • AlexNet采用5个卷积层
  • VGG把5个卷积层替换成5个卷积块
  • GoogLeNet采用5个不同的模块组成主体卷积部分

用表格的形式表示GoogLeNet的网络结构如下所示:
在这里插入图片描述
上述就是GoogLeNet的结构,可以看出,和AlexNet统一使用5个卷积层、VGG统一使用5个卷积块不同,GoogLeNet在主体卷积部分是卷积层与Inception块混合使用。另外,需要注意一下,在输出层GoogleNet采用全局平均池化,得到的是高和宽均为1的卷积层,而不是通过reshape得到的全连接层。

需要注意的是,上图中 “#3×3reduce” 和 “#5×5reduce” 表示在3×3和5×5卷积之前,使用的降维层中的1×1滤波器的数量。pool proj代表max-pooling后的投影数量(即先max-pooling,再PW降维),所有的reductions(降维)和projections(投影)也都使用激活函数ReLU。

下面就来详细介绍一下GoogLeNet的模型结构。

输入

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

第一模块

第一模块采用的是一个单纯的卷积层紧跟一个最大池化层。

卷积层:卷积核大小7*7,步长为2,padding为3,输出通道数64,输出特征图尺寸为(224-7+3*2)/2+1=112.5(向下取整)=112,输出特征图维度为112x112x64,卷积后进行ReLU操作。

池化层:窗口大小3*3,步长为2,输出特征图尺寸为((112 -3)/2)+1=55.5(向上取整)=56,输出特征图维度为56x56x64。

关于卷积和池化中的特征图大小计算方式,可以参考我的博客神经网络之多维卷积的那些事

第二模块

第二模块采用2个卷积层,后面跟一个最大池化层。
在这里插入图片描述

卷积层:

  1. 先用64个1x1的卷积核(3x3卷积核之前的降维)将输入的特征图(56x56x64)变为56x56x64,然后进行ReLU操作。参数量是1*1*64*64=4096
  2. 再用卷积核大小3*3,步长为1,padding为1,输出通道数192,进行卷积运算,输出特征图尺寸为(56-3+1*2)/1+1=56,输出特征图维度为56x56x192,然后进行ReLU操作。参数量是3*3*64*192=110592

第二模块卷积运算总的参数量是110592+4096=114688,即114688/1024=112K

池化层: 窗口大小3*3,步长为2,输出通道数192,输出为((56 - 3)/2)+1=27.5(向上取整)=28,输出特征图维度为28x28x192。

第三模块(Inception 3a层)

Inception 3a层,分为四个分支,采用不同尺度,图示如下:

在这里插入图片描述
再看下表格结构,来分析和计算吧:
在这里插入图片描述

  1. 使用64个1x1的卷积核,运算后特征图输出为28x28x64,然后RuLU操作。参数量1*1*192*64=12288
  2. 96个1x1的卷积核(3x3卷积核之前的降维)运算后特征图输出为28x28x96,进行ReLU计算,再进行128个3x3的卷积,输出28x28x128。参数量1*1*192*96+3*3*96*128=129024
  3. 16个1x1的卷积核(5x5卷积核之前的降维)将特征图变成28x28x16,进行ReLU计算,再进行32个5x5的卷积,输出28x28x32。参数量1*1*192*16+5*5*16*32=15872
  4. pool层,使用3x3的核,输出28x28x192,然后进行32个1x1的卷积,输出28x28x32.。总参数量1*1*192*32=6144

将四个结果进行连接,对这四部分输出结果的第三维并联,即64+128+32+32=256,最终输出28x28x256。总的参数量是12288+129024+15872+6144=163328,即163328/1024=159.5K,约等于159K。

第三模块(Inception 3b层)

Inception 3b层,分为四个分支,采用不同尺度。

  1. 128个1x1的卷积核,然后RuLU,输出28x28x128
  2. 128个1x1的卷积核(3x3卷积核之前的降维)变成28x28x128,进行ReLU,再进行192个3x3的卷积,输出28x28x192
  3. 32个1x1的卷积核(5x5卷积核之前的降维)变成28x28x32,进行ReLU,再进行96个5x5的卷积,输出28x28x96
  4. pool层,使用3x3的核,输出28x28x256,然后进行64个1x1的卷积,输出28x28x64

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

第四模块(Inception 4a、4b、4c、4e)

与Inception3a,3b类似
在这里插入图片描述
第五模块(Inception 5a、5b)

与Inception3a,3b类似
在这里插入图片描述

输出层

前面已经多次提到,在输出层GoogLeNet与AlexNet、VGG采用3个连续的全连接层不同,GoogLeNet采用的是全局平均池化层,得到的是高和宽均为1的卷积层,然后添加丢弃概率为40%的Dropout,输出层激活函数采用的是softmax。

在这里插入图片描述

激活函数

GoogLeNet每层使用的激活函数为ReLU激活函数。

辅助分类器

根据实验数据,发现神经网络的中间层也具有很强的识别能力,为了利用中间层抽象的特征,在某些中间层中添加含有多层的分类器。如下图所示,红色边框内部代表添加的辅助分类器。GoogLeNet中共增加了两个辅助的softmax分支,作用有两点,一是为了避免梯度消失,用于向前传导梯度。反向传播时如果有一层求导为0,链式求导结果则为0。二是将中间某一层输出用作分类,起到模型融合作用。最后的loss=loss_2 + 0.3 * loss_1 + 0.3 * loss_0。实际测试时,这两个辅助softmax分支会被去掉。

在这里插入图片描述

GoogLeNet其他版本

上面介绍的GoogLeNet模型是Inception v1版本,还有Inception v2,v3,v4版本

Inception V2

  1. 学习VGGNet的特点,用两个33卷积代替55卷积,可以降低参数量。
  2. 提出BN算法。BN算法是一个正则化方法,可以提高大网络的收敛速度。就是每一batch的输入分布标准化处理,使得规范化为N(0,1)的高斯分布,收敛速度大大提高。详情可以参考我的博客Batch Normalization:批量归一化详解
    在这里插入图片描述

Inception V3

学习Factorization into small convolutions的思想,在Inception V2的基础上,将一个二维卷积拆分成两个较小卷积,例如将7*7卷积拆成1*7卷积和7*1卷积,这样做的好处是降低参数量。该paper中指出,通过这种非对称的卷积拆分比对称的拆分为几个相同的小卷积效果更好,可以处理更多,更丰富的空间特征,这就是Inception V3网络结构。

在这里插入图片描述

Inception V4

借鉴了微软的ResNet网络结构思想,后面写到Resnet再介绍吧。

GoogLeNet测试样本处理

  1. 对于一个测试样本,将图像的短边缩放成4种尺寸,分别为256,288,320,352。
  2. 从每种尺寸的图像的左边,中间,右边(或者上面,中间,下面)分别截取一个方形区域,共三块方形区域。
  3. 对于每一个方形区域,我们取其四角和中心,裁切出5个区域,再将方形区域缩小到224×224,共6快区域,加上它们的镜像版本(将图像水平翻转),一共得到4×3×6×2=144张图像。这样的方法在实际应用中是不必要的,可能存在更合理的修剪方法。下图展示了不同修剪方法和不同模型数量的组合结果:

在这里插入图片描述
上表中,通过改变模型数量以及切分数量,展示几种测试策略对于图片进行预测的效果,所有数据报告基于验证数据集,以避免测试集上的过拟合。

使用多个模型时,每个模型的Softmax分类器在多个修剪图片作为输入时都得到多个输出值,然后再对所有分类器的softmax概率值求平均。

效果如下所示:

在这里插入图片描述

可以看到,GoogLeNet在验证集和测试集上top-5的错误率都降到了6.67%,在当年参赛者中排名第一。

GoogleNet代码实现

Inception实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import torch
from torch import nn, optim
import torch.nn.functional as F
class Inception(nn.Module):
# c1 - c4为每条线路里的层的输出通道数
def __init__(self, in_c, c1, c2, c3, c4):
super(Inception, self).__init__()
# 线路1,单1 x 1卷积层
self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
# 线路2,1 x 1卷积层后接3 x 3卷积层
self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1 x 1卷积层后接5 x 5卷积层
self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3 x 3最大池化层后接1 x 1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_c, 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) # 在通道维上连结输出

GlobalAvgPool2d与FlattenLayer

1
2
3
4
5
6
7
8
9
10
11
12
class GlobalAvgPool2d(nn.Module):
# 全局平均池化层可通过将池化窗口形状设置成输入的高和宽实现
def __init__(self):
super(GlobalAvgPool2d, self).__init__()
def forward(self, x):
return F.avg_pool2d(x, kernel_size=x.size()[2:])

class FlattenLayer(torch.nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x): # x shape: (batch, *, *, ...)
return x.view(x.shape[0], -1)

GoogLeNet实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class GoogLeNet(nn.Module):
def __init__(self, num_classes=1000):
super(GoogLeNet, self).__init__()

self.b1 = nn.Sequential(nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

self.b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

self.b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
Inception(256, 128, (128, 192), (32, 96), 64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

self.b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
Inception(512, 160, (112, 224), (24, 64), 64),
Inception(512, 128, (128, 256), (24, 64), 64),
Inception(512, 112, (144, 288), (32, 64), 64),
Inception(528, 256, (160, 320), (32, 128), 128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

self.b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
GlobalAvgPool2d())
self.output=nn.Sequential(FlattenLayer(),
nn.Dropout(p=0.4),
nn.Linear(1024, 1000))

def forward(self, x):
x=b1(x)
x=b2(x)
x=b3(x)
x=b4(x)
x=b5(x)
x=output(x)
return x

测试输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
net = GoogLeNet()
X = torch.rand(1, 3, 224, 224)
# 可以对照表格看一下各层输出的尺寸
for blk in net.children():
X = blk(X)
print('output shape: ', X.shape)
"""
# 输出:
output shape: torch.Size([1, 64, 56, 56])
output shape: torch.Size([1, 192, 28, 28])
output shape: torch.Size([1, 480, 14, 14])
output shape: torch.Size([1, 832, 7, 7])
output shape: torch.Size([1, 1024, 1, 1])
output shape: torch.Size([1, 1000])
"""

【参考文档】

  1. GoogLeNet中的inception结构,你看懂了吗
  2. GoogleNet 论文解析及代码实现
  3. 带你快速学会 GoogLeNet 神经网络
  4. 卷积神经网络之GoogLeNet
  5. GoogLeNet学习笔记
  6. GoogLeNet模型
  7. 深度学习之pytorch计算机视觉-唐进民著
  8. Going Deeper with Convolutions, CVPR 2014

我把GoogLeNet的论文上传到百度云上了,有需要请自提,链接:https://pan.baidu.com/s/1Tcg6-s1pHCE2WZ9TBaQ90A 提取码:ivft