稠密连接网络(DenseNet):一个表现更优异ResNet

目录

1. 前言

2. DenseNet的核心思想

3. DenseNet的结构组成

3.1 稠密块(Dense Block)

3.2 过渡层(Transition Layer)

3.3 初始卷积层和分类层

4. DenseNet与ResNet的对比

5. PyTorch实现DenseNet结构搭建

6. 总结


1. 前言

深度学习在计算机视觉领域取得了显著的进展,而卷积神经网络(CNN)作为其核心模型之一,不断演化出各种改进架构。其中,稠密连接网络(DenseNet)因其独特的连接方式和高效的参数利用率而备受关注。DenseNet通过创新的稠密连接机制,显著提升了网络的性能和效率,成为现代深度学习研究中的重要里程碑。

其与ResNet有着密不可分的关系,所以,我们将与ResNet进行对比,来更好的理解DenseNet。本文将从理论和实践两个方面深入探讨DenseNet的核心思想,并通过PyTorch实现一个完整的DenseNet模型。

关注残差神经网络可以去看:

《残差神经网络(ResNet)概念解析与用法实例:简洁的图像处理任务》

2. DenseNet的核心思想

DenseNet的核心在于其“稠密连接”机制:网络中的每一层不仅接收前一层的输出,还接收之前所有层的输出作为输入。具体来说,在一个稠密块(Dense Block)中,第 l 层的输入是第 0 层到第 l−1 层所有特征图的拼接(concatenation)。这种设计与ResNet的“残差连接”(通过加法融合特征)不同,DenseNet使用拼接操作直接保留了每一层的原始特征,而不是对其进行融合。

这种连接方式带来了以下几个显著优势:

  1. 缓解梯度消失问题:通过缩短梯度传播路径,DenseNet使得浅层特征可以更容易地接收到深层传来的梯度信号。

  2. 增强特征复用:DenseNet允许每一层直接访问之前所有层的输出,避免了特征的重复学习。

  3. 减少参数量:由于特征复用,DenseNet在达到相同精度时通常需要更少的参数。

  4. 提高信息流和梯度流:稠密连接确保了信息在网络中的高效流动。

3. DenseNet的结构组成

DenseNet的网络架构由多个稠密块(Dense Blocks)和过渡层(Transition Layers)交替组成,辅以初始卷积层和最终的分类层。

3.1 稠密块(Dense Block)

稠密块是DenseNet的基本构建单元。每一层的输入是前所有层输出的拼接,输出特征图的通道数固定为一个较小的值 k(称为“增长率”,Growth Rate)。随着层数的增加,特征图的通道数会线性增长。

3.2 过渡层(Transition Layer)

过渡层的作用是在稠密块之间压缩特征图的通道数,同时调整空间分辨率。过渡层通常包括一个1x1卷积(减少通道数)和一个平均池化层(减小特征图的空间尺寸)。

3.3 初始卷积层和分类层

初始卷积层用于提取初始特征,分类层用于最终的分类任务。

4. DenseNet与ResNet的对比

特性 DenseNet ResNet
连接方式 每一层都与其前面的所有层密集连接 每一层仅与其前一层进行残差连接
参数效率 更高,由于特征复用 相对较低
特征复用 高度的特征复用,所有前面层的输出都用作每一层的输入 仅前一层的输出被用于下一层
梯度流动 由于密集连接,梯度流动更容易 通过残差连接改善梯度流动,但相对于DenseNet较弱
过拟合抑制 更强,尤其在数据集小的情况下 相对较弱
计算复杂度 一般来说更低,尽管有更多的连接 一般来说更高,尤其是在深层网络中
网络深度 可以更深,且更容易训练 可以很深,但通常需要更仔细的设计
可适应性 架构灵活,易于修改 相对灵活,但大多数改动集中在残差块的设计
创新点 密集连接 残差连接
主要应用 图像分类、目标检测、语义分割等 图像分类、目标检测、人脸识别等

5. PyTorch实现DenseNet结构搭建

以下是使用PyTorch实现DenseNet的完整代码:

import torch
import torch.nn as nn
import torch.nn.functional as F
from collections import OrderedDict

