本专栏介绍基于深度学习进行图像识别的经典和前沿模型,将持续更新,包括不仅限于:AlexNet, ZFNet,VGG,GoogLeNet,ResNet,DenseNet,SENet,MobileNet,ShuffleNet,EifficientNet,Vision Transformer,Swin Transformer,Visual Attention Network,ConvNeXt, MLP-Mixer,As-MLP,ConvMixer,MetaFormer
GoogLeNet 系列文章目录
- 前言
- 一、GoogLeNet V1
- 1、Motivation
- 2、Architectural Details
- 小结
- 二、GoogLeNetV2
- 1、Motivation
- 2、Architectural Details
- 小结
- 三、GoogLeNetV3
- 1、Motivation
- 2 、General Design Principles
- 3 、优化辅助分类器
- 4、优化下采样操作
- 5 、优化标签
- 6、Architectural Details
- 7 、小结
- 四、GoogLeNetV4
- 1、 GoogLeNet Inception V4网络结构
- 2、 GoogLeNet Inception Residual V4网络结构
- 3 、小结
- 五、GoogLeNetV5
- 1 、inception回顾
- 2 、inception改进
- 3 、Xception
- 4 、对比深度可分离卷积
- 5、Architectural Details
- 6 、GoogLeNetV5小结
- 六、 代码实现
- 总结
前言
2014年,GoogLeNet和VGGNet是当年ImageNet挑战赛(ILSVRC14)的双雄,GoogLeNet获得了图片分类大赛第一名、VGG紧随其后,这两类模型结构的共同特点是网络深度更深了。VGG继承了LeNet以及AlexNet的一些框架结构,而GoogLeNet则做了更加大胆的网络结构尝试,虽然深度只有22层,但大小却比AlexNet和VGG小很多,GoogleNet参数为500万个,AlexNet参数个数是GoogleNet的12倍,VGGNet参数又是AlexNet的3倍,因此在内存或计算资源有限时,GoogleNet是比较好的选择;从模型结果来看,GoogLeNet的性能却更加优越。
小知识:GoogLeNet是谷歌(Google)研究出来的深度网络结构,为什么不叫“GoogleNet”,而叫“GoogLeNet”,据说是为了向“LeNet”致敬,因此取名为“GoogLeNet”
GoogLeNetV1论文名称:Going Deeper with Convolutions
GoogLeNetV1论文下载链接:
https://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Szegedy_Going_Deeper_With_2015_CVPR_paper.pdf
GoogLeNetV1 pytorch代码实现:https://github.com/Arwin-Yu/Deep-Learning-Classification-Models-Based-CNN-or-Attention
GoogLeNetV2论文名称:Batch normalization: Accelerating deep network training by reducing internal covariate shift
GoogLeNetV2论文下载链接:http://proceedings.mlr.press/v37/ioffe15.pdf
GoogLeNetV3论文名称:Rethinking the Inception Architecture for Computer Vision
GoogLeNetV3论文下载链接:
https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Szegedy_Rethinking_the_Inception_CVPR_2016_paper.pdf
GoogLeNetV4论文名称:Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning
GoogLeNetV4论文下载链接:https://www.aaai.org/ocs/index.php/AAAI/AAAI17/paper/viewPDFInterstitial/14806/14311
GoogLeNetV5论文名称:Xception: Deep learning with depthwise separable convolutions
GoogLeNetV5论文下载链接:
https://openaccess.thecvf.com/content_cvpr_2017/papers/Chollet_Xception_Deep_Learning_CVPR_2017_paper.pdf
一、GoogLeNet V1
1、Motivation
一般来说,提升网络性能最直接的办法就是增加网络深度和宽度,或者输入数据的大小;但这种方式存在以下问题:
(1)参数太多,如果训练数据集有限,很容易产生过拟合;
(2)网络越大、参数越多,计算复杂度越大,难以应用;
(3)网络越深,容易出现梯度弥散问题(梯度越往后穿越容易消失),难以优化模型。
所以,有人调侃“深度学习”其实是“深度调参”。
文章认为解决上述两个缺点的根本方法是将全连接甚至一般的卷积都转化为稀疏连接。一方面现实生物神经系统的连接也是稀疏的(即,神经系统传递某种信息时,只有少部分神经元被激活,大部分处于不反应状态),另一方面有文献表明:对于大规模稀疏的神经网络,可以通过分析激活值的统计特性,和对高度相关的输出进行聚类来逐层构建出一个最优网络。这点表明臃肿的稀疏网络可能被不失性能地被简化。
早些的时候,一方面为了打破网络对称性和提高学习能力,另一方面硬件设备能力有限,传统的卷积网络(LeNet时代)都使用了随机稀疏连接(每次卷积得到的feature maps随机选取一部分送入后续计算)。但是,计算机软硬件对非均匀稀疏数据的计算效率很差,所以在AlexNet中又重新启用了常规的卷积(每次卷积得到的feature maps全部送入后续计算),目的是为了更好地优化并行运算。
所以,现在的问题是有没有一种方法,既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能。大量的文献表明可以将稀疏矩阵聚类为较为密集的子矩阵集合来提高计算性能,据此论文提出了名为Inception 的结构来实现此目的。
2、Architectural Details
作者提出的inception结构如下图所示,将四个不同卷积核尺寸的卷积操作聚类成一个集合。
具体来说,就是将输入信息复制四份,分别送入四个不同的分支,分支中是卷积核尺寸不同的卷积操作,四个分支卷积计算后的feature maps在channel维度上合并,得到一组feature map送入后续操作。如下图所示:
对上图做以下说明:
- 卷积核的大小在神经网络里是一种超参数,没有一种严格的数学理论证明那种尺寸的卷积核更适合提取特征,因此GoogLeNet选择了成年人的方式:我全都要。
- 采用不同大小的卷积核意味着不同大小的计算感受野,最后拼接操作意味着不同尺度特征的融合;
- 之所以卷积核大小采用1、3和5,主要是为了方便对齐。设定卷积步长stride=1之后,只要分别设定pad=0、1、2,那么卷积之后便可以得到相同维度的特征,然后这些特征就可以直接在channel维度拼接在一起了;
- 文章说很多地方都表明pooling挺有效,所以Inception里面也嵌入了pooling操作。
- 网络越到后面,特征越抽象,而且每个特征所涉及的感受野也更大了,因此随着层数的增加,3x3和5x5卷积的比例也要增加。
但是,使用5x5的卷积核仍然会带来相对较大的计算量。 为此,作者先采用1x1卷积核来进行降维。
例如:上一层的输出数据形状为(100x100x128),经过具有256个输出的5x5卷积层之后(stride=1,pad=2),输出数据形状为(100x100x256)。其中,卷积层的参数为128x5x5x256。假如上一层输出先经过具有32个输出的1x1卷积层,再经过具有256个输出的5x5卷积层,那么最终的输出数据的形状仍为(100x100x256),但卷积参数量已经减少为128x1x1x32 + 32x5x5x256,大约减少了4倍。
改进后的网络模型子结构如下:
Inception模块的改进后的代码实现如下所示。
1.class Inception(nn.Module):
2. def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
3. super(Inception, self).__init__()
4.
5. self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)
6.
7. self.branch2 = nn.Sequential(
8. BasicConv2d(in_channels, ch3x3red, kernel_size=1),
9. BasicConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1) # 保证输出大小等于输入大小
10. )
11.
12. self.branch3 = nn.Sequential(
13. BasicConv2d(in_channels, ch5x5red, kernel_size=1),
14. BasicConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2) # 保证输出大小等于输入大小
15. )
16.
17. self.branch4 = nn.Sequential(
18. nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
19. BasicConv2d(in_channels, pool_proj, kernel_size=1)
20. )
21.
22. def forward(self, x):
23. branch1 = self.branch1(x)
24. branch2 = self.branch2(x)
25. branch3 = self.branch3(x)
26. branch4 = self.branch4(x)
27.
28. outputs = [branch1, branch2, branch3, branch4]
29. return torch.cat(outputs, 1)
完整的GoogLeNet就是由这种inception块堆叠而成的;如下图所示:
- 1 . 显然GoogLeNet采用了模块化的结构(stage,block,layer),如上图红黄绿圈出来的方框是不同的stage,每个stage中由很多block(这里的block是inception)堆叠而成,每个block中由很多神经网络层组成。这样模块化结构的优点是方便增添和修改模型结构;
- 2 . 网络最后采用了average pooling来代替全连接层 ,事实证明可以将TOP1 accuracy提高百分之0.6,而且,average pooling允许网络接收不同大小的图片输入了,在最后还是加了一个全连接层,主要是为了方便以后大家finetune;
- 3 . 最后一个全连接层前依然使用了Dropout ;
- 4 . 为了避免梯度消失,网络额外增加了2个softmax辅助分类器(图中黄色stage的第二个和第四个block处,后续会有详细介绍),用于帮助模型反前传导梯度。此外,实际测试的时候,这两个额外的softmax会被去掉。
下图给出网络的详细参数,红黄蓝三个模块与上图相对应。
小结
GoogLeNetV1模型的最大特点在于使用了Inception模块,该模块可以同时使用多种不同尺寸的卷积核和池化层来提取特征,从而增加网络的表达能力和准确性,同时减少了模型的参数数量。Inception模块包含了多个不同的卷积和池化分支,每个分支都有不同的卷积核尺寸和步长。在训练过程中,网络会自动学习如何选择最优的分支以及如何将它们组合起来。
另外,GoogLeNetV1还采用了全局平均池化来替代打平操作,减少模型的参数数量,并防止过拟合。全局平均池化可以将整个特征图进行平均化操作,得到一个特征向量作为最终的输出。
GoogLeNetV1还引入了一种叫做“辅助分类器”的技术,用于帮助网络更快地收敛。该技术在网络中添加了两个辅助的分类器,分别在中间层和末尾层进行分类,可以在训练过程中提供额外的监督信号,从而促进网络的训练。
GoogLeNetV1在ImageNet图像分类竞赛中取得了优异的成绩,准确率达到了74.8%,并且模型参数数量仅为AlexNet的1/12。这表明了GoogLeNetV1在参数数量和分类准确率之间取得了很好的平衡。
二、GoogLeNetV2
GoogLeNetV2最大的贡献就是提出了BatchNormalization数据归一化方法。
1、Motivation
首先,GoogLeNet V1出现的同期,性能与之接近的大概只有VGGNet了,并且二者在图像分类之外的很多领域都得到了成功的应用。但是相比之下,GoogLeNet的计算效率明显高于VGGNet,大约只有500万参数,只相当于Alexnet的1/12(GoogLeNet的caffemodel大约50M,VGGNet的caffemodel则要超过600M)。
而且,二者的发展方向不同;从某种角度可以这样理解:Vgg追求的是网络深度;GoogLeNet追求的是网络宽度
GoogLeNet的表现很好,但是,如果想要通过简单地放大Inception结构来构建更大的网络,则会导致计算过程中的数值不稳定性问题。 为了提升GoogLeNetv1训练速度和稳健性,提出对模型结构的部分做归一化处理,也就是对每个训练的mini-batch做归一化,叫做Batch Normalization(BN)。BN在之后的网络模型中频繁出现,成为神经网络中必不可少的一环,BN的主要好处如下:
- BN使得模型可以使用较大的学习率而不用特别关心诸如梯度爆炸或消失等优化问题;
- BN降低了模型效果对初始权重的依赖;
- BN不仅可以加速收敛,还起到了正则化作用,提高了模型泛化性;
作者认为:网络训练过程中参数不断改变导致后续每一层输入的分布也发生变化,而学习的过程又要使每一层适应输入的分布,因此我们不得不降低学习率、小心地初始化。作者将分布发生变化称之为internal covariate shift。
解决这个问题的方法是在训练网络的时会将输入减去均值,目的是为了加快训练。为什么减均值可以加快训练呢,这里做一个简单地说明:
首先,图像数据是高度相关的,相似的图像抽象到高维空间的数据分布是接近的。假设其分布如下图a所示(一个点代表一个图像,简化为2维)。由于初始化的时候,我们的参数一般都是0均值的,因此开始的拟合y=Wx+b,基本过原点附近,如图b红色虚线。因此,网络需要经过多次学习才能逐步达到如紫色实线的拟合,即收敛的比较慢。如果我们对输入数据先作减均值操作,如图c,显然可以加快学习。
再举一个可视化的解释:BN就是对神经网络每层输入数据的数据分布,做了下面左图不规律的数据分布到右图规则的数据分布的归一化操作。箭头表示模型寻找最优解的过程,显然右图的方式更方便,更容易。
最后, BN的公式如下:
输入信息:
x
\mathrm{x}
x 的值超过小批量:
B
=
{
x
1
⋯
m
}
B=\left\{x_{1 \cdots m}\right\}
B={x1⋯m};
要学习的参数:
γ
、
β
\gamma 、 \beta
γ、β
输出信息:
{
y
i
=
B
N
γ
,
β
(
x
i
)
}
。
\left\{y_i=B N_{\gamma, \beta}\left(x_i\right)\right\}_{\text {。 }}
{yi=BNγ,β(xi)}。
μ
B
←
1
m
∑
i
=
1
m
x
i
//小批量均值
σ
B
2
←
1
m
∑
i
=
1
m
(
x
i
−
μ
B
)
2
//小批量差异
X
i
^
←
x
i
−
μ
B
σ
B
+
ϵ
//标准化
y
i
←
γ
^
x
i
+
β
≡
B
N
β
,
γ
(
x
i
)
//按比例变换
\begin{aligned} & \mu_{\mathcal{B}} \leftarrow \frac{1}{m} \sum_{i=1}^m x_i & \text { //小批量均值 } \\ \\ & \sigma_{\mathcal{B}}^2 \leftarrow \frac{1}{m} \sum_{i=1}^m\left(x_i-\mu_{\mathcal{B}}\right)^2 & \text { //小批量差异 } \\ \\ & \widehat{\mathrm{X}_{\mathrm{i}}} \leftarrow \frac{\mathrm{x}_{\mathrm{i}}-\mu_{\mathcal{B}}}{\sqrt{\sigma_{\mathcal{B}}+\epsilon}} & \text { //标准化 }\\ \\ & y_i \leftarrow \hat{\gamma} x_i+\beta \equiv B N_{\beta, \gamma}\left(x_i\right) & \text { //按比例变换}\\ & \end{aligned}
μB←m1i=1∑mxiσB2←m1i=1∑m(xi−μB)2Xi
←σB+ϵxi−μByi←γ^xi+β≡BNβ,γ(xi) //小批量均值 //小批量差异 //标准化 //按比例变换
算法过程。
- 沿着通道计算每个 batch 的均值 u \mathrm{u} u;
- 沿着通道计算每个 batch 的方差 σ ∧ 2 \sigma \wedge 2 σ∧2;
- 对 x \mathrm{x} x 做归一化, x ′ = ( x − u ) / δ 2 + ε x^{\prime}=(x-\mathrm{u}) / \sqrt{\delta^2+\varepsilon} x′=(x−u)/δ2+ε ;
- 加入缩放和平移变量 γ \gamma γ 和 β \beta β, 归一化后的值, y = γ x ′ + β y=\gamma x^{\prime}+\beta y=γx′+β 加入缩放平移变量的原因是:不一定每次都 是标准正态分布, 也许需要偏移或者拉伸。保证每一次数据经过归一化后还保留原有学习得来的特 征, 同时又能完成归一化操作, 加速训练。这两个参数是用来学习的参数。
2、Architectural Details
GoogLeNetV2网络详细参数如下,除了BN,基本没有什么太大的变动。
小结
GoogLeNetV2模型引入了批量数据归一化(Batch Normalization)技术,这可以使得网络更加稳定和收敛更快。在训练过程中,Batch Normalization可以对每个小批量的数据进行标准化操作,从而使得输入数据更加平稳和稳定。
GoogLeNetV2在ImageNet图像分类竞赛中取得了优异的成绩,准确率达到了78.8%。它的性能和效率都比GoogLeNetV1更好。
三、GoogLeNetV3
GoogLeNet Inception V3在《Rethinking the Inception Architecture for Computer Vision》中提出,该论文的亮点在于:
- 提出四个通用的网络结构设计准则
- 引入卷积分解提高效率(空间可分离卷积)
- 引入高效的feature map降维方法
- 平滑样本标签
1、Motivation
在V1版本中,文章也没给出有关构建Inception结构注意事项的清晰描述。因此,在文章中作者首先给出了一些已经被证明有效的用于放大网络的通用准则和优化方法。这些准则和方法适用但不局限于Inception结构。
2 、General Design Principles
准则1:模型设计者应避免在神经网络的前若干层产生特征表示的瓶颈。
神经网络的特征提取过程包括多层卷积。一个直观且符合常识的理解是:如果网络前面的特征提取过程过于粗糙,那么就可能会丢失细节信息,即使后面的结构再精细也无法有效地进行特征表示和组合。
例如,如果一开始就直接从35×35×320被抽样降维到了17×17×320,那么特征的细节就会大量丢失,即使后续使用Inception结构进行各种特征提取和组合也无济于事。因此,在对特征图进行降维的同时,一般会对channel通道进行升维。
因此,随着层数的加深,特征图的大小应该逐渐变小,但为了保证特征能得到有效表示和组合,其通道数量会逐渐增加。一个简单的理解是:卷积操作就可以在图像的空间维度上进行特征提取,并把提取到的特征转移到channel维度上。
准则2:在模型中增加卷积次数可以解耦更多特征,帮助网络的收敛。
当输出特征相互独立时,输入信息就能被更彻底地分解,而子特征内部相关性就会更高。将相关性强的特征聚集在了一起会更容易收敛。简单点说就是:提取到的特征越多,对下游任务的帮助就越大。例如,如果只知道眼睛这一个特征,识别某个人会很难;。但如果能够了解到五官的所有特征,那么问题就会更容易解决,从而提高识别准确率。
对于神经网络的某一层,通过更多的输出分支,可以产生互相解耦的特征表示,从而产生更多高阶稀疏特征,而加速收敛,具体方法如下。
首先,一个前置小知识:5×5大小的卷积核可以使用两个3×3的小卷积核代替。因为5×5大小的卷积核感受野是5×5=25,而两个3×3的卷积核堆叠在一起时,第一层的感受野是9,第二层的感受野是25,两者感受野相同。同理,3×3 kernel大小的卷积核可以使用一个3×1和一个1×3的小卷积核代替。具体图例如图所示。
所以,为了在网络中增加更多的卷积次数,GoogLeNetV3对inception做了如下改进:首先将inception中的5X5卷积使用两个3X3卷积进行替代,再组合使用1X3和3X1的卷积来替代3X3的卷积。
值得一提的是:一个n×n卷积核可以分解为通过顺序相连的两个1×n和n×1的卷积,这种操作也称空间可分离卷积(有点像矩阵分解),如果n=3,计算性能可以提升1-(3+3)/9=33%。当然,它的缺点也很明显,并不是所有的卷积核都可以拆成两个1×n和n×1卷积核相乘的形式。实际上,作者发现在网络的前期使用这种分解效果并不好,只有在中度大小的feature map上使用效果才会更好。(对于mxm大小的feature map,建议m在12到20之间)。
至于Figure7中的1 x n和 n x 1 的卷积组合方式是并联,是因为作者希望模型变得更宽而不是更深,以解决表征性瓶颈。如果该模块没有被拓展宽度,而是变得更深,那么维度会过多减少,造成信息损失。在General Design Principles的1和2中也有解释。
准则3:对模型的特征维度进行合理的压缩,可以减少计算量
GoogLeNetV1中提出的用1×1卷积核先对特征维度降维再进行特征提取就是利用这个准则。这是因为在降维过程中,相邻单元之间存在强关联性,因此在输出用于空间聚合的情况下,信息损失要小得多。由于这些信号易于压缩,降维甚至有助于更快地学习。
准则4:模型网络结构的深度和宽度(特征维度数)要做到平衡
深度和宽度都是神经网络的重要参数。深度通常与网络的抽象能力相关,而宽度则与网络的容量相关。在设计网络时,需要在深度和宽度之间找到合适的平衡,以便网络能够有效地学习复杂的模式。
3 、优化辅助分类器
GoogLeNetV1中的辅助分类器可以帮助网络训练时回传梯度,并在一定程度上能够起到正则的作用。不过GoogLeNetV3在训练时发现,GoogLeNetV1中的辅助分类器存在问题:辅助分类器在训练初期的时候并不能加速收敛,只有当训练快结束时它才会略微提高网络精度。因此,在GoogLeNetV3版本中,第一个辅助分类器被去掉了。
4、优化下采样操作
一般情况下,如果想让图像缩小,可以有如下两种方式:
先池化再作Inception卷积,或者先作Inception卷积再作池化。
方法一(左图)先作pooling(池化)会导致特征表示遇到瓶颈(特征缺失),
方法二(右图)是正常的缩小,但计算量很大。
为了同时保持特征表示且降低计算量,将网络结构改为下图,使用两个并行化的模块来降低计算量(卷积、池化并行执行,再进行合并):
5 、优化标签
深度学习中通常使用One Hot向量作为分类标签,用于指示分类器的唯一结果。这种标签类似于信号与系统中的脉冲函数,也被称为“Dirac delta”,即只在某个位置上取1,其它位置上都是0。这种方式会鼓励模型对不同类别的输出差异较大的分数,或者说,模型过分相信它的判断。然而,对于一个由多人标注的数据集中,不同人标注的规则可能不同,每个人的标注也可能会有一些错误。模型对标签的过分信任会导致过拟合。
标签平滑(Label-Smoothing Regularization, LSR)是应对该问题的有效方法之一,它的具体思想是降低我们对于标签的信任,例如我们可以将目标标签从1稍微降到0.9,或者将从0稍微升到0.1。转换成Python代码,就是。
New_labels = (1.0 - label_smoothing) * one_hot_labels + label_smoothing / num_classes
在网络实现的时候,令“label_smoothing = 0.1,num_classes = 1000”。Label Smooth提高了网络精度0.2%。Label Smooth稍微地平滑了原本突兀的“one_hot_labels”,避免了网络过度学习标签而产生的弊端。
6、Architectural Details
7 、小结
GoogLeNetV3模型旨在通过重新设计Inception模块的结构,提高深度卷积神经网络的性能和效率。GoogLeNetV3总结了设计网络的四个准则,并且优化了下采样操作,辅助分类器和标签平滑。通过这些技巧和方法,GoogLeNetV3在图像分类、目标检测和语义分割等计算机视觉任务中取得了很好的表现。
四、GoogLeNetV4
《Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning》一文中的亮点是:提出了效果更好的GoogLeNet Inception v4网络结构;与残差网络融合,提出效果不逊于v4但训练速度更快的GoogLeNet Inception ResNet结构。
1、 GoogLeNet Inception V4网络结构
2、 GoogLeNet Inception Residual V4网络结构
总而言之,GoogLeNetV4模型变得更加复杂了,笔者认为这样复杂的模型设计实际上融合了大量的人为先验知识;不如像ResNet这样简单的模型,通过大量的数据来让模型自己学习总结知识。模型学习到的知识往往比人为赋予的归纳偏置上限更高。所以这里不再对这些结构做展开介绍了。
3 、小结
GoogLeNet V4,也被称为 Inception V4,是一个卷积神经网络(Convolutional Neural Network,CNN)模型,它是 Inception 系列模型的一个重要扩展。Inception 系列模型最初由 Google 提出,旨在通过复杂的 Inception 模块优化网络的结构,以提高效率和性能。
Inception V4 模型引入了许多改进,包括:
- 更深更宽的网络:Inception V4 有更多的层和更宽的层,这使得它能够学习更复杂的模式。然而,这也增加了计算的复杂性。
- 引入残差连接:在 Inception V4 中,引入了残差连接,这是一种跳跃连接,可以帮助梯度更好地流过网络。这种结构的灵感来自于 ResNet(残差网络),它已经在深度学习中显示出了其强大的性能。
- 进一步优化的 Inception 模块:Inception V4 进一步优化了 Inception 模块,包括了更多的分支和更复杂的结构。这使得模型能够更好地平衡宽度和深度,从而提高性能。
总的来说,Inception V4 通过引入一系列的优化和改进,提高了网络的性能和效率。然而,这也增加了模型的复杂性和计算负担。不过,考虑到其在各种任务上的优秀表现,这种复杂性是值得的。
五、GoogLeNetV5
深度可分离卷积(Depthwise Separable Convolution)最初由Laurent Sifre在其博士论文Rigid-Motion Scattering For Image Classification中提出。
这篇文章主要从Inception模块的角度出发,探讨了Inception和深度可分离卷积的关系,以一个全新的角度解释深度可分离卷积。再结合经典的残差网络(详见ResNet),一个新的架构Xception应运而生。Xception取义自Extreme Inception,即Xception是一种极端的Inception模型。
1 、inception回顾
Inception的核心思想是将channel分成若干个不同感受野大小的通道,除了能获得不同的感受野,Inception还能大幅的降低参数数量。我们看下图1中一个简单版本的Inception模型:
2 、inception改进
对于一个输入的Feature Map,首先通过三组 1X1 卷积得到三组Feature Map,它和先使用一组 1X1 卷积得到Feature Map,再将这组Feature Map分成三组是完全等价的。假设图中 1X1 卷积核的个数都是 K1 ,3X3 的卷积核的个数都是 K2,输入Feature Map的通道数为 m ,那么这个简单版本的参数个数为:
m
×
k
1
+
3
×
3
×
3
×
k
1
/
3
×
k
2
/
3
=
m
×
k
1
+
3
×
k
1
×
k
2
m \times k_1+3 \times 3 \times 3 \times k_1 / 3 \times k_2 / 3=m \times k_1+3 \times k_1 \times k_2
m×k1+3×3×3×k1/3×k2/3=m×k1+3×k1×k2
对比相同通道数,但是没有分组的普通卷积,普通卷积的参数数量为:
m
×
k
1
+
3
×
3
×
k
1
×
k
2
m \times k_1+3 \times 3 \times k_1 \times k_2
m×k1+3×3×k1×k2
即普通卷积的参数数量约为Inception的三倍。
3 、Xception
如果Inception是将 3x3 卷积分成3组,那么考虑一种极端的情况,我们如果将Inception的 1x1 得到的 k1 个通道的Feature Map完全分开呢?也就是使用 k1 个不同的 3x3 卷积分别在每个通道上进行卷积,它的参数数量是 m × k 1 + k 1 × 3 × 3 \mathrm{m} \times \mathrm{k}_1+\mathrm{k}_1 \times 3 \times 3 m×k1+k1×3×3
这个的参数数量是普通卷积的 1/k ,我们把这种形式的Inception叫做Extreme Inception,如图所示。
4 、对比深度可分离卷积
深度可分离卷积的详细介绍请移步MobileNet系列文章,下面是深度可分离卷积的操作简图:
可以看出,两者非常类似。唯一的区别是 1X1 卷积的执行顺序是在 Depthwise Conv之前还是之后而已。 两个算法的提出时间近似,不存在谁抄袭谁的问题。他们从不同的角度揭示了深度可分离卷积的强大作用,MobileNet的思路是通过将普通 卷积拆分的形式来减少参数数量,而Xception是通过对Inception的充分解耦来完成的。
5、Architectural Details
6 、GoogLeNetV5小结
作为GoogLeNet系列文章的终章,GoogLeNetV5 模型以实验结果为导向,放弃了GoogLeNetV1-V4中将1×1、3×3、5×5卷积核并列的结构。与GoogLeNetV4中复杂的模型结构相比,Xception这种简单的模型结构反而取得了更好的性能。这也是为什么笔者没有详细介绍GoogLeNetV4 模型的原因。在笔者看来,GoogLeNetV4 模型的设计中过于强调了人为的思维方式,试图将人类对图像的理解方式直接赋予给模型。但模型和人的思维方式是不同的,因此设计一个简单的结构,让模型从数据中自主学习会更加有效。
六、 代码实现
这里给出V1模型搭建的python代码(基于pytorch实现)。完整的代码是基于图像分类问题的(包括训练和推理脚本,自定义层等)详见我的GitHub: 完整代码链接
import torch.nn as nn
import torch
import torch.nn.functional as F
class BasicConv2d(nn.Module):
def __init__(self, in_channels, out_channels, **kwargs):
super(BasicConv2d, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
x = self.conv(x)
x = self.relu(x)
return x
class Inception(nn.Module):
def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
super(Inception, self).__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, 1)
class InceptionAux(nn.Module):
def __init__(self, in_channels, num_classes):
super(InceptionAux, self).__init__()
self.averagePool = nn.AvgPool2d(kernel_size=5, stride=3)
self.conv = BasicConv2d(in_channels, 128, kernel_size=1) # output[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.averagePool(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, 1)
x = F.dropout(x, 0.5, training=self.training)
# N x 2048
x = F.relu(self.fc1(x), inplace=True)
x = F.dropout(x, 0.5, training=self.training)
# N x 1024
x = self.fc2(x)
# N x num_classes
return x
class GoogLeNet(nn.Module):
def __init__(self, num_classes=1000, aux_logits=False, init_weights=False):
super(GoogLeNet, self).__init__()
self.aux_logits = aux_logits
self.conv1 = BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3)
self.maxpool1 = nn.MaxPool2d(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.maxpool2 = nn.MaxPool2d(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.maxpool3 = nn.MaxPool2d(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.maxpool4 = nn.MaxPool2d(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 self.aux_logits:
self.aux1 = InceptionAux(512, num_classes)
self.aux2 = InceptionAux(528, num_classes)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.4)
self.fc = nn.Linear(1024, num_classes)
if init_weights:
self._initialize_weights()
def forward(self, x):
# N x 3 x 224 x 224
x = self.conv1(x)
# N x 64 x 112 x 112
x = self.maxpool1(x)
# N x 64 x 56 x 56
x = self.conv2(x)
# N x 64 x 56 x 56
x = self.conv3(x)
# N x 192 x 56 x 56
x = self.maxpool2(x)
# N x 192 x 28 x 28
x = self.inception3a(x)
# N x 256 x 28 x 28
x = self.inception3b(x)
# N x 480 x 28 x 28
x = self.maxpool3(x)
# N x 480 x 14 x 14
x = self.inception4a(x)
# N x 512 x 14 x 14
if self.training and self.aux_logits: # eval model lose this layer
aux1 = self.aux1(x)
x = self.inception4b(x)
# N x 512 x 14 x 14
x = self.inception4c(x)
# N x 512 x 14 x 14
x = self.inception4d(x)
# N x 528 x 14 x 14
if self.training and self.aux_logits: # eval model lose this layer
aux2 = self.aux2(x)
x = self.inception4e(x)
# N x 832 x 14 x 14
x = self.maxpool4(x)
# N x 832 x 7 x 7
x = self.inception5a(x)
# N x 832 x 7 x 7
x = self.inception5b(x)
# N x 1024 x 7 x 7
x = self.avgpool(x)
# N x 1024 x 1 x 1
x = torch.flatten(x, 1)
# N x 1024
x = self.dropout(x)
x = self.fc(x)
# N x 1000 (num_classes)
if self.training and self.aux_logits: # eval model lose this layer
return x, aux2, aux1
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
def googlenet(num_classes):
model = GoogLeNet( num_classes=num_classes)
return model
总结
本文对GoogLeNet一系列模型(V1,V2,V3,V4, V5), 从网络模型设计的动机和网络模型结构进行了详细的分析讲解。