文章目录

  • 一、GoogLeNet(Inception V1)
  • 1.1 动机与思路
  • 1.2 InceptionV1
  • 1.3 GoogLeNet的复现



一、GoogLeNet(Inception V1)

1.1 动机与思路

受到NiN网络的启发,谷歌引入了一种全新的网络架 构:Inception block,并将使用Inception V1的网络架构称为GoogLeNet(虽然从名字上来看致敬了 LeNet5算法,但GoogLeNet已经基本看不出LeNet那种经典的卷积+池化+全连接的结构了)。

Inception直译是“起始时间”,也是电影《盗梦空间》的英文名称。或许谷歌团队是无心插柳,但 Inception块的出现成为了深度视觉发展历史上的一个新的起点。从2014年的竞赛结果来看,Inception V1的效果只比VGG19好一点点(只比VGG降低了0.6%的错误率),两个架构在深度上也没有差太多, 但在之后的研究中,Inception展现出比VGG强大许多的潜力——不仅需要的参数量少很多,架构可以达 到的上限也更高。随着架构的迭代更新,Inception V3和V4已经是典型的SOTA模型,可以在ImageNet 数据集上达到3%的错误率,但VGG在ILSVRC上的表现基本就是模型的极限了。

pytorch 怎么利用nccl_2d

GoogLeNet在设计之初就采用了一种与传统 CNN完全不同的构建思路。自从LeNet5定下了卷积、池化、线性层串联的基本基调,研究者们在相当长 的一段时间内都在这条道路上探索,最终抵达的终点就是VGG。VGG找出了能够最大程度加大模型深 度、增强模型学习能力的架构,并且利用巧妙的参数设计让特征图的尺寸得以控制,但VGG以及其他串 联架构的缺点也是显而易见的,最关键的(甚至有些老生常谈的)一点就是参数过多,各层之间的链接 过于“稠密”(Dense),计算量过大,并且很容易过拟合。为了解决这个问题,我们之前已经提出了多 种方法,其中最主流的是:

1、使用我们在上一节中提出的分组卷积、舍弃全连接层等用来消减参数量的操作,让神经元与神经元之 间、或特征图与特征图之间的连接数变少,从而让网络整体变得“稀疏”;

2、引入随机的稀疏性。例如,使用类似于Dropout的方式来随机地让特征矩阵或权重矩阵中的部分数据 为0;

3、引入GPU进行计算。
在2014年之前,以上操作就是我们目前为止接触的所有架构在减少参数量、防止过拟合上做出的努力。

其中NiN主要使用方法1,AlexNet和VGG主要使用方法2和3,但这些方法其实都存在一定的问题:

1、首先,分组卷积等操作虽然能够有效减少参数量,却也会让架构的学习水平变得不稳定。在神经网络由 稠密变得稀疏(Sparse)的过程中,网络的学习能力会波动甚至会下降,并且网络的稀疏性与学习能力 之间的下降关系是不明确的,即我们无法精确控制稀疏的程度来把握网络的学习能力,只能靠孜孜不倦 的尝试来测试学习能力较强的架构。

2、其次,随机的稀疏性与GPU计算之间其实是存在巨大矛盾的。现代硬件不擅长处理在随机或非均匀稀疏 的数据上的计算,并且这种不擅长在矩阵计算上表现得尤其明显。这与现代硬件查找、缓存的具体流程 有关,当数据表现含有不均匀的稀疏性时(即数据中0的分布不太均匀时),即便实际需要的计算量是原 来的1/100,也无法弥补数据查找(finds)和缓存缺失(cache misses)所带来的时间延迟。简单来说,GPU 擅长的是简单大量的计算操作,不同计算之间的相似性越高,GPU的计算性能就越能发挥出来,这种“相似性”表现在数据的分布相似(例如,都是偏态分布)、计算方式相似(例如,都是先相乘再相加)等方方面面。相对的,稠密的连接却可以以更快的速度被计算。这并不是说稀疏的网络整体计算时间会更长,而是说 在相同参数量/连接数下,稠密的结构比稀疏的结构计算更快。

此时就需要权衡了——稠密结构的学习能力更强,但会因为参数量过于巨大而难以训练。稀疏结构的参 数量少,但是学习能力会变得不稳定,并且不能很好地利用现有计算资源。在2013年的时候,按分布让 权重为0的Dropout刚刚诞生(2012年发表论文),在每层输出之后调整数据分布的Batch Normlization还没有诞生(2015年发表论文),创造VGG架构的团队选择了传统道路,即在学习能力更 强的稠密架构上增加Dropout,但GoogLeNet团队的思路是:使用普通卷积、池化层这些稠密元素组成 的块去无限逼近(approximate)一个稀疏架构,从而构造一种参数量与稀疏网络相似的稠密网络。

