DenseNet及其变体PeleeNet、VoVNet(阅读笔记)

原文

1 DenseNet

1.1特点

1)神经网络一般需要使用池化等操作缩小特征图尺寸来提取语义特征,而Dense Block需要保持每一个Block内的特征图尺寸一致来直接进行Concatnate操作,因此DenseNet被分成了多个Block。Block的数量一般为4。
在这里插入图片描述2)两个相邻的Dense Block之间的部分被称为Transition(过渡)层,具体包括BN、ReLU、1×1卷积、2×2平均池化操作。1×1卷积的作用是降维,起到压缩模型的作用,而平均池化则是降低特征图的尺寸,使feature maps的尺寸减半。
在这里插入图片描述具体的Block实现细节如上图所示,每一个Block由若干个Bottleneck的卷积层组成。Bottleneck由BN、ReLU、1×1卷积、BN、ReLU、3×3卷积的顺序构成。

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

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

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

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

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

1.2优缺点

通过Concatnate操作使得大量的特征被复用,每个层独有的特征图的通道是较少的,因此相比ResNet, DenseNet参数更少且计算更高效。改善了整个网络中的information flow和梯度,使得训练更为容易密集连接具有正则化效果,能降低训练集size较小的任务的过拟合现象。

DenseNet的不足在于由于需要进行多次Concatnate操作,数据需要被复制多次,显存容易增加得很快,需要一定的显存优化技术。

1.3代码实现(pytorch)

首先实现DenseBlock中的内部结构,这里是BN+ReLU+1x1 Conv+BN+ReLU+3x3 Conv结构,最后也加入dropout层以用于训练过程。

class _DenseLayer(nn.Sequential):
    """Basic unit of DenseBlock (using bottleneck layer) """
    def __init__(self, num_input_features, growth_rate, bn_size, drop_rate):
        super(_DenseLayer, self).__init__()
        #num_input_features输入特征图的通道数
        self.add_module("norm1", nn.BatchNorm2d(num_input_features))
        #inplace=True 对原变量进行覆盖以节省内存
        self.add_module("relu1", nn.ReLU(inplace=True))
        #1*1卷积降维,节约3*3卷积的运算量,growth_rate每经过一层denselayer增加的通道数(在一个block内通常相同),bnszie3*3卷积处的倍增率,在上结构图中为4
        self.add_module("conv1", nn.Conv2d(num_input_features, bn_size*growth_rate,kernel_size=1, stride=1, bias=False))
        self.add_module("norm2", nn.BatchNorm2d(bn_size*growth_rate))
        self.add_module("relu2", nn.ReLU(inplace=True))
        self.add_module("conv2", nn.Conv2d(bn_size*growth_rate, growth_rate,
kernel_size=3, stride=1, padding=1, bias=False))
        self.drop_rate = drop_rate

    def forward(self, x):
        new_features = super(_DenseLayer, self).forward(x)
        if self.drop_rate > 0:
            new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)
        return torch.cat([x, new_features], 1)

据此,实现DenseBlock模块,内部是密集连接方式(输入特征数线性增长):

class _DenseBlock(nn.Sequential):
    """DenseBlock"""
    #num_layers 一个block的denselayer层数
    def __init__(self, num_layers, num_input_features, bn_size, growth_rate, drop_rate):
        super(_DenseBlock, self).__init__()
        for i in range(num_layers):
            layer = _DenseLayer(num_input_features+i*growth_rate,growth_rate, bn_size,drop_rate)
            self.add_module("denselayer%d" % (i+1,), layer)

此外,实现Transition层,它主要是一个卷积层和一个池化层:

class _Transition(nn.Sequential):
    """Transition layer between two adjacent DenseBlock"""
    def __init__(self, num_input_feature, num_output_features):
        super(_Transition, self).__init__()
        self.add_module("norm", nn.BatchNorm2d(num_input_feature))
        self.add_module("relu", nn.ReLU(inplace=True))
        #降维降运算量
        self.add_module("conv", nn.Conv2d(num_input_feature, num_output_features,kernel_size=1, stride=1, bias=False))
        #下采样
        self.add_module("pool", nn.AvgPool2d(2, stride=2))