class _DenseLayer(nn.Sequential):
    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):
    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):
    def __init__(self, num_input_features, num_output_features):
        super(_Transition, self).__init__()
        self.add_module('norm', nn.BatchNorm2d(num_input_features))
        self.add_module('relu', nn.ReLU(inplace=True))
        self.add_module('conv', nn.Conv2d(num_input_features, num_output_features, kernel_size=1, stride=1, bias=False))
        self.add_module('pool', nn.AvgPool2d(kernel_size=2, stride=2))

class DenseNet(nn.Module):
    def __init__(self, growth_rate=32, block_config=(6, 12, 24, 16), num_init_features=64, bn_size=4, drop_rate=0, num_classes=1000):
        super(DenseNet, self).__init__()
        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(kernel_size=3, stride=2, padding=1)),
        ]))
        num_features = num_init_features
        for i, num_layers in enumerate(block_config):
            block = _DenseBlock(num_layers=num_layers, num_input_features=num_features, bn_size=bn_size, growth_rate=growth_rate, drop_rate=drop_rate)
            self.features.add_module('denseblock%d' % (i + 1), block)
            num_features = num_features + num_layers * growth_rate
            if i != len(block_config) - 1:
                trans = _Transition(num_input_features=num_features, num_output_features=num_features // 2)
                self.features.add_module('transition%d' % (i + 1), trans)
                num_features = num_features // 2
        self.features.add_module('norm5', nn.BatchNorm2d(num_features))
        self.classifier = nn.Linear(num_features, num_classes)

    def forward(self, x):
        features = self.features(x)
        out = F.relu(features, inplace=True)
        out = F.avg_pool2d(out, kernel_size=7, stride=1).view(features.size(0), -1)
        out = self.classifier(out)
        return out

if __name__ == '__main__':
    model = DenseNet()
    print(model)
  • nn.Module 是 PyTorch 中所有神经网络模块的基类,适合定义复杂的网络结构。

  • nn.Sequentialnn.Module 的一个子类,适合定义简单的线性网络,代码更简洁。

 self.add_module 是 PyTorch 中 nn.Module 类的一个方法,用于向神经网络中添加一个模块(如卷积层、全连接层等)。

(1)DenseLayer类

_DenseLayer 的设计有以下几个关键点:

  1. 特征复用:通过拼接操作,每一层都能直接访问前面所有层的特征。

  2. 瓶颈设计:使用1x1卷积减少通道数,降低计算复杂度。

  3. Dropout:通过随机丢弃特征图的某些部分,防止过拟合。

  4. 高效参数利用:每一层的输出特征图通道数固定为 growth_rate,确保参数高效利用。

第一部分代码调用了 _DenseLayer 的父类 nn.Sequentialforward 方法,将输入 x 依次通过 _DenseLayer 中定义的所有层,生成新的特征图 new_features

training=self.training:布尔值,表示是否处于训练模式。self.trainingnn.Module 类的一个属性,自动设置为 True 在训练模式下,False 在推理模式下。

(2)_DenseBlock类

表示一个稠密块,由多个 _DenseLayer 组成。

通过循环构建多个 _DenseLayer,每一层的输入通道数会随着层数增加而增长。

(3)_Transition类

用于在稠密块之间进行特征图的压缩和降采样。

  • 初始化方法 (__init__)

    • num_input_features:输入特征图的通道数。

    • num_output_features:输出特征图的通道数(通常是输入的一半)。

 (4)DenseNet类

包含多个稠密块和过渡层。

  • growth_rate:每层输出的特征图通道数。

  • block_config:每个稠密块的层数(例如 (6, 12, 24, 16))。

  • num_init_features:初始卷积层的输出通道数。

OrderedDict 是 Python 的 collections 模块中的一个类,用于存储键值对,并且保持插入顺序。在这里,它用于定义 nn.Sequential 中的层的顺序和名称。 

6. 总结

DenseNet通过其独特的稠密连接机制,在特征复用、参数效率和梯度流动等方面表现出显著优势。与ResNet相比,DenseNet在更少的参数下实现了更高的性能,尤其适合计算资源有限的场景。通过本文的理论分析和代码实现,我们希望读者能够深入理解DenseNet的核心思想,并掌握如何使用PyTorch实现这一高效的网络架构。我是橙色小博,关注我,一起在人工智能领域学习进步!