2014年,GoogLeNet和VGG是当年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”
一般来说,提升网络性能最直接的办法就是增加网络深度和宽度,或者输入数据的大小;这也就意味着网络训练种巨量的参数。但这种方式存在以下问题:
(1)参数太多,如果训练数据集有限,很容易产生过拟合;
(2)网络越大、参数越多,计算复杂度越大,难以应用;
(3)网络越深,容易出现梯度弥散问题(梯度越往后穿越容易消失),难以优化模型。
所以,有人调侃“深度学习”其实是“深度调参”。
文章认为解决上述两个缺点的根本方法是将全连接甚至一般的卷积都转化为稀疏连接。一方面现实生物神经系统的连接也是稀疏的,另一方面有文献表明:对于大规模稀疏的神经网络,可以通过分析激活值的统计特性和对高度相关的输出进行聚类来逐层构建出一个最优网络。这点表明臃肿的稀疏网络可能被不失性能地简化。
早些的时候,为了打破网络对称性和提高学习能力,传统的网络都使用了随机稀疏连接例如Droupout
。但是,计算机软硬件对非均匀稀疏数据的计算效率很差,所以在AlexNet中又重新启用了全连接层,目的是为了更好地优化并行运算。
所以,现在的问题是有没有一种方法,既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能。大量的文献表明可以将稀疏矩阵聚类为较为密集的子矩阵来提高计算性能,据此论文提出了名为Inception 的结构来实现此目的。
大人的方式:我全都要
但是,使用5x5的卷积核仍然会带来巨大的计算量。 为此,采用1x1卷积核来进行降维。
例如:上一层的输出为100x100x128,经过具有256个输出的5x5卷积层之后(stride=1,pad=2),输出数据为100x100x256。其中,卷积层的参数为128x5x5x256。假如上一层输出先经过具有32个输出的1x1卷积层,再经过具有256个输出的5x5卷积层,那么最终的输出数据仍为为100x100x256,但卷积参数量已经减少为128x1x1x32 + 32x5x5x256,大约减少了4倍。
改进后的网络模型子结构如下:
完整的GoogLeNet就是由这种inception块堆叠而成的;如下图所示:
GoogLeNetV2最大的贡献就是提出了BatchNormalization数据归一化方法。
首先,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的好处如下:
作者认为:网络训练过程中参数不断改变导致后续每一层输入的分布也发生变化,而学习的过程又要使每一层适应输入的分布,因此我们不得不降低学习率、小心地初始化。作者将分布发生变化称之为internal covariate shift。
解决这个问题的方法是在训练网络的时会将输入减去均值,目的是为了加快训练。为什么减均值可以加快训练呢,这里做一个简单地说明:
首先,图像数据是高度相关的,相似的图像分布接近。假设其分布如下图a所示(一个点代表一个图像,简化为2维)。由于初始化的时候,我们的参数一般都是0均值的,因此开始的拟合y=Wx+b,基本过原点附近,如图b红色虚线。因此,网络需要经过多次学习才能逐步达到如紫色实线的拟合,即收敛的比较慢。如果我们对输入数据先作减均值操作,如图c,显然可以加快学习。
再举一个可视化的解释:BN就是对神经网络每层的输入数据的数据分布,做了下面左图不规律的数据分布到右图规则的数据分布的归一化操作。箭头表示模型寻找最优解的过程,显然有图的方式更方便,更容易。
GoogLeNet Inception V3在《Rethinking the Inception Architecture for Computer Vision》中提出(注意,在这篇论文中作者把该网络结构叫做v2版,我们以最终的v4版论文的划分为标准),该论文的亮点在于:
在V1版本中,文章也没给出有关构建Inception结构注意事项的清晰描述。因此,在文章中作者首先给出了一些已经被证明有效的用于放大网络的通用准则和优化方法。这些准则和方法适用但不局限于Inception结构。
避免特征表示上的瓶颈,尤其在神经网络的前若干层
神经网络包含一个自动提取特征的过程,例如多层卷积,直观并符合常识的理解:如果在网络初期特征提取的太粗,细节已经丢了,后续即使结构再精细也没法做有效表示了;
所以feature map的大小应该是随着层数的加深逐步变小,但为了保证特征能得到有效表示和组合其通道数量会逐渐增加。
举个例子,刚开就始直接从35×35×320被抽样降维到了17×17×320,特征细节被大量丢失,即使后面有Inception去做各种特征提取和组合也没用,因此,再对feature map进行降维的同时,一般会对channel通道进行升维。
特征的数目越多收敛的越快
相互独立的特征越多,输入的信息就被分解的越彻底,分解的子特征间相关性低,子特征内部相关性高,把相关性强的聚集在了一起会更容易收敛。规则2和规则1可以组合在一起理解,特征越多能加快收敛速度,但是无法弥补Pooling造成的特征损失,Pooling造成的representational bottleneck要靠其他方法来解决。
对于神经网络的某一层,通过更多的激活输出分支可以产生互相解耦的特征表示,从而产生高阶稀疏特征,从而加速收敛,注意下图的1×3和3×1激活输出:
合理的压缩特征维度数,来减少计算量
inception-v1中提出的用1x1卷积先降维再作特征提取就是利用这点。不同维度的信息有相关性,降维可以理解成一种无损或低损压缩,即使维度降低了,仍然可以利用相关性恢复出原有的信息。
整个网络结构的深度和宽度(特征维度数)要做到平衡
只有等比例的增大深度和维度才能最大限度的提升网络的性能。很有道理,也很模糊,没有给出具体的参数指导,后期的模型如 EfficientNet(V1, V2)就填了这个坑。
如上图所示为GoogLeNetV3模型详细结构,下面对figure5, figure6,figure7做详细解释:
利用两个3X3的卷积代替5X5的卷积,在保证感受野的同时,减少了计算量
一个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之间)。
模块中的滤波器组被扩展(即变得更宽而不是更深),以解决表征性瓶颈。如果该模块没有被拓展宽度,而是变得更深,那么维度会过多减少,造成信息损失。在General Design Principles的1和2中也有解释,如下图所示:
V1中的辅助分类器有点问题:auxiliary classifiers在训练初期的时候并不能加速收敛,只有当训练快结束的时候它才会略微提高网络精度。
就把第一个auxiliary classifiers去掉了!(又回去了…)并且,auxiliary classifiers能够起到regularizer的作用。
一般情况下,如果想让图像缩小,可以有如下两种方式:
先池化再作Inception卷积,或者先作Inception卷积再作池化。
方法一(左图)先作pooling(池化)会导致特征表示遇到瓶颈(特征缺失),
方法二(右图)是正常的缩小,但计算量很大。
为了同时保持特征表示且降低计算量,将网络结构改为下图,使用两个并行化的模块来降低计算量(卷积、池化并行执行,再进行合并):
深度学习用的labels一般都是one hot向量,用来指示classifier的唯一结果,这样的labels有点类似信号与系统里的脉冲函数,或者叫“Dirac delta”,即只在某一位置取1,其它位置都是0。
Labels的脉冲性质会引发两个不良后果:一是over-fitting,另外一个是降低了网络的适应性。
一种解决方法是加正则项,即对样本标签给个概率分布做调节:
在网络实现的时候,令 label_smoothing = 0.1,num_classes = 1000。Label smooth提高了网络精度0.2%。
Label smooth 把原来很突兀的one_hot_labels稍微的平滑了一点,避免了网络过度学习labels而产生的弊端。
《Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning》一文中的亮点是:提出了效果更好的GoogLeNet Inception v4网络结构;与残差网络融合,提出效果不逊于v4但训练速度更快的GoogLeNet Inception ResNet结构。
这里给出模型搭建的python代码(基于pytorch实现)。完整的代码是基于图像分类问题的(包括训练和推理脚本,自定义层等)详见我的GitHub: 完整代码链接
from re import S
import torch.nn as nn
import torch
import torch.nn.functional as F
from custom_layers.CustomLayers import ConvActivation, Inception
class Auxiliary_classcification(nn.Module):
def __init__(self, input_channels, num_classes):
super().__init__()
self.avgpool = nn.AvgPool2d(kernel_size=5, stride=3)
self.conv = ConvActivation(input_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.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)
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
class GoogLeNet(nn.Module):
def __init__(self, num_classes=None, aux_logits=False, init_weights=False):
super().__init__()
self.aux_logits = aux_logits
self.conv1 = ConvActivation(3, 64, kernel_size=7, stride=2, padding=3)
self.maxpool = nn.MaxPool2d(3, stride=2, ceil_mode=True)
self.conv2 = ConvActivation(64, 192, kernel_size=3, padding=1)
self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)
# input_channels, out_nc1x1, out_nc3x3_reduce, out_nc3x3, out_nc5x5_reduce, out_nc5x5, out_nc_pool
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 = Auxiliary_classcification(input_channels=512, num_classes=num_classes)
self.aux2 = Auxiliary_classcification(input_channels=528, num_classes=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._initial_weights()
def forward(self, x):
x = self.conv1(x) # [b*3*224*224] --> [b*64*112*112]
x = self.maxpool(x) # [b*64*112*112] --> [b*64*56*56]
x = self.conv2(x) # [b*64*56*56] --> [b*192*56*56]
x = self.maxpool2(x) # [b*192*56*56] --> [b*192*28*28]
x = self.inception3a(x) # [b*192*28*28] --> [b*256*28*28]
x = self.inception3b(x) # [b*256*28*28] --> [b*480*28*28]
x = self.maxpool3(x) # [b*256*14*14] --> [b*480*14*14]
x = self.inception4a(x) # [b*480*14*14] --> [b*512*14*14]
if self.training and self.aux_logits: # eval model lose this layer
aux1 = self.aux1(x)
x = self.inception4b(x) # [b*512*14*14] --> [b*512*14*14]
x = self.inception4c(x) # [b*512*14*14] --> [b*512*14*14]
x = self.inception4d(x) # [b*512*14*14] --> [b*528*14*14]
if self.training and self.aux_logits: # eval model lose this layer
aux2 = self.aux2(x)
x = self.inception4e(x) # [b*528*14*14] --> [b*832*14*14]
x = self.maxpool3(x) # [b*832*7*7] --> [b*832*7*7]
x = self.inception5a(x) # [b*832*7*7] --> [b*832*7*7]
x = self.inception5b(x) # [b*832*7*7] --> [b*1024*7*7]
x = self.avgpool(x) # [b*1027*7*7] --> [b*1024*1*1]
x = torch.flatten(x, 1) # [b*1027*1*1] --> [b*1024]
x = self.fc(x) # [b*1024] --> [b*num_classes]
if self.training and self.aux_logits: # eval model lose this layer
return x, aux2, aux1
return x
def _initial_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)
对GoogLeNet一系列模型(V1,V2,V3,V4), 从网络模型设计的动机和网络模型结构进行了详细的分析讲解。