深度学习三(PyTorch物体检测实战)

文章目录

  • 深度学习三(PyTorch物体检测实战)
    • 1、网络骨架:Backbone
      • 1.1、神经网络基本组成
        • 1.1.1、卷积层
        • 1.1.2、激活函数层
        • 1.1.3、池化层
        • 1.1.4、Dropout层
        • 1.1.5、BN层
        • 1.1.6、全连接层
        • 1.1.7、深入理解感受野
        • 1.1.8、详解空洞卷积(Dilated Convolution)
      • 1.2、走向深度:VGGNet
      • 1.3、纵横交错:Inception
      • 1.4、里程碑:RestNet
      • 1.5、继往开来:DenseNet
      • 1.6、特征金字塔:FPN
      • 1.7、为检测而生:DetNet

文章来源: <深度学习之PyTorch物体检测实战>     编著: 董洪义
书籍下载: www.hzbook.com

1、网络骨架:Backbone

当前的物体检测算法虽然各不相同,但第一步通常是利用卷积神经网络处理输入图像,生成深层的特征图,然后再利用各种算法完成区域生成与损失计算,这部分卷积神经网络是整个检测算法的“骨架”,也被称为Backbone。
        Backbone是物体检测技术的基础,其中也涌现出了多种经典的结构,如VGGNet、ResNet和DenseNet等。

1.1、神经网络基本组成

物体检测算法使用的通常是包含卷积计算且具有深度结构的前馈神经网络,如卷积层、池化层、全连接层等不同的基本层,这些层有着不同的作用。

1.1.1、卷积层

卷积本是分析数学中的一种运算,在深度学习中使用的卷积运算通常是离散的。作为卷积神经网络中最基础的组成部分,卷积的本质是用卷积核的参数来提取数据的特征,通过矩阵点乘运算与求和运算来得到结果。

如下图所示为一个基本二维卷积的运算过程,公式为y=ωx+b。这里的特征图(x)大小为1×5×5,即输入通道数为1,卷积核(ω)的大小为3×3,偏置(b)为1,为保证输出维度和输入特征维度一致,还需要有填充(padding),这里使用zero-padding,即用0来填充。


        卷积核参数与对应位置像素逐位相乘后累加作为一次计算结果。以上图左上角为例,其计算过程为1×0+0×0+1×0+0×0+1×1+0×8+1×0+0×6+1×7+1=9,然后在特征图上进行滑动,即可得到所有的计算结果。

