初识DenseNet

  • Post author:
  • Post category:其他




DenseNet

密集连接网络,顾名思义,就是每个层都会和其之前的任意层进行连接,其名称也是由此而来,从下图也可以看到每层之间都进行了连接。

在这里插入图片描述



设计理念

接下来讲解一下其设计理念,主要与ResNet的ShortCut方式进行比对,相对于ResNet,DenseNet提出了一种更激进的密集连接机制:各层之间进行互连,具体来说就是每个层都会接收其之前的所有层作为输入。可以通过下面两图进行直观的比对,第一张图是ResNet的连接机制,第二张图是DenseNet的连接机制。可以看到,ResNet是每个层与前面的某层(一般是2-3层)通过ShortCut方式短路连接在一起,连接的方式是通过逐元素相加。而在DenseNet中每个层都会与前面的任一层在channel维度上进行连接(各层的特征图的size都是一致的),并作为下一层的输入。这样对于一个总共L层的DenseNet网络,其共包含了L*(L+1)/2个连接,相比于ResNet,这是一种非常密集的连接方式,而且DenseNet是在Channel维度上进行连接而不是逐元素相加,这可以实现特征重用,减少参数量提升效率,连接方式是两者之间最主要的区别。

图1 ResNet网络的短路连接机制(其中+代表的是元素级相加操作)

图1 ResNet网络的短路连接机制(其中+代表的是元素级相加操作)

图2 DenseNet网络的密集连接机制(其中c代表的是channel级连接操作)

图2 DenseNet网络的密集连接机制(其中c代表的是channel级连接操作)

在这里插入图片描述

DenseNet的前向传播过程如下图所示,可以更直观的理解其密集连接方式,例如说h3的输入不仅包括来自h2的x2,还包括前面两层的x1和x0,它们是在channel维度上进行连接的。

在这里插入图片描述

CNN网络一般要经过池化层或者stride>1的卷积层来降低特征图的大小,而DenseNet的密集连接方式是需要各层的特征图大小保持一致的。为此DenseNet采用了DenseBlock+Transition的方式,其中DenseBlock是包含很多层的模块,同一个模块中的各层的特征图大小相同,各层之间采用密集连接的方式。而Transition模块是连接两个相邻的DenseBlock,并且通过Pooling使特征图大小降低。简单来说就是把密集连接放在了DenseBlock中来实现,而各个DenseBlock之间则通过Pooling来降低特征图的大小。

在这里插入图片描述



网络结构

如下图所示,DenseNet的网络结构主要由DenseBlock和Transition组成:

在这里插入图片描述

接下来详细说一下网络的实现细节,在DenseBlock中,各个层的特征图大小一致,可以在channel维度上进行连接。DenseBlock中的非线性组合函数H采用的是BN+RELU+3×3 Conv的结构,如下图所示。另外值得注意的一点是,与ResNet不同,所有DenseBlock中各个卷积层之后均输出k个特征图,即得到的特征图的通道channel数为k,也可以理解为是采用了k个卷积核。k在DenseNet中称之为growth rate,这是一个超参数,一般情况下使用较小的k(比如说是12)就可以得到较好的性能。假定输入层的channel数为k0,那么第l层输入的channel数为k0+k(l-1),因此随着层数的增加,尽管k设定的很小,DenseBlock的输入也会变得非常多,不过这是由于特征重用造成的,每个层仅有k个特征是自己独有的。

在这里插入图片描述

图8 DenseBlock中的非线性转换结构

由于后面层的输入channel会非常的大,在DenseBlock内部可以采用bottleneck层来减少计算量,主要是在原有的结构中增加1×1 Conv,如下图所示,变为了BN+ReLU+1×1 Conv+BN+ReLU+3×3 Conv,称为DenseNet-B结构。其中1×1 Conv得到的4k个特征图起到的作用就是降低特征数量,从而提升计算效率。

在这里插入图片描述

图9 使用bottleneck层的DenseBlock结构

在这里插入图片描述

