4.10 DenseNet网络#
在4.9节内容中,我们详细介绍了ResNet模型的原理和实现方法,它的成功不仅解决了深度神经网络的退化问题而且在ImageNet等大型数据集上有着很好的表现,成为了深度学习领域的重要成果之一。因此,ResNet也为后续的网络设计提供了一种新的思考方向,启发了后续许多网络结构的设计,例如在本节内容中将要介绍的DenseNet模型。
4.10.1 DenseNet动机#
在传统的卷积神经网络中每一层的输入只来自于上一层的输出,这导致了每一层的特征图只能连接到相邻的下一层而无法跨层连接,这使得随着网络深度增加不仅出现了梯度消失或爆炸的现象,而且还出现了网络退化的问题。虽然残差网络的出现使得这两个问题在很大程度上得到了缓解,但是随着网络模型加深依旧存在着梯度消失的问题;其次,在传统的卷积神经网络中需要通过增加网络深度的方式来提高特征的表达能力从而导致模型参数急剧增加;最后,由于每一层的特征只能连接到近邻的后一层,使得浅层的特征可能无法充分传递到深层网络中导致特征传递不充分[1]。
受到ResNet残差结构连接方式的启发,作者提出了一种基于密集连接的网络结构DenseNet[1]。DenseNet通过将上一层和前面所有层的输出拼接起来并作为当前层的输入,从而实现:①允许梯度更加直接地传递到浅层网络有助于缓解梯度消失的问题;②通过特征共享的方式来增加特征的表达能力从而降低了模型的参数量;③使得特征能在网络层中更加自由地传递有助于特征的充分利用。
4.10.2 DenseNet结构#
整体来说,DenseNet模型主要由密集连接层和迁移层所构成,而两者的核心也都是由卷积层所构成,后续整个DenseNet网络也将基于这两个部分来进行构建。下面开始分别就这两部分进行介绍。
1. 理解密集连接层
对于密集连接层来说,它是由一个个密集块所构成,而密集块则是由两个基础的卷积层所构成,整个密集块的结构如图4-48所示。

在图4-48中,首先是一个卷积核大小为$1\times1$的卷积层,其卷积核的个数为$4K$,$K$为论文中所指的增长率(Growth Rate)用于控制密集块输出的特征通道数,4可以理解为控制系数用于调节两个卷积层之间的特征通道数,紧接着最后便是一个$3\times3$的卷积层。同时从这里可以看出,对于每个卷积层来说其先进行了归一化,然后是非线性变换操作,最后才是线性变换,即“BN+ReLU+Conv”的顺序;而在ResNet中这一顺序为“Conv+BN+ReLU”。
进一步,由多个密集块便可以构造得到一个密集连接层,如图4-49所示。

如图4-49所示,对于传统的网络结构来说$L$层网络便有$L$条连接,而在密集连接层中则有$\frac{L}{2}(L+1)$条连接,而这一连接方式也DenseNet名字的由来。在图4-49中,对于第1个密集块来说,假定其输入形状的长、宽和通道数分别为$m$、$n$和$c$则其输出形状便是$m\times n\times K$;对于第2个密集块来说其输入便是第1个密集块的输入和第1个密集块输出的组合,即形状为$m\times n\times (K+c)$,且其输出同样为$m\times n\times K$;以此类推,第3个密集块的输入形状便是$m\times n\times (2\cdot K+c)$,其输出形状依然为$m\times n\times K$。
由此可以得出,对于第$l$个密集块来说,其输入通道数为$(l-1)\cdot K+c$,其中$l=1,2...,L$,这个递推公式在模型实现时还会用到。
2. 理解迁移层
根据图4-49可知,对于每个密集连接层来说其输出特征图的通道数和长宽均没有发生改变,因此作者提出了迁移层(Transition Layer)的概念来逐一对密集连接层的输出特征进行压缩,其结构如图4-50所示。

在图4-50中,首先同样是一个卷积核大小为$1\times1$的卷积层,其卷积核的个数为$\theta m$,其中$m$表示输入通道数,$\theta$表示通道压缩率( Compression Factor),默认为0.5即通道数减半;最后是一个窗口大小为$2\times 2$的平均池化层。
3. 整体网络结构
在清楚密集连接层和迁移层的构造原理后,我们便可以构造得到最终的DenseNet网络模型。同ResNet一样,我们可以通过配置不同的密集连接数量返回得到不同的DenseNet结构,如论文中给出的DenseNet121、DenseNet169、DenseNet201和DenseNet264。为了方便后续介绍以及与ResNet18大致保持一致,下面我们将以DenseNet21为例进行介绍,其网络结构信息如表4-4所示。
| 网络层 | 输出形状 | 18-layer |
|---|---|---|
| Convolution | $112\times112$ | $\begin{bmatrix}7\times7,&64\end{bmatrix}\times1$ |
| Pooling | $56\times56$ | Max Pooling |
| DenseLayer1 | $56\times56$ | $\begin{bmatrix}1\times1\\ 3\times3\end{bmatrix}\times2$ |
| Transition1 | $28\times28$ | $1\times1$ Conv, $2\times2$ Average Pooling |
| DenseLayer2 | $28\times28$ | $\begin{bmatrix}1\times1\\ 3\times3\end{bmatrix}\times2$ |
| Transition2 | $14\times14$ | $1\times1$ Conv, $2\times2$ Average Pooling |
| DenseLayer3 | $14\times14$ | $\begin{bmatrix}1\times1\\ 3\times3\end{bmatrix}\times2$ |
| Transition3 | $7\times7$ | $1\times1$ Conv, $2\times2$ Average Pooling |
| DenseLayer4 | $7\times7$ | $\begin{bmatrix}1\times1\\ 3\times3\end{bmatrix}\times2$ |
| ClassificatinLayer | $1\times1$ | $7\times7$ Global Average Pooling, softmax |
如表4-4所示,从上到下ResNet21一共有10个部分,其中第2列表示每个部分特征图输出的形状;第3列表示各层对应的参数信息,以DenseLayer1这一行为例,其表示一共使用了2个密集块结构且每个密集层由两个卷积层构成。由此,根据表4-4中的结构信息便可以得到如图4-51所示的网络结构图。