在PyTorch中使用卷积非常简单,接下来从代码角度介绍如何完成卷积操作。


         >>> from torch import nn
    
     # 使用torch.nn中的Conv2d()搭建卷积层
     >>> conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=1, dilation=1, groups=1, bias=True)
    
     # 查看卷积核的基本信息,本质上是一个Module
     >>> conv
     Conv2d(1, 1, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    
     # 通过.weight与.bias查看卷积核的权重与偏置
     >>> conv.weight.shape
     torch.Size([1, 1, 3, 3])
     >>> conv.bias.shape
     torch.Size([1])
    
     # 输入特征图,需要注意特征必须是四维,第一维作为batch数,即使是1也要保留
     >>> input = torch.ones(1, 1, 5, 5)
     >>> output=conv(input)
    
     # 当前配置的卷积核可以使输入和输出的大小一致
     >>> input.shape
     torch.Size([1, 1, 5, 5])
     >>> output.shape
     torch.Size([1, 1, 5, 5])


对于torch.nn.Conv2d()来说,传入的参数含义如下:

·in_channels:输入特征图的通道数,如果是RGB图像,则通道数为3。卷积中的特征图通道数一般是2的整数次幂。

·out_channels:输出特征图的通道数。

·kernel_size:卷积核的尺寸,常见的有1、3、5、7。

·stride:步长,即卷积核在特征图上滑动的步长,一般为1。如果大于1,则输出特征图的尺寸会小于输入特征图的尺寸。

·padding:填充,常见的有零填充、边缘填充等,PyTorch默认为零填充。

·dilation:空洞卷积,当大于1时可以增大感受野的同时保持特征图的尺寸(后面会细讲),默认为1。

·groups:可实现组卷积,即在卷积操作时不是逐点卷积,而是将输入通道分为多个组,稀疏连接达到降低计算量的目的(后续会细讲), 默认为1。

·bias:是否需要偏置,默认为True。

在实际使用中,特征图的维度通常都不是1,假设输入特征图维度为m×win×hin,输出特征图维度为n×wout×hout,则卷积核的维度为n×m×k×k,在此产生的乘法操作次数为n×wout×hout×m×k×k

1.1.2、激活函数层

神经网络如果仅仅是由线性的卷积运算堆叠组成,则其无法形成复杂的表达空间,也就很难提取出高语义的信息,因此还需要加入非线性的映射,又称为激活函数,可以逼近任意的非线性函数,以提升整个神经网络的表达能力。在物体检测任务中,常用的激活函数有Sigmoid、 ReLU及Softmax函数。

1.Sigmoid函数
    Sigmoid型函数又称为Logistic函数,模拟了生物的神经元特性,即当神经元获得的输入信号累计超过一定的阈值后,神经元被激活而处于兴奋状态,否则处于抑制状态。其函数表达如式(3-1)所示。

        Sigmoid函数曲线与梯度曲线如下图所示。可以看到,Sigmoid函数将特征压缩到了(0,1)区间,0端对应抑制状态,而1对应激活状态,中间部分梯度较大。

        PyTorch实现Sigmoid函数很简单,示例如下:


         # 引入torch.nn模块
     >>> import torch
     >>> from torch import nn
     >>> input = torch.ones(1,1,2,2)
     >>> input
     tensor([[[[ 1., 1.],
                 [ 1., 1.]]]])
    
     # 使用nn.Sigmoid()实例化sigmoid
     >>> sigmoid = nn.Sigmoid()
     >>> sigmoid(input)
     tensor([[[[ 0.7311, 0.7311],
                 [ 0.7311, 0.7311]]]])


Sigmoid函数可以用来做二分类,但其计算量较大,并且容易出现梯度消失现象。从曲线图(上图)中可以看出,在Sigmoid函数两侧的特征导数接近于0,这将导致在梯度反传时损失的误差难以传递到前面的网络层(因为根据链式求导,梯度接近于0)。

2.ReLU函数
    为了缓解梯度消失现象,修正线性单元(Rectified Linear Unit, ReLU)被引入到神经网络中。由于其优越的性能与简单优雅的实现,ReLU已经成为目前卷积神经网络中最为常用的激活函数之一。ReLU函数的表达式如式(3-2)所示。

        ReLU函数及其梯度曲线如下图所示。可以看出,在小于0的部分,值与梯度皆为0,而在大于0的部分中导数保持为1,避免了Sigmoid函数中梯度接近于0导致的梯度消失问题。

        下面是PyTorch实现ReLU激活函数示例。


         >>> import torch
     >>> from torch import nn
     >>> input = torch.randn(1,1,2,2)
     >>> input
     tensor([[[[ 1.8021, 0.5564],
                 [-1.2117, 1.2384]]]])
    
     # nn.ReLU()可以实现inplace操作,即可以直接将运算结果覆盖到输入中,以节省内存
     >>> relu = nn.ReLU(inplace=True)
    
     # 可以看出大于0的值保持不变,小于0的值被置为0
     >>> relu(input)
     tensor([[[[ 1.8021, 0.5564],
                 [ 0.0000, 1.2384]]]])


ReLU函数计算简单,收敛快,并在众多卷积网络中验证了其有效性。

3.Leaky ReLU函数
    ReLU激活函数虽然高效,但是其将负区间所有的输入都强行置为0,Leaky ReLU函数优化了这一点,在负区间内避免了直接置0,而是赋予很小的权重,其函数表达式如式(3-3)所示。

        以上公式中的ai代表权重,即小于0的值被缩小的比例。Leaky ReLU的函数曲线如下图所示。

下面使用PyTorch来实现简单的Leaky ReLU激活函数过程。


         >>> import torch
     >>> from torch import nn
     >>> input = torch.randn(1,1,2,2)
     >>> input
     tensor([[[[-1.7528, 0.1343],
                 [-0.9622, 0.0120]]]])
    
     # 利用nn.LeakyReLU()构建激活函数,并且其为0.04,即ai为25,True代表in-place操作
     >>> leakyrelu = nn.LeakyReLU(0.04, True)
    
     # 从结果看大于0的值保持不变,小于0的值被以0.04的比例缩小
     tensor([[[[-0.0701, 0.1343],
                 [-0.0385, 0.0120]]]])


虽然从理论上讲,Leaky ReLU函数的使用效果应该要比ReLU函数好,但是从大量实验结果来看并没有看出其效果比ReLU好。此外,对于ReLU函数的变种,除了Leaky ReLU函数之外,还有PReLU和RReLU函数等,这里不做详细介绍。

4.Softmax函数
    在物体检测中,通常需要面对多物体分类问题,虽然可以使用Sigmoid函数来构造多个二分类器,但比较麻烦,多物体类别较为常用的分类器是Softmax函数。

在具体的分类任务中,Softmax函数的输入往往是多个类别的得分,输出则是每一个类别对应的概率,所有类别的概率取值都在0~1之 间,且和为1。Softmax函数的表达如式(3-4)所示,其中,Vi表示第i个类别的得分,C代表分类的类别总数,输出Si为第i个类别的概率。


        在PyTorch中,Softmax函数在torch.nn.functional库中,使用方法如下:


         >>> import torch.nn.functional as F
     >>> score = torch.randn(1,4)
     >>> score
     tensor([[ 1.3858, -0.4449, -1.7636, 0.9768]])
    
     # 利用torch.nn.functional.softmax()函数,第二个参数表示按照第几个维度进行Softmax计算
     >>> F.softmax(score, 1)
     tensor([[ 0.5355, 0.0858, 0.0230, 0.3557]])


1.1.3、池化层

在卷积网络中,通常会在卷积层之间增加池化(Pooling)层,以降低特征图的参数量,提升计算速度,增加感受野,是一种降采样操作。 池化是一种较强的先验,可以使模型更关注全局特征而非局部出现的位置,这种降维的过程可以保留一些重要的特征信息,提升容错能力,并且还能在一定程度上起到防止过拟合的作用。

在物体检测中,常用的池化有最大值池化(Max Pooling)与平均值池化(Average Pooling)。池化层有两个主要的输入参数,即核尺寸 kernel_size与步长stride。如下图所示为一个核尺寸与步长都为2的最大值池化过程,以左上角为例9、20、15与26进行最大值池化,保留26。


        下面是PyTorch对于池化层的实现。


         >>> import torch
     >>> from torch import nn
    
     # 池化主要需要两个参数,第一个参数代表池化区域大小,第二个参数表示步长
     >>> max_pooling = nn.MaxPool2d(2, stride=2)
     >>> aver_pooling = nn.AvgPool2d(2, stride=2)
     >>> input = torch.randn(1,1,4,4)
     >>> input
     tensor([[[[ 1.4873, -0.2228, -0.3972, -0.1336],
                 [ 0.6129, 0.4522, -0.3175, -1.2225],
                 [-1.0811, 2.3458, -0.4562, -1.9391],
                 [-0.3609, -2.0500, -1.2374, -0.2012]]]])
    
     # 调用最大值池化与平均值池化,可以看到size从[1, 1, 4, 4]变为了[1, 1, 2, 2]
     >>> max_pooling(input)
     tensor([[[[ 1.4873, -0.1336],
                 [ 2.3458, -0.2012]]]])
     >>> aver_pooling(input)
     tensor([[[[ 0.5824, -0.5177],
                 [-0.2866, -0.9585]]]])


1.1.4、Dropout层

在深度学习中,当参数过多而训练样本又比较少时,模型容易产生过拟合现象。过拟合是很多深度学习乃至机器学习算法的通病,具体表现为在训练集上预测准确率高,而在测试集上准确率大幅下降。2012 年,Hinton等人提出了Dropout算法,可以比较有效地缓解过拟合现象的发生,起到一定正则化的效果。

Dropout的基本思想如下图所示,在训练时,每个神经元以概率p保留,即以1-p的概率停止工作,每次前向传播保留下来的神经元都不同,这样可以使得模型不太依赖于某些局部特征,泛化性能更强。在测试时,为了保证相同的输出期望值,每个参数还要乘以p。当然还有另外一种计算方式称为Inverted Dropout,即在训练时将保留下的神经元乘以1/p,这样测试时就不需要再改变权重。


        至于Dropout为什么可以防止过拟合,可以从以下3个方面解释。

·多模型的平均:不同的固定神经网络会有不同的过拟合,多个取平均则有可能让一些相反的拟合抵消掉,而Dropout每次都是不同的神经元失活,可以看做是多个模型的平均,类似于多数投票取胜的策略。

·减少神经元间的依赖:由于两个神经元不一定同时有效,因此减少了特征之间的依赖,迫使网络学习有更为鲁棒的特征,因为神经网络不应该对特定的特征敏感,而应该从众多特征中学习更为共同的规律,这也起到了正则化的效果。

·生物进化:Dropout类似于性别在生物进化中的角色,物种为了适应环境变化,在繁衍时取雄性和雌性的各一半基因进行组合,这样可以适应更复杂的新环境,避免了单一基因的过拟合,当环境发生变化时也不至于灭绝。

在PyTorch中使用Dropout非常简单,示例如下:


         >>> import torch
     >>> from torch import nn
    
     # PyTorch将元素置0来实现Dropout层,第一个参数为置0概率,第二个为是否原地操作
     >>> dropout = nn.Dropout(0.5, inplace=False)
     >>> input = torch.randn(2, 64, 7, 7)
     >>> output = dropout(input)


Dropout被广泛应用到全连接层中,一般保留概率设置为0.5,而在较为稀疏的卷积网络中则一般使用下一节将要介绍的BN层来正则化模型,使得训练更稳定。

1.1.5、BN层

为了追求更高的性能,卷积网络被设计得越来越深,然而网络却变得难以训练收敛与调参。原因在于,浅层参数的微弱变化经过多层线性变换与激活函数后会被放大,改变了每一层的输入分布,造成深层的网络需要不断调整以适应这些分布变化,最终导致模型难以训练收敛。

由于网络中参数变化导致的内部节点数据分布发生变化的现象被称做ICS(Internal Covariate Shift)。ICS现象容易使训练过程陷入饱和区,减慢网络的收敛。前面提到的ReLU从激活函数的角度出发,在一定程度上解决了梯度饱和的现象,而2015年提出的BN层,则从改变数据分布的角度避免了参数陷入饱和区。由于BN层优越的性能,其已经是当前卷积网络中的“标配”。

BN层首先对每一个batch的输入特征进行白化操作,即去均值方差过程。假设一个batch的输入数据为x:B={x1,…,xm},首先求该batch数据的均值与方差,如式(3-5)和式(3-6)所示。


        以上公式中,m代表batch的大小,μBμ_BμB​为批处理数据的均值,σB2{σ}^{2}_BσB2​为批处理数据的方差。在求得均值方差后,利用式(3-7)进行去均值方差操作:

        白化操作可以使输入的特征分布具有相同的均值与方差,固定了每一层的输入分布,从而加速网络的收敛。然而,白化操作虽然从一定程度上避免了梯度饱和,但也限制了网络中数据的表达能力,浅层学到的参数信息会被白化操作屏蔽掉,因此,BN层在白化操作后又增加了一 个线性变换操作,让数据尽可能地恢复本身的表达能力,如公式(3- 7)和公式(3-8)所示。


        BN层可以看做是增加了线性变换的白化操作,在实际工程中被证明了能够缓解神经网络难以训练的问题。BN层的优点主要有以下3点:

·缓解梯度消失,加速网络收敛。BN层可以让激活函数的输入数据落在非饱和区,缓解了梯度消失问题。此外,由于每一层数据的均值与方差都在一定范围内,深层网络不必去不断适应浅层网络输入的变化, 实现了层间解耦,允许每一层独立学习,也加快了网络的收敛。

·简化调参,网络更稳定。在调参时,学习率调得过大容易出现震荡与不收敛,BN层则抑制了参数微小变化随网络加深而被放大的问题,因此对于参数变化的适应能力更强,更容易调参。

·防止过拟合。BN层将每一个batch的均值与方差引入到网络中,由于每个batch的这两个值都不相同,可看做为训练过程增加了随机噪音,可以起到一定的正则效果,防止过拟合。

在测试时,由于是对单个样本进行测试,没有batch的均值与方差,通常做法是在训练时将每一个batch的均值与方差都保留下来,在测试时使用所有训练样本均值与方差的平均值。

PyTorch中使用BN层很简单,示例如下:


         >>> from torch import nn
    
     # 使用BN层需要传入一个参数为num_features,即特征的通道数
     >>> bn = nn.BatchNorm2d(64)
    
     # eps为公式中的є,momentum为均值方差的动量,affine为添加可学习参数
     >>> bn
     BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
     >>> input = torch.randn(4, 64, 224, 224)
     >>> output = bn(input)
    
     # BN层不改变输入、输出的特征大小
     >>> output.shape
     torch.Size([4, 64, 224, 224])


尽管BN层取得了巨大的成功,但仍有一定的弊端,主要体现在以下两点:

·由于是在batch的维度进行归一化,BN层要求较大的batch才能有效地工作,而物体检测等任务由于占用内存较高,限制了batch的大小,这会限制BN层有效地发挥归一化功能。

·数据的batch大小在训练与测试时往往不一样。在训练时一般采用滑动来计算平均值与方差,在测试时直接拿训练集的平均值与方差来使用。这种方式会导致测试集依赖于训练集,然而有时训练集与测试集的数据分布并不一致。

因此,我们能不能避开batch来进行归一化呢?答案是可以的,最新的工作GN(Group Normalization)从通道方向计算均值与方差,使用更为灵活有效,避开了batch大小对归一化的影响。

具体来讲,GN先将特征图的通道分为很多个组,对每一个组内的参数做归一化,而不是batch。GN之所以能够工作的原因,笔者认为是在特征图中,不同的通道代表了不同的意义,例如形状、边缘和纹理等,这些不同的通道并不是完全独立地分布,而是可以放到一起进行归一化分析。

1.1.6、全连接层

全连接层(Fully Connected Layers)一般连接到卷积网络输出的特征图后边,特点是每一个节点都与上下层的所有节点相连,输入与输出 都被延展成一维向量,因此从参数量来看全连接层的参数量是最多的, 如下图所示。

        在物体检测算法中,卷积网络的主要作用是从局部到整体地提取图像的特征,而全连接层则用来将卷积抽象出的特征图进一步映射到特定维度的标签空间,以求取损失或者输出预测结果。

感知机例子即是使用了三层的全连接网络进行分类, PyTorch使用全连接层需要指定输入的与输出的维度。示例如下:


         >>> import torch
     >>> from torch import nn
    
     # 第一维表示一共有4个样本
     >>> input = torch.randn(4, 1024)
     >>> linear = nn.Linear(1024, 4096)
     >>> output = linear(input)
     >>> input.shape
     torch.Size([4, 1024])
     >>> output.shape
     torch.Size([4, 4096])


然而,随着深度学习算法的发展,全连接层的缺点也逐渐暴露了出来,最致命的问题在于其参数量的庞大。在此以VGGNet为例说明,其第一个全连接层的输入特征为7×7×512=25088个节点,输出特征是大小为4096的一维向量,由于输出层的每一个点都来自于上一层所有点的权重相加,因此这一层的参数量为25088×4096≈108。相比之下,VGGNet 最后一个卷积层的卷积核大小为3×3×512×512≈2.4×106,全连接层的参数量是这一个卷积层的40多倍。

大量的参数会导致网络模型应用部署困难,并且其中存在着大量的参数冗余,也容易发生过拟合的现象。在很多场景中,我们可以使用全局平均池化层(Global Average Pooling,GAP)来取代全连接层,这种思想最早见于NIN(Network in Network)网络中,总体上,使用GAP有 如下3点好处:

·利用池化实现了降维,极大地减少了网络的参数量。

·将特征提取与分类合二为一,一定程度上可以防止过拟合。

·由于去除了全连接层,可以实现任意图像尺度的输入。

1.1.7、深入理解感受野

感受野(Receptive Field)是指特征图上的某个点能看到的输入图像的区域,即特征图上的点是由输入图像中感受野大小区域的计算得到的。举个简单的例子,如下图所示为一个三层卷积网络,每一层的卷积核为3×3,步长为1,可以看到第一层对应的感受野是3×3,第二层是 5×5,第三层则是7×7。


        卷积层和池化层都会影响感受野,而激活函数层通常对于感受野没有影响。对于一般的卷积神经网络,感受野可由式(3-9)和式(3-10) 计算得出。


        其中,RFl+1RF_{l+1}RFl+1​与RFlRF_lRFl​分别代表第l+1层与第l层的感受野,k代表第l+1层 卷积核的大小,SlS_lSl​代表前l层的步长之积。注意,当前层的步长并不影响当前层的感受野。

通过上述公式求取出的感受野通常很大,而实际的有效感受野 (Effective Receptive Field)往往小于理论感受野。从上图也可以看出,虽然第三层的感受野是7×7,但是输入层中边缘点的使用次数明显比中间点要少,因此做出的贡献不同。经过多层的卷积堆叠之后,输入层对于特征图点做出的贡献分布呈高斯分布形状。

理解感受野是理解卷积神经网络工作的基础,尤其是对于使用Anchor作为强先验区域的物体检测算法,如Faster RCNN和SSD,如何设置Anchor的大小,Anchor应该对应在特征图的哪一层,都应当考虑感受野。通常来讲,Anchor的大小应该与感受野相匹配,尤其是有效的感受野,过大或过小都不好。

在卷积网络中,有时还需要计算特征图的大小,一般可以按照式(3-11)进行计算。


        其中,nin与nout分别为输入特征图与输出特征图的尺寸,p代表这一 层的padding大小,k代表这一层的卷积核大小,s为步长。

1.1.8、详解空洞卷积(Dilated Convolution)

空洞卷积最初是为解决图像分割的问题而提出的。常见的图像分割算法通常使用池化层来增大感受野,同时也缩小了特征图尺寸,然后再利用上采样还原图像尺寸。特征图缩小再放大的过程造成了精度上的损失,因此需要有一种操作可以在增加感受野的同时保持特征图的尺寸不变,从而替代池化与上采样操作,在这种需求下,空洞卷积就诞生了。

在近几年的物体检测发展中,空洞卷积也发挥了重要的作用。因为虽然物体检测不要求逐像素地检测,但是保持特征图的尺寸较大,对于小物体的检测及物体的定位来说也是至关重要的。

空洞卷积,顾名思义就是卷积核中间带有一些洞,跳过一些元素进行卷积(即选取黑点元素进行卷积)。在此以3×3卷积为例,其中,图a)是普通的卷积过程,在卷积核紧密排列在特征图上滑动计算,而图b)代表了空洞数为2的空洞卷积,可以看到,在特征图上每2行或者2列选取元素与卷积核卷积。类似地,图c)代表了空洞数为3的空洞卷积。

