Densely Connected Convolutional Networks 密集连接卷积网络

什么是DenseNet?

DenseNet是由清华大学的Zhuang Liu、康奈尔大学的Gao Huang和Kilian Q.Weinberger,以及Facebook研究员Laurens van der Maaten在CVPR 2017所作,并且还获得了2017CVPR最佳论文!下面我们就来看看DenseNet是何方神圣吧。

我们知道,当靠近输入的层和靠近输出的层之间的连接越短,卷积神经网络就可以做得更深,精度更高且可以更加有效的训练。而DenseNet在此基础上将每一层与之前所有层相连接。传统的L层卷积神经网络有L个连接——位于每一层和其后一层之间—,而我们的神经网络有L*(L+1)/2个直接链接。对于每一层,其输入的特征是之前所有的层,而它自己的特征图作为之后所有层的输入。

下图展示了具体的密集连接方式:

DenseNet有以下几个引人注目的优点:缓解梯度消失问题,加强特征传播,鼓励特征复用,极大的减少了参数量。DenseNets需要更少的计算来实现高性能

ResNet

训练深层的神经网络,会遇到梯度消失和梯度爆炸(vanishing/exploding gradients)的问题,影响了网络的收敛,但是这很大程度已经被标准初始化(normalized initialization)和BN(Batch Normalization)所处理

当深层网络能够开始收敛,会引起网络退化(degradation problem)问题,即随着网络深度增加,准确率会饱和,甚至下降。这种退化不是由过拟合引起的,因为在适当的深度模型中增加更多的层反而会导致更高的训练误差
ResNet就通过引入深度残差连接来解决网络退化的问题,建立前面层与后面层之间的短链接(shortcuts),从而解决深度CNN模型难训练的问题

具体表达式如下:
在这里插入图片描述
在ResNet中前面层与后面层建立的短链接(shortcuts)方式是add相加操作,因此要求输入与输出的shape完全一样!这主要是由ResNet结构中的Identity Block来完成的,更大程度地去加深网络的深度,实现了深度残差网络。但是add操作可能会阻碍网络中的信息流。

DenseNets和Resnet之间的主要区别,DenseNets通过特征重用来挖掘网络的潜力,产生易于训练且参数效率高的精简模型。将不同层学习到的特征映射进行堆叠操作,可以增加后续层的输入变化,并提高效率。

DenseNet

DenseNet的整体网络架构如下图所示:

一个有三个Dense Block的DenseNet网络。每个Dense Block中含有多个conv blocks,conv blocks中特征图的高度和宽度不发生变化,进行的是通道数的变化。 两个相邻Dense Block之间的层称为Transition Layer过渡层,通过卷积和池化改变特征图的大小

Dense connectivity 紧密连接

为了进一步改善层与层之间的信息流,提出了一种不同的连接模式:引入从任何层到所有后续层的直接连接。正如上图所展示的那样,例如第l层接收前面所有层的特征映射,即x0,x1,…,xl-1作为输入:
在这里插入图片描述
H函数实现每层的非线性变换,主要由BN,Relu和3x3Conv组成,式中[x0,x1,…,xl-1]指的是第0,…,l-1层中生成的特征图进行串联。

Bottleneck layers

每一个Dense Block中的conv block经过H非线性输出k个特征图,但是它通常是输入特征图更多。因此在3x3卷积之前引入1x1卷积作为瓶颈层,以减少特征映射的数量,从而提高计算效率。

Growth rate

如果每个函数H生成k个特征映射,则第l层具有k0+k×(l-1)个输入特征映射,其中k0是输入的通道数。DenseNet与现有网络体系结构的一个重要区别是,DenseNet可以有非常窄的层,例如k=12。我们把超参数k称为网络的growth rate。相对较小的growth rate足以获得最新的结果。
实现代码为:

