论文阅读: Going deeper with convolutions

论文阅读: Going deeper with convolutions

这是关于ILSVRC2014的优胜者GoogleNet的论文,论文详细地讲解了这个模型的核心模块**”Inception"**结构是如何提出的,实际上对于作者说的”稀疏性“,实在是没有理解。由此先写下自己理解的部分。

1、摘要

作者提出了一种”Inception“的结构来提升深度卷积神经网络的性能,并在ILSVRC2014中获得了成功。该结构的主要特点是提高了网络内部的计算效率(关于这一点,其实并不是十分理解)。Inception的设计遵从了Hebbian principle,同时结合多尺度处理的直觉经验。

**Hebbian principle:**指的是如果两个神经元常常同同时产生动作电位,或者说同时激活,则这两的神经元之间的连接就会变强,反之变弱。对应到神经网络中就是使用更少的卷积核,通过多尺度的卷积核进行不相关的特征组,实际上就是预先把相关性强的特征汇集,加速收敛。

2、引言

作者强调了最近以来深度学习令人欣喜的一点是,在图像分类、目标检测上的发展更多的源自于创新的想法(Idea),更有效的算法以及针对网络结构的改进。基于当下的移动端、嵌入式计算的要求,深度学习模型的算力、内存变得越发重要,进而说明本文提出的GoogleNet不单纯以提升准确性为目的,同时考虑了实用性。

3、相关工作

作者列举了自LeNet以来的深度学习模型在视觉任务上的成功,同时说明对于大型的数据集,传统的提升网络模型性能的方式是加深网络的层次、增加每一层中神经元的数量,同时使用一些诸如Dropout的方法来抑制过拟合。收到Serre等人的启发,作者想到不同尺寸的滤波器来处理多尺度的信息。

另外,Lin等人”Network in Network"的工作目的在于提升网络的特征表达能力,应用到CNN上就可以看成在原来堆叠的Conv上加入了额外的1x1卷积,即增加的非线性。在Inception结构里,1x1卷积的另一个主要作用是做特征的降维(dimension reduction),通过1x1卷积可以压缩特征通道,可以在相同的计算资源下加深、括宽网络结构,以获得更好的性能。

4、动机及考虑

扩大网络(深度、宽度)会导致参数体量变大,同时更容易过拟合,尤其在数据集受限的情况下。增大网络的另一个缺点就是急剧增加的计算量合理地分配计算资源变得尤为重要。解决上述问题的方法就是将全连接改为稀疏连接,哪怕是在卷积层内部(我的理解是在上一层生成的特征图上随机进行下一层连接?)

随后,作者提到了在当前的计算架构下,进行非一致性稀疏数据的计算是很没有效率的,因为运算时查找的开销以及cache命中率的低下使得转换为稀疏矩阵也不见得有多好的效果(??? 那还要稀疏连接,迷惑)。作者还提到,自Lecu等人开始,传统的Convnet都采用的是随机稀疏连接(random and sparse connection)因为这样可以打破特征对称性加快学习。但是到了Alexnet又回到了全连接,因为需要保持网络的一致性以利于并行计算。

那么,作者提出一个新的问题:能否找到一种新的网络架构可以利用额外的稀疏性,同时也要能够在当下的硬件条件下利用密集矩阵的计算来完成。大量关于稀疏矩阵计算的文献表明,将稀疏矩阵聚类成密集的相关子矩阵,在稀疏矩阵的乘法运算中往往能达到较高的性能,这样的方法同样也可以应用于构建非一致的深度学习架构。这可能就是作者为什么要使用多尺度卷积核的原因吧。。。。。

5、架构的细节

作者提到,Inception结构的主要思路是在卷积网络中,如何找到现有的密集组件(结构)来近似、覆盖一个最优的局部稀疏结构。按照我的理解就是在当下密集连接下怎么进行稀疏的近似,这一点和之前“稀疏矩阵转换为密集相关子矩阵”恰好是一个逆过程。而作者的做法就是在同一个层次上使用多个尺度的卷积核来提取信息,并把提取的特征整合形成下一层的输入。

除了使用多尺度的卷积以外,在Inception结构中,作者还加入了maxpooling层以提升性能。同时,作者提到随着网络层次的加深,提取的特征越发抽象,则特征额空间聚合性变弱,因此需要在高层次使用更高比例的3x3、5x5卷积(关于这一点,我并没有在后面的网络架构中看出3x3、5x5卷积的比例有提高。。。。)

初始的Inception结构如下所示:
Inception naive