在代码实现时,空洞卷积有一个额外的超参数dilation rate,表示空洞数,普通卷积dilation rate默认为1,图3.11中的b与c的dilation rate分别为2与3。

在上图中,同样的一个3×3卷积,却可以起到5×5、7×7等卷积的效果。可以看出,空洞卷积在不增加参数量的前提下,增大了感受野。 假设空洞卷积的卷积核大小为k,空洞数为d,则其等效卷积核大小k’计算如式(3-12)所示。


        在计算感受野时,只需要将原来的卷积核大小k更换为k’即可。

空洞卷积的优点显而易见,在不引入额外参数的前提下可以任意扩大感受野,同时保持特征图的分辨率不变。这一点在分割与检测任务中十分有用,感受野的扩大可以检测大物体,而特征图分辨率不变使得物体定位更加精准。

PyTorch对于空洞卷积也提供了方便的实现接口,在卷积时传入dilation参数即可。具体如下:


         >>> from torch import nn
    
     # 定义普通卷积,默认dilation为1
     >>> conv1 = nn.Conv2d(3, 256, 3, stride=1, padding=1, dilation=1)
     >>> conv1
     Conv2d(3, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    
     # 定义dilation为2的卷积,打印卷积后会有dilation的参数
     >>> conv2 = nn.Conv2d(3, 256, 3, stride=1, padding=1, dilation=2)
     >>> conv2
     Conv2d(3, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), dilation=(2, 2))


当然,空洞卷积也有自己的一些缺陷,主要表现在以下3个方面:

·网格效应(Gridding Effect):由于空洞卷积是一种稀疏的采样方式,当多个空洞卷积叠加时,有些像素根本没有被利用到,会损失信息的连续性与相关性,进而影响分割、检测等要求较高的任务。

·远距离的信息没有相关性:空洞卷积采取了稀疏的采样方式,导致远距离卷积得到的结果之间缺乏相关性,进而影响分类的结果。

·不同尺度物体的关系:大的dilation rate对于大物体分割与检测有利,但是对于小物体则有弊无利,如何处理好多尺度问题的检测,是空洞卷积设计的重点。

对于上述问题,有多篇文章提出了不同的解决方法,典型的有图森未来提出的HDC(Hybrid Dilated Convolution)结构。该结构的设计准则是堆叠卷积的dilation rate不能有大于1的公约数,同时将dilation rate设置为类似于[1,2,5,1,2,5]这样的锯齿类结构。此外各dilation rate之间还需要满足一个数学公式,这样可以尽可能地覆盖所有空洞,以解决网格效应与远距离信息的相关性问题,具体细节可参考相关资料。

1.2、走向深度:VGGNet

随着AlexNet在2012年ImageNet大赛上大放异彩后,卷积网络进入了飞速的发展阶段,而2014年的ImageNet亚军结构VGGNet(Visual Geometry Group Network)则将卷积网络进行了改良,探索了网络深度与性能的关系,用更小的卷积核与更深的网络结构,取得了较好的效果,成为卷积结构发展史上较为重要的一个网络。

VGGNet网络结构组成如下图所示,一共有6个不同的版本,最常用的是VGG16。从下图中可以看出,VGGNet采用了五组卷积与三个全连接层,最后使用Softmax做分类。VGGNet有一个显著的特点:每次经过池化层(maxpool)后特征图的尺寸减小一倍,而通道数则增加一 倍(最后一个池化层除外)。


        AlexNet中有使用到5×5的卷积核,而在VGGNet中,使用的卷积核基本都是3×3,而且很多地方出现了多个3×3堆叠的现象,这种结构的优点在于,首先从感受野来看,两个3×3的卷积核与一个5×5的卷积核是一样的;其次,同等感受野时,3×3卷积核的参数量更少。更为重要的是,两个3×3卷积核的非线性能力要比5×5卷积核强,因为其拥有两个激活函数,可大大提高卷积网络的学习能力。

下面使用PyTorch来搭建VGG16经典网络结构,新建一个vgg.py文件,并输入以下内容:


         from torch import nn
     class VGG(nn.Module):
         def __init__(self, num_classes=1000):
             super(VGG, self).__init__()
    
             layers = []
             in_dim = 3
             out_dim = 64
    
             # 循环构造卷积层,一共有13个卷积层
             for i in range(13):
                 layers += [nn.Conv2d(in_dim, out_dim, 3, 1, 1), nn.ReLU(inplace=True)]
                 in_dim = out_dim
    
                 # 在第2、4、7、10、13个卷积层后增加池化层
                 if i==1 or i==3 or i==6 or i==9 or i==12:
                     layers += [nn.MaxPool2d(2, 2)]
    
                     # 第10个卷积后保持和前边的通道数一致,都为512,其余加倍
                     if i!=9:
                         out_dim*=2
             self.features = nn.Sequential(*layers)
    
             # VGGNet的3个全连接层,中间有ReLU与Dropout层
             self.classifier = nn.Sequential(
                 nn.Linear(512 * 7 * 7, 4096),
                 nn.ReLU(True),
                 nn.Dropout(),
                 nn.Linear(4096, 4096),
                 nn.ReLU(True),
                 nn.Dropout(),
                 nn.Linear(4096, num_classes)
             )
    
         def forward(self, x):
             x = self.features(x)
    
             # 这里是将特征图的维度从[1, 512, 7, 7]变到[1, 512*7*7]
             x = x.view(x.size(0), -1)
             x = self.classifier(x)
             return x


