本文基于d2l项目内容整理,介绍稠密连接网络(DenseNet)中稠密块的核心概念和实现方法,包括稠密连接的设计思想、结构特点以及代码实现。
稠密连接网络 (DenseNet) 在残差网络 (ResNet) 跳跃连接的恒等映射基础上进行了扩展。
1. 稠密块的结构
1.1 稠密连接原理
网络中的每层不仅只接受来自上一层的输入,而将上方所有层的输出全部拼接在一起作为层的输入(而非逐元素相加),这种结构被称为稠密块 (dense block)。
若用 $\mathbf{x}{0}$ 表示原始数据,$\mathbf{x}_1,\mathbf{x}_2,\ldots,\mathbf{x}{\ell-1}$分别表示从第 1 层到第 $\ell-1$层的输出,$[\cdots]$表示数据在通道维度上的拼接,$\mathbf{H}{\ell}$ 表示在 $\mathrm{\ell}$ 层执行的一系列批量归一化、激活、卷积操作,$\mathbf{k}$ 表示每层通道数的增长率 (growth rate),则经过稠密连接 (dense connection) 在第$\mathrm{\ell}$层稠密块的输出 $\mathbf{x}{\ell}$ 为:
1.2 稠密连接的优势
稠密连接的核心优势:
稠密连接使网络能复用之前所有层的特征,每一层只产生少量特征增量,具有以下优点:
- 梯度流畅:梯度能顺利地从深层传递到浅层
- 特征复用:提高了特征复用率并显著减少了模型参数
- 信息流动:在促进信息流动的同时,保证了特征的高效表达
- 隐式监督:起到了类似隐式深层监督的效果
1.3 与残差块的对比
残差块 vs 稠密块:
- 残差块:通过逐元素相加实现跨层连接,最小的残差块只需要 1 组卷积层的跳跃连接
- 稠密块:通过逐通道拼接实现跨层连接,依赖于多层输出,至少需要 2 层卷积层才能构成最小稠密块
为了限制网络参数规模,避免通道数无限增长,需在两个稠密块之间使用过渡层 (transition layer) 连接。
2. 思路与代码实现
2.1 实现思路
稠密块的实现思路如下:
- 使用 PyTorch,通过继承
nn.Module
实现DenseBlock
稠密块,并重载__init__()
方法自定义模块结构、重载forward()
方法自定义数据流向;
- 提供
in_channels
和layers_num
参数,分别作为稠密块的输入通道数和稠密块中堆叠的稠密层数,使用参数growth_rate
控制每个稠密层的特征图通道数增量(增长率 $\mathrm{k}$ );
对于每个稠密层:
借鉴了 ResNet v2 中的预激活架构,即由 BatchNorm → ReLU → 3×3 卷积层组成;
由于需要动态创建并手动执行前向传播的逻辑,使用nn.ModuleList
实例存储每个稠密层,保证输入与通道数等于初始输入的通道数加上之前所有层的增长率累积,输出通道数即为增长率;
在向前传播中,遍历**nn.ModuleList**
实例中的每个稠密层,在通道维度上将新特征与输入特征拼接。
2.2 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import torch from torch import Tensor, nn
class DenseBlock(nn.Module): def __init__(self, in_channels: int, layers_num: int, growth_rate: int): """ 稠密块
由多组 “BatchNorm → ReLU → Conv” 结构(稠密层,DenseLayer)组成,每循环一次这样的结构,通道数增长 k。输出结果在通道上完成拼接。
:param in_channels: 输入特征图的通道数 :param layers_num: 堆叠的稠密层 (DenseLayer) 数量 :param growth_rate: 增长率 (k)。每个稠密层输出的新特征图通道数,将与先前层的拼接 """ super().__init__()
self.growth_rate = growth_rate
self.dense_layers = nn.ModuleList([ self._get_dense_layer(in_channels + growth_rate * i) for i in range(layers_num) ])
def _get_dense_layer(self, connected_channels: int) -> nn.Sequential: """ 返回单个稠密层实例 BatchNorm → ReLU → Conv(3x3) :param connected_channels: 该稠密层的输入通道数,等于初始输入的通道数加上之前所有层的增长率累积 """ dense_layer = nn.Sequential( nn.BatchNorm2d(connected_channels), nn.ReLU(), nn.Conv2d(connected_channels, self.growth_rate, kernel_size=3, padding=1, bias=False) ) return dense_layer
def forward(self, x: Tensor) -> Tensor: for layer in self.dense_layers: out = layer(x) x = torch.cat((x, out), dim=1) return x
|
2.3 模型测试
使用torchinfo
库的summary
函数执行更便利的输出维度测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| from torchinfo import summary
model = DenseBlock(in_channels=3, layers_num=2, growth_rate=10) summary(model, input_size=(4, 3, 8, 8)) ========================================================================================== Layer (type:depth-idx) Output Shape Param ========================================================================================== DenseBlock [4, 23, 8, 8] -- ├─ModuleList: 1-1 -- -- │ └─Sequential: 2-1 [4, 10, 8, 8] -- │ │ └─BatchNorm2d: 3-1 [4, 3, 8, 8] 6 │ │ └─ReLU: 3-2 [4, 3, 8, 8] -- │ │ └─Conv2d: 3-3 [4, 10, 8, 8] 270 │ └─Sequential: 2-2 [4, 10, 8, 8] -- │ │ └─BatchNorm2d: 3-4 [4, 13, 8, 8] 26 │ │ └─ReLU: 3-5 [4, 13, 8, 8] -- │ │ └─Conv2d: 3-6 [4, 10, 8, 8] 1,170 ========================================================================================== Total params: 1,472 Trainable params: 1,472 Non-trainable params: 0 Total mult-adds (Units.MEGABYTES): 0.37 ========================================================================================== Input size (MB): 0.00 Forward/backward pass size (MB): 0.07 Params size (MB): 0.01 Estimated Total Size (MB): 0.08 ==========================================================================================
|
使用一个具有 2 层稠密层的稠密块,当输入通道数为 3,增长率为 10 时,输出通道数为 3 + 10×2 = 23。此外,特征图大小保持不变。
输出维度分析:
- 输入通道数:3
- 稠密层数:2 层
- 增长率:10
- 输出通道数:3 + 10×2 = 23
- 特征图尺寸:保持不变 (8×8)
总结
本文介绍了稠密连接网络中稠密块的核心概念:
- 稠密连接原理:通过通道拼接而非逐元素相加实现跨层连接
- 设计优势:提高特征复用率、促进梯度流动、减少参数量
- 实现要点:使用 ModuleList 动态创建稠密层,实现特征图的通道拼接
- 过渡层的必要性:防止通道数无限增长,控制网络复杂度