DenseNet 网络结构
DenseNet比ResNet更优的CNN模型在计算机视觉领域卷积神经网络CNN已经成为最主流的方法比如最近的GoogLenetVGG-19Incepetion等模型。CNN史上的一个里程碑事件是ResNet模型的出现ResNet可以训练出更深的CNN模型从而实现更高的准确度。ResNet模型的核心是通过建立前面层与后面层之间的“短路连接”shortcutsskip connection这有助于训练过程中梯度的反向传播从而能训练出更深的CNN网络。今天我们要介绍的是DenseNet模型它的基本思路与ResNet一致但是它建立的是前面所有层与后面层的密集连接dense connection它的名称也是由此而来。DenseNet的另一大特色是通过特征在channel上的连接来实现特征重用feature reuse。这些特点让DenseNet在参数和计算成本更少的情形下实现比ResNet更优的性能DenseNet也因此斩获CVPR 2017的最佳论文奖。本篇文章首先介绍DenseNet的原理以及网路架构然后讲解DenseNet在Pytorch上的实现。设计理念相比ResNetDenseNet提出了一个更激进的密集连接机制即互相连接所有的层具体来说就是每个层都会接受其前面所有层作为其额外的输入。图1为ResNet网络的连接机制作为对比图2为DenseNet的密集连接机制。可以看到ResNet是每个层与前面的某层一般是2~3层短路连接在一起连接方式是通过元素级相加。而在DenseNet中每个层都会与前面所有层在channel维度上连接concat在一起这里各个层的特征图大小是相同的后面会有说明并作为下一层的输入。对于一个 层的网络DenseNet共包含 个连接相比ResNet这是一种密集连接。而且DenseNet是直接concat来自不同层的特征图这可以实现特征重用提升效率这一特点是DenseNet与ResNet最主要的区别。图1 ResNet网络的短路连接机制其中代表的是元素级相加操作图2 DenseNet网络的密集连接机制其中c代表的是channel级连接操作如果用公式表示的话传统的网络在 层的输出为而对于ResNet增加了来自上一层输入的identity函数在DenseNet中会连接前面所有层作为输入其中上面的 代表是非线性转化函数non-liear transformation它是一个组合操作其可能包括一系列的BN(Batch Normalization)ReLUPooling及Conv操作。注意这里 层与 层之间可能实际上包含多个卷积层。DenseNet的前向过程如图3所示可以更直观地理解其密集连接方式比如 的输入不仅包括来自 的 还包括前面两层的 和 它们是在channel维度上连接在一起的。图3 DenseNet的前向过程CNN网络一般要经过Pooling或者stride1的Conv来降低特征图的大小而DenseNet的密集连接方式需要特征图大小保持一致。为了解决这个问题DenseNet网络中使用DenseBlockTransition的结构其中DenseBlock是包含很多层的模块每个层的特征图大小相同层与层之间采用密集连接方式。而Transition模块是连接两个相邻的DenseBlock并且通过Pooling使特征图大小降低。图4给出了DenseNet的网路结构它共包含4个DenseBlock各个DenseBlock之间通过Transition连接在一起。图4 使用DenseBlockTransition的DenseNet网络网络结构如前所示DenseNet的网络结构主要由DenseBlock和Transition组成如图5所示。下面具体介绍网络的具体实现细节。图6 DenseNet的网络结构在DenseBlock中各个层的特征图大小一致可以在channel维度上连接。DenseBlock中的非线性组合函数 采用的是BNReLU3x3 Conv的结构如图6所示。另外值得注意的一点是与ResNet不同所有DenseBlock中各个层卷积之后均输出 个特征图即得到的特征图的channel数为 或者说采用 个卷积核。 在DenseNet称为growth rate这是一个超参数。一般情况下使用较小的 比如12就可以得到较佳的性能。假定输入层的特征图的channel数为 那么 层输入的channel数为 因此随着层数增加尽管 设定得较小DenseBlock的输入会非常多不过这是由于特征重用所造成的每个层仅有 个特征是自己独有的。图6 DenseBlock中的非线性转换结构由于后面层的输入会非常大DenseBlock内部可以采用bottleneck层来减少计算量主要是原有的结构中增加1x1 Conv如图7所示即BNReLU1x1 ConvBNReLU3x3 Conv称为DenseNet-B结构。其中1x1 Conv得到 个特征图它起到的作用是降低特征数量从而提升计算效率。图7 使用bottleneck层的DenseBlock结构对于Transition层它主要是连接两个相邻的DenseBlock并且降低特征图大小。Transition层包括一个1x1的卷积和2x2的AvgPooling结构为BNReLU1x1 Conv2x2 AvgPooling。另外Transition层可以起到压缩模型的作用。假定Transition的上接DenseBlock得到的特征图channels数为 Transition层可以产生 个特征通过卷积层其中 是压缩系数compression rate。当 时特征个数经过Transition层没有变化即无压缩而当压缩系数小于1时这种结构称为DenseNet-C文中使用 。对于使用bottleneck层的DenseBlock结构和压缩系数小于1的Transition组合结构称为DenseNet-BC。DenseNet共在三个图像分类数据集CIFARSVHN和ImageNet上进行测试。对于前两个数据集其输入图片大小为 所使用的DenseNet在进入第一个DenseBlock之前首先进行进行一次3x3卷积stride1卷积核数为16对于DenseNet-BC为 。DenseNet共包含三个DenseBlock各个模块的特征图大小分别为 和 每个DenseBlock里面的层数相同。最后的DenseBlock之后是一个global AvgPooling层然后送入一个softmax分类器。注意在DenseNet中所有的3x3卷积均采用padding1的方式以保证特征图大小维持不变。对于基本的DenseNet使用如下三种网络配置 , 。而对于DenseNet-BC结构使用如下三种网络配置 , 。这里的 指的是网络总层数网络深度一般情况下我们只把带有训练参数的层算入其中而像Pooling这样的无参数层不纳入统计中此外BN层尽管包含参数但是也不单独统计而是可以计入它所附属的卷积层。对于普通的 网络除去第一个卷积层、2个Transition中卷积层以及最后的Linear层共剩余36层均分到三个DenseBlock可知每个DenseBlock包含12层。其它的网络配置同样可以算出各个DenseBlock所含层数。对于ImageNet数据集图片输入大小为 网络结构采用包含4个DenseBlock的DenseNet-BC其首先是一个stride2的7x7卷积层卷积核数为 然后是一个stride2的3x3 MaxPooling层后面才进入DenseBlock。ImageNet数据集所采用的网络配置如表1所示表1 ImageNet数据集上所采用的DenseNet结构实验结果及讨论这里给出DenseNet在CIFAR-100和ImageNet数据集上与ResNet的对比结果如图8和9所示。从图8中可以看到只有0.8M的DenseNet-100性能已经超越ResNet-1001并且后者参数大小为10.2M。而从图9中可以看出同等参数大小时DenseNet也优于ResNet网络。其它实验结果见原论文。图8 在CIFAR-100数据集上ResNet vs DenseNet图9 在ImageNet数据集上ResNet vs DenseNet综合来看DenseNet的优势主要体现在以下几个方面由于密集连接方式DenseNet提升了梯度的反向传播使得网络更容易训练。由于每层可以直达最后的误差信号实现了隐式的“deep supervision”参数更小且计算更高效这有点违反直觉由于DenseNet是通过concat特征来实现短路连接实现了特征重用并且采用较小的growth rate每个层所独有的特征图是比较小的由于特征复用最后的分类器使用了低级特征。要注意的一点是如果实现方式不当的话DenseNet可能耗费很多GPU显存一种高效的实现如图10所示更多细节可以见这篇论文Memory-Efficient Implementation of DenseNets。不过我们下面使用Pytorch框架可以自动实现这种优化。图10 DenseNet的更高效实现方式使用Pytorch实现DenseNet这里我们采用Pytorch框架来实现DenseNet目前它已经支持Windows系统。对于DenseNetPytorch在torchvision.models模块里给出了官方实现这个DenseNet版本是用于ImageNet数据集的DenseNet-BC模型下面简单介绍实现过程。首先实现DenseBlock中的内部结构这里是BNReLU1x1 ConvBNReLU3x3 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(inplaceTrue)) self.add_module(conv1, nn.Conv2d(num_input_features, bn_size*growth_rate, kernel_size1, stride1, biasFalse)) self.add_module(norm2, nn.BatchNorm2d(bn_size*growth_rate)) self.add_module(relu2, nn.ReLU(inplaceTrue)) self.add_module(conv2, nn.Conv2d(bn_size*growth_rate, growth_rate, kernel_size3, stride1, padding1, biasFalse)) 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, pself.drop_rate, trainingself.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_featuresi*growth_rate, growth_rate, bn_size, drop_rate) self.add_module(denselayer%d % (i1,), 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(inplaceTrue)) self.add_module(conv, nn.Conv2d(num_input_feature, num_output_features, kernel_size1, stride1, biasFalse)) self.add_module(pool, nn.AvgPool2d(2, stride2))最后我们实现DenseNet网络class DenseNet(nn.Module): DenseNet-BC model def __init__(self, growth_rate32, block_config(6, 12, 24, 16), num_init_features64, bn_size4, compression_rate0.5, drop_rate0, num_classes1000): :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_size7, stride2, padding3, biasFalse)), (norm0, nn.BatchNorm2d(num_init_features)), (relu0, nn.ReLU(inplaceTrue)), (pool0, nn.MaxPool2d(3, stride2, padding1)) ])) # 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 bnReLU self.features.add_module(norm5, nn.BatchNorm2d(num_features)) self.features.add_module(relu5, nn.ReLU(inplaceTrue)) # 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, stride1).view(features.size(0), -1) out self.classifier(out) return out选择不同网络参数就可以实现不同深度的DenseNet这里实现DenseNet-121网络而且Pytorch提供了预训练好的网络参数def densenet121(pretrainedFalse, **kwargs): DenseNet121 model DenseNet(num_init_features64, growth_rate32, block_config(6, 12, 24, 16), **kwargs) if pretrained: # .s are no longer allowed in module names, but pervious _DenseLayer # has keys norm.1, relu.1, conv.1, norm.2, relu.2, conv.2. # They are also in the checkpoints in model_urls. This pattern is used # to find such keys. pattern re.compile( r^(.*denselayer\d\.(?:norm|relu|conv))\.((?:[12])\.(?:weight|bias|running_mean|running_var))$) state_dict model_zoo.load_url(model_urls[densenet121]) for key in list(state_dict.keys()): res pattern.match(key) if res: new_key res.group(1) res.group(2) state_dict[new_key] state_dict[key] del state_dict[key] model.load_state_dict(state_dict) return model下面我们使用预训练好的网络对图片进行测试这里给出top-5预测值densenet densenet121(pretrainedTrue) densenet.eval() img Image.open(./images/cat.jpg) trans_ops transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) images trans_ops(img).view(-1, 3, 224, 224) outputs densenet(images) _, predictions outputs.topk(5, dim1) labels list(map(lambda s: s.strip(), open(./data/imagenet/synset_words.txt).readlines())) for idx in predictions.numpy()[0]: print(Predicted labels:, labels[idx])给出的预测结果为Predicted labels: n02123159 tiger cat Predicted labels: n02123045 tabby, tabby cat Predicted labels: n02127052 lynx, catamount Predicted labels: n02124075 Egyptian cat Predicted labels: n02119789 kit fox, Vulpes macrotis注完整代码见xiaohu2015/DeepLearning_tutorials。小结这篇文章详细介绍了DenseNet的设计理念以及网络结构并给出了如何使用Pytorch来实现。值得注意的是DenseNet在ResNet基础上前进了一步相比ResNet具有一定的优势但是其却并没有像ResNet那么出名吃显存问题深度不能太大。期待未来有更好的网络模型出现吧参考文献DenseNet-CVPR-Slides.Densely Connected Convolutional Networks.