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提升为何如此巨大)