在终端中进入上述vgg.py文件的同级目录,输入python3进入交互式环境,利用下面代码调用该模块。


         >>> import torch
     >>> from vgg import VGG
    
     # 实例化VGG类,在此设置输出分类数为21,并转移到GPU上
     >>> vgg = VGG(21).cuda()
     >>> input = torch.randn(1, 3, 224, 224).cuda()
     >>> input.shape
     torch.Size([1, 3, 224, 224])
    
     # 调用VGG,输出21类的得分
     >>> scores = vgg(input)
     >>> scores.shape
     torch.Size([1, 21])
    
     # 也可以单独调用卷积模块,输出最后一层的特征图
     >>> features = vgg.features(input)
     >>> features.shape
     torch.Size([1, 512, 7, 7])
    
     # 打印出VGGNet的卷积层,5个卷积组一共30层
     >>> vgg.features
     Sequential(
         (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (1): ReLU(inplace)
         (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (3): ReLU(inplace)
         (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
         (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (6): ReLU(inplace)
         (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (8): ReLU(inplace)
         (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
         (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (11): ReLU(inplace)
         (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (13): ReLU(inplace)
         (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (15): ReLU(inplace)
         (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
         (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (18): ReLU(inplace)
         (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (20): ReLU(inplace)
         (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (22): ReLU(inplace)
         (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
         (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (25): ReLU(inplace)
         (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (27): ReLU(inplace)
         (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
         (29): ReLU(inplace)
         (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
     )
    
     # 打印出VGGNet的3个全连接层
     >>> vgg.classifier
     Sequential(
         (0): Linear(in_features=25088, out_features=4096, bias=True)
         (1): ReLU(inplace)
         (2): Dropout(p=0.5)
         (3): Linear(in_features=4096, out_features=4096, bias=True)
         (4): ReLU(inplace)
         (5): Dropout(p=0.5)
         (6): Linear(in_features=4096, out_features=21, bias=True)
     )


VGGNet简单灵活,拓展性很强,并且迁移到其他数据集上的泛化能力也很好,因此时至今日有很多检测与分割算法仍采用VGGNet的网络骨架。

1.3、纵横交错:Inception

一般来说,增加网络的深度与宽度可以提升网络的性能,但是这样做也会带来参数量的大幅度增加,同时较深的网络需要较多的数据,否则容易产生过拟合现象。除此之外,增加神经网络的深度容易带来梯度消失的现象。在2014年的ImageNet大赛上,获得冠军的Inception v1(又名GoogLeNet)网络较好地解决了这个问题。

Inception v1网络是一个精心设计的22层卷积网络,并提出了具有良好局部特征结构的Inception模块,即对特征并行地执行多个大小不同的卷积运算与池化,最后再拼接到一起。由于1×1、3×3和5×5的卷积运算对应不同的特征图区域,因此这样做的好处是可以得到更好的图像表征信息。

Inception模块如下图所示,使用了三个不同大小的卷积核进行卷积运算,同时还有一个最大值池化,然后将这4部分级联起来(通道拼接),送入下一层。


        在上述模块的基础上,为进一步降低网络参数量,Inception又增加了多个1×1的卷积模块。如下图所示,这种1×1的模块可以先将特征图 维,再送给3×3和5×5大小的卷积核,由于通道数的降低,参数量也有了较大的减少。值得一提的是,用1×1卷积核实现降维的思想,在后面的多个轻量化网络中都会使用到。


        Inception v1网络一共有9个上述堆叠的模块,共有22层,在最后的Inception模块处使用了全局平均池化。为了避免深层网络训练时带来的梯度消失问题,作者还引入了两个辅助的分类器,在第3个与第6个 Inception模块输出后执行Softmax并计算损失,在训练时和最后的损失一并回传。

Inception v1的参数量是AlexNet的112\frac{1}{12}121​,VGGNet的13\frac{1}{3}31​,适合处理大规模数据,尤其是对于计算资源有限的平台。下面使用PyTorch来搭建一 个单独的Inception模块,新建一个inceptionv1.py文件,代码如下:


         import torch
     from torch import nn
     import torch.nn.functional as F
    
     # 首先定义一个包含conv与ReLU的基础卷积类
     class BasicConv2d(nn.Module):
         def __init__(self, in_channels, out_channels, kernel_size, padding=0):
             super(BasicConv2d, self).__init__()
             self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, padding=padding)
    
             def forward(self, x):
                 x = self.conv(x)
                 return F.relu(x, inplace=True)
    
     # Inceptionv1的类,初始化时需要提供各个子模块的通道数大小
     class Inceptionv1(nn.Module):
         def __init__(self, in_dim, hid_1_1, hid_2_1, hid_2_3, hid_3_1, out_3_5, out_4_1):
             super(Inceptionv1, self).__init__()
    
             # 下面分别是4个子模块各自的网络定义
             self.branch1x1 = BasicConv2d(in_dim, hid_1_1, 1)
             self.branch3x3 = nn.Sequential(
                 BasicConv2d(in_dim, hid_2_1, 1),
                 BasicConv2d(hid_2_1, hid_2_3, 3, padding=1)
             )
             self.branch5x5 = nn.Sequential(
                 BasicConv2d(in_dim, hid_3_1, 1),
                 BasicConv2d(hid_3_1, out_3_5, 5, padding=2)
             )
             self.branch_pool = nn.Sequential(
             nn.MaxPool2d(3, stride=1, padding=1),
                 BasicConv2d(in_dim, out_4_1, 1)
             )
    
         def forward(self, x):
             b1 = self.branch1x1(x)
             b2 = self.branch3x3(x)
             b3 = self.branch5x5(x)
             b4 = self.branch_pool(x)
    
             # 将这四个子模块沿着通道方向进行拼接
             output = torch.cat((b1, b2, b3, b4), dim=1)
             return output


在终端中进入上述Inceptionv1.py文件的同级目录,输入python3进入交互式环境,利用下面的代码调用该模块。


         >>> import torch
     >>> from inceptionv1 import Inceptionv1
    
     # 网络实例化,输入模块通道数,并转移到GPU上
     >>> net_inceptionv1 = Inceptionv1(3, 64, 32, 64, 64, 96, 32).cuda()
     >>> net_inceptionv1
     Inceptionv1(
         # 第一个分支,使用1×1卷积,输出通道数为64
         (branch1x1): BasicConv2d(
             (conv): Conv2d(3, 64, kernel_size=(1, 1), stride=(1, 1))
         )
    
         # 第二个分支,使用1×1卷积与3×3卷积,输出通道数为64
         (branch3x3): Sequential(
             (0): BasicConv2d(
                 (conv): Conv2d(3, 32, kernel_size=(1, 1), stride=(1, 1))
             )
             (1): BasicConv2d(
                 (conv): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
             )
         )
    
         # 第三个分支,使用1×1卷积与5×5卷积,输出通道数为96
         (branch5x5): Sequential(
             (0): BasicConv2d(
                 (conv): Conv2d(3, 64, kernel_size=(1, 1), stride=(1, 1))
             )
             (1): BasicConv2d(
                 (conv): Conv2d(64, 96, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2))
             )
         )
    
         # 第四个分支,使用最大值池化与1×1卷积,输出通道数为32
         (branch_pool): Sequential(
             (0): MaxPool2d(kernel_size=3, stride=1, padding=1, dilation=1, ceil_mode=False)
             (1): BasicConv2d(
                 (conv): Conv2d(3, 32, kernel_size=(1, 1), stride=(1, 1))
             )
         )
     )
    
     >>> input = torch.randn(1, 3, 256, 256).cuda()
     >>> input.shape
     torch.Size([1, 3, 256, 256])
     >>> output = net_inceptionv1(input)
    
     # 可以看到输出的通道数是输入通道数的和,即256=64+64+96+32
     >>> output.shape
     torch.Size([1, 256, 256, 256])


在Inception v1网络的基础上,随后又出现了多个Inception版本。Inception v2进一步通过卷积分解与正则化实现更高效的计算,增加了BN层,同时利用两个级联的3×3卷积取代了Inception v1版本中的5×5卷积,如下图所示,这种方式既减少了卷积参数量,也增加了网络的非线性能力。

        使用PyTorch来搭建一个单独的Inception v2模块,默认输入的通道数为192,新建一个inceptionv2.py文件,代码如下:


         import torch
     from torch import nn
     import torch.nn.functional as F
    
     # 构建基础的卷积模块,与Inception v2的基础模块相比,增加了BN层
     class BasicConv2d(nn.Module):
         def __init__(self, in_channels, out_channels, kernel_size, padding=0):
             super(BasicConv2d, self).__init__()
             self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, padding=padding)
             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)
    
     class Inceptionv2(nn.Module):
         def __init__(self):
             super(Inceptionv2, self).__init__()
         # 对应1x1卷积分支
         self.branch1 = BasicConv2d(192, 96, 1, 0)
    
         # 对应1x1卷积与3x3卷积分支
         self.branch2 = nn.Sequential(
             BasicConv2d(192, 48, 1, 0),
             BasicConv2d(48, 64, 3, 1)
         )
    
         # 对应1x1卷积、3x3卷积与3x3卷积分支
         self.branch3 = nn.Sequential(
             BasicConv2d(192, 64, 1, 0),
             BasicConv2d(64, 96, 3, 1),
             BasicConv2d(96, 96, 3, 1)
         )
    
         # 对应3x3平均池化与1x1卷积分支
         self.branch4 = nn.Sequential(
         nn.AvgPool2d(3, stride=1, padding=1, count_include_pad=False),
             BasicConv2d(192, 64, 1, 0)
         )
    
         # 前向过程,将4个分支进行torch.cat()拼接起来
         def forward(self, x):
             x0 = self.branch1(x)
             x1 = self.branch2(x)
             x2 = self.branch3(x)
             x3 = self.branch4(x)
             out = torch.cat((x0, x1, x2, x3), 1)
             return out


在终端中进入上述Inceptionv2.py文件的同级目录,输入python3进入交互式环境,利用下面的代码调用该模块。


         >>> import torch
     >>> from inceptionv2 import Inceptionv2
     >>> net_inceptionv2 = Inceptionv2().cuda()
     >>> net_inceptionv2
     Inceptionv2(
         # 第1个分支,使用1×1卷积,输出通道数为96
         (branch1): BasicConv2d(
             (conv): Conv2d(192, 96, kernel_size=(1, 1), stride=(1, 1))
             (bn): BatchNorm2d(96, eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
         )
    
         # 第2个分支,使用1×1卷积与3×3卷积,输出通道数为64
         (branch2): Sequential(
             (0): BasicConv2d(
                 (conv): Conv2d(192, 48, kernel_size=(1, 1), stride=(1, 1))
                 (bn): BatchNorm2d(48,eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
             )
             (1): BasicConv2d(
                 (conv): Conv2d(48, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
                 (bn): BatchNorm2d(64,eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
             )
         )
    
         # 第3个分支,使用1×1卷积与两个连续的3×3卷积,输出通道数为96
         (branch3): Sequential(
             (0): BasicConv2d(
                 (conv): Conv2d(192, 64, kernel_size=(1, 1), stride=(1, 1))
                 (bn): BatchNorm2d(64,eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
             )
             (1): BasicConv2d(
                 (conv): Conv2d(64, 96, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
                 (bn): BatchNorm2d(96,eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
             )
             (2): BasicConv2d(
                 (conv): Conv2d(96, 96, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
                 (bn): BatchNorm2d(96,eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
             )
         )
    
         # 第4个分支,使用平均池化与1×1卷积,输出通道数为64
         (branch4): Sequential(
             (0): AvgPool2d(kernel_size=3, stride=1, padding=1)
             (1): BasicConv2d(
                 (conv): Conv2d(192, 64, kernel_size=(1, 1), stride=(1, 1))
                 (bn): BatchNorm2d(64,eps=0.001, momentum=0.1, affine=True, track_running_stats=True)
             )
         )
     )
    
     >>> input = torch.randn(1, 192, 32, 32).cuda()
     >>> input.shape
     torch.Size([1, 192, 32, 32])
    
     # 将输入传入实例的网络
     >>> output = net_inceptionv2(input)
    
     # 输出特征图的通道数为:96+64+96+64=320
     >>> output.shape
     torch.Size([1, 320, 32, 32])


更进一步,Inception v2将n×n的卷积运算分解为1×n与n×1两个卷积,如下图所示,这种分解的方式可以使计算成本降低33%。


        此外,Inception v2还将模块中的卷积核变得更宽而不是更深,形成第三个模块,以解决表征能力瓶颈的问题。Inception v2网络正是由上述的三种不同类型的模块组成的,其计算也更加高效。

Inception v3在Inception v2的基础上,使用了RMSProp优化器,在辅助的分类器部分增加了7×7的卷积,并且使用了标签平滑技术。

Inception v4则是将Inception的思想与残差网络进行了结合,显著提升了训练速度与模型准确率,这里对于模块细节不再展开讲述。至于残差网络这一里程碑式的结构,正是由下一节的网络ResNet引出的。

1.4、里程碑:RestNet

VGGNet与Inception出现后,学者们将卷积网络不断加深以寻求更优越的性能,然而随着网络的加深,网络却越发难以训练,一方面会产生梯度消失现象;另一方面越深的网络返回的梯度相关性会越来越差,接近于白噪声,导致梯度更新也接近于随机扰动。

ResNet(Residual Network,残差网络)较好地解决了这个问题,并获得了2015年ImageNet分类任务的第一名。此后的分类、检测、分割等 任务也大规模使用ResNet作为网络骨架。

ResNet的思想在于引入了一个深度残差框架来解决梯度消失问题, 即让卷积网络去学习残差映射,而不是期望每一个堆叠层的网络都完整 地拟合潜在的映射(拟合函数)。如下图所示,对于神经网络,如果我们期望的网络最终映射为H(x),左侧的网络需要直接拟合输出H(x), 而右侧由ResNet提出的子模块,通过引入一个shortcut(捷径)分支,将需要拟合的映射变为残差F(x):H(x)-x。ResNet给出的假设是:相较于直接优化潜在映射H(x),优化残差映射F(x)是更为容易的。


        在ResNet中,上述的一个残差模块称为Bottleneck。ResNet有不同网络层数的版本,如18层、34层、50层、101层和152层,这里以常用的50层来讲解。ResNet-50的网络架构如下图所示,最主要的部分在于中间经历了4个大的卷积组,而这4个卷积组分别包含了3、4、6这3个 Bottleneck模块。最后经过一个全局平均池化使得特征图大小变为1×1, 然后进行1000维的全连接,最后经过Softmax输出分类得分。

        由于F(x)+x是逐通道进行相加,因此根据两者是否通道数相同,存在两种Bottleneck结构。对于通道数不同的情况,比如每个卷积组的第一个Bottleneck,需要利用1×1卷积对x进行Downsample操作,将通道数变为相同,再进行加操作。对于相同的情况下,两者可以直接进行相加。

利用PyTorch实现一个带有Downsample操作的Bottleneck结构,新建一个resnet_bottleneck.py文件,代码如下:


         import torch.nn as nn
     class Bottleneck(nn.Module):
         def __init__(self, in_dim, out_dim, stride=1):
             super(Bottleneck, self).__init__()
    
             # 网路堆叠层是由1×1、3×3、1×1这3个卷积组成的,中间包含BN层
             self.bottleneck = nn.Sequential(
                 nn.Conv2d(in_dim, in_dim, 1, bias=False),
                 nn.BatchNorm2d(in_dim),
                 nn.ReLU(inplace=True),
                 nn.Conv2d(in_dim, in_dim, 3, stride, 1, bias=False),
                 nn.BatchNorm2d(in_dim),
                 nn.ReLU(inplace=True),
                 nn.Conv2d(in_dim, out_dim, 1, bias=False),
                 nn.BatchNorm2d(out_dim),
             )
             self.relu = nn.ReLU(inplace=True)
    
             # Downsample部分是由一个包含BN层的1×1卷积组成
             self.downsample = nn.Sequential(
                 nn.Conv2d(in_dim, out_dim, 1, 1),
                 nn.BatchNorm2d(out_dim),
             )
    
         def forward(self, x):
             identity = x
             out = self.bottleneck(x)
             identity = self.downsample(x)
    
             # 将identity(恒等映射)与网络堆叠层输出进行相加,并经过ReLU后输出
             out += identity
             out = self.relu(out)
             return out


在终端中进入上述resnet_bottleneck.py文件的同级目录,输入 python3进入交互式环境,利用下面的代码调用该模块。


         >>> import torch
     >>> from resnet_bottleneck import Bottleneck
    
     # 实例化Bottleneck,输入通道数为64,输出为256,对应第一个卷积组的第一个Bottleneck
     >>> bottleneck_1_1 = Bottleneck(64, 256).cuda()
     >>> bottleneck_1_1
    
     # Bottleneck作为卷积堆叠层,包含了1×1、3×3、1×1这3个卷积层
     Bottleneck(
         (bottleneck): Sequential(
             (0): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             (2): ReLU(inplace)
             (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
             (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             (5): ReLU(inplace)
             (6): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
         )
         (relu): ReLU(inplace)
    
         # 利用Downsample结构将恒等映射的通道数变为与卷积堆叠层相同,保证可以相加
         (downsample): Sequential(
             (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1))
             (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
         )
     )
    
     >>> input = torch.randn(1, 64, 56, 56).cuda()
    
     # 将输入送到Bottleneck结构中
     >>> output = bottleneck_1_1(input)
     >>> input.shape
     torch.Size([1, 64, 56, 56])
     >>> output.shape
    
     # 相比输入,输出的特征图分辨率没变,而通道数变为4倍
     torch.Size([1, 256, 56, 56])


1.5、继往开来:DenseNet

上一节的ResNet通过前层与后层的“短路连接”(Shortcuts),加强了前后层之间的信息流通,在一定程度上缓解了梯度消失现象,从而可以将神经网络搭建得很深。更进一步,本节的主角DenseNet最大化了这种前后层信息交流,通过建立前面所有层与后面层的密集连接,实现了特征在通道维度上的复用,使其可以在参数与计算量更少的情况下实现 比ResNet更优的性能,提出DenseNet的《Densely Connected Convolutional Networks》也一举拿下了2017年CVPR的最佳论文。

DenseNet的网络架构如下图所示,网络由多个Dense Block与中间的卷积池化组成,核心就在Dense Block中。Dense Block中的黑点代表一个卷积层,其中的多条黑线代表数据的流动,每一层的输入由前面的所有卷积层的输出组成。注意这里使用了通道拼接(Concatnate)操作,而非ResNet的逐元素相加操作。


        DenseNet的结构有如下两个特性:

·神经网络一般需要使用池化等操作缩小特征图尺寸来提取语义特征,而Dense Block需要保持每一个Block内的特征图尺寸一致来直接进行Concatnate操作,因此DenseNet被分成了多个Block。Block的数量一般为4。

·两个相邻的Dense Block之间的部分被称为Transition层,具体包括 BN、ReLU、1×1卷积、2×2平均池化操作。1×1卷积的作用是降维,起 到压缩模型的作用,而平均池化则是降低特征图的尺寸。

具体的Block实现细节如下图所示,每一个Block由若干个Bottleneck的卷积层组成,对应上图中的黑点。Bottleneck由BN、 ReLU、1×1卷积、BN、ReLU、3×3卷积的顺序构成。


        关于Block,有以下4个细节需要注意:

·每一个Bottleneck输出的特征通道数是相同的,例如这里的32。同时可以看到,经过Concatnate操作后的通道数是按32的增长量增加的,因此这个32也被称为GrowthRate。

·这里1×1卷积的作用是固定输出通道数,达到降维的作用。当几十个Bottleneck相连接时,Concatnate后的通道数会增加到上千,如果不增加1×1的卷积来降维,后续3×3卷积所需的参数量会急剧增加。1×1卷积的通道数通常是GrowthRate的4倍。

·上图2中的特征传递方式是直接将前面所有层的特征Concatnate后传到下一层,这种方式与具体代码实现的方式是一致的,而不像上图1中,前面层都要有一个箭头指向后面的所有层。

·Block采用了激活函数在前、卷积层在后的顺序,这与一般的网络上是不同的。

利用PyTorch来实现DenseNet的一个Block,新建一个 densenet_block.py文件,代码如下:


         import torch
     from torch import nn
     import torch.nn.functional as F
    
     # 实现一个Bottleneck的类,初始化需要输入通道数与GrowthRate这两个参数
     class Bottleneck(nn.Module):
         def __init__(self, nChannels, growthRate):
             super(Bottleneck, self).__init__()
    
             # 通常1×1卷积的通道数为GrowthRate的4倍
             interChannels = 4*growthRate
             self.bn1 = nn.BatchNorm2d(nChannels)
             self.conv1 = nn.Conv2d(nChannels, interChannels, kernel_size=1, bias=False)
             self.bn2 = nn.BatchNorm2d(interChannels)
             self.conv2 = nn.Conv2d(interChannels, growthRate, kernel_size=3, padding=1, bias=False)
    
         def forward(self, x):
             out = self.conv1(F.relu(self.bn1(x)))
             out = self.conv2(F.relu(self.bn2(out)))
    
             # 将输入x同计算的结果out进行通道拼接
             out = torch.cat((x, out), 1)
             return out
    
     class Denseblock(nn.Module):
         def __init__(self, nChannels, growthRate, nDenseBlocks):
             super(Denseblock, self).__init__()
             layers = []
    
             # 将每一个Bottleneck利用nn.Sequential()整合起来,输入通道数需要线性增长
             for i in range(int(nDenseBlocks)):
                 layers.append(Bottleneck(nChannels, growthRate))
                 nChannels += growthRate
             self.denseblock = nn.Sequential(*layers)
    
         def forward(self, x):
             return self.denseblock(x)


在终端中进入上述densenet_block.py文件的同级目录,输入python3进入交互式环境,利用下面的代码调用该模块。


         >>> import torch
     >>> from densenet_block import Denseblock
    
     # 实例化DenseBlock,包含了6个Bottleneck
     >>> denseblock = Denseblock(64, 32, 6).cuda()
    
     # 查看denseblock的网络结构,由6个Bottleneck组成
     >>> denseblock
     Denseblock(
         (denseblock): Sequential(
             # 第1个Bottleneck的输入通道数为64,输出固定为32
             (0): Bottleneck(
                 (bn1): BatchNorm2d(64,eps=1e-05,momentum=0.1,affine=True, track_running_stats=True)
                 (conv1): Conv2d(64, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
             )
    
             # 第2个Bottleneck的输入通道数为96,输出固定为32
             (1): Bottleneck(
                 (bn1): BatchNorm2d(96,eps=1e-05,momentum=0.1,affine=True, track_running_stats=True)
                 (conv1): Conv2d(96, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
             )
    
             # 第3个Bottleneck的输入通道数为128,输出固定为32
             (2): Bottleneck(
                 (bn1): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv1): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
             )
    
             # 第4个Bottleneck的输入通道数为160,输出固定为32
             (3): Bottleneck(
                 (bn1): BatchNorm2d(160, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv1): Conv2d(160, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
             )
    
             # 第5个Bottleneck的输入通道数为192,输出固定为32
             (4): Bottleneck(
                 (bn1): BatchNorm2d(192, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv1): Conv2d(192, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
             )
    
             # 第6个Bottleneck的输入通道数为224,输出固定为32
             (5): Bottleneck(
                 (bn1): BatchNorm2d(224, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv1): Conv2d(224, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
             )
         )
     )
    
     >>> input = torch.randn(1, 64, 256, 256).cuda()
    
     # 将输入传入denseblock结构中
     >>> output = denseblock(input)
    
     # 输出的通道数为:224+32=64+32×6=256
     >>> output.shape
     torch.Size([1, 256, 256, 256])


DenseNet网络的优势主要体现在以下两个方面:

·密集连接的特殊网络,使得每一层都会接受其后所有层的梯度,而不是像普通卷积链式的反传,因此一定程度上解决了梯度消失的问题。

·通过Concatnate操作使得大量的特征被复用,每个层独有的特征图的通道是较少的,因此相比ResNet,DenseNet参数更少且计算更高效。

DenseNet的不足在于由于需要进行多次Concatnate操作,数据需要被复制多次,显存容易增加得很快,需要一定的显存优化技术。另外,DenseNet是一种更为特殊的网络,ResNet则相对一般化一些,因此 ResNet的应用范围更广泛。

1.6、特征金字塔:FPN

为了增强语义性,传统的物体检测模型通常只在深度卷积网络的最后一个特征图上进行后续操作,而这一层对应的下采样率(图像缩小的倍数)通常又比较大,如16、32,造成小物体在特征图上的有效信息较少,小物体的检测性能会急剧下降,这个问题也被称为多尺度问题。

解决多尺度问题的关键在于如何提取多尺度的特征。传统的方法 图像金字塔(Image Pyramid),主要思路是将输入图片做成多个尺度,不同尺度的图像生成不同尺度的特征,这种方法简单而有效,大量使用在了COCO等竞赛上,但缺点是非常耗时,计算量也很大。

从上面内容可以知道,卷积神经网络不同层的大小与语义信息 不同,本身就类似一个金字塔结构。2017年的FPN(Feature Pyramid Network)方法融合了不同层的特征,较好地改善了多尺度检测问题。

FPN的总体架构如下图所示,主要包含自下而上网络、自上而下网络、横向连接与卷积融合4个部分。


        ·自下而上:最左侧为普通的卷积网络,默认使用ResNet结构,用作提取语义信息。C1代表了ResNet的前几个卷积与池化层,而C2至C5分别为不同的ResNet卷积组,这些卷积组包含了多个Bottleneck结构, 组内的特征图大小相同,组间大小递减。

·自上而下:首先对C5进行1×1卷积降低通道数得到P5,然后依次进行上采样得到P4、P3和P2,目的是得到与C4、C3与C2长宽相同的特征,以方便下一步进行逐元素相加。这里采用2倍最邻近上采样,即直接对临近元素进行复制,而非线性插值。

·横向连接(Lateral Connection):目的是为了将上采样后的高语义特征与浅层的定位细节特征进行融合。高语义特征经过上采样后,其长宽与对应的浅层特征相同,而通道数固定为256,因此需要对底层特征 C2至C4进行11卷积使得其通道数变为256,然后两者进行逐元素相加得到P4、P3与P2。由于C1的特征图尺寸较大且语义信息不足,因此没有把C1放到横向连接中。

·卷积融合:在得到相加后的特征后,利用3×3卷积对生成的P2至P4再进行融合,目的是消除上采样过程带来的重叠效应,以生成最终的特征图。

对于实际的物体检测算法,需要在特征图上进行RoI(Region of Interests,感兴趣区域)提取,而FPN有4个输出的特征图,选择哪一个特征图上面的特征也是个问题。FPN给出的解决方法是,对于不同大小的RoI,使用不同的特征图,大尺度的RoI在深层的特征图上进行提取, 如P5,小尺度的RoI在浅层的特征图上进行提取,如P2,具体确定方法,感兴趣的读者可以自行查看。

FPN将深层的语义信息传到底层,来补充浅层的语义信息,从而获得了高分辨率、强语义的特征,在小物体检测、实例分割等领域有着非常不俗的表现。

使用PyTorch来搭建一个完整的FPN网络,新建一个fpn.py,代码如下:


         import torch.nn as nn
     import torch.nn.functional as F
     import math
    
     # ResNet的基本Bottleneck类
     class Bottleneck(nn.Module):
         # 通道倍增数
         expansion = 4
         def __init__(self, in_planes, planes, stride=1, downsample=None):
             super(Bottleneck, self).__init__()
         self.bottleneck = nn.Sequential(
             nn.Conv2d(in_planes, planes, 1, bias=False),
             nn.BatchNorm2d(planes),
             nn.ReLU(inplace=True),
             nn.Conv2d(planes, planes, 3, stride, 1, bias=False),
             nn.BatchNorm2d(planes),
             nn.ReLU(inplace=True),
             nn.Conv2d(planes, self.expansion * planes, 1, bias=False),
             nn.BatchNorm2d(self.expansion * planes),
         )
         self.relu = nn.ReLU(inplace=True)
         self.downsample = downsample
    
         def forward(self, x):
             identity = x
             out = self.bottleneck(x)
             if self.downsample is not None:
                 identity = self.downsample(x)
             out += identity
             out = self.relu(out)
             return out
    
     # FPN的类,初始化需要一个list,代表ResNet每一个阶段的Bottleneck的数量
     class FPN(nn.Module):
         def __init__(self, layers):
             super(FPN, self).__init__()
             self.inplanes = 64
    
             # 处理输入的C1模块
             self.conv1 = nn.Conv2d(3, 64, 7, 2, 3, bias=False)
             self.bn1 = nn.BatchNorm2d(64)
             self.relu = nn.ReLU(inplace=True)
             self.maxpool = nn.MaxPool2d(3, 2, 1)
    
             # 搭建自下而上的C2、C3、C4、C5
             self.layer1 = self._make_layer(64, layers[0])
             self.layer2 = self._make_layer(128, layers[1], 2)
             self.layer3 = self._make_layer(256, layers[2], 2)
             self.layer4 = self._make_layer(512, layers[3], 2)
    
             # 对C5减少通道数,得到 P5
             self.toplayer = nn.Conv2d(2048, 256, 1, 1, 0)
    
             # 3×3卷积融合特征
             self.smooth1 = nn.Conv2d(256, 256, 3, 1, 1)
             self.smooth2 = nn.Conv2d(256, 256, 3, 1, 1)
             self.smooth3 = nn.Conv2d(256, 256, 3, 1, 1)
    
             # 横向连接,保证通道数相同
             self.latlayer1 = nn.Conv2d(1024, 256, 1, 1, 0)
             self.latlayer2 = nn.Conv2d( 512, 256, 1, 1, 0)
             self.latlayer3 = nn.Conv2d( 256, 256, 1, 1, 0)
    
         # 构建C2到C5,注意区分stride值为1和2的情况
         def _make_layer(self, planes, blocks, stride=1):
             downsample = None
             if stride != 1 or self.inplanes != Bottleneck.expansion * planes:
                 downsample = nn.Sequential(
                     nn.Conv2d(self.inplanes, Bottleneck.expansion * planes, 1, stride, bias=False),
                     nn.BatchNorm2d(Bottleneck.expansion * planes)
                 )
             layers = []
             layers.append(Bottleneck(self.inplanes, planes, stride, downsample))
             self.inplanes = planes * Bottleneck.expansion
             for i in range(1, blocks):
                 layers.append(Bottleneck(self.inplanes, planes))
             return nn.Sequential(*layers)
    
         # 自上而下的上采样模块
         def _upsample_add(self, x, y):
             _,_,H,W = y.shape
             return F.upsample(x, size=(H,W), mode='bilinear') + y
    
         def forward(self, x):
             # 自下而上
             c1 = self.maxpool(self.relu(self.bn1(self.conv1(x))))
             c2 = self.layer1(c1)
             c3 = self.layer2(c2)
             c4 = self.layer3(c3)
             c5 = self.layer4(c4)
    
             # 自上而下
             p5 = self.toplayer(c5)
             p4 = self._upsample_add(p5, self.latlayer1(c4))
             p3 = self._upsample_add(p4, self.latlayer2(c3))
             p2 = self._upsample_add(p3, self.latlayer3(c2))
    
             # 卷积融合,平滑处理
             p4 = self.smooth1(p4)
             p3 = self.smooth2(p3)
             p2 = self.smooth3(p2)
             return p2, p3, p4, p5


在终端中进入上述fpn.py文件的同级目录,输入python3进入交互式环境,利用下面的代码调用上述的FDN模块。


         >>> import torch
     >>> from fpn import FPN
    
     # 利用list来初始化FPN网络
     >>> net_fpn = FPN([3, 4, 6, 3]).cuda()
    
     # 查看FPN的第一个卷积层
     >>> net_fpn.conv1
     Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
    
     # 查看FPN的第一个BN层
     >>> net_fpn.bn1
     BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    
     # 查看FPN的第一个ReLU层
     >>> net_fpn.relu
     ReLU(inplace)
    
     # 查看FPN的第一个池化层,使用最大值池化
     MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
    
     # 查看FPN的第一个layer,即前面的C2,包含了3个Bottleneck
     >>> net_fpn.layer1
     Sequential(
         # layer1中第1个Bottleneck模块
         (0): Bottleneck(
             (bottleneck): Sequential(
                 (0): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (2): ReLU(inplace)
                 (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
                 (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (5): ReLU(inplace)
                 (6): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (7): BatchNorm2d(256,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
    
             # 这里存在一个通道增加模块
             (relu): ReLU(inplace)
             (downsample): Sequential(
                 (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (1): BatchNorm2d(256,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
         )
    
         # layer1中第2个Bottleneck模块
         (1): Bottleneck(
             (bottleneck): Sequential(
                 (0): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (2): ReLU(inplace)
                 (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
                 (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (5): ReLU(inplace)
                 (6): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (7): BatchNorm2d(256,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
             (relu): ReLU(inplace)
         )
    
         # layer1中第3个Bottleneck模块
         (2): Bottleneck(
             (bottleneck): Sequential(
                 (0): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (2): ReLU(inplace)
                 (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
                 (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (5): ReLU(inplace)
                 (6): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (7): BatchNorm2d(256,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
             (relu): ReLU(inplace)
         )
     )
    
     # 查看fpn的layer2,即上面的C3,包含了4个Bottleneck
     >>> net_fpn.layer2
     Sequential(
         # layer2中第1个Bottlenec
         (0): Bottleneck(
             (bottleneck): Sequential(
                 (0): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (1): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (2): ReLU(inplace)
                 (3): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
                 (4): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (5): ReLU(inplace)
                 (6): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (7): BatchNorm2d(512,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
             (relu): ReLU(inplace)
             (downsample): Sequential(
                 (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
                 (1): BatchNorm2d(512,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
         )
    
         # layer2中第2个Bottleneck
         (1): Bottleneck(
             (bottleneck): Sequential(
                 (0): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (1): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (2): ReLU(inplace)
                 (3): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
                 (4): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (5): ReLU(inplace)
                 (6): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (7): BatchNorm2d(512,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
             (relu): ReLU(inplace)
         )
    
         # layer2中第3个Bottleneck
         (2): Bottleneck(
             (bottleneck): Sequential(
                 (0): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (1): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (2): ReLU(inplace)
                 (3): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
                 (4): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (5): ReLU(inplace)
                 (6): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (7): BatchNorm2d(512,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
             (relu): ReLU(inplace)
         )
    
         # layer2中第4个Bottleneck
         (3): Bottleneck(
             (bottleneck): Sequential(
                 (0): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (1): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (2): ReLU(inplace)
                 (3): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
                 (4): BatchNorm2d(128,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                 (5): ReLU(inplace)
                 (6): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
                 (7): BatchNorm2d(512,eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             )
             (relu): ReLU(inplace)
         )
     )
    
     # 1×1的卷积,以得到P5
     >>> net_fpn.toplayer
     Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1))
    
     # 对P4进行平滑的卷积层
     >>> net_fpn.smooth1
     Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    
     # 对C4进行横向处理的卷积层
     >>> net_fpn.latlayer1
     Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
    
     >>> input = torch.randn(1, 3, 224, 224).cuda()
     >>> output = fpn(input)
    
     # 返回的P2、P3、P4、P5,这4个特征图通道数相同,但特征图尺寸递减
     >>> output[0].shape
     torch.Size([1, 256, 56, 56])
     >>> output[1].shape
     torch.Size([1, 256, 28, 28])
     >>> output[2].shape
     torch.Size([1, 256, 14, 14])
     >>> output[3].shape
     torch.Size([1, 256, 7, 7])


1.7、为检测而生:DetNet

前面几节的网络骨架,如VGGNet和ResNet等,虽从各个角度出发提升了物体检测性能,但究其根本是为ImageNet的图像分类任务而设计的。而图像分类与物体检测两个任务天然存在着落差,分类任务侧重于全图的特征提取,深层的特征图分辨率很低;而物体检测需要定位出物体位置,特征图分辨率不宜过小,因此造成了以下两种缺陷:

·大物体难以定位:对于FPN等网络,大物体对应在较深的特征图上检测,由于网络较深时下采样率较大,物体的边缘难以精确预测,增加了回归边界的难度。

·小物体难以检测:对于传统网络,由于下采样率大造成小物体在较深的特征图上几乎不可见;FPN虽从较浅的特征图来检测小物体,但浅层的语义信息较弱,且融合深层特征时使用的上采样操作也会增加物体检测的难度。

针对以上问题,旷视科技提出了专为物体检测设计的DetNet结构,引入了空洞卷积,使得模型兼具较大感受野与较高分辨率,同时避免了FPN多次上采样,实现了较好的检测效果。

DetNet的网络结构如下图所示,仍然选择性能优越的ResNet-50作为基础结构,并保持前4个stage与ResNet-50相同,具体的结构细节有以下3点:


        ·引入了一个新的Stage 6,用于物体检测。Stage 5与Stage 6使用了DetNet提出的Bottleneck结构,最大的特点是利用空洞数为2的3×3卷积取代了步长为2的3×3卷积。

·Stage 5与Stage 6的每一个Bottleneck输出的特征图尺寸都为原图的116\frac{1}{16}161​,通道数都为256,而传统的Backbone通常是特征图尺寸递减,通道数递增。

·在组成特征金字塔时,由于特征图大小完全相同,因此可以直接从右向左传递相加,避免了上一节的上采样操作。为了进一步融合各通道的特征,需要对每一个阶段的输出进行1×1卷积后再与后一Stage传回的特征相加。

DetNet这种精心设计的结构,在增加感受野的同时,获得了较大的特征图尺寸,有利于物体的定位。与此同时,由于各Stage的特征图尺 寸相同,避免了上一节的上采样,既一定程度上降低了计算量,又有利于小物体的检测。

DetNet中Bottleneck的细节如下图所示,左侧的两个Bottleneck A 与Bottleneck B分别对应上图的A与B,右侧的为原始的ResNet残差结构。DetNet与ResNet两者的基本思想都是卷积堆叠层与恒等映射的相加,区别在于DetNet使用了空洞数为2的3×3卷积,这样使得特征图尺寸保持不变,而ResNet是使用了步长为2的3×3卷积。B相比于A,在恒等映射部分增加了一个1×1卷积,这样做可以区分开不同的Stage,并且实验发现这种做法对于特征金字塔式的检测非常重要。


        使用PyTorch来实现DetNet的两个Bottleneck结构A和B,新建一个detnet_bott-leneck.py文件,代码如下:


         from torch import nn
     class DetBottleneck(nn.Module):
         # 初始化时extra为False时为Bottleneck A,为True时则为Bottleneck B
         def __init__(self, inplanes, planes, stride=1, extra=False):
             super(DetBottleneck, self).__init__()
    
             # 构建连续3个卷积层的Bottleneck
             self.bottleneck = nn.Sequential(
                 nn.Conv2d(inplanes, planes, 1, bias=False),
                 nn.BatchNorm2d(planes),
                 nn.ReLU(inplace=True),
                 nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=2, dilation=2, bias=False),
                 nn.BatchNorm2d(planes),
                 nn.ReLU(inplace=True),
                 nn.Conv2d(planes, planes, 1, bias=False),
                 nn.BatchNorm2d(planes),
             )
             self.relu = nn.ReLU(inplace=True)
             self.extra = extra
    
             # Bottleneck B的1×1卷积
             if self.extra:
                 self.extra_conv = nn.Sequential(
                     nn.Conv2d(inplanes, planes, 1, bias=False),
                     nn.BatchNorm2d(planes)
                 )
    
         def forward(self, x):
             # 对于Bottleneck B来讲,需要对恒等映射增加卷积处理,与ResNet类似
             if self.extra:
                 identity = self.extra_conv(x)
             else:
                 identity = x
             out = self.bottleneck(x)
             out += identity
             out = self.relu(out)
             return out


在终端中进入上述detnet_bottleneck.py文件的同级目录,输入python3进入交互式环境,利用下面的代码调用上述DetNet网络的Bottleneck结构:


         >>> import torch
     >>> from detnet_bottleneck import DetBottleneck
    
     # 完成一个Stage 5,即B-A-A的结构,Stage 4输出通道数为1024
     >>> bottleneck_b = DetBottleneck(1024, 256, 1, True).cuda()
    
     # 查看Bottleneck B的结构,带有extra的卷积层
     >>> bottleneck_b
     DetBottleneck(
         (bottleneck): Sequential(
             (0): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             (2): ReLU(inplace)
             (3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(2, 2), dilation=(2, 2), bias=False)
             (4): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             (5): ReLU(inplace)
             (6): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
         )
         (relu): ReLU(inplace)
         (extra_conv): Sequential(
             (0): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
         )
     )
    
     >>> bottleneck_a1 = DetBottleneck(256, 256).cuda()
    
     # 查看Bottleneck A1的结构
     >>> bottleneck_a1
     DetBottleneck(
         (bottleneck): Sequential(
             (0): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             (2): ReLU(inplace)
             (3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(2, 2), dilation=(2, 2), bias=False)
             (4): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             (5): ReLU(inplace)
             (6): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
         )
         (relu): ReLU(inplace)
     )
    
     >>> bottleneck_a2 = DetBottleneck(256, 256).cuda()
    
     # 查看Bottleneck A2的结构,与Bottleneck A1相同
     >>> bottleneck_a2
     DetBottleneck(
         (bottleneck): Sequential(
             (0): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             (2): ReLU(inplace)
             (3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(2, 2), dilation=(2, 2), bias=False)
             (4): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
             (5): ReLU(inplace)
             (6): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
             (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
         )
         (relu): ReLU(inplace)
     )
    
     >>> input = torch.randn(1, 1024, 14, 14).cuda()
    
     # 将input作为某一层的特征图,依次传入Bottleneck B、A1与A2三个模块
     >>> output1 = bottleneck_b(input)
     >>> output2 = bottleneck_a1(output1)
     >>> output3 = bottleneck_a2(output2)
    
     # 三个Bottleneck输出的特征图大小完全相同
     >>> output1.shape, output2.shape, output3.shape
     (torch.Size([1, 256, 14, 14]), torch.Size([1, 256, 14, 14]), torch.Size([1, 256, 14, 14]))


深度学习三(PyTorch物体检测实战)相关推荐

  1. 深度学习二(Pytorch物体检测实战)

    深度学习二(Pytorch物体检测实战) 文章目录 深度学习二(Pytorch物体检测实战) 1.PyTorch基础 1.1.基本数据结构:Tensor 1.1.1.Tensor数据类型 1.1.2. ...

  2. 《深度学习之PyTorch物体检测实战》—读书笔记

    随书代码 物体检测与PyTorch 深度学习 为了赋予计算机以人类的理解能力与逻辑思维,诞生了人工智能(Artificial Intelligence, AI)这一学科.在实现人工智能的众多算法中,机 ...

  3. 深度学习之PyTorch物体检测实战——新书赠送活动

    点击我爱计算机视觉标星,更快获取CVML新技术 另外52CV联合北京大学出版社抽奖送书 8 本: 为防止羊毛党机器人,在我爱计算机视觉公众号后台回复"PyTorch物体检测",即可 ...

  4. 深度学习之PyTorch物体检测

    深度学习之PyTorch物体检测 董洪义 著 ISBN:9787111641742 包装:平装 开本:16开 用纸:胶版纸 出版社:机械工业出版社 出版时间:2020-01-01

  5. 中科院张士峰:基于深度学习的通用物体检测算法对比探索

    https://www.toutiao.com/a6674792954369933838/ 人工智能论坛如今浩如烟海,有硬货.有干货的讲座却百里挑一.由中国科学院大学主办,中国科学院大学学生会承办,读 ...

  6. [深度学习]Object detection物体检测之概述

    一.Object detection物体检测与其他计算机视觉问题的区别与联系 在这里.有必要解释一下几大计算机视觉问题的区别与联系.说起物体检测是,那是计算机视觉之中一个比较热门的问题. 而它与图像识 ...

  7. 【深度学习】解决物体检测中的小目标问题

    为了提高模型在小物体上的性能,我们建议使用以下技术: 提高图像捕获分辨率 提高模型的输入分辨率 平铺图像 通过扩充生成更多数据 自动学习模型锚 过滤掉多余的类 为什么小目标问题很难? 小物体问题困扰着 ...

  8. 网易云课程:深度学习与PyTorch入门实战

    网易云课程:深度学习与PyTorch入门实战 01 深度学习初见 1.1 深度学习框架简介 1.2 pytorch功能演示 2开发环境安装 3回归问题 3.1简单的回归问题(梯度下降算法) 3.3回归 ...

  9. 基于深度学习的高精度交警检测识别系统(PyTorch+Pyside6+YOLOv5模型)

    摘要:基于深度学习的高精度交警检测识别系统可用于日常生活中检测与定位交警目标,利用深度学习算法可实现图片.视频.摄像头等方式的交警目标检测识别,另外支持结果可视化与图片或视频检测结果的导出.本系统采用 ...

最新文章

  1. Intellij IDEA下一个Tomcat启动带多个虚拟目录和JDNI数据源应用的方法
  2. 线程的几种状态_拜托:不要再问我线程有多少种状态了
  3. 河北师范大学计算机专业保研率,河北省内13所高校2021届保研率一览
  4. howdoi 简单分析
  5. 求解相机参数Camera Calibration
  6. c语言4x4按键计算器代码,4X4按键实现计算器功能.doc
  7. Python中的公共操作(运算符,公共方法,容器类型转换)
  8. C# 编程指南-事件
  9. 贪吃蛇的c语言运行程序,用C语言编写贪吃蛇游戏的程序
  10. 学习笔记10--自动驾驶汽车软件架构
  11. 云服务器网站logo,云服务器logo
  12. 任正非的《北国之春》
  13. 自己写了个安卓小说下载器
  14. 网站关键词密度定义,关键词密度对网站优化有什么关系!
  15. 华为一碰传nfc_详解:华为免费升级的“一碰传”到底是个什么东西?
  16. 测试世界各地打开网站的速度
  17. 变异凯撒(实验吧CTF题库-密码学)
  18. 遍历两个数组,使得值相等的时候,给第一个数组增加一个值为 true 的属性。
  19. 一般英文论文的大体结构
  20. android简易记账,简单记账(便捷快速记账)

热门文章

  1. 复化梯形公式matlab程序_【HPC】高性能数值计算-梯形面积法
  2. win10更新后开不了机_win7在线更新window10系统
  3. 全国高等学校计算机水平考试总结,参加全国计算机等级考试的经历和总结
  4. sessionStorage与clone方法在项目中的应用
  5. Linux发邮件之mail命令
  6. 33. 高精度练习之乘法
  7. 2016-02-24 获取设备 通知开关
  8. WPF框架MVVM简单例子
  9. 定位于定位优化(iOS)
  10. 视频预训练界的HERO!微软提出视频-语言全表示预训练模型HERO,代码已开源!...