最后我们实现DenseNet网络:

class DenseNet(nn.Module):
    "DenseNet-BC model"
    def __init__(self, growth_rate=32, block_config=(6, 12, 24, 16), num_init_features=64,bn_size=4, compression_rate=0.5, drop_rate=0, num_classes=1000):
        """ 
        :param growth_rate: (int) number of filters used in DenseLayer, `k` in the paper        
        :param block_config: (list of 4 ints) number of layers in each DenseBlock       
        :param num_init_features: (int) number of filters in the first Conv2d        
        :param bn_size: (int) the factor using in the bottleneck layer        :param compression_rate: (float) the compression rate used in Transition Layer        
        :param drop_rate: (float) the drop rate after each DenseLayer        :param num_classes: (int) number of classes for classification       
        """
        super(DenseNet, self).__init__()
        # first Conv2d
        self.features = nn.Sequential(OrderedDict([
            ("conv0", nn.Conv2d(3, num_init_features, kernel_size=7, stride=2, padding=3, bias=False)),
            ("norm0", nn.BatchNorm2d(num_init_features)),
            ("relu0", nn.ReLU(inplace=True)),
            ("pool0", nn.MaxPool2d(3, stride=2, padding=1))
        ]))

        # DenseBlock
        num_features = num_init_features
        #block_config每个block的denselayer层数
        for i, num_layers in enumerate(block_config):
            block = _DenseBlock(num_layers, num_features,bn_size,growth_rate, drop_rate)
            self.features.add_module("denseblock%d" % (i + 1), block)
            num_features += num_layers*growth_rate
            if i != len(block_config) - 1:
                transition = _Transition(num_features, int(num_features*compression_rate))
                self.features.add_module("transition%d" % (i + 1), transition)
                num_features = int(num_features * compression_rate)

        # final bn+ReLU
        self.features.add_module("norm5", nn.BatchNorm2d(num_features))
        self.features.add_module("relu5", nn.ReLU(inplace=True))

        # classification layer
        self.classifier = nn.Linear(num_features, num_classes)

        # params initialization
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.bias, 0)
                nn.init.constant_(m.weight, 1)
            elif isinstance(m, nn.Linear):
                nn.init.constant_(m.bias, 0)

    def forward(self, x):
        features = self.features(x)
        out = F.avg_pool2d(features, 7, stride=1).view(features.size(0), -1)
        out = self.classifier(out)
        return out

2 Pelee:目标检测轻量级网络

PeleeNet被用于解决存储和计算能力受限的情况。

2.1 Two-Way Dense Layer:

在这里插入图片描述上边左边(a)图是DenseNet中设计的基本模块,右边(b)图代表PeleeNet中设计的基本模块,除了将原本的主干分支的filter(通道数)减半(主干分支感受野为3x3),还添加了一个新的分支,在新的分支中使用了两个3x3的卷积,这个分支感受野为5x5。这样就提取得到的特征就不只是单一尺度,能够同时兼顾小目标和大目标。

2.2 Stem Block

ResNet和DenseNet在第一层都是用的是一个7x7、stride为2的卷积层,浅层网络的作用是提取图像的边缘、纹理等信息。Stem Block的设计就是打算以比较小的代价取代7x7的卷积。
在这里插入图片描述stem block使用了strided 3x3卷积和最大值池化两种的优势引申而来的池化策略(组合池化使用的是最大值池化和均值池化),可以丰富特征层。

2.3 瓶颈层设置动态变化的通道数

在DenseNet中,有一个超参数k-growth rate, 用于控制各个卷积层通道个数,DenseNet的瓶颈层中,将其固定为增长率的4倍,DenseNet中,前几个稠密层的瓶颈通道数比输入通道数多很多,这也意味着对这些层来说,瓶颈层增加了计算开销。

PeleeNet仍然给所有稠密层添加了瓶颈层,但是数量是依据输入形式而动态调整的,来保证通道数量不会超过输入通道数。将瓶颈层的通道个数根据输入的形状动态调整,节约了28.5%的计算消耗。

2.4 无压缩的过渡层