原始的结构出现了一个不可忽视的问题:如果特征图的通道数过大(即上一层卷积核数量过多)会导致当前layer的卷积计算耗费较大,当在模块内加上了pooling层之后,这个问题越发明显(因为pooling层是保持通道数的),由此针对原始结构做了改进,加上1x1卷积做降维处理)

改进后的Inception结构如下图所示:

Inception

需要注意的一点是:对于maxpooling层来说降维处理是在pooling之后。

Inception架构的主要优点在于可以在控制计算复杂性的情况下显著增加每一层的神经元数量(由于conv之前的1x1卷积)同时,这种架构也让视觉信息能够在不同尺度下进行处理、聚合,进而同时提取不同尺度的特征。

6、GoogleNet

GoogleNet是Inception结构的一个范例,作者及其团队利用这个22层的GooglNet参加了ILSVRC2014竞赛。具体的网络结构如下:

GoogleNet structure

其中**#1x1、#3x3、#5x5表示卷积核的个数,#3x3 reduce、#5x5 reduce**表示随后的1x1卷积核的数量,pool proj 表示maxpooling层之后的1x1卷积核数。整个网络以224x224x3(RGB)作为输入,同时在执行classifier之前加入了一层平均池化(avg pooling)和额外的线性层(linear)。实验结果表明将全连接层改为平均池化在Top-1准确率上有0.6%的提升。

另外,随着网络结构的加深,GoogleNet也不免会出现梯度在反向传播时消失的问题,作者发现相对较浅的网络的良好表现证明网络中间层产生的特征应该具有很强的鉴别力。所以作者在GoogleNet的层中间加入了两层额外的辅助分类器(auxiliary classifiers)来增强反向传播时的梯度信号,并增加额外的正则化(additional regularization),这两个分类器分别加在Inception(4a)和Inception(4d)之后,并且只在训练阶段有用,推理阶段去除。

辅助分类器的结构如下:

在这里插入图片描述

  • 平均池化:5x5(strides = 3), 在(4a)中输出为(4,4,512),(4d)中输出(4,4,528)
  • 1x1卷积:1x1(strides = 1, kernal_num = 128),得到(4,4,128)的输出
  • 全连接FC: 1024个神经元,由(4,4,128)得到(1,1,1024)输出
  • Dropout: 70%的dropout比率
  • FC with Sottmax: (1,1,1024)得到(1,1,1000)类别得分
7、训练方法

基于CPU实现,优化方法SGD+Momentum(0.9),学习率衰减(learning rate decay)方式为每8个epoch降低4%。同时GoogleNet的图像采样方式也有很大的变化,采样图像中大小均匀分布在图像面积的8% ~ 100%之间,并随机选取高宽比在3/4 ~ 4/3之间的各大小patch。同时,采用了Andrew Howard等人提出的光度扭曲(photometric distortions)来更好的抑制过拟合问题。另外,GoogleNet还采用了随机插值的方法resize图像。

8、ILSVRC分类任务

GoogleNet在进行ILSVRC任务时没有使用额外的数据集用以训练,同时在测试阶段采用了一些方法提高准确性:

  1. 训练了7个类似的Inception模型用以模型集成
  2. 使用更“疯狂”的图像剪裁方法:首先将输入的测试图像根据短边resize成4个不同的尺度(256,288,320,352),然后截取其左、中、右三个部分的正方形patch;随后在每个patch上取(左上,右上,左下,右下,中间)五个部分的224x224的patch,加上自身resize成224x224;再对于每一个patch进行镜像(mirror),所以一张图像被crop成(3x4x6x2 = 144)张图像,将这144图像的平均得分作为预测输出。
  3. 同时作者还比较了在crop上做maxpooling(max pooling over crops),以及在多个分类结果上做平均(average over classifiers),发现通过简单的平均可以获得更好的性能。

以下展示了GoogleNet在竞赛中的表现(Table2)以及不同的ensemble和crops策略下GoogleNet的性能(Table3)

在这里插入图片描述

在这里插入图片描述

9、ILSVRC2014 检测任务

在检测任务中,当预测的类别与真实类别一致,且目标边界框与参考框重合超过50%就认为该检测目标检测结果正确,而其他的检测结果被视作假阳性(false positive),FP结果往往都是被目标函数惩罚的(penalized)。由于一张图像中可能存在多个尺度的多个目标(当然有可能没有),所以结果常常以平均精度均值(mean average precision mAP)作为指标。

