前言

DenseNet是CVPR2017的最佳论文,由康奈尔大学黄高博士(Gao Huang)、清华大学本科生刘壮(Zhuang Liu)、Facebook 人工智能研究院研究科学家 Laurens van der Maaten 及康奈尔大学计算机系教授 Kilian Q. Weinberger 所作,有兴趣的同学可以结合原文阅读。

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

在这里插入图片描述
标准的 L 层卷积网络有 L L L 个连接,即每一层与它的前一层和后一层相连,而DenseNet将前面所有层与后面层连接,故有 ( 1 + 2 + . . . + L ) ∗ L = ( L + 1 ) ∗ L / 2 (1+2+...+L)*L=(L+1)*L/2 (1+2+...+L)L=(L+1)L/2 个连接。这里看完有些摸不着头脑没关系,接下来我们会具体展开。

Dense Block

Dense Block是DenseNet的一个基本模块,这里我们从一般的神经网络说起:
在这里插入图片描述
上图是标准神经网络的一个图,输入和输出的公式是 X l = H l ( X l − 1 ) X_l = H_l(X_{l-1}) Xl=Hl(Xl1),其中 H l H_l Hl是一个组合函数,通常包括BN、ReLU、Pooling、Conv操作, X l − 1 X_{l-1} Xl1是第 l l l 层输入的特征图, X l X_{l} Xl是第 l l l 层输出的特征图。
在这里插入图片描述
上图则是ResNet的示意图,我们知道ResNet是跨层相加,输入和输出的公式是 X l = H l ( X l − 1 ) + X l − 1 X_l = H_l(X_{l-1})+X_{l-1} Xl=Hl(Xl1)+Xl1
在这里插入图片描述
而对于DesNet,则是采用跨通道concat的形式来连接,用公式来说则是 X l = H l ( X 0 , X 1 , . . . , X l − 1 X_l = H_l(X_0,X_1,...,X_{l-1} Xl=Hl(X0,X1,...,Xl1),这里要注意所有的层的输入都来源于前面所有层在channel维度的concat,我们用一张动图体会一下:
在这里插入图片描述
特征传递方式是直接将前面所有层的特征concat后传到下一层,而不是前面层都要有一个箭头指向后面的所有层,这与具体代码实现是一致的,后面会具体的实现。

这里要注意,因为我们是直接跨通道直接做concat,所以这里要求不同层concat之前他们的特征图大小应当是相同的,所以DenseNet分为了好几个Dense Block,每个Dense Block内部的feature map的大小相同,而每个Dense Block之间使用一个Transition模块来进行下采样过渡连接,这个后文会介绍。

Growth rate

假如输入特征图的channel为 K 0 K_0 K0,那么第 l l l 层的channel数就为 K 0 + ( l − 1 ) K K_0+(l-1)K K0+(l1)K,我们将其称之为网络的增长率(growth rate)。因为每一层都接受前面所有层的特征图,即特征传递方式是直接将前面所有层的特征concat后传到下一层,所以这个 K K K不能很大,要注意这个K的实际含义就是这层新提取出的特征。
在这里插入图片描述

Bottleneck

在刚才Dense Block中的非线性组合函数是指BN+ReLU+3x3 Conv的组合,尽管每前进一层,只产生K张新特征图,但还是嫌多,于是在进行3×3卷积之前先用一个 1×1卷积将输入的特征图个数降低到 4*k,我们发现这个设计对于DenseNet来说特别有效。所以我们的非线性组合函数就变成了BN+ReLU+1x1 Conv+BN+ReLU+3x3 Conv的结构,由此形成的网络结构我们称之为DenseNet-B
在这里插入图片描述

增加了1x1的卷积的Dense Block也称为Bottleneck结构,实现细节如下:
在这里插入图片描述
有以下几个细节需要注意:

  1. 每一个Bottleneck输出的特征通道数是相同的,例如这里的K=32。同时可以看到,经过concat操作后的通道数是按K的增长量增加的,因此这个K也被称为GrowthRate。
  2. 这里1×1卷积的作用是固定输出通道数,达到降维的作用,1×1卷积输出的通道数通常是GrowthRate的4倍。当几十个Bottleneck相连接时,concat后的通道数会增加到上千,如果不增加1×1的卷积来降维,后续3×3卷积所需的参数量会急剧增加。比如,输入通道数64,增长率K=32,经过15个Bottleneck,通道数输出为64+15*32=544,再经过第16个Bottleneck时,如果不使用1×1卷积,第16个Bottleneck层参数量是3*3*544*32=156672,如果使用1×1卷积,第16个Bottleneck层参数量是1*1*544*128+3*3*128*32=106496,可以看到参数量大大降低。
  3. Dense Block采用了激活函数在前、卷积层在后的顺序,即BN-ReLU-Conv的顺序,这种方式也被称为pre-activation。通常的模型relu等激活函数处于卷积conv、批归一化batchnorm之后,即Conv-BN-ReLU,也被称为post-activation。作者证明,如果采用post-activation设计,性能会变差。想要更清晰的了解pre-activition,可以参考我的博客ResNet残差网络及变体详解中的Pre Activation ResNet。

Transition layer

在这里插入图片描述

两个相邻的Dense Block之间的部分被称为Transition层,具体包括BN、ReLU、1×1卷积、2×2平均池化操作。通过1×1卷积层来减小通道数,并使用步幅为2的平均池化层减半高和宽,从而进一步降低模型复杂度。

压缩因子

为进一步提高网络的紧密度,我们可以在转换层(transition layers)减少feature-maps的数量。我们引入一个压缩因子 θ \theta θ,假定上一层得到的feature map的channel大小为 m m m,那经过Transition层就可以产生 θ m \theta m θm 个特征,其中 θ \theta θ在0和1之间。在DenseNet-C中,我们令 θ \theta θ=0.5。当模型结构即含瓶颈层,又含压缩层时,我们记模型为DenseNet-BC。

DenseNet网络结构

DenseNet网络构成如下:
在这里插入图片描述
上图中,增长率K=32,采用pre-activation,即BN-ReLU-Conv的顺序。

以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个通道数,经过7×7卷积将224×224的输入图片缩减至112×112;Denseblock块由layer堆叠而成,layer的尺寸都相同:1×1+3×3的两层conv(每层conv = BN+ReLU+Conv);Denseblock间由过渡层构成,过渡层通过1×1卷积层来减小通道数,并使用步幅为2的平均池化层减半高和宽。最后经过全局平均池化 + 全连接层的1000路softmax得到输出。

DenseNet优缺点

DenseNet的优点主要有3个:

  1. 更强的梯度流动
    在这里插入图片描述
    DenseNet可以说是一种隐式的强监督模式,因为每一层都建立起了与前面层的连接,误差信号可以很容易地传播到较早的层,所以较早的层可以从最终分类层获得直接监管(监督)。
  2. 能够减少参数总量
    在这里插入图片描述
    3.保存了低维度的特征
    在这里插入图片描述
    在标准的卷积网络中,最终输出只会利用提取最高层次的特征。
    在这里插入图片描述
    在DenseNet中,它使用了不同层次的特征,倾向于给出更平滑的决策边界。这也解释了为什么训练数据不足时DenseNet表现依旧良好。

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

实验效果

这里给出DenseNet在CIFAR-100和ImageNet数据集上与ResNet的对比结果,首先来看下DenseNet与ResNet在CIFAR-100数据集上实验结果,如下图所示,可以看出,只有0.8M大小的DenseNet-100性能已经超越ResNet-1001,并且后者参数大小为10.2M。

在这里插入图片描述

下面是DenseNet与ResNet在ImageNet数据集上的比较,可以看出,同等参数大小时,DenseNet也优于ResNet网络。其它实验结果见原论文。
在这里插入图片描述

Pytorch实现DenseNet

首先实现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__()
        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)
        # 在通道维上将输入和输出连结
        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: 增长率,即K=32
        :param block_config: 每一个DenseBlock的layers数量,这里实现的是DenseNet-121
        :param num_init_features: 第一个卷积的通道数一般为2*K=64
        :param bn_size: bottleneck中1*1conv的factor=4,1*1conv输出的通道数一般为factor*K=128
        :param compression_rate: 压缩因子
        :param drop_rate: dropout层将神经元置0的概率,为0时表示不使用dropout层
        :param num_classes: 分类数
        """
        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

【参考文档】
深入解析DenseNet(含大量可视化及计算)
来聊聊DenseNet及其变体PeleeNet、VoVNet
稠密连接网络(DenseNet)
深度学习网络篇——DenseNet
论文笔记DenseNet
DenseNet:比ResNet更优的CNN模型
Densely Connected Convolutional Networks

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