class Bottleneck(nn.Module):
    def __init__(self, nChannels, growthRate):
        super(Bottleneck, self).__init__()
        interChannels = 4*growthRate
        # 瓶颈结构1x1卷积减少输入的通道数
        self.bn1 = nn.BatchNorm2d(nChannels)
        self.conv1 = nn.Conv2d(nChannels, interChannels, kernel_size=1,bias=False)
        # H函数实现非线性输出BN+Relu+BN
        self.bn2 = nn.BatchNorm2d(interChannels)
        self.conv2 = nn.Conv2d(interChannels, growthRate, 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((x, out), 1)
        return out

Pooling layers 池化层

当特征映射的大小发生变化时,上面式子中使用的连接操作是不可行的。然而,卷积网络的一个重要组成部分是向下采样层来改变特征映射的大小。因此在Dense Block之间添加Transition Layer来进行特征图大小的压缩,而Transition Layer主要由一个1x1卷积块以及步长为2的AveragePooling2D来进行特征图大小压缩。

Compression 压缩

为了进一步提高模型的紧凑性,在Transition Layer中减少特征映射的数量。如果一个Desne Block中包含m个特征映射,在Transition Layer中生成的特征图数量为θm,其中0<θ≤1被称为压缩因子。当θ=1时,Transition Layer的特征图数量保持不变,论文中将θ设置为0.5。
Transition Layer实现代码如下:

# reduction即为压缩因子
nOutChannels = int(math.floor(nChannels*reduction))
class Transition(nn.Module):
    def __init__(self, nChannels, nOutChannels):
        super(Transition, self).__init__()
        self.bn1 = nn.BatchNorm2d(nChannels)
        self.conv1 = nn.Conv2d(nChannels, nOutChannels, kernel_size=1,bias=False)

    def forward(self, x):
        out = self.conv1(F.relu(self.bn1(x)))
        out = F.avg_pool2d(out, 2)
        return out

下图包含了具有四个Dense Block的DenseNet网络架构,根据每一个Dense Block中1x1conv 和3x3conv(即conv block)重复次数的不同,可以分为DenseNet-121,DenseNet-1169,DenseNet-201和DenseNet-264。所有网络的growth rate为32。
输入图片在DenseNet网络的shape的具体变化如下:

我们以上述DenseNet-121为例,来进行DenseNet网络的详解。我们将输入图片进行数据增强后,resize成224x224的shape作为DenseNet的输入input(224x224,3),经过一个kernel size为7且stride=2的卷积块(Conv+BN+Relu),此时conv1的shape为(112,112,64),再经过一次MaxPooling,poo1的shape变为(56,56,64);进入第一个Dense Block(1),此时的输入为poo1(56,56,64),Dense Block中首先是瓶颈层,即将pool1利用1x1卷积进行降维,降至4xgrowth rate,然后再进行3x3卷积特征提取,将输出通道固定为growth rate,将1x1conv和3
x3conv称为Dense Block中的一个conv block。然后在进行Dense Block中特有的残差边短接,即将该输出(56,56,growth rate)与输入pool1进行通道数的堆叠,shape变为(56,56,64+growth rate);然后又将该特征层作为输入,继续重复瓶颈结构和残差边短接操作,在DenseNet-121的Dense Block(1)中,上述的conv block和残差边短接一共进行了6次,故输出dense1的shape变为(56,56,64+6growth rate)=(56,56,256),论文中是将growth rate设置为32。需要注意的是,上述Dense Block(1)中并没有发生特征图高度和宽度的改变,仅仅是进行了通道数的堆叠concatenate操作,即层与层之间的密集连接。接着是进入第一个Transition Layer(1),在Transition Layer(1)中首先利用1x1卷积进行通道数的压缩,压缩为一半的通道数,提高模型的紧凑性,shape此时变为(56,56,(64+6growth rate)/2)=(56,56,128)。然后再利用一个步长stride=2的平均池化AveragePooling2D来压缩特征层的高度和宽度,此时trans1的shape变为(28,28,(64+6growth rate)/2)=(56,56,128)。然后再将此特征层作为输入,进入第二个Dense Block(2),Dense Block(2)中进行的操作和Dense Block(1)一样,只是conv block和残差边短接重复的次数不一样而已,Dense Block(2)中一共进行了12次,此时dense2的shape为(28,28,(64+6growth rate)/2+12growth rate)=(28,28,512)。接着进入第二个Transition Layer(2),操作和Transition Layer(1)一样,先利用1x1卷积进行通道数压缩,再利用平均池化压缩特征层的高度和宽度,即trans2的shape为(14,14,((64+6growth rate)/2+12growth rate)/2)=(14,14,256)。再进入第三个Dense Block(3),conv block和残差边短接一共进行了24次,输出dense3的shape为(14,14,((64+6growth rate)/2+12growth rate)/2+24growth rate)=(14,14,1024),进入Transition Layer(3),输出trans3的shape为(7,7,512)。再进入第四个Dense Block(4),conv block和残差边短接一共进行了16次,输出dense4的shape为(7,7,1024)。然后进行一个全局平均池化,最后接一个全连接层将输出通道数固定为n_classes类别数量。

DenseNet121的pytorch代码具体如下:

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import torchvision.transforms as transforms
import torchvision.models as models
from torchsummary import summary

import sys
import math


# 瓶颈结构,1x1卷积将输入通道数降低为4*growthRate,3x3卷积将通道数固定growthRate
class Bottleneck(nn.Module):
    def __init__(self, nChannels, growthRate):
        super(Bottleneck, self).__init__()
        interChannels = 4 * growthRate
        self.bn1 = nn.BatchNorm2d(nChannels)
        self.conv1 = nn.Conv2d(nChannels, interChannels, kernel_size=1, bias=False)
        self.bn2 = nn.BatchNorm2d(interChannels)
        self.conv2 = nn.Conv2d(interChannels, growthRate, 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((x, out), 1)
        return out

class SingleLayer(nn.Module):
    def __init__(self, nChannels, growthRate):
        super(SingleLayer, self).__init__()
        self.bn1 = nn.BatchNorm2d(nChannels)
        self.conv1 = nn.Conv2d(nChannels, growthRate, kernel_size=3, padding=1, bias=False)

    def forward(self, x):
        out = self.conv1(F.relu(self.bn1(x)))
        out = torch.cat((x, out), 1)
        return out

# Transition Layer将1x1卷积进行通道数压缩一半,然后再进行平均池化
class Transition(nn.Module):
    def __init__(self, nChannels, nOutChannels):
        super(Transition, self).__init__()
        self.bn1 = nn.BatchNorm2d(nChannels)
        self.conv1 = nn.Conv2d(nChannels, nOutChannels, kernel_size=1, bias=False)

    def forward(self, x):
        out = self.conv1(F.relu(self.bn1(x)))
        out = F.avg_pool2d(out, 2)
        return out


class DenseNet(nn.Module):
    def __init__(self, growthRate, nDenseBlocks, reduction, bottleneck, nClasses):
        super(DenseNet, self).__init__()

        # growthRate=32
        nChannels = 2 * growthRate
        self.conv1 = nn.Conv2d(3, nChannels, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(nChannels)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=0, ceil_mode=True)

        self.dense1 = self._make_dense(nChannels, growthRate, nDenseBlocks[0],bottleneck)
        nChannels += nDenseBlocks[0] * growthRate
        nOutChannels = int(math.floor(nChannels * reduction))
        self.trans1 = Transition(nChannels, nOutChannels)

        nChannels = nOutChannels
        self.dense2 = self._make_dense(nChannels, growthRate, nDenseBlocks[1],bottleneck)
        nChannels += nDenseBlocks[1] * growthRate
        nOutChannels = int(math.floor(nChannels * reduction))
        self.trans2 = Transition(nChannels, nOutChannels)

        nChannels = nOutChannels
        self.dense3 = self._make_dense(nChannels, growthRate, nDenseBlocks[2],bottleneck)
        nChannels += nDenseBlocks[2] * growthRate
        nOutChannels = int(math.floor(nChannels * reduction))
        self.trans3 = Transition(nChannels, nOutChannels)

        nChannels = nOutChannels
        self.dense4 = self._make_dense(nChannels, growthRate, nDenseBlocks[3],bottleneck)
        nChannels += nDenseBlocks[3] * growthRate


        self.bn2 = nn.BatchNorm2d(nChannels)
        self.fc = nn.Linear(nChannels, nClasses)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                m.bias.data.zero_()

    def _make_dense(self, nChannels, growthRate, nDenseBlocks, bottleneck):
        layers = []
        for i in range(int(nDenseBlocks)):
            if bottleneck:
                layers.append(Bottleneck(nChannels, growthRate))
            else:
                layers.append(SingleLayer(nChannels, growthRate))
            nChannels += growthRate
        return nn.Sequential(*layers)

    def forward(self, x):
        print(x.shape)
        out = self.conv1(x)
        print(out.shape)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.maxpool(out)
        print(out.shape)

        out = self.trans1(self.dense1(out))
        print(out.shape)
        out = self.trans2(self.dense2(out))
        print(out.shape)
        out = self.trans3(self.dense3(out))
        print(out.shape)
        out = self.dense4(out)
        print(out.shape)
        out = torch.squeeze(F.avg_pool2d(F.relu(self.bn2(out)), 7))
        print(out.shape)
        out = F.log_softmax(self.fc(out))
        return out

if __name__ == '__main__':
    DenseNet121= DenseNet(growthRate=32, nDenseBlocks=[6, 12, 24, 16], reduction=0.5, bottleneck=True,nClasses=1000)
    model = DenseNet121.train().cuda()
    summary(model,(3,224,224))

在这里插入图片描述
DenseNet的缺点:DneseNet在训练时十分消耗内存,这是由于算法实现不优带来的。当前的深度学习框架对 DenseNet 的密集连接没有很好的支持,所以只能借助于反复的拼接(Concatenation)操作,将之前层的输出与当前层的输出拼接在一起,然后传给下一层。对于大多数框架(如Torch和TensorFlow),每次拼接操作都会开辟新的内存来保存拼接后的特征。这样就导致一个 L 层的网络,要消耗相当于 L(L+1)/2 层网络的内存(第 l 层的输出在内存里被存了 (L-l+1) 份)

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值