Pytorch: 稠密连接网络-DenseNet
Copyright: Jingmin Wei, Pattern Recognition and Intelligent System, School of Artificial and Intelligence, Huazhong University of Science and Technology
本教程不商用,仅供学习和参考交流使用,如需转载,请联系本人。
Reference
import torch
import torch.nn as nn
DenseNet 简介
ResNet 中的跨层连接设计引申出了数个后续工作,比较有名的是稠密连接网络(DenseNet)。 它与 ResNet 的主要区别如图所示。DenseNet 最大化了前后层信息交流,通过建立前面所有层和后面层的稠密连接,实现了特征在通道维度上的复用,使其可以在参数和计算量更少的情况下实现比 ResNet 更优的性能。
图: ResNet 与 DenseNet 在跨层连接上的主要区别:相加(+)和连结(cat)。ResNet 使用的是 sum ,有可能阻碍信息传递。DenseNet 使用 concatenate 来聚合不同级别的特征,这样,每一层都能把前面所有层的特征图谱当作输入。
图中将部分前后相邻的运算抽象为模块 A 和模块 B 。与 ResNet 的主要区别在于,DenseNet 里模块 B 的输出不是像 ResNet 那样和模块 A 的输出相加,而是在通道维上连结。这样模块 A 的输出可以直接传入模块 B 后面的层。在这个设计里,模块 A 直接跟模块 B 后面的所有层连接在了一起。这也是它被称为“稠密连接”的原因。
DenseNet 的主要构建模块是稠密块(dense block)和过渡层(transition layer)。前者定义了输入和输出是如何连结的,后者则用来控制通道数,使之不过大。
DenseNet 的网络架构如图所示,网络由多个Dense Block与中间的卷积池化组成,核心就在 Dense Block 中。Dense Block 中的黑点代表一个卷积层,其中的多条黑线代表数据的流动,每一层的输入由前面的所有卷积层的输出组成。注意这里使用了通成代表数(Concatnate)操作,而非 ResNet 的逐元元素相加操作。
DenseNet 的结构有如下两个特性:
-
神经网络一般需要使用池化等操作缩小特征图尺寸来提取语义特征,而 Dense Block 需要保持每一个 Block 内的特征图尺寸一致来直接进行 Concatnate 操作,因此 DenseNet 被分成了多个Block 。 Block 的数量一般为 4 4 4 。
-
两个相邻的 Dense Block 之间的部分被称为 Transition 层,具体包括 BN、ReLU、 1 × 1 1\times1 1×1 卷积、 2 × 2 2\times2 2×2 平均池化操作。 1 × 1 1\times1 1×1 卷积的作用是降维,起到压缩模型的作用,而平均池化则是降低特征图的尺寸。
具体的 Block 实现细节如图所示,每一个 Block 由若干个 Bottleneck 的卷积层组成,对应上图中的黑点。Bottleneck 由 BN、ReLU、 1 × 1 1\times1 1×1 卷积、BN、ReLU、 3 × 3 3\times3 3×3 卷积的顺序构成。
关于 Block ,有以下 4 4 4 个细节需要注意:
-
每一个 Bottleneck 输出的特征通道数是相同的,例如这里的 32 32 32 。同时可以看到,经过 Concatnate 操作后的通道数是按 32 32 32 的增长量增加的,因此这个 32 32 32 也被称为 GrowthRate 。
-
这里 1 × 1 1\times1 1×1 卷积的作用是固定输出通道数,达到降维的作用。当几十个 Bottleneck 相连接时,Concatnate 后的通道数会增加到上干,如果不增加 1 × 1 1\times1 1×1 卷积来降维,后续 3 × 3 3\times3 3×3 卷积所需的参数量会急剧增加。 1 × 1 1\times1 1×1 卷积的通道数通常是 GrowthRate 的 4 4 4 倍。
-
图中的特征传递方式是直接将前面所有层层的特征 Concatnate 后传到下一层,这种方式与具体代码实现的方式是一致的,而不像 Figure 2 中,前面层都要有有一个箭头指向后面的所有层。
-
Block 采用了激活函数在前、卷积层在后的顺序,这与一般的网络上是不同的。
代码实现
稠密块实现
DenseNet 使用了 ResNet 改良版的“批量归一化、激活和卷积”结构,我们首先在 conv_block 函数里实现这个结构。
稠密块由多个 conv_block 组成,每块使用相同的输出通道数。但在前向计算时,我们将每块的输入和输出在通道维上连结。
图中的 Growth Rate 为 32 32 32 ,初始的 in_channels 为 64 64 64 ,分别使用了 3 × 3 3\times3 3×3 和 1 × 1 1\times1 1×1 的卷积。
class Bottleneck(nn.Module):
def __init__(self, in_channels, growth_rate, bn_size