Press "Enter" to skip to content

来聊聊DenseNet及其变体PeleeNet、VoVNet

加入极市专业CV交流群,与  1 0000+来自港科大、北大、清华、中科院、CMU、腾讯、百度  等名校名企视觉开发者互动交流!

 

同时提供每月大咖直播分享、真实项目需求对接、干货资讯汇总,行业技术交流。关注  极市平台  公众号  , 回复  加群, 立刻申请入群~

 

来源|AI算法修炼营

 

前面,基础积累系列的文章讲了ResNet网络及其变体,具体可以参考文章: 来聊聊ResNet及其变体 ,ResNet通过前层与后层的 “短路连接”(Shortcuts) ,加强了前后层之间的信息流通,在一定程度上缓解了梯度消失现象,从而可以将神经网络搭建得 很深 。更进一步,这次的主角 DenseNet最大化了这种前后层信息交流,通过建立前面所有层与后面层的密集连接,实现了特征在通道维度上的复用 ,使其可以在参数与计算量更少的情况下实现比ResNet更优的性能。

 

DenseNet

 

论文地址:

 

https://arxiv.org/pdf/1608.06993.pdf

 

DenseNet这是CVPR2017的最佳论文,由康奈尔大学博士后黄高博士(Gao Huang)、清华大学本科生刘壮(Zhuang Liu)、Facebook 人工智能研究院研究科学家 Laurens van der Maaten 及康奈尔大学计算机系教授 Kilian Q. Weinberger 所作。该作者在思想上借鉴了ResNet和Inception的网络。 ResNet解决了网络深的时候梯度消失问题,它是从深度方向研究的。宽度方向是GoogleNet的Inception。 而 DenseNet作者是从feature入手,通过对feature的极致利用能达到更好的效果和减少参数。

 

当CNN的层数变深时,输出到输入的路径就会变得更长,这就会出现一个问题:梯 度经过这幺长的路径反向传播回输入的时候很可能就会消失,那有没有一种方法可以让网络又深梯度又不会消失?

 

DenseNet提出了一种很简单的方法, DenseNet直接通过将前面所有层与后面的层建立密集连接来对特征进行重用来解决这个问题 ,连接方式可以看下面这张图:引用于文章《深入解析DenseNet》

 

 

1.1、具体结构

 

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

 

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采用了 激活函数在前、卷积层在后的顺序 ,这与一般的网络上是不同的。

 

 

以DenseNet-121为例,看下其网络构成:

 

1、DenseNet-121由121层权重层组成(其中4个Dense block,共计2×(6+12+24+16) = 116层权重,加上初始输入的1卷积层+3过渡层+最后输出的全连接层,共计121层);

 

2、训练时采用了DenseNet-BC结构,压缩因子0.5,增长率k = 32;

 

3、初始卷积层有2k个filter,经过7×7卷积将224×224的输入图片缩减至112×112;Denseblock块由layer堆叠而成,layer的尺寸都相同:1×1+3×3的两层conv(每层conv = BN+ReLU+Conv);Denseblock间由过渡层构成,过渡层通过2×2 stride2使特征图尺寸缩小一半。最后经过全局平均池化 + 全连接层的1000路softmax得到输出

 

1.2、优缺点

 

通过Concatnate操作 使得大量的特征被复用,每个层独有的特征图的通道是较少的 ,因此相比ResNet, DenseNet参数更少且计算更高效。与传统CNNs相比参数更少(尽管看上去相反),因为其不需要学习冗余特征。

 

改善了整个网络中的information flow和梯度,使得训练更为容易

 

密集连接具有正则化效果,能降低训练集size较小的任务的过拟合现象。密集连接的特殊网络, 使得每一层都会接受其后所有层的梯度, 而不是像普通卷积链式的反传,因此一定程度上解决了梯度消失的问题。

 

 

使用DenseNet能够减少参数总量

 

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

 

1.3、使用Pytorch实现DenseNet

 