对于ImageNet数据集,图片输入大小为224 × 224,网络结构采用包含4个DenseBlock的DenseNet-BC,其首先是一个stride=2的7×7卷积层(卷积核数为2 k 2k2k),然后是一个stride=2的3×3 MaxPooling层,后面才进入DenseBlock。ImageNet数据集所采用的网络配置如表1所示:

在这里插入图片描述

表1 ImageNet数据集上所采用的DenseNet结构



实验结果及讨论

这里给出DenseNet在CIFAR-100和ImageNet数据集上与ResNet的对比结果,如图10和11所示。从图10中可以看到,只有0.8M的DenseNet-100性能已经超越ResNet-1001,并且后者参数大小为10.2M。而从图11中可以看出,同等参数大小时,DenseNet也优于ResNet网络。其它实验结果见原论文。

在这里插入图片描述

图10 在CIFAR-100数据集上ResNet vs DenseNet

在这里插入图片描述

图11 在ImageNet数据集上ResNet vs DenseNet

综合来看,DenseNet的优势主要体现在以下几个方面:

  • 由于密集连接方式,DenseNet提升了梯度的反向传播,使得网络更加容易训练。由于每层都可以直达最后的误差信号,实现了隐式的深层监督deep supervision。
  • 参数更小且计算更加高效,这是由于DenseNet是通过concat特征来实现的短路连接,实现了特征重用,并且采用了更小的growth rate,每个层所特有的特征是比较小的
  • 由于特征的复用,最后的分类器使用了低级的特征



特征重用

文中作者特意进行了实验来研究整体网络从特征重用中的受益度,作者在C10+数据集上训练了一个L=40(即每个Block12层的)和k=12的DenseNet网络,在每一个DenseBlock中,通过平均绝对权重作为当前层对之前层某一层的依赖程度的量化表示,从而可以得到三个DenseBlock中各个层对其之前层的依赖程度,并通过热力图的形式进行了可视化,从蓝到红代表依赖程度逐渐上升,如下图所示:其中第一行代表了各个Block的输入,则对第一个Block就是图片输入经过最初的Conv,接下来两个Block的输入就是其之前Block的输出,每一列的各个小方格就是当前Target(L)层对之前各个s层的依赖程度,最后一列对前两个Block为过渡层,对最后一个Block为最后的分类层

在这里插入图片描述

实验有以下四个发现:

    1. 同一个Block中所有层都传递其权重作为其他层的输入,这也意味着在同一个Block中,之前层提取的特征确实为之后的层所使用。
    1. 过渡层的权重也在各个层之间传递,意味着信息通过少量的连接从开始层到结束层直接流动。
    1. 查看第二和第三个Block的第一行(大多为蓝色意味着基本无依赖)可以发现其均分配了最少的权重给过渡层的输出,这意味着过渡层输出了太多冗余的特征,这也证实了DenseNet-BC将过渡层输出压缩的有效性。
    1. 看最后一个Block中的最后一列即分类层,虽然也使用了整个Block的其他层的权重,但对最后一层的即其前面1层的依赖度较高(看颜色,最后一行箭头所指),之前两个Block也是这样,意味着网络的后期可能产生了一些更高级的特征。



代码实现-Pytorch

这里简单介绍一下Pytorch中的官方实现,即Pytorch在torchvision.models模块里的实现方式,这个DenseNet版本是用于ImageNet数据集的DenseNet-BC模型,下面简单介绍实现过程。

首先实现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

选择不同网络参数,就可以实现不同深度的DenseNet,这里实现DenseNet-121网络,而且Pytorch提供了预训练好的网络参数:

    def densenet121(pretrained=False, **kwargs):
        """DenseNet121"""
        model = DenseNet(num_init_features=64, growth_rate=32, 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(pretrained=True)
    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, dim=1)

    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])



小结

个人理解DenseNet的创新之处就是在于其密集连接方式,且是在channel维度上进行拼接而不是直接相加,这使得低层的特征也可以传递到很深的层,且这种连接方式也可以使Loss对各个层进行隐形的直接的深层监督。各个层只需保留自我独有的特征即可,不需要再像其他网络那样各个层都需要保留传递有用的特征,会造成有用的特征在各层之间存在冗余,这也应该是其更容易训练其效果更优的主要原因。



版权声明:本文为Noonebirdyou原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。