在目标检测任务中,GoogleNet采用的方法大体上与R-CNN一致,但是采用了Inception结构对区域提取(预测)(region proposal)进行增强, 同时也加入了选择性搜索。在对候选区域进行分类时,集成了6个ConvNet的结果,将准确率从40%提升至43.9%。 相比起R-CNN,GoolgNet没有使用边界框回归。

在这里插入图片描述

Table4是在检测任务中的性能比较,而对于当个模型,GoogleNet的检测mAP低于DeepInsight,可以预见若是加上上下文模型以及边界框回归,GoogleNet的性能应该还有提升。

在这里插入图片描述

10、结论

GoogleNet向我们通过当下的密集模块的设计来近似最优的稀疏结构是提升视觉神经网络性能的可行方法,通过Inception的设计可以在不显著增加计算量的前提下提升模型的性能。

11、代码浅析
import warnings
from collections import namedtuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.jit.annotations import Optional, Tuple
from torch import Tensor
from .utils import load_state_dict_from_url

__all__ = ['GoogLeNet', 'googlenet', "GoogLeNetOutputs", "_GoogLeNetOutputs"]

model_urls = {
    # GoogLeNet ported from TensorFlow
    'googlenet': 'https://download.pytorch.org/models/googlenet-1378be20.pth',
}

GoogLeNetOutputs = namedtuple('GoogLeNetOutputs', ['logits', 'aux_logits2', 'aux_logits1'])
GoogLeNetOutputs.__annotations__ = {'logits': Tensor, 'aux_logits2': Optional[Tensor],
                                    'aux_logits1': Optional[Tensor]}

# Script annotations failed with _GoogleNetOutputs = namedtuple ...
# _GoogLeNetOutputs set here for backwards compat
_GoogLeNetOutputs = GoogLeNetOutputs


def googlenet(pretrained=False, progress=True, **kwargs):
    r"""GoogLeNet (Inception v1) model architecture from
    `"Going Deeper with Convolutions" <http://arxiv.org/abs/1409.4842>`_.

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
        progress (bool): If True, displays a progress bar of the download to stderr
        aux_logits (bool): If True, adds two auxiliary branches that can improve training.
            Default: *False* when pretrained is True otherwise *True*
        transform_input (bool): If True, preprocesses the input according to the method with which it
            was trained on ImageNet. Default: *False*
    """
    if pretrained:
        if 'transform_input' not in kwargs:
            kwargs['transform_input'] = True
        if 'aux_logits' not in kwargs:
            kwargs['aux_logits'] = False
        if kwargs['aux_logits']:
            warnings.warn('auxiliary heads in the pretrained googlenet model are NOT pretrained, '
                          'so make sure to train them')
        original_aux_logits = kwargs['aux_logits']
        kwargs['aux_logits'] = True
        kwargs['init_weights'] = False
        model = GoogLeNet(**kwargs)
        state_dict = load_state_dict_from_url(model_urls['googlenet'],
                                              progress=progress)
        model.load_state_dict(state_dict)
        if not original_aux_logits:
            model.aux_logits = False
            model.aux1 = None
            model.aux2 = None
        return model

    return GoogLeNet(**kwargs)


