Densely Connected Convolutional Networks(DenseNet)

introduction

作者提出了一种具有密集连接的卷积神经网络DenseNet, 在该网络中,每一层的输入都是前面所有层的集合,而该层学习到的特征图也用于后面所有层作为输入, 因此对于一个L层的网络,DenseNet有\frac{L(L+1)}{2}个连接。

Fig.1 DenseNet网络结构示意图

用公式说明的话,传统网络在第L层的输出为:
x_l = H_l(x_{l-1}) \tag{1}
ResNet使用了identity mapping, 它的优点所以输出表达式为:
x_l = H_l(x_{l-1}) + x_{l-1} \tag{2}
在DenseNet中,则会连接前面所有层作为输出
x_l = H_l([x_0, x_1,...,x_{l-1}]) \tag{3}

网络结构

DenseNet的网络结构主要由DenseBlock和Transition两种结构组成,整个网络结构如图Fig.2所示


Fig.2 DenseNet的网络结构

Dense Block

在DenseBlock中,每一层的特征图大小一致,可以在channel维度上进行连接,每一层使用的非线性组合函数H(.)采用的结构为BN+ReLu+3x3 Conv。不过考虑到后面层的输入会越来越大,所以引入了1x1的卷积层用于减少特征数量,提升计算效率。引入1x1的卷积层后的Bottleneck结构变为BN+ReLU+1x1 Conv+ BN+ReLU+3x3 Conv.
从公式(3)可以看出,如果每一个H_l产生k个特征图的话(即特征图的channel数为k),那么第l^{th}层就有k_0 + k*(l-1)个输入特征,其中k_0指的是原始输入图片的通道数。事实上每一层网络只有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层后可输出\theta m个特征图,其中\theta \in (0,1]在论文中称为压缩系数(Compression rate)。当\theta<1时网络结构称之为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所示


Table 1 DenseNet网络结构参数

在CIFAR and SVHN数据集上实验结果和方法对比结果如表2所示,具体细节可看原论文


Table 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354