背 景
在本系列第五篇文章ResNet中,我们提到在网络层间加入“跳接”(shortcut connection)来连接不同层级的feature map,从而缓解深层网络训练时会出现的梯度消失问题。DenseNet延续了这个思想,在不同层间建立密集连接,将特征重用最大化,同时一定程度上减少了参数量,并获得了CVPR 2017的最佳论文奖。
01 DenseNet与ResNet
DenseNet一定程度上来说是对ResNet的延伸与改进,那么就不免对二者展开比较,它们的结构对比如下图所示。
DenseNet与ResNet的不同点在于,ResNet每一层的输出会加上来自上一层的输入,公式表达即
Xl=Hl(Xl-1)+Xl-1
其中Xl为第l层的输出,Xl-1为第l-1层的输出,也即第l层的输入。而DenseNet每一层会对前面所有特征层的输出进行叠加再传到下一层,即
Xl=Hl([X0,X1,...,Xl-1])
其中特征层堆叠采用Concat方法,相比ResNet求和的做法,Concat操作也更有利于梯度反向传播过程中信息流的传递。整体网络递进过程如下图所示。
然而大量的特征图叠加,会使得后续层的输入通道持续增多,从而导致模型参数量过大。因此DenseNet使用了Bottleneck层作为主体,通过增加1×1的卷积层来调整内部的通道数,减少输出特征图数量。此思想在ResNet50/101等深层残差网络中也有过类似的表达。
02 DenseNet网络架构
上文描述的DensNet网络结构通常被当作一个DenseBlock,通过若干个不同维度的DenseBlock相连即可组成不同层数的DenseNet。表格中列举了四类在ImageNet数据集上使用的网络层配置。均由初始的7×7卷积,3×3池化,主体部分四个Denseblock以及最后的全局平均池化与全连接层组成。
其中,每两个DenseBlock之间由一个Transition层相连,Transition层包含一个1×1的卷积和2×2的池化层,同样起到降低通道数、压缩模型的作用。DenseNet整体网络结构如下图所示。
作者在ImageNet数据集上与不同层级的ResNet进行对比,如下图所示,在相同的错误率水平(y轴)下,DenseNet所需的参数量(左图)与浮点计算量(右图)均少于ResNet,进一步验证了上述结构的有效性。
03 代码实现
使用pytorch框架实现DenseNet中DenseBlock、Transition等各个模块以及最终的121层DenseNet网络,代码如下:
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)
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)
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))
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):
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
完整训练流程见:
https://github.com/PopSmartTech/deeplearning
04 总 结
DenseNet充分发挥特征重用优势,融合多尺度特征提升分类精度,密集连接方式利于梯度反向传播,进一步减轻梯度消失问题,降低网络训练难度。同时,利用DenseBlock和Transition等方法有效减少参数量与计算复杂度。但是由于显存占用高、结构相对复杂等原因,在应用层面并不如ResNet来的广泛,不过其理论贡献无疑给后续的研究工作带来深刻启发。
参考资料:
[1] Densely Connected Convolutional Networks
https://arxiv.org/pdf/1608.06993.pdf
[2] DenseNet:比ResNet更优的CNN模型 https://zhuanlan.zhihu.com/p/37189203
本文所有文字版权均属“宝略科技”,更多内容请搜索关注【宝略科技】微信公众号~
本文暂时没有评论,来添加一个吧(●'◡'●)