首先实现DenseBlock中的内部结构,这里是 BN+ReLU+1×1 Conv+BN+ReLU+3×3 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__()
self.add_module("norm1", nn.BatchNorm2d(num_input_features))
self.add_module("relu1", nn.ReLU(inplace=True))
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"""
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
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

Pelee:目标检测轻量级网络

 

论文地址:

 

https://arxiv.org/pdf/1804.06882.pdf

 

代码地址:

 

https://github.com/Robert-JunWang/PeleeNet

 

PeleeNet借鉴了DenseNet的级联模式和主要步骤,被用于 解决存储和计算能力受限的情况。 在ImageNet数据集上,PeleeNet只有MobileNet模型的66%,并且比MobileNet精度更高。PeleeNet作为backbone实现SSD能够在VOC2007数据集上达到76.4%的mAP。文章总体上 参考DenseNet的设计思路 ,提出了三个核心模块进行改进,有一定参考价值。

 

PeleeNet实际上是 DenseNet的变体 ,使用的依然是DenseNet的连接方法, 核心的设计原则也和DenseNet相仿(特征重用) 。PeleeNet的主要结构:

 

Two-Way Dense Layer:

 

 

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

 

Stem Block

 

这个模块设计受 Inceptionv4和DSOD 的启发,想要设计一个计算代价比较小的模块。 ResNet和DenseNet在第一层都是用的是一个7×7、stride为2的卷积层,浅层网络的作用是提取图像的边缘、纹理等信息 。Stem Block的设计就是 打算以比较小的代价取代7×7的卷积 。 该结构可以有效的提升特征表达能力且不会增加额外的计算开销,比其他的方法(增加通道或增加增长率)都要好。

 

 

具体结构,先使用strided 3×3卷积进行快速降维,然后用了两分支的结构,一个分支用strided 3×3卷积, 另一个分支使用了一个maxpool。

 

这一部分和组合池化非常相似, stem block使用了strided 3×3卷积和最大值池化两种的优势引申而来的池化策略(组合池化使用的是最大值池化和均值池化),可以丰富特征层。

 

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

 

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

 

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

 

 

无压缩的过渡层

 

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

 

卷积+relu+bn组合顺序

 

为了提高速度, 采用了conv+bn+relu的组合(而不是DenseNet中的预激活组合(conv+relu+bn)), post-activation中,在推断阶段 所有的BN层可以和卷积层合并,可以促进模型推理加速。 为了弥补这个改变给准确度带来的负面影响,使用更浅更宽的网络结构, 在最后一个dense block后添加一个1×1的卷积层来获得更强的特征表达能力。

 

 

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

 

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

 

VoVNet:考虑成本和效率

 

论文地址:

 

https://arxiv.org/abs/1904.09730

 

代码地址:

 

https://github.com/stigma0617/VoVNet.pytorch

 

https://github.com/youngwanLEE/vovnetdetectron2/blob/master/vovnet/vovnet.py

 

DenseNet在目标检测任务上表现很好。因为它通过聚合不同感受野特征层的方式,保留了中间特征层的信息。但是, ResNet是目标检测模型最常用的backbone,DenseNet其实比ResNet提取特征能力更强,而且其参数更少,计算量(FLOPs)也更少,用于目标检测虽然效果好,但是 速度较慢 ,这主要是因为 DenseNet中密集连接所导致的高内存访问成本和能耗。 VoVNet就是为了解决DenseNet这一问题,基于VoVNet的目标检测模型性能超越基于DenseNet的模型,速度也更快,相比ResNet也是性能更好。

 

根据shuffleNet v2,减小flops和model size并不能保证GPU运算时间的减少。比如,shuffleNet v2和mobileNet v2有类似的计算量(FLOPS),但是前者比后者快, squeezeNet虽然比VGG少了50倍的参数量,但是它比VGG能耗更多。 那幺哪些是影响GPU运算效率和能耗的因素呢?

 

除了FLOPs和模型大小外,还需要考虑其他因素对能耗和模型推理速度的影响。这里考虑两个重要的因素: 内存访问成本(Memory Access Cost,MAC)和GPU计算效率。

 

内存访问成本MAC

 

对于CNN来说, 内存访问比计算对能耗贡献还 大,如果网络中间特征比较大,甚至在同等模型大小下内存访问成本会增加,所以要充分考虑CNN层的MAC。在ShuffleNetV2论文中给出计算卷积层MAC的方法:

 

这里的分别为卷积核大小,特征高和框,以及输入和输出的通道数。卷积层的计算量,如果固定的话,那幺有:

 

根据均值不等式,可以知道 当输入和输出的channel数相同时MAC才取下界,此时的设计是最高效的。

 

GPU计算效率

 

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

 

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

 

DenseNet的一大问题就是密集连接太重了,而且每个layer都会聚合前面层的 特征,其实造成的是 特征冗余,而且从模型weights的L1范数会发现中间层对最后的分类层贡献较少 ,这不难理解,因为后面的特征其实已经学习到了这些中间层的核心信息。这种信息冗余反而是可以优化的方向。

 

OSA(One-Shot Aggregation)模块

 

 

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

 

那幺OSA模块效果如何,论文中拿DenseNet-40来做对比,Dense Block层数是12,OSA模块也设计为12层,但是保持和Dense Block类似的参数大小和计算量,此时OSA模块的输出将更大。最终发现在CIFAR-10数据集上acc仅比DenseNet下降了1.2%。但是如果将OSA模块的层数降至5,而提升layer的通道数为43,会发现与DenseNet-40模型效果相当。这说明 DenseNet中很多中间特征可能是冗余的。尽管OSA模块性能没有提升,但是MAC低且计算更高效,这对于目标检测非常重要,因为检测模型一般的输入都是较大的。

 

 

VoVNet首先是一个由3个3×3卷积层构成的stem block,然后4个阶段的OSA模块,每个stage的最后会采用一个stride为2的3×3 max pooling层进行降采样,模型最终的output stride是32。 与其他网络类似,每次降采样后都会提升特征的channel数。

 

VoVNet V2

 

VoVNet V2 来自论文: 《 CenterMask: Real-Time Anchor-Free Instance Segmentation 》 。在实例分割任务中用作backbone。

 

论文地址:

 

https://arxiv.org/pdf/1911.06667.pdf

 

代码地址:

 

https://github.com/youngwanLEE/CenterMask

 

VoVNetV2在VoVNet的基础上,引入了ResNet的残差连接和SENet的SE模块。具体来说,Backbone的结构改进包括OSA module改进,以及SE module改进。

 

 

1)添加了输入到输出的 残差连接网络 ,解除了随着网络深度叠加带来的性能饱和与梯度问题;

 

2)在 输出的内部添加了一个channel上的attention模块eSE 。原始的SE模块中使用两个FC去进行channel权重映射,但是为了减少计算量通常会将FC中的channel给剪裁一些(小于输入的channel),这就引入了一些信息的损失,为此文章 直接将两个FC替换为了一个FC 。

 

 

 

参考文章

 

https://zhuanlan.zhihu.com/p/37189203

 

https://blog.csdn.net/jiaoyangwm/article/details/89330823

 

https://blog.csdn.net/watermelon1123/article/details/103277773

 

Pelee:移动端实时检测骨干网络

 

VoVNet:实时目标检测的新backbone网络

 

https://zhuanlan.zhihu.com/p/79677425

 

https://blog.csdn.net/nature553863/article/details/105716161

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注