这种思路的核心不是通过减少连接、减少扫描次数等“制造空隙”的方式来降低稠密网络的参数量,而是直 接在架构设计上找出一种参数量非常少的稠密网络。

在数学中,我们常常使用稀疏的方式去逼近稠密的结构(这种操作叫做稀疏估计 sparse approximation),但反过来用稠密结构去近似稀疏架构的情况却几乎没有,因此能否真正实现这种“逼 近”是不得而知的,不过这种奇思妙想正是谷歌作为一个科技公司能够持续繁荣的根基之一。在 GoogLeNet的论文中,作者们表示,在拓扑学中,几何图形或空间在连续改变形状后还能保持性状不变,这说明不同的结构可以提供相似的属性。同时,也有论文表示,稀疏数据可以被聚类成携带高度相似信息的密集数据来加速硬件计算,考虑到神经元和特征图的本质其实都是数据的组合,那稀疏的神经元应该也可以被聚类成携带高度相似信息的密集神经元,如果神经元可以被聚类,那这很可能说明稀疏架构在一定程度上应该可以被稠密架构所替代。

1.2 InceptionV1

与之前VGG和AlexNet中从上向下串联卷积层的方式不同, Inception块使用了卷积层、池化层并联的方式。在一个Inception块中存在4条线路,每条线路可以被叫做一个分枝(branch):第一条线路上只有一个1x1卷积层,只负责降低通道数;第二条路线由一个1x1 卷积层和一个3x3卷积层组成,本质上是希望使用3x3卷积核进行特征提取,但先使用1x1卷积核降低通道数以此来降低参数量和计算量(降低模型的复杂度);第三条线路由一个1x1卷积层和一个5x5卷积层组成,其基本思路与第二条线路一致;最后一条线路由一个3x3池化层和一个1x1卷积层组成,将池化也当做一种特征提取的方式,并在池化后使用1x1卷积层来降低通道数。不难注意到,所有的线路都使用了巧妙的参数组合,让特征图的尺寸保持不变,因此在四条线路分别输出结果之后,Inception块将四种 方式生成的特征图拼接在一起,形成一组完整的特征图,这组完整的特征图与普通卷积生成的特征图在 结构、计算方式上并无区别,因此可以被轻松地输入任意卷积、池化或全连接的结构。在论文中, GoogLeNet自然是使用了224x224的ImageNet数据集,不过在下面的架构图中我们使用了尺寸较小的特征图进行表示。

pytorch 怎么利用nccl_pytorch 怎么利用nccl_02


首先,同时使用多种卷积核可以确保各种类型和层次的信息都被提取出来在普通的卷积网络中,我们必须选择不同尺寸的过滤器(卷积核、池化核)对图像进行特征提取1x1卷积核可以最大程度提取像素与像素之间的位置信息,尺寸较大的卷积核则更多可以提取相邻像素之间的联系信息最大池化层则可以提取出局部中最关键的信息,但在串联结构中,对同一张图片/特征图,我们只能选择一个过滤器来 使用,这意味着我们很可能会损失其他过滤器可以提取出的信息。而在Inception中,我们一次性使用了全部可能的方式,因此无需再去考虑究竟哪一种提取方式才是最好的,在输出的时候,Inception将所有 核提取出来的特征图堆积整合,确保提取出的信息是最全面的。

其次,并联的卷积池化层计算效率更高。串联的卷积计算必须一层一层进行,但并联的卷积/池化层可以 同时进行计算,这种将特征提取的过程并行处理的方式可以极速加快计算的运行效率。同时,由于每个 元素之间都是稠密连接,并不存在任何类似于分组卷积那样减少连接数量的操作,使得inception可以高 效利用现有硬件在稠密矩阵上的计算性能。

大量使用1x1卷积层来整合信息,既实现了“聚类信息”又实现了大规模降低参数量,让特征图数量实现了前所未有的增长。出现在每一条线路的1x1卷积层承担了调整特征图数目的作用,它可以自由将特征图 上的信息聚合为更少的特征图,让特征图信息之间的聚合更加“密集”。同时,每个1x1卷积核之后都跟着 ReLU激活函数,这增加了一次使用非线性方式处理数据的机会,某种程度上也是增加了网络的“深度”。 除此之外,1x1卷积层最重要的作用是控制住了整体的参数量,从而解放了特征图的数量。这一点可以 从VGG和GoogLeNet整体架构的参数量上轻松看出来。

pytorch 怎么利用nccl_pytorch 怎么利用nccl_03


