【卷积神经网络系列】九、DenseNet


参考资料

论文:

Densely Connected Convolutional Networks

Memory-Efficient Implementation of DenseNets

博客:

DenseNet:比ResNet更优的CNN模型

深入解析DenseNet(含大量可视化及计算)

深入解读DenseNet(附源码)

深度学习模型之——DenseNet算法详解及优点分析

如何评价Densely Connected Convolutional Networks?


一、简介

 在DenseNet出现之前,CNN的进化一般通过层数的加深(ResNet)或者加宽(Inception)的思想进行:

  • ResNet模型的核心是通过建立前面层与后面层之间的“短路连接”(shortcuts,skip connection),这有助于训练过程中梯度的反向传播,从而能训练出更深的CNN网络。
  • 而DenseNet的基本思路与ResNet一致,但是它建立的是前面所有层与后面层的密集连接(dense connection),它的名称也是由此而来。
  • DenseNet的另一大特色是通过特征在channel上的连接来实现特征重用(feature reuse),不但减缓了梯度消失的现象参数量也更少。

 这些特点让DenseNet在参数和计算成本更少的情形下实现比ResNet更优的性能。

在这里插入图片描述


二、前言

(1)标准神经网络

在这里插入图片描述

 上图是标准神经网络的一个图,输入和输出的公式是:
X l = H l ( X L − 1 ) X_l=H_l(X_L−1) Xl=Hl(XL1)
 其中 H l H_l Hl是一个组合函数,通常包括BN,ReLU,Pooling,Conv操作。

(2)ResNet

在这里插入图片描述

 上图则是ResNet的示意图,ResNet是每个层与前面的某层(一般是2~3层)短路连接在一起,连接方式是通过元素级相加,输入和输出的公式是:
X l = H l ( X L − 1 ) + X L − 1 X_l=H_l(X_L−1)+X_L−1 Xl=Hl(XL1)+XL1
(3)DenseNet

在这里插入图片描述

 在DenseNet中,每个层都会与前面所有层在channel维度上连接concat在一起(这里各个层的特征图大小是相同的),并作为下一层的输入,输入和输出的公式是:
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,...,Xl1])


三、网络结构

 CNN网络一般要经过Pooling或者stride>1的Conv来降低特征图的大小,而DenseNet的密集连接方式需要特征图大小保持一致。为了解决这个问题,DenseNet网络中使用DenseBlock+Transition的结构,其中:

  • DenseBlock是包含很多层的模块,每个层的特征图大小相同,层与层之间采用密集连接方式
  • Transition模块是连接两个相邻的DenseBlock,并且通过Pooling使特征图大小降低

 下图给出了DenseNet的网络结构,它共包含3个DenseBlock,各个DenseBlock之间通过Transition连接。

在这里插入图片描述


3.1 Dense Block

 在DenseBlock中,各个层的特征图大小一致,可以在Channel维度上连接。DenseBlock中的非线性组合函数 H ( ⋅ ) H(⋅) H()采用的是BN+ReLU+3x3 Conv的结构,如下图所示。

在这里插入图片描述

 另外值得注意的一点是,与ResNet不同,所有DenseBlock中各个层卷积之后均输出 K K K个特征图,即得到的特征图的Channel数为 K K K,或者说采用 K K K个卷积核。 K K K在DenseNet称为growth rate,这是一个超参数。一般情况下使用较小的 K K K(比如12),就可以得到较佳的性能。

上面所述的意思是:Dense Block内部不管每一层的输入是多少,输出的Channel都是K,因为每一层的卷积核的数量为K。

假定输入层的特征图的Channel数为 K 0 K_0 K0,那么 L L L层输入的Channel数为 K 0 + K ( L − 1 ) K_0+K(L−1) K0+K(L1),因此随着层数增加,尽管 K K K设定得较小,Dense Block的输入会非常多,不过这是由于特征重用所造成的,每个层仅有 K K K个特征是自己独有的。

在这里插入图片描述


3.2 DenseNet-B

 由于后面层的输入会非常大,DenseBlock内部可以采用Bottleneck层来减少计算量,主要是原有的结构中增加1x1 Conv,如图所示,即BN+ReLU+1x1 Conv+BN+ReLU+3x3 Conv,称为DenseNet-B结构。其中1x1 Conv得到4k个特征图它起到的作用是降低特征数量,从而提升计算效率。

在这里插入图片描述


3.3 DenseNet-C