如图4-51所示,输入形状为3通道大小是$224\times224$的图片,在经过第1个卷积层和池化层处理后形状变成了形状为$56\times56\times64$的特征图;紧接着便是一个密集层和迁移层的处理,其输出形状分别为$56\times56\times128$和$28\times28\times64$;然后再次进入到一个密集层中,同时该密集层包含有两个密集块,最后该密集层的输出形状为$28\times28\times128$;进一步便是重复迁移层和密集层,最后通过再经过一个最大池化层得到一个一维向量,最后通过一个Softmax层完成分类任务。具体每个结构计算后特征图的形状可以参见图4-51中的对应标注。
4.10.3 DenseNet实现#
在介绍完整个模型结构后下面开始分步进行实现。首先,我们依旧是按照模型中的最小结构来依次进行实现,即先实现DenseBlock和DenseLayer,然后再实现TransitionLayer,最后再整体实现DenseNet。以下完整示例代码可以参见Code/Chapter04/C09_DenseNet/文件。
1.辅助模块
根据图4-48可知,模块DenseBlock主要是有一个$1\times1$的卷积层和一个$3\times3$的卷积层所有构成,其实现代码如下所示:
1 class DenseBlock(nn.Module):
2 def __init__(self, in_channels, growth_rate, dense_block_coef=4, drop_rate=0.5):
3 super().__init__()
4 self.block = nn.Sequential(nn.BatchNorm2d(in_channels),
5 nn.ReLU(inplace=True),
6 nn.Conv2d(in_channels, dense_block_coef * growth_rate,
7 kernel_size=1, stride=1, bias=False),
8 nn.BatchNorm2d(dense_block_coef * growth_rate),
9 nn.ReLU(inplace=True),
10 nn.Conv2d(dense_block_coef * growth_rate, growth_rate,
11 kernel_size=3, stride=1, padding=1, bias=False),
12 nn.Dropout(drop_rate))
13
14 def forward(self, input):
15 if isinstance(input, torch.Tensor):
16 prev_features = [input]
17 else:
18 prev_features = input
19 concated_features = torch.cat(prev_features, 1)
20 new_features = self.block(concated_features)
21 return new_features在上述代码中,第2行in_channels表示输入该密集块特征图的通道数,growth_rate表示输出通道的数量(论文中默认为32),basic_block_coef表示控制$1\times1$卷积和$3\times3$卷积之间的通道数系数(论文中默认为4)。第5~8行和第9~11行分别是两个卷积层的定义。第14~20行便是整个密集块的前向传播过程,其中15~18行用来判断输入密集块的是否为由多个网络层输出特征构成的列表,因为对于每个密集层的输入来说并不是一个列表,第19行是将当前层之前所有层的输出堆叠起来作为当前层的输入。
进一步,基于DenseBlock可以完成密集层DenseLayer的实现,示例代码如下所示:
1 class DenseLayer(nn.Module):
2 def __init__(self, num_dense_blocks, in_channels,
3 dense_block_coef=4, growth_rate=32, drop_rate=0.5):
4 super().__init__()
5 dense_blocks = []
6 for i in range(num_dense_blocks):
7 dense_blocks.append(DenseBlock(in_channels + i * growth_rate,
8 growth_rate,dense_block_coef, drop_rate))
9 self.dense_blocks = nn.Sequential(*dense_blocks)
10
11 def forward(self, init_features):
12 for dense_block in self.dense_blocks:
13 out = dense_block(init_features)
14 init_features = torch.cat((init_features, out), dim=1)
15 return init_features在上述代码中,第2行num_dense_blocks用于指定一个密集层中密集块的个数,in_channels表示输入当前密集层特征图的通道数,即上一个密集层输出的特征图通道数。第5~8行是根据配置参数循环构建完成一个密集连接层,其中第7行中DenseBlock的第1个参数便是上面介绍的第$l$个密集块输入通道数的计算规则。第11~15行便是密集连接层中各个密集块的前向传播计算过程,其中第14行便是对各个层的输出进行堆叠并输入到下一层中。
最后一个辅助模块便是迁移层的实现,根据图4-50中的结构其实现代码如下所示:
1 class TransitionLayer(nn.Module):
2 def __init__(self, in_channels, out_channels):
3 super().__init__()
4 self.transition_layer = nn.Sequential(nn.BatchNorm2d(in_channels),
5 nn.ReLU(inplace=True),
6 nn.Conv2d(in_channels, out_channels,
7 kernel_size=1, stride=1, bias=False),
8 nn.AvgPool2d(kernel_size=2, stride=2))
9
10 def forward(self, x):
11 return self.transition_layer(x)在上述代码中,第4~8行是定义整个迁移层的网络结构。第10~11层便是其对应的前向传播过程。