DenseNet(密集连接网络)是一种深度学习神经网络架构,由Kaiming He等人在2017年提出。相较于传统的卷积神经网络(CNN),DenseNet具有更加密集的连接方式,每一层都与其前面所有层直接相连。这种结构有助于缓解梯度消失问题,并且可以促进信息和梯度的流动,有助于提升训练深度网络的效果。DenseNet在一些图像识别、物体检测和图像分割等领域取得了很好的效果。本文将详细介绍DenseNet网络,让我们来深入了解。
DenseNet (Dense Connected Network) is a deep learning neural network architecture, proposed by Kaiming He et al. in 2017. Compared to traditional Convolutional Neural Networks (CNNs), DenseNet has a more densely connected approach, where each layer is directly connected to all its preceding layers. This structure helps to alleviate the problem of gradient vanishing and can facilitate the flow of information and gradients, which helps to improve the effect of training deep networks.DenseNet has achieved good results in some fields such as image recognition, object detection and image segmentation. In this article, we will introduce the DenseNet network in detail, so that we can get a deeper understanding.
文献链接:Densely Connected Convolutional Networks
目前的工作表明如果将接近输入和输出的层之间短接,卷积神经网络可以更深、精度更高且高效。在本篇论文中,我们利用到观察到的这个结果提出了密集卷积网络(DenseNet),它的每一层在前向反馈模式中都和后面的层有连接,与L层传统卷积神经网络有L个连接不同,DenseNet中每个层都和其之后的层有连接,因此L层的DenseNet有 L(L+1)/2 个连接关系。对于每一层,它的输入包括前一层的输出和该层之前所有层的输入。DenseNets有几个引入注目的优势:
我们在四个目标识别的基准测试集(CIFAR-10、CIFAR-100、SVHN 和 ImageNet)上评估了我们的结构,可以发现DenseNet在减少计算量的同时取得了更好的表现。
当CNN网络的深度持续增加时,一个新的研究问题就会出现:输入部分的信息流或者梯度在经过很多层之后,当到达网络结束(或开始)的地方时会消失。关于这个问题的研究有很多,可以发现的是虽然在网络拓扑和训练时方法各异,但是它们都有一个关键的特征:即前层和后层之间有短接。
在这篇文章中,遵从上面的直觉并提出了一种简单的连接模式:可以最大化网络中各层之间的信息流动,我们将所有层之间都相互连接,为了保留前向的特征,每个层都会获得前层的额外输入并将本层的特征传递给后续的层。Figure 1展示了这个关系,与ResNets相比,我们没有对上一层的输入和上一层的输出进行特征融合后再送入下一层,而是将前面所有层的特征图它们进行拼接。因此,第l层有l个输入,它由前面卷积层的特征图构成,并且本层的特征图也会传递给后续的L-l个层,所以一个L层的网络总共会有 L(L+1)/2 个连接,而不是传统结构的L个。
x0作为整个网络结构的输入,网络由L层组成,每一层的运算用一个非线性转换Hl()表示,这里的l表示第几层。Hl()可以看成由BN、ReLU、池化、卷积操作定义的复合运算。同时将第l层的输出定义为xl。
传统的卷积神经网络中,第l层的输入为第l-1层的输出,第l层的输出为: x l = H l ( x l ? 1 ) x_l=H_l(x_{l}-1) xl?=Hl?(xl??1)ResNets添加了旁路支路: x l = H l ( x l ? 1 ) + x l ? 1 x_l=H_l(x_l-1)+x_{l-1} xl?=Hl?(xl??1)+xl?1?ResNets的优势是梯度可以直接通过恒等映射从后面的层传到前层来,然而,恒等映射 和 Hl的输出通过叠加结合在一起,这一定程度上阻碍了网络中的信息流。
DenseBlock包含很多层,每个层的特征图大小相同(才可以在通道上进行连结),层与层之间采用密集连接方式。
上图是一个包含5层layer的Dense Block。可以看出Dense Block互相连接所有的层,具体来说就是每一层的输入都来自于它前面所有层的特征图,每一层的输出均会直接连接到它后面所有层的输入。所以对于一个L层的DenseBlock,共包含 L*(L+1)/2 个连接(等差数列求和公式),如果是ResNet的话则为(L-1)*2+1。从这里可以看出:相比ResNet,Dense Block采用密集连接。而且Dense Block是直接concat来自不同层的特征图,这可以实现特征重用(即对不同“级别”的特征——不同表征进行总体性地再探索),提升效率,这一特点是DenseNet与ResNet最主要的区别。
Note:k —— DenseNet中的growth rate(增长率),这是一个超参数。一般情况下使用较小的k(比如12),就可以得到较佳的性能。
假定输入层的特征图的通道数为k0,那么L层输入的channel数为 k0+k*(L-1),因此随着层数增加,尽管k设定得较小,DenseBlock中每一层输入依旧会越来越多。
另外一个特殊的点:DenseBlock中采用BN+ReLU+Conv的结构,平常我们常见的是Conv+BN+ReLU。这么做的原因是:卷积层的输入包含了它前面所有层的输出特征,它们来自不同层的输出,因此数值分布差异比较大,所以它们在输入到下一个卷积层时,必须先经过BN层将其数值进行标准化,然后再进行卷积操作。
X
l
=
H
l
(
[
x
0
,
x
1
,
.
.
.
,
x
l
?
1
]
)
X_l=H_l([x_0,x_1,...,x_l-1])
Xl?=Hl?([x0?,x1?,...,xl??1])
[x0,x1,…,x(l-1)]是将第0、1、…、l-1层的feature map拼接在一起。当特征图的尺寸发生变化时,上式中的拼接操作是不可行的,然而,卷积网络的一个重要部分就是通过下采样改变特征图的尺寸。为了在我们的网络结构中做下采样,我们将网络划分为多个密集连接卷积网络块,如Figure 2所示,我们将块之间的层看作转换层,它是由BN、1x1卷积层、池化层构成,目的是做卷积和池化。
如果每一个Hl都产生k个feature map,那么第l个层就会有 k0 + k x (l - 1) 个输入的feature map,k0表示输入层的通道数,DenseNet和已经存在的网络结构中一个重要区别是DenseNet的通道数很窄,比如k=12,我们将超参数k定义为网络的growth rate,在后面的分析中我们会看到小的growth rate对于在测试集上获得很好的表现也是足够的。一个解释就是每一层都可以访问块中前面层,因此,可以理解为网络的“集体认识”,可以把网络的特征图看作是全局变量,每过一个层,就往全局变量中添加k个特征图。
尽管每个层的输出都只有k个通道,但是它的输入通道数很大。可以在 3x3 的卷积层之前使用 1x1 的瓶颈层来提高计算效率,我们发现这样的设计非常高效,一个 bottleneck层代指 BN-ReLU-Conv(1x1)-BN-ReLU-Conv(3x3),这样的网络结构称为 DenseNet-B。在我们的实验中,1x1 的卷积层的输出通道为 4k。
为了使模型更加紧密,我们通过transition层减少特征图的数量。如果一个dense block包含m个特征图,那么通过transition层会产生 theta * m(下取整)个输出feature map,在这里 0<theta<=1。当 theta=1时,输出通道数不会发生变化,我们将 theta<1 的DenseNet 记为 DenseNet-C,同时在我们的实验中,将theta设置为0.5.当bottleneck层和theta<1的transition层同时使用时,DenseNet可以称为 DenseNet-BC。
在除了ImageNet之外的数据集上,实验中的DenseNet都用了三个dense block(ImageNet用了四个块),每个块总包含有相同数量的层。其他的具体细节可以参考论文第三节中的Implementation Details部分。
在ImageNet数据集上,我们使用了带有四个dense block的DenseNet-BC结构,输入图片尺寸为224x224。最开始的卷积层为2k个7x7x输入图片通道数的卷积核,步长为2;所有层的feature-maps的数量也都由k设置,对ImageNet使用的网络配置如table1所示:
DenseNet-121是指网络总共有121层:(6+12+24+16)*2 + 3(transition layer) + 1(7x7 Conv) + 1(Classification layer) = 121;
再详细说下bottleneck和transition layer操作。在每个Dense Block中都包含很多个子结构,以DenseNet-169的Dense Block(3)为例,包含32个11和33的卷积操作,也就是第32个子结构的输入是前面31层的输出结果,每层输出的channel是32(growth rate),那么如果不做bottleneck操作,第32层的33卷积操作的输入就是3132+(上一个Transition Layer的输出channel),近1000了。而加上11的卷积,代码中的11卷积的channel是growth rate4,也就是128,然后再作为33卷积的输入。这就大大减少了计算量,这就是bottleneck。至于transition layer,放在两个Dense Block中间,是因为每个Dense Block结束后的输出channel个数很多,需要用11的卷积核来降维。还是以DenseNet-169的Dense Block(3)为例,虽然第32层的33卷积输出channel只有32个(growth rate),但是紧接着还会像前面几层一样有通道的concat操作,即将第32层的输出和第32层的输入做concat,前面说过第32层的输入是1000左右的channel,所以最后每个Dense Block的输出也是1000多的channel。因此这个transition layer有个参数reduction(范围是0到1),表示将这些输出缩小到原来的多少倍,默认是0.5,这样传给下一个Dense Block的时候channel数量就会减少一半,这就是transition layer的作用。文中还用到dropout操作来随机减少分支,避免过拟合,毕竟这篇文章的连接确实多。
我们设计实验在几个基准测试集上验证了DenseNet的有效性,并着重与ResNet 和它的几个变体做了比较。
训练的具体细节:
在DenseNet和stochastic depth regularization之间存在着一种有趣的联系,在stochastic depth中,残差块中的层可以随意丢弃,这使得周围层之间可能直接相连。但是池化层从未丢弃,这看起来有些和DenseNet类似,尽管两者的方法不同,但我们可以从stochastic depth的角度理解DenseNet——引入了正则化的意味。
DenseNet允许本层访问之前所有层的feature maps,我们设计了一组实验来验证了这个想法,在C10+数据集上L=40、k=12,对于一个块内的l层,我们计算和它相连接的s层的权重平均值的绝对值,Figure 5展示了三个dense块的热力图,权重平均值绝对值展示了这一层对之前某一层特征的复用率。
可以发现:
BN-ReLu-Conv
class BN_Conv2d(nn.Module):
"""
BN_CONV_RELU
"""
def __init__(self, in_channels: object, out_channels: object, kernel_size: object, stride: object, padding: object,
dilation=1, groups=1, bias=False) -> object:
super(BN_Conv2d, self).__init__()
self.seq = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride,
padding=padding, dilation=dilation, groups=groups, bias=bias),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
return F.relu(self.seq(x))
Dense Block
class DenseBlock(nn.Module):
def __init__(self, input_channels, num_layers, growth_rate):
super(DenseBlock, self).__init__()
self.num_layers = num_layers
self.k0 = input_channels
self.k = growth_rate
self.layers = self.__make_layers()
def __make_layers(self):
layer_list = []
for i in range(self.num_layers):
layer_list.append(nn.Sequential(
BN_Conv2d(self.k0+i*self.k, 4*self.k, 1, 1, 0),
BN_Conv2d(4 * self.k, self.k, 3, 1, 1)
))
return layer_list
def forward(self, x):
feature = self.layers[0](x)
out = torch.cat((x, feature), 1)
for i in range(1, len(self.layers)):
feature = self.layers[i](out)
out = torch.cat((feature, out), 1)
return out
网络搭建并测试
class DenseNet(nn.Module):
def __init__(self, layers: object, k, theta, num_classes) -> object:
super(DenseNet, self).__init__()
# params
self.layers = layers
self.k = k
self.theta = theta
# layers
self.conv = BN_Conv2d(3, 2*k, 7, 2, 3)
self.blocks, patches = self.__make_blocks(2*k)
self.fc = nn.Linear(patches, num_classes)
def __make_transition(self, in_chls):
out_chls = int(self.theta*in_chls)
return nn.Sequential(
BN_Conv2d(in_chls, out_chls, 1, 1, 0),
nn.AvgPool2d(2)
), out_chls
def __make_blocks(self, k0):
"""
make block-transition structures
:param k0:
:return:
"""
layers_list = []
patches = 0
for i in range(len(self.layers)):
layers_list.append(DenseBlock(k0, self.layers[i], self.k))
patches = k0+self.layers[i]*self.k # output feature patches from Dense Block
if i != len(self.layers)-1:
transition, k0 = self.__make_transition(patches)
layers_list.append(transition)
return nn.Sequential(*layers_list), patches
def forward(self, x):
out = self.conv(x)
out = F.max_pool2d(out, 3, 2, 1)
# print(out.shape)
out = self.blocks(out)
# print(out.shape)
out = F.avg_pool2d(out, 7)
# print(out.shape)
out = out.view(out.size(0), -1)
out = F.softmax(self.fc(out))
return out
搭建网络并测试:
def densenet_121(num_classes=1000):
return DenseNet([6, 12, 24, 16], k=32, theta=0.5, num_classes=num_classes)
def densenet_169(num_classes=1000):
return DenseNet([6, 12, 32, 32], k=32, theta=0.5, num_classes=num_classes)
def densenet_201(num_classes=1000):
return DenseNet([6, 12, 48, 32], k=32, theta=0.5, num_classes=num_classes)
def densenet_264(num_classes=1000):
return DenseNet([6, 12, 64, 48], k=32, theta=0.5, num_classes=num_classes)
def test():
net = densenet_264()
summary(net, (3, 224, 224))
x = torch.randn((2, 3, 224, 224))
y = net(x)
print(y.shape)
test()
当k=32,θ=0.5时,DenseNet_264网络的测试结果如下图,可以看到DenseNet的参数量确实比ResNet要少得多。
本周看了 DenseNet 网络这篇经典论文,让我对DenseNet网络基本原理有了一定的了解,DenseNet作为一种新型神经网络架构,其密集连接方式和优秀的性能使其成为一种值得研究的模型,下周我将继续保持论文阅读的习惯,同时也进一步提升自己的代码能力。