Transition层包括一个1x1的卷积和2x2的AvgPooling,结构为BN+ReLU+1x1 Conv+2x2 AvgPooling

  • 对于Transition层,它主要是连接两个相邻的DenseBlock,并且降低特征图大小
  • 另外,Transition层可以起到压缩模型的作用。

在这里插入图片描述

 假定Transition的上接DenseBlock得到的特征图Channels数为 m m m ,Transition层可以产生 [ θ m ] [θm] [θm] 个特征(通过卷积层),其中 θ ∈ ( 0 , 1 ] θ∈(0,1] θ(0,1] 是压缩系数(compression rate)。

  • θ = 1 θ=1 θ=1 时,特征个数经过Transition层没有变化,即无压缩;
  • θ < 1 θ<1 θ<1 时,这种结构称为DenseNet-C,文中使用 θ = 0.5 θ=0.5 θ=0.5

 对于使用Bottleneck层的DenseBlock结构和压缩系数小于1的Transition组合结构称为DenseNet-BC


3.4 整个网络结构计算图

在这里插入图片描述

 对于ImageNet数据集,图片输入大小为 224×224x3 ,网络结构采用包含4个DenseBlock的DenseNet-BC,其首先是一个stride=2的7x7卷积层(卷积核数为 2k ),然后是一个stride=2的3x3 MaxPooling层,后面才进入DenseBlock。ImageNet数据集所采用的网络配置如表1所示:

在这里插入图片描述


四、DenseNet的优势

 综合来看,DenseNet的优势主要体现在以下几个方面:

  • 由于密集连接方式,DenseNet提升了梯度的反向传播,使得网络更容易训练。由于每层可以直达最后的误差信号,实现了隐式的“deep supervision”;
  • 参数更小且计算更高效,这有点违反直觉,由于DenseNet是通过concat特征来实现短路连接,实现了特征重用,并且采用较小的growth rate,每个层所独有的特征图是比较小的;
  • 抗过拟合。神经网络每一层提取的特征都相当于对输入数据的一个非线性变换,而随着深度的增加,变换的复杂度也逐渐增加(更多非线性函数的复合)。相比于一般神经网络的分类器直接依赖于网络最后一层(复杂度最高)的特征,DenseNet 可以综合利用浅层复杂度低的特征,因而更容易得到一个光滑的具有更好泛化性能的决策函数。

(1)更强的梯度流动

 DenseNet可以说是一种隐式的强监督模式,因为每一层都建立起了与前面层的连接,误差信号可以很容易地传播到较早的层,所以较早的层可以从最终分类层获得直接监管。

在这里插入图片描述

(2)参数更少计算效率更高

 在ResNet中,参数量与 C × C C\times C C×C 成正比,而在DenseNet中参数量与 l × k × k l \times k \times k l×k×k 成正比,因为 k k k 远小于 C C C,所以DenseNet的参数量小得多。

在这里插入图片描述

(3)保存了低维度的特征

 在标准的卷积网络中,最终输出只会利用提取最高层次的特征。而在DenseNet中,它使用了不同层次的特征,它倾向于给出更平滑的决策边界。这也解释了为什么训练数据不足时DenseNet表现依旧良好。

在这里插入图片描述
在这里插入图片描述

五、相关问题

5.1 密集连接是否会带来冗余

实际上 DenseNet 比其他网络效率更高,其关键就在于网络每层计算量的减少以及特征的重复利用。DenseNet 的每一层只需学习很少的特征,使得参数量和计算量显著减少

在这里插入图片描述

 从图中可以观察到网络中比较靠后的层确实也会用到非常浅层的特征。

5.2 DenseNet的显存占用

 DenseNet的参数量和FLOPs确实不高,但是参考陈云:科普帖:深度学习中GPU和显存分析,显存大致由参数和输入、梯度等占位符组成,而DenseNet这部分确实是很高的,比如输入在一个block里所有的BN层都会重复备份一份。论文的附属论文Memory-Efficient Implementation of DenseNets是这么描述的:

Most naive implementations of DenseNets do however also have a quadratic memory dependency with respect to feature maps. This growth is responsible for the vast majority of the memory consumption, and as we argue in this report, it is implementation issue and not an inherent aspect of the DenseNet architecture.