在DenseNet中,过渡层是用于将特征图空间分辨率缩小的,并且过渡层中通道数会小于前一层的通道数。在PeleeNet中将过渡层和前一层通道数设置为一样的数值。

2.5 conv+relu+bn组合顺序

为了提高速度,采用了conv+bn+relu的组合(而不是DenseNet中的预激活组合(conv+relu+bn))。

2.6 整体结构

在这里插入图片描述
整个网络由一个stem block和四阶特征提取器构成。除了最后一个阶段的每个阶段的最后一层都是步长为2的平均池化,四阶段结构是一般大型模型设计的通用结构。

尽管ShuffleNet使用三阶段的结构,并在每个阶段的开始都压缩了特征图大小,尽管这样可以提升计算速度,但是本文认为前面的阶段对视觉任务尤为重要,且过早的减小特征图大小会损坏特征表达能力,因此仍然使用四阶段结构,前两阶段的层数是专门控制在一个可接受的范围内的。

3 VoVNet:考虑成本和效率

DenseNet其实比ResNet提取特征能力更强,而且其参数更少,计算量(FLOPs)也更少,用于目标检测虽然效果好,但是速度较慢,这主要是因为DenseNet中密集连接所导致的高内存访问成本和能耗。

考虑两个重要的因素:内存访问成本(Memory Access Cost,MAC)和GPU计算效率。

内存访问成本MAC
当输入和输出的channel数相同时MAC才取下界,此时的设计是最高效的。

GPU计算效率
GPU计算的优势在于并行计算机制,这意味着当要计算的tensor较大时会充分发挥GPU的计算能力。如果将一个较大的卷积层拆分成几个小的卷积层,尽管效果是相同的,但是却是GPU计算低效的。所以如果功效一样,尽量采用较少的层。比如MobileNet中采用深度可分离卷积(depthwise conv+1x1 conv)虽然降低了FLOPs,但是因为额外的1x1卷积而不利于GPU运算效率。相比FLOPs,我们更应该关注的指标是FlOPs per Second,即用总的FLOPs除以总的GPU推理时间:Flops/s指标,这个指标越高说明GPU利用越高效。

DenseNet中Dense Block密集连接会聚合前面所有的layer,这导致每个layer的输入channel数线性增长。受限于FLOPs和模型参数,每层layer的输出channel数是固定大小,这带来的问题就是输入和输出channel数不一致,此时的MAC不是最优的。另外,由于输入channel数较大,DenseNet采用了1x1卷积层先压缩特征,这个额外层的引入对GPU高效计算不利。

3.1OSA(One-Shot Aggregation)模块

在这里插入图片描述
OSA只在最后一次性聚合前面所有的layer。这一改动将会解决DenseNet的问题,因为每个layer的输入channel数是固定的,这里可以让输出channel数和输入一致而取得最小的MAC,而且也不再需要1x1卷积层来压缩特征,所以OSA模块是GPU计算高效的。

3.2结构

在这里插入图片描述VoVNet首先是一个由3个3x3卷积层构成的stem block,然后4个阶段的OSA模块,每个stage的最后会采用一个stride为2的3x3 max pooling层进行降采样,模型最终的output stride是32。

4 VoVNet V2(centermask)

VoVNet V2来自论文:《CenterMask: Real-Time Anchor-Free Instance Segmentation》。在实例分割任务中用作backbone。
VoVNetV2在VoVNet的基础上,引入了ResNet的残差连接和SENet的SE模块。具体来说,Backbone的结构改进包括OSA module改进,以及SE module改进。
在这里插入图片描述
1)添加了输入到输出的残差连接网络,解除了随着网络深度叠加带来的性能饱和与梯度问题;

2)在输出的内部添加了一个channel上的attention模块eSE。原始的SE模块中使用两个FC去进行channel权重映射,但是为了减少计算量通常会将FC中的channel给剪裁一些(小于输入的channel),这就引入了一些信息的损失,为此文章直接将两个FC替换为了一个FC。
在这里插入图片描述在这里插入图片描述感想:VOV系列一通操作猛如虎,,层数越深,加速越明显。
v2相较于v1,改进基本于无(好奇vovnet-99提升为何如此巨大)

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值