上图是GoogLeNet的主体架构。Inception内部是稠密部件的并联,而整个GoogLeNet则是数个 Inception块与传统卷积结构的串联。这张架构图来自GoogLeNet的原始论文,其中patch_size就是过滤 器的尺寸,3x3 reduce和5x5 reduce就是指inception块中3x3和5x5卷积层之前的1x1卷积层的输出 量,pool proj中写的数字实际上是池化层后的1x1卷积层的输出量。与其他架构图相似,虽然没有被展 示出来,但在每一个卷积层之后都有ReLU激活函数;同样的,从输出层的特征图尺寸来看,应该有不少卷积层中都含有padding,但无论在论文或架构中都没有被展示出来。当我们来查看GoogLeNet的架构 图时,可能很容易就注意到以下几点:

1、在inception的前面有着几个传统的卷积层,并且第一个卷积层采用了和LeNet相似的处理方法:先 利用较大的卷积核大幅消减特征图的尺寸,当特征图尺寸下降到28x28后再使用inception进行处理。如 果将卷积+池化看做一个block(块),那inception之前已有两个blocks了,所以Inception的编号是从3 开始。其中,block3、4、和5分别有2个、5个、2个Inception。

2、Inception中虽然已经包含池化层,但inception之后还是有用来让特征图尺寸减半的池化层,并且和 VGG一样,让特征图尺寸减半的池化层也是5个,最终将特征图尺寸缩小为7x7。不难发现,在 GoogLeNet的主体架构中,Inception实际上取代了传统架构中卷积层的地位,不过inception中有2层卷积层,因此网络总体有22层,比VGG19多了三层。

3、在架构的最后,使用了核尺寸为7x7的平均池化层。考虑到此时的特征图尺寸已经是7x7,这个池化 层实际上一个用来替代全连接层的全局平均池化层,这和NiN中的操作一样。在全局平均池化层的最 后,又跟上了一个线性层,用于输出softmax的结果。

如果将inception看做卷积层,那GoogLeNet的主体架构也不是标新立异的类型。不过,除了主体架构之 外,GoogLeNet还使用了“辅助分类器”(auxiliary classifier)以提升模型的性能。辅助分类器是除了主 体架构中的softmax分类器之外,另外存在的两个分类器。在整体架构中,这两个分类器的输入分别是 inception4a和inception4d的输出结果,他们的结构如下:

pytorch 怎么利用nccl_pytorch 怎么利用nccl_04


将主体架构与辅助分类器结合,我们可以得到GoogLeNet的完整架构(见下图,该架构同样来自于原始论文。

在谷歌团队测试GoogLeNet网络性能的实验中,他们注意到稍微浅一些的GoogLeNet也有非常好的表 现,因此他们认为位于中层的inception输出的特征应该对分类结果至关重要,如果能够在迭代中加重这 些中层inception输出的特征的权重,就可能将模型引导向更好的反向。因此,他们将位于中间的 inceptions的结果使用辅助分类器导出,并让两个辅助分类器和最终的分类器一共输出三个softmax结 果、依次计算三个损失函数的值,并将三个损失加权平均得到最终的损失。如此,只要基于最终的损失 进行反向传播,就可以加重在训练过程中中层inceptions输出结果的权重了。这种思想有点类似于传统 机器学习算法中的“集成”思想,一个GoogLeNet实际上集成了两个浅层网络和一个深层网络的结果来进 行学习和判断,在一个架构中间增加集成的思想,不得不说是GoogLeNet的一大亮点。

值得一提的是,在论文的架构图中包含了一种叫做局部响应归一化(Local Response Normalization, LRN)的功能,这个功能最初是在AlexNet的架构中被使用,但我们从来没有说明过它的细节。主要是因 为LRN是一个饱受争议、又对模型效果提升没有太多作用的功能,现在已基本被BN所替代(使用BN的 inception被称为Inception V2)。同时,GoogLeNet的论文中也并没有给出LRN的具体细节,因此现在 实现GoogLeNet的各个深度学习框架也基本上不考虑LRN的存在了。相对的,我们把所有的LRN层删掉后,在每个卷积层的后面加上了BN层,以确保更好的拟合效果。

1.3 GoogLeNet的复现

定义三个基本的类:

class BasicConv2d(nn.Module):
    def __init__(self, in_channels: int, out_channels: int, **kwargs):
        super(BasicConv2d, self).__init__()
        self.conv = nn.Sequential(nn.Conv2d(in_channels, out_channels, bias=False, **kwargs),
                                  nn.BatchNorm2d(out_channels),
                                  nn.ReLU(inplace=True))


    def forward(self,x):
        x = self.conv(x)
        return x

#测试
#BasicConv2d(2, 10, kernel_size=3)

#接下来,定义Inception块,由于inception块是串联的,所以不能通过sequential进行打包,需要用原始的self.形式

class Inception(nn.Module):
    def __init__(self, in_channels:int, ch1x1:int, ch3x3red:int, ch3x3:int, ch5x5red:int, ch5x5, pool_proj:int):
        super(Inception, self).__init__()
        self.bratch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)

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

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

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

    def forward(self, x):
        brach1 = self.bratch1(x)
        brach2 = self.bratch2(x)
        brach3 = self.bratch3(x)
        brach4 = self.bratch4x(x)
        outputs = [brach1, brach2, brach3, brach4]
        return torch.cat(outputs, 1)

#Inception(256, 564, 96, 128, 16, 32, 32)

#还需要单独定义的是辅助分类器(Auxiliary Classifier)的类。
#辅助分类器的结构其实与我们之前所写的传统卷积网络很相似,因此我们可以使用
#nn.Sequential来进行打包,并将分类器分成.features_和.clf_两部分来进行构建:
class AuxClf(nn.Module):
    def __init__(self, in_channels, num_classes):
        super(AuxClf, self).__init__()
        self.features = nn.Sequential(nn.AvgPool2d(5, stride=3),
                                      BasicConv2d(in_channels, 128, kernel_size=1))

        self.clf_ = nn.Sequential(nn.Linear(4*4*128, 1024),
                                  nn.ReLU(inplace=True),
                                  nn.Dropout(0.7),
                                  nn.Linear(1024, num_classes))

    def forward(self, x):
        x = self.features(x)
        x.view(-1, 4*4*128)
        x = self.clf_(x)
        return x

在定义好三个单独的类后,我们再依据GoogLeNet的完整架构将所有内容实现。虽然GoogLeNet的主体结构是串联,但由于存在辅助分类器,我们无法在使用nn.Sequential时单独将辅助分类器的结果提取出 来。如果按照辅助分类器存在的地方对架构进行划分,又会导致架构整体在层次上与GoogLeNet的架构 图有较大的区别,因此我们最终还是使用了self.的形式。在我们自己使用GoogLeNet时,我们不一定总要使用辅助分类器。如果不使用辅助分类器,我们则可以使用nn.Sequential来打包整个代码。包含辅助分类器的具体代码如下:

class GoogLeNet(nn.Module):
    def __init__(self,num_classes: int = 1000,blocks=None):
        super().__init__()
#我们可以自由输入三种不同类型的类的名字
#使用这种方法,当我们修改或重新定义任意的类时,我们只需要修改列表中的元素,而不需要修改 整个架构
        if blocks is None:
            blocks = [BasicConv2d, Inception, AuxClf]
        conv_block = blocks[0]
        inception_block = blocks[1]
        aux_clf_block = blocks[2]

        #block1
        self.conv1 = conv_block(3, 64, kernel_size=7, stride=2, padding=3)
        self.maxpool1 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

        #block2
        self.conv2 = conv_block(64, 64, kernel_size=1)
        self.conv3 = conv_block(64, 192, kernel_size=3, padding=1)
        self.maxpool2 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

        # block3
        self.inception3a = inception_block(192, 64, 96, 128, 16, 32, 32)
        self.inception3b = inception_block(256, 128, 128, 192, 32, 96, 64)
        self.maxpool3 = nn.MaxPool2d(3, stride=2, ceil_mode=True)

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

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

        #auxclf
        self.aux1 = aux_clf_block(512, num_classes)
        self.aux2 = aux_clf_block(528, num_classes)

        #clf
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.dropout = nn.Dropout(0.4)
        self.fc = nn.Linear(1024, num_classes)

    def forward(self,x):
        # block1
        x = self.maxpool1(self.conv1(x))

        # block2
        x = self.maxpool2(self.conv3(self.conv2(x)))

        # block3
        x = self.inception3a(x)
        x = self.inception3b(x)
        x = self.maxpool3(x)

        # block4
        x = self.inception4a(x)
        aux1 = self.aux1(x)

        x = self.inception4b(x)
        x = self.inception4c(x)
        x = self.inception4d(x)

        aux2 = self.aux2(x)

        x = self.inception4e(x)
        x = self.maxpool4(x)

        # block5
        x = self.inception5a(x)
        x = self.inception5b(x)
        # clf
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.dropout(x)
        x = self.fc(x)
        return x, aux2, aux1

# 建立数据,测试
x = torch.ones(10, 3, 224, 224)
net = GoogLeNet()
fc2, fc1 ,fc0 = net(x)
print(fc0.shape)
#torch.Size([10, 1000])
print(fc1.shape)
#torch.Size([10, 1000])
print(fc2.shape)
#torch.Size([10, 1000])