然而,大多数原始的DenseNets实现对于特征映射也有二次内存依赖性。这种增长导致了绝大多数内存消耗,正如我们在本报告中所说,这是实现问题,而不是DenseNet体系结构的固有方面。

 附属论文解决了这个问题,所以这个问题现在是不太存在的。如何评价Densely Connected Convolutional Networks?高票回答很好的解决了这个问题。主要使用了共享内存分配减少了BN的中间层使用的显存。

在这里插入图片描述


六、论文复现

参考:

PyTorch实现DenseNet亲身实践

(1)定义带BN的卷积:卷积+BN+ReLu

class ConvBNReLU(nn.Module):
    def __init__(self, in_channels, out_channels, **kwargs):
        super(ConvBNReLU, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
        self.bn = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU6(inplace=True)

    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        x = self.relu(x)
        return x

(2)Dense Block:

  • grow rate,也就是K
  • 卷积层数量L
  • 输入特征层数in_channels
class DenseBlock(nn.Module):

    def __init__(self, input_channels, num_layers, growth_rate):
        """
        Args:
            input_channels:     输入通道数,也就是初始值K0
            num_layers:         DenseBlock中的卷积层数L
            growth_rate:        超参数K
        """
        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(
                # 1x1卷积核:输入为K0+K(L-1),输出为4*K(降维)
                ConvBNReLU(in_channels=self.k0+i*self.k, out_channels=4*self.k,
                           kernel_size=1, stride=1, padding=0),
                # 3x3卷积核:输入为4*K,输出为K
                ConvBNReLU(in_channels=4*self.k, out_channels=self.k,
                           kernel_size=3, stride=1, padding=1)
            ))
        return nn.Sequential(*layer_list)

    def forward(self, x):
        # 取出第一层进行前馈运算
        feature = self.layers[0](x)
        # 拼接
        out = torch.cat((x, feature), 1)
        
        """
        注意看循环:上一层的out变量作为下一层的输入,然后这一层的out在上一层的基础上concat当前层的输出,
        就实现了密集链接——比如,第1层卷积输出的特征是f1输入是x,那第1层的out包括[x, f1];
        然后将第1层的out作为第2层输入得到f2,第2层的out再concat就变成了[x, f1, f2]。
        """
        for i in range(1, len(self.layers)):
            feature = self.layers[i](out)
            out = torch.cat((feature, out), 1)
            
        return out

(3)网络搭建

class DenseNet(nn.Module):

    def __init__(self, layers, k, theta, num_classes):
        """
        Args:
            layers:         DenseBlock中的卷积层数L
            k:              超参数K
            theta:          Trandition的缩放系数[0,1]
            num_classes:    最终分类数
        """
        super(DenseNet, self).__init__()
        # params
        self.layers = layers
        self.k = k
        self.theta = theta
        
        # layers
        self.conv = ConvBNReLU(in_channels=3, out_channels=2*self.k,    # 刚开始的7x7卷积核
                                kernel_size=7, stride=2, padding=3)     # 将通道数变为2*self.k
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 最大池化3x3
        
        self.blocks, patches = self.__make_blocks(2*self.k)             # 构造DenseNet-BC
        
        self.avgpool = nn.AvgPool2d(kernel_size=7)  # 最后的GAP
        self.fc = nn.Linear(patches, num_classes)   # 全连接层

    # Transition(1x1->2x2avgpool)
    def __make_transition(self, in_channels):
        # 输出=输入*缩放系数
        out_channels = int(self.theta*in_channels)
        return nn.Sequential(
            ConvBNReLU(in_channels=in_channels, out_channels=out_channels,
                        kernel_size=1, stride=1, padding=0),
            nn.AvgPool2d(kernel_size=2)
        ), out_channels

    # 传入参数为网络初始的K0
    def __make_blocks(self, k0):
        """
            make DenseNet-BC
        """
        layers_list = []
        patches = 0     # 最终输出的channels
        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
            
            # 如果不是最后一层,因为最后一层没有Transition
            if i != len(self.layers)-1:
                # 不断通过Transition获得最新的输出channels,然后更新K0
                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 = self.maxpool(out)

        # 主干网络DenseNet-BC
        out = self.blocks(out)

        # 输出
        out = self.avgpool(out)
        out = out.view(out.size(0), -1)
        out = self.fc(out)
        
        return out

(4)测试

在这里插入图片描述

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()
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    net.to(device)
    summary(net, (3, 224, 224))
    # x = torch.randn((2, 3, 224, 224)).to(device)
    # y = net(x)
    # print(y.shape)

test()

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

travellerss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值