introduction
作者提出了一种具有密集连接的卷积神经网络DenseNet, 在该网络中,每一层的输入都是前面所有层的集合,而该层学习到的特征图也用于后面所有层作为输入, 因此对于一个L层的网络,DenseNet有个连接。
用公式说明的话,传统网络在第L层的输出为:
ResNet使用了identity mapping, 它的优点所以输出表达式为:
在DenseNet中,则会连接前面所有层作为输出
网络结构
DenseNet的网络结构主要由DenseBlock和Transition两种结构组成,整个网络结构如图Fig.2所示
Dense Block
在DenseBlock中,每一层的特征图大小一致,可以在channel维度上进行连接,每一层使用的非线性组合函数采用的结构为BN+ReLu+3x3 Conv。不过考虑到后面层的输入会越来越大,所以引入了1x1的卷积层用于减少特征数量,提升计算效率。引入1x1的卷积层后的Bottleneck结构变为BN+ReLU+1x1 Conv+ BN+ReLU+3x3 Conv.
从公式(3)可以看出,如果每一个产生k个特征图的话(即特征图的channel数为k),那么第层就有个输入特征,其中指的是原始输入图片的通道数。事实上每一层网络只有K个特征是自己独有的,其他的特征则为前面层的特征复用。作者认为正是这种全局特征复用,让非常深的层也可以访问浅层次的特征,从而可以得到比较好的效果。论文中将k作为一个超参数growth rate,一般来说K取得较小如12即可获得比较好的效果。
Bottleneck的pytorch实现如下,每个DenseBlock由多个Bottleneck组成
class Bottleneck(nn.Module):
def __init__(self, in_planes, growth_rate):
super(Bottleneck, self).__init__()
self.bn1 = nn.BatchNorm2d(in_planes)
self.conv1 = nn.Conv2d(in_planes, 4*growth_rate, kernel_size=1, bias=False)
self.bn2 = nn.BatchNorm2d(4*growth_rate)
self.conv2 = nn.Conv2d(4*growth_rate, growth_rate, kernel_size=3, padding=1, bias=False)
def forward(self, x):
out = self.conv1(F.relu(self.bn1(x)))
out = self.conv2(F.relu(self.bn2(out)))
out = torch.cat([out,x], 1)
return out
Transition
下采样层是卷积神经网络中比较重要的部分,通过下采样层可以降低特征图大小,增大感受野,同时可以保持某种不变性(旋转、平移、伸缩等)。不过DenseBlock中每一层特征图大小都需一致,所以引入Transition层用于连接两个DenseBlock. Transition层包括一个1x1的卷积和2x2的AvgPooling。Transition层还有着压缩模型的作用。假设上一个DenseBlock的输出有m个特征图,则经过Transition层后可输出个特征图,其中在论文中称为压缩系数(Compression rate)。当时网络结构称之为DenseNet-C, 当同时使用Bottleneck和压缩系数时模型称之为DenseNet-BC.
Transition的pytorch实现如下
class Transition(nn.Module):
def __init__(self, in_planes, out_planes):
super(Transition, self).__init__()
self.bn = nn.BatchNorm2d(in_planes)
self.conv = nn.Conv2d(in_planes, out_planes, kernel_size=1, bias=False)
def forward(self, x):
out = self.conv(F.relu(self.bn(x)))
out = F.avg_pool2d(out, 2)
return out
整体结构
整体结构的pytorch实现如下, 全部代码可看github
class DenseNet(nn.Module):
def __init__(self, block, nblocks, growth_rate=12, compression_rate=0.5, num_classes=10, verbose=False):
"""
:param block: (nn.Sequential)Bottleneck layers
:param nblocks: (array) number of layers in each DenseBlock
:param growth_rate: (int) number of filters used in DenseLayer
:param compression_rate: (float 0-1)the compression rate used in Transition Layer
:param num_classes: (int) number of classes for classification
"""
super(DenseNet, self).__init__()
self.growth_rate = growth_rate
num_planes = 2*growth_rate
self.conv1 = nn.Conv2d(3, num_planes, kernel_size=3, padding=1, bias=False)
self.dense1 = self._make_dense_layers(block, num_planes, nblocks[0])
num_planes += nblocks[0]*growth_rate
out_planes = int(math.floor(num_planes*compression_rate))
self.trans1 = Transition(num_planes, out_planes)
num_planes = out_planes
self.dense2 = self._make_dense_layers(block, num_planes, nblocks[1])
num_planes += nblocks[1]*growth_rate
out_planes = int(math.floor(num_planes*compression_rate))
self.trans2 = Transition(num_planes, out_planes)
num_planes = out_planes
self.dense3 = self._make_dense_layers(block, num_planes, nblocks[2])
num_planes += nblocks[2]*growth_rate
out_planes = int(math.floor(num_planes*compression_rate))
self.trans3 = Transition(num_planes, out_planes)
num_planes = out_planes
self.dense4 = self._make_dense_layers(block, num_planes, nblocks[3])
num_planes += nblocks[3]*growth_rate
self.bn = nn.BatchNorm2d(num_planes)
self.linear = nn.Linear(num_planes, num_classes)
self.verbose = verbose
def _make_dense_layers(self, block, in_planes, nblock):
layers = []
for i in range(nblock):
layers.append(block(in_planes, self.growth_rate))
in_planes += self.growth_rate
return nn.Sequential(*layers)
def forward(self, x):
out = self.conv1(x)
if self.verbose:
print("conv1 size: ", out.size())
out = self.trans1(self.dense1(out))
if self.verbose:
print("dense1 size: ", out.size())
out = self.trans2(self.dense2(out))
if self.verbose:
print("dense2 size: ", out.size())
out = self.trans3(self.dense3(out))
if self.verbose:
print("dense3 size: ", out.size())
out = self.dense4(out)
if self.verbose:
print("dense4 size: ", out.size())
out = F.avg_pool2d(F.relu(self.bn(out)), 4)
if self.verbose:
print("avg_pool2d size: ", out.size())
out = out.view(out.size(0), -1)
if self.verbose:
print("view size: ", out.size())
out = self.linear(out)
if self.verbose:
print("linear size: ", out.size())
return out
实验结果
DenseNet在CIFAR、SVHN和ImageNet三个数据集上做了测试,其中CIFAR和SVHN图片输入大小为32x32,所以在使用DenseBlock前,先进行了一次3x3的卷积(卷积核数为16),再选用了三个DenseBlock,三个DenseBlock的特征图大小分别为32x32, 16x16和8x8。而对于ImageNet,因为图片输入大小为224x2244,所以先采用了一个7x7的卷积层接上一个3x3的MaxPooling层,再送入DenseBlock层。ImageNet数据集所采用的网络配置如表1所示
在CIFAR and SVHN数据集上实验结果和方法对比结果如表2所示,具体细节可看原论文
总结
DenseNet的优点如下
- 密集连接:相比于ResNet通过相加的方式将不同层的特征结合起来,DenseNet将不同层的特征从通道维度上连接起来(concat), 这样的好处是实现了特征的重复利用。同时因为使用了密集连接,就可以将每一层设计的比较窄(narrow), 达到降低冗余性的目的。
- 抗过拟合:从实验结果可以看出,在不对CIFAR100做数据增强的情况下,错误率可以从最好的结果28.20%降到19.64%。过拟合的直观解释是:由于特征复用,最后的分类器综合利用了浅层的特征,从而泛化能力更强。
- 参数少,降低计算量:由于DenseNet采用特征复用,并且使用了较小的growth rate, 因此每一层只需要学习很少的特征,从而显著减少了参数量和计算量
参考文献
[1] DenseNet:比ResNet更优的CNN模型
[2] CVPR 2017最佳论文作者解读:DenseNet 的“what”、“why”和“how”|CVPR 2017
[3] https://github.com/kuangliu/pytorch-cifar