class GoogLeNet(nn.Module):
    __constants__ = ['aux_logits', 'transform_input']

    def __init__(self, num_classes=1000, aux_logits=True, transform_input=False, init_weights=None,
                 blocks=None):
        super(GoogLeNet, self).__init__()
        if blocks is None:
            blocks = [BasicConv2d, Inception, InceptionAux]
        if init_weights is None:
            warnings.warn('The default weight initialization of GoogleNet will be changed in future releases of '
                          'torchvision. If you wish to keep the old behavior (which leads to long initialization times'
                          ' due to scipy/scipy#11299), please set init_weights=True.', FutureWarning)
            init_weights = True
        assert len(blocks) == 3
        conv_block = blocks[0]
        inception_block = blocks[1]
        inception_aux_block = blocks[2]

        self.aux_logits = aux_logits
        self.transform_input = transform_input

        self.conv1 = conv_block(3, 64, kernel_size=7, stride=2, padding=3)
        self.maxpool1 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
        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)

        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)

        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(2, stride=2, ceil_mode=True)

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

        if aux_logits:
            self.aux1 = inception_aux_block(512, num_classes)
            self.aux2 = inception_aux_block(528, num_classes)
        else:
            self.aux1 = None
            self.aux2 = None

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

        if init_weights:
            self._initialize_weights()

    def _initialize_weights(self):
        for m in self.modules():   #torch.nn.modules()采用广度优先遍历格层级结构
            if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear):
                import scipy.stats as stats   #scipy.stats是一个统计函数库
                X = stats.truncnorm(-2, 2, scale=0.01)  #stats.truncnorm()截断正态分布
                values = torch.as_tensor(X.rvs(m.weight.numel()), dtype=m.weight.dtype)   #m.weight.numel()是把维度相乘, X.rvs()是生成随机变量
                values = values.view(m.weight.size())   #改变维度
                with torch.no_grad():
                    m.weight.copy_(values)   #将随机权重复制到weight当中,使用no_grad()关掉计算图,其实就是让这一步操作不被反向传播时追踪
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)   #当该层时BatchNorm层时,weight全部初始化为1, bias全部初始化为0

    def _transform_input(self, x):   #将输入的图像进行转换
        # type: (Tensor) -> Tensor
        if self.transform_input:
            x_ch0 = torch.unsqueeze(x[:, 0], 1) * (0.229 / 0.5) + (0.485 - 0.5) / 0.5    #torch.unsqueeze(tensor, dim)表示在dim上增加一个维度
            x_ch1 = torch.unsqueeze(x[:, 1], 1) * (0.224 / 0.5) + (0.456 - 0.5) / 0.5
            x_ch2 = torch.unsqueeze(x[:, 2], 1) * (0.225 / 0.5) + (0.406 - 0.5) / 0.5
            x = torch.cat((x_ch0, x_ch1, x_ch2), 1)
        return x

    def _forward(self, x):
        # type: (Tensor) -> Tuple[Tensor, Optional[Tensor], Optional[Tensor]]
        # 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
        aux1 = torch.jit.annotate(Optional[Tensor], None)
        if self.aux1 is not None:
            if self.training:
                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
        aux2 = torch.jit.annotate(Optional[Tensor], None)   #这个看不太懂
        if self.aux2 is not None:
            if self.training:
                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)
        return x, aux2, aux1

    @torch.jit.unused
    def eager_outputs(self, x: Tensor, aux2: Tensor, aux1: Optional[Tensor]) -> GoogLeNetOutputs:
        if self.training and self.aux_logits:
            return _GoogLeNetOutputs(x, aux2, aux1)
        else:
            return x   # type: ignore[return-value]

    def forward(self, x):
        # type: (Tensor) -> GoogLeNetOutputs
        x = self._transform_input(x)
        x, aux1, aux2 = self._forward(x)
        aux_defined = self.training and self.aux_logits
        if torch.jit.is_scripting():
            if not aux_defined:
                warnings.warn("Scripted GoogleNet always returns GoogleNetOutputs Tuple")
            return GoogLeNetOutputs(x, aux2, aux1)
        else:
            return self.eager_outputs(x, aux2, aux1)


class Inception(nn.Module):

    def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj,
                 conv_block=None):
        super(Inception, self).__init__()
        if conv_block is None:
            conv_block = BasicConv2d
        self.branch1 = conv_block(in_channels, ch1x1, kernel_size=1)

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

        self.branch3 = nn.Sequential(
            conv_block(in_channels, ch5x5red, kernel_size=1),   #pytorch的实现中并没有使用5x5的卷积,而是将5x5改成了3x3,应该是个Bug
            # Here, kernel_size=3 instead of kernel_size=5 is a known bug.
            # Please see https://github.com/pytorch/vision/issues/906 for details.
            conv_block(ch5x5red, ch5x5, kernel_size=3, padding=1)
        )

        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1, ceil_mode=True),
            conv_block(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 outputs

    def forward(self, x):
        outputs = self._forward(x)
        return torch.cat(outputs, 1)   #将四个分支的特征图在通道方向上连接起来


class InceptionAux(nn.Module):   #带有辅助分类器的Inception模块

    def __init__(self, in_channels, num_classes, conv_block=None):
        super(InceptionAux, self).__init__()
        if conv_block is None:
            conv_block = BasicConv2d
        self.conv = conv_block(in_channels, 128, kernel_size=1)

        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 = F.adaptive_avg_pool2d(x, (4, 4))    #InceptionAux中先做了一个avg-pooling的操作
        # 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)
        # N x 2048
        x = F.relu(self.fc1(x), inplace=True)
        # N x 1024
        x = F.dropout(x, 0.7, training=self.training)
        # N x 1024
        x = self.fc2(x)
        # N x 1000 (num_classes)    得到1000个类别的辅助分类输出

        return x    


class BasicConv2d(nn.Module):    #BasicConv2d包括了卷积层(Conv2d)和批量归一化层(BatchNorm2d)

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

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

Reference
  1. 论文地址
  2. 代码
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值