上一节的ResNet通过前层与后层的“短路连接”(Shortcuts) , 加强了前后层之间的信息流通, 在一定程度上缓解了梯度消失现象, 从而可以将神经网络搭建得很深。 更进一步, 本节的主角DenseNet最大化了这种前后层信息交流, 通过建立前面所有层与后面层的密集连接, 实现了特征在通道维度上的复用, 使其可以在参数与计算量更少的情况下实现比ResNet更优的性能, 提出DenseNet的《Densely ConnectedConvolutional Networks》 也一举拿下了2017年CVPR的最佳论文。
DenseNet的网络架构如图3.19所示, 网络由多个Dense Block与中间的卷积池化组成, 核心就在Dense Block中。 Dense Block中的黑点代表一个卷积层, 其中的多条黑线代表数据的流动, 每一层的输入由前面的所有卷积层的输出组成。 注意这里使用了通道拼接(Concatnate) 操作, 而非ResNet的逐元素相加操作。
DenseNet的结构有如下两个特性:
·神经网络一般需要使用池化等操作缩小特征图尺寸来提取语义特征, 而Dense Block需要保持每一个Block内的特征图尺寸一致来直接进行Concatnate操作, 因此DenseNet被分成了多个Block。 Block的数量一般为4。
·两个相邻的Dense Block之间的部分被称为Transition层, 具体包括BN、 ReLU、 1×1卷积、 2×2平均池化操作。 1×1卷积的作用是降维, 起到压缩模型的作用, 而平均池化则是降低特征图的尺寸,
具体的Block实现细节如图3.20所示, 每一个Block由若干个Bottleneck的卷积层组成, 对应图3.19中的黑点。 Bottleneck由BNReLU、 1×1卷积、 BN、 ReLU、 3×3卷积的顺序构成。
关于Block, 有以下4个细节需要注意:
·每一个Bottleneck输出的特征通道数是相同的, 例如这里的32。 同时可以看到, 经过Concatnate操作后的通道数是按32的增长量增加的,因此这个32也被称为GrowthRate。
·这里1×1卷积的作用是固定输出通道数, 达到降维的作用。 当几十个Bottleneck相连接时, Concatnate后的通道数会增加到上千, 如果不增加1×1的卷积来降维, 后续3×3卷积所需的参数量会急剧增加。 1×1卷积的通道数通常是GrowthRate的4倍。
·图3.20中的特征传递方式是直接将前面所有层的特征Concatnate后传到下一层, 这种方式与具体代码实现的方式是一致的, 而不像图3.19中, 前面层都要有一个箭头指向后面的所有层。
·Block采用了激活函数在前、 卷积层在后的顺序, 这与一般的网络上是不同的。
利用PyTorch来实现DenseNet的一个Block, 新建一个densenet_block.py文件, 代码如下:
1 import torch 2 from torch import nn 3 import torch.nn.functional as F 4 5 # 实现一个Bottleneck的类, 初始化需要输入通道数与GrowthRate这两个参数 6 class Bottleneck(nn.Module): 7 8 def __init__(self, nChannels, growthRate): 9 10 super(Bottleneck, self).__init__() 11 # 通常1×1卷积的通道数为GrowthRate的4倍 12 interChannels = 4*growthRate 13 self.bn1 = nn.BatchNorm2d(nChannels) 14 self.conv1 = nn.Conv2d(nChannels, interChannels, kernel_size=1, bias=False) 15 16 self.bn2 = nn.BatchNorm2d(interChannels) 17 self.conv2 = nn.Conv2d(interChannels, growthRate, kernel_size=3, 18 padding=1, bias=1) 19 20 def forward(self, x): 21 out = self.conv1(F.relu(self.bn1(x))) 22 out = self.conv2(F.relu(self.bn2(out))) 23 # 将输入x同计算的结果out进行通道拼接 24 out = torch.cat((x, out), 1) 25 return out 26 27 class Denseblock(nn.Module): 28 29 def __init__(self, nChannels, growthRate, nDenseBlocks): 30 super(Denseblock, self).__init__() 31 layers = [] 32 # 将每一个Bottleneck利用nn.Sequential()整合起来, 输入通道数需要线性增长 33 for i in range(int(nDenseBlocks)): 34 layers.append(Bottleneck(nChannels, growthRate)) 35 nChannels += growthRate 36 self.denseblock = nn.Sequential(*layers) 37 38 def forward(self, x): 39 return self.denseblock(x)
1 import torch 2 from densenet_block import Denseblock 3 4 # 实例化DenseBlock, 包含了6个Bottleneck 5 denseblock = Denseblock(64, 32, 6).cuda() 6 7 # 查看denseblock的网络结构, 由6个Bottleneck组成 8 print(denseblock) 9 >> Denseblock( 10 (denseblock): Sequential( 11 # 第1个Bottleneck的输入通道数为64, 输出固定为32 12 (0): Bottleneck( 13 (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 14 (conv1): Conv2d(64, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 15 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 16 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) 17 ) 18 # 19 # 第2个Bottleneck的输入通道数为96, 输出固定为32 20 (1): Bottleneck( 21 (bn1): BatchNorm2d(96, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 22 (conv1): Conv2d(96, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 23 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 24 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) 25 ) 26 # 第3个Bottleneck的输入通道数为128, 输出固定为32 27 (2): Bottleneck( 28 (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 29 (conv1): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 30 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 31 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) 32 ) 33 #第4个Bottleneck的输入通道数为160, 输出固定为32 34 (3): Bottleneck( 35 (bn1): BatchNorm2d(160, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 36 (conv1): Conv2d(160, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 37 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 38 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) 39 ) 40 # 第5个Bottleneck的输入通道数为192, 输出固定为32 41 (4): Bottleneck( 42 (bn1): BatchNorm2d(192, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 43 (conv1): Conv2d(192, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 44 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 45 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) 46 ) 47 # 第6个Bottleneck的输入通道数为224, 输出固定为32 48 (5): Bottleneck( 49 (bn1): BatchNorm2d(224, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 50 (conv1): Conv2d(224, 128, kernel_size=(1, 1), stride=(1, 1), bias=False) 51 (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) 52 (conv2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) 53 ) 54 ) 55 ) 56 57 input = torch.randn(1, 64, 256, 256).cuda() 58 output = denseblock(input) # 将输入传入denseblock结构中 59 print(input.shape) 60 >> torch.Size([1, 64, 256, 256]) 61 62 # 输出的通道数为: 224+32=64+32×6=256 63 print(output.shape) 64 >> torch.Size([1, 256, 256, 256])
DenseNet网络的优势主要体现在以下两个方面:
·密集连接的特殊网络, 使得每一层都会接受其后所有层的梯度,而不是像普通卷积链式的反传, 因此一定程度上解决了梯度消失的问题。
·通过Concatnate操作使得大量的特征被复用, 每个层独有的特征图 的通道是较少的, 因此相比ResNet, DenseNet参数更少且计算更高效。DenseNet的不足在于由于需要进行多次Concatnate操作, 数据需要被复制多次, 显存容易增加得很快, 需要一定的显存优化技术。 另外,DenseNet是一种更为特殊的网络, ResNet则相对一般化一些, 因此ResNet的应用范围更广泛。