PyTorch 入门与实践(六)卷积神经网络进阶(DenseNet)

来自 B 站刘二大人的《PyTorch深度学习实践》P11 的学习笔记

上一篇 卷积神经网络进阶(GoogLeNet、ResNet) 我们实践了 GoogleNet 和 ResNet 两大经典网络:

  • GoogleNet 从多路选择学习(Inception Module)出发,构建更好的神经网络;
  • ResNet 从残差连接(Residual Block)保留浅层特征出发,构建更深的神经网络。

DenseNet1 紧随其后,再次研究了超深度神经网络的梯度消失问题和跳连解决方案。他们提到,ResNet 等的方法尽管在神经网络拓扑和训练过程上有所不同,但它们都有一个关键特征:使用短路径连接前后两层。

DenseNet 结构

DenseNet 更加偏执,为了确保网络在深度传播中能保留更多信息,他们不仅连接前后两层,在一个 Dense Block 中每一层都和后面所有层相连,并且是将特征在通道上堆叠再传入下一层(像 Inception Module 的输出),而不是 ResNet 那样的元素相加

这当然能保留更多信息,不过这显存消耗可想而知,这就是为什么它叫 Dense,一个 L 层的 Dense Block 有 L ( L + 1 ) 2 \frac{L(L+1)}{2} 2L(L+1) 条连接。

0

一、Dense Block

每个 Dense Block 里的卷积层都是 3×3 卷积,下表第三列乘号后面的数表示一个 Dense Block 中有多少层这样的卷积。
00

  • Bottleneck Layers
    在每个 3×3 卷积之前引入 1×1 卷积作为瓶颈层,可以减少输入特征的通道数,从而提高计算效率。

所以,如果 Dense Block 里面是 1×1 卷积和 3×3 卷积的组合,那么这些 Dense Blocks 组成的 DenseNet 被称为 DenseNet-B。

二、Transition Layer

当然,为了控制显存占用,DenseNet 主要结构除了这 Dense Block 外还有每个 Block 后连接的过渡层(Transition Layer),它通过一个 1×1 的卷积层来控制通道数,并使用平均池化来减半特征图的高和宽。
1
所以,Dense Blocks 之间存在过渡层,那么这些 Dense Blocks 组成的 DenseNet 被称为 DenseNet-C。

两者都存在的 DenseNet,称为 DenseNet-BC,这是 DenseNet 的终极配置。。。

三、Growth Rate

增长率 k k k,它表示每一个卷积层输出的通道数。论文中说:设 k 0 k_0 k0 是初始输入的图像的通道数,那么 l l l 层的 Dense Net 就会产生 k 0 + k ( l − 1 ) k_0 + k(l-1) k0+k(l1) 个通道的特征图,这个 k k k 就被称为增长率。

这表示通道数“爆炸”速度,但是多搞一个新名字出来吓唬人,我们当然可以直接说这是每个卷积层输出的通道数: o u t _ c h a n n e l s out\_channels out_channels,但是在代码实现过程中你就能发现,这个 k k k 要作为系数用于计算 Dense Block 中每一层卷积的输入通道,所以把输出通道称为增长率,着实细节!

利用 Bottleneck layer,Translation layer 以及较小的 Growth rate 使得网络变窄,参数减少,有效抑制了过拟合,同时计算量也减少了2

DenseNet 最后的分类器使用全局平均池化接一层全连接层。

四、Implementation Details

对于图像较小的数据集,比如 CIFAR-10/100、SVHN,DenseNet 由 3 个 Dense Block 组成。

  • 在输入第一个 Dense Block 之前先经过一个 padding=1 的 3×3 卷积,输出 32 通道;
  • 使用 DenseNet-BC 配置,在每个 Dense Block 之间加入过渡层,使特征图通道数和宽高减半;
  • 在最后一个 Dense Block 的末尾,执行全局平均池化,然后连接 Softmax 分类器。

输入 3 个 Dense Block 中的特征图大小分别为:32×32、16×16、8×8。采用 { L = 40 , k = 12 } \{L = 40,k = 12\} {L=40,k=12} 的 DenseNet-BC 配置。

五、代码实现
  • conv_block 是 Dense Block 的基础结构,包含一层 3×3 卷积的 basic_block,和一层可选的 1×1 卷积的 bottleneck:

    关于 nn.Sequential 可以查阅官方文档,里面举的例子十分清晰易懂。

    from collections import OrderedDict
    import torch
    from torch import nn
    
    def conv_block(in_channels, out_channels, bo=True):
        """
        Dense Block 的基本组件, 一层 3×3 卷积和一层可选的 1×1 卷积
        :param in_channels:
        :param out_channels: growth rate k
        :param bo: 是否使用 bottleneck
        :return: 一个 conv_block
        """
        # 有无 bottleneck 会影响下面 3×3 卷积的输入通道数,所以要判断一下
        in_channels_ = out_channels * 2 if bo else in_channels
    
        bo_layers = nn.Sequential(OrderedDict([
            ('bn0', nn.BatchNorm2d(in_channels)),
            ('relu0', nn.ReLU()),
            ('conv1x1', nn.Conv2d(in_channels, in_channels_, kernel_size=1)),
        ]))
    
        basic_blk = nn.Sequential(OrderedDict([
            ('bn1', nn.BatchNorm2d(in_channels_)),
            ('relu1', nn.ReLU()),
            ('conv3x3', nn.Conv2d(in_channels_, out_channels, kernel_size=3, padding=1)),
        ]))
    
        # 如果不用 bottleneck 可以传入一个空的 Sequential
        bottleneck = bo_layers if bo else nn.Sequential()  
    
        blk = nn.Sequential()
        blk.add_module('bottleneck', bottleneck)
        blk.add_module('basic_blk', basic_blk)
        return blk
    
  • Dense Block

    当前 conv_block 的输入是前面所有 conv_block 的输出堆叠起来的,所以第 l l l 层的输入通道数为:
    k 0 + k × ( l − 1 ) k_0 + k \times (l - 1) k0+k×(l1)
    k 0 k_0 k0 是初始输入层的通道数 k k k 就是上面提到的 Growth Rate,也就是 conv_block 中每个卷积层的输出通道数 out_channels

    def dense_block(in_channels, conv_blk_num=4, k=12, bo=True):
        """
        dense_block 由上面多个 conv_block 组成,用 for 循环添加到 nn.Sequential() 中
        :param in_channels:
        :param conv_blk_num: 3×3卷积层个数
        :param k: = out_channels,每个3×3卷积层的输出通道数
        :param bo: 是否加入 bottleneck
        :return: 一个 dense_block
        """
        dense_block = nn.Sequential()
        for i in range(conv_blk_num):
            # 当前 conv_block 的输入是前面所有 conv_block 的输出堆叠起来的,所以输入通道数按照论文中这个公式变化
            in_channels_ = in_channels + i * k
            dense_block.add_module(f'conv_blk_{i}', conv_block(in_channels_, k, bo=bo))
        
        return dense_block
    
  • transition_layer 是 Dense Block 之间的过渡层,用 1×1 卷积减少 Dense Block 的输出通道数,否则会越叠越多,导致内存爆炸:

    def transition_layer(input, in_channels, out_channels):
        """
        过渡层,在 Dense Block 和 Dense Block 之间,把前一个的输出通道减半
        :param in_channels: 前一个的输出通道
        :param out_channels: 输出通道减半
        :return:
        """
        device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")  # 有 cuda 则使用 GPU
        blk = nn.Sequential(OrderedDict([
            ('bn', nn.BatchNorm2d(in_channels)),
            ('relu', nn.ReLU()),
            ('conv1x1', nn.Conv2d(in_channels, out_channels, kernel_size=1)),
            ('avgpool', nn.AvgPool2d(2))
        ]))
        blk.to(device)
        return blk(input)
    
  • 我们可以先试验一个多层 1×1 和 3×3 卷积组成的 Dense Block 的网络结构和输出特征图的形状:

    def forward(x, model, transition=True):
        for i, blk in enumerate(model):
            # 遍历 dense block 的每一层卷积,把它们的输出都堆叠起来
            print(i)
            y = blk(x)
            x = torch.cat((x, y), dim=1)
            print(x.shape)
    
        if transition:
            x = transition_layer(x, x.shape[1], x.shape[1]//2)
        print("x final size:", x.shape)
    
        return x
    
    if __name__ == '__main__':
        in_channels = 16
        input = torch.randn(1, in_channels, 28, 28)  # (mini-batch, channels, H, W)
        # print(input.shape)
    
        # bo=True 则加入 bottleneck,用1×1卷积减少3×3卷积的运算通道
        dense_block = dense_block(in_channels=in_channels, conv_blk_num=4, k=12, bo=True)
        print(dense_block)  # 打印网络结构
        # transition=True 则把 dense_block 的输出减半
        output_dense = forward(input, model=dense_block, transition=False)
    
  • DenseNet 的基础结构由 3 个 Dense Blocks 以及可选的 Dense Block 之间的过渡层(Transition layers)组成,这些可选组件都可以通过参数来设定,因为我们已经全部实现了:

    from collections import OrderedDict
    import torch
    from torch import nn
    from torch.nn import functional as F
    
    def transition_layer(input, in_channels, out_channels):
        """
        过渡层,在 Dense Block 和 Dense Block 之间,把前一个的输出通道和长宽减半
        :param in_channels: 前一个的输出通道
        :param out_channels: 输出通道减半
        :return:
        """
        device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")  # 有 cuda 则使用 GPU
        blk = nn.Sequential(OrderedDict([
            ('bn', nn.BatchNorm2d(in_channels)),
            ('relu', nn.ReLU()),
            ('conv1x1', nn.Conv2d(in_channels, out_channels, kernel_size=1)),
            ('avgpool', nn.AvgPool2d(2))
        ]))
        blk.to(device)
        return blk(input)
    
    
    def conv_block(in_channels, out_channels, bo=False):
        """
        Dense Block 的基本组件
        :param in_channels:
        :param out_channels: growth rate k
        :return:
        """
        in_channels_ = out_channels * 2 if bo else in_channels
    
        bo_layers = nn.Sequential(OrderedDict([
            ('bn0', nn.BatchNorm2d(in_channels)),
            ('relu0', nn.ReLU()),
            ('conv1x1', nn.Conv2d(in_channels, in_channels_, kernel_size=1)),
        ]))
    
        basic_blk = nn.Sequential(OrderedDict([
            ('bn1', nn.BatchNorm2d(in_channels_)),
            ('relu1', nn.ReLU()),
            ('conv3x3', nn.Conv2d(in_channels_, out_channels, kernel_size=3, padding=1)),
        ]))
    
        bottleneck = bo_layers if bo else nn.Sequential()
    
        blk = nn.Sequential()
        blk.add_module('bottleneck', bottleneck)
        blk.add_module('basic_blk', basic_blk)
        return blk
    
    
    class DenseBlock(nn.Module):
        """
        由 conv_blk 组成
        :param in_channels: 动态的,由上一个 Dense Block 和过渡层决定
        :param out_channels: growth rate k
        :param dense_blk_num: 由多少 conv_blk 组成一个 Dense Block
        """
        def __init__(self,
                     in_channels,
                     out_channels,
                     conv_blk_num=4,
                     bo=False,
                     transition=True):
            super(DenseBlock, self).__init__()
            self.transition = transition
            self.net = nn.Sequential()
            
            for i in range(conv_blk_num):
                # 要多少就加多少
                in_channels_ = in_channels + i * out_channels
                self.net.add_module(f'conv_blk_{i}',
                                    conv_block(in_channels_, out_channels, bo))
    
        def forward(self, x):
            for blk in self.net:
                y = blk(x)
                x = torch.cat((x, y), dim=1)
            if self.transition:
                out_channels = x.shape[1]
                x = transition_layer(x, out_channels, out_channels // 2)
            print(x.shape)
            return x
    
    
    class DenceNet(nn.Module):
        """
        由 Dense Block 组成
        :param in_channels: 3-cifar-10, 1-mnist
        :param out_channels: growth rate k
        :param dense_blk_num: 由多少 Dense Block 组成
        """
        def __init__(self,
                     in_channels=16,
                     out_channels=12,
                     conv_blk_num=4,
                     dense_blk_num=3,
                     bo=False,
                     transition=True):
            super(DenceNet, self).__init__()
            self.bn0 = nn.BatchNorm2d(3)
            self.conv0 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
    
            self.dense_net = nn.Sequential()
            in_channels_ = in_channels
            for i in range(dense_blk_num):
                # 计算通道数,有点绕,想搞清楚可以手动遍历
                in_channels_ += conv_blk_num * out_channels if i > 0 else 0
                in_channels_ = in_channels_ // 2 if i > 0 else in_channels_
                print("in_channels_:", in_channels_)
    
                if transition:
                    self.is_transition = dense_blk_num - 1 - i  # 最后一个 dense Block 不用接过渡层
    
                self.dense_net.add_module(f"dense_blk_{i}",
                                          DenseBlock(in_channels_,
                                                     out_channels,
                                                     conv_blk_num=conv_blk_num,
                                                     bo=bo,
                                                     transition=self.is_transition))
            self.aap = nn.AdaptiveAvgPool2d(10)  # 全局平均池化,输出 10×10
            self.fc = nn.Linear(8800, 10)
    
        def forward(self, x):
            batch_size = x.size(0)
    
            x = self.conv0(F.relu(self.bn0(x)))  # N,16,,
            x = self.dense_net(x)
            x = self.aap(x)
            x = x.view(batch_size, -1)  # batch×通道数×10×10=8800
            x = F.softmax(self.fc(x))
            return x
    
    if __name__ == '__main__':
        # 模拟数据输入网络
        in_channels, k = 16, 12
        model = DenceNet(in_channels=in_channels,
                         out_channels=k,
                         conv_blk_num=4,  # 每个Dense Block中的3×3卷积个数
                         dense_blk_num=3,  # 每个DenseNet包含的Dense Block个数
                         bo=False,
                         transition=True
                         )
        print(model)
        input = torch.randn(1, 3, 32, 32)
        output = model(input)
        print("final x shape:", output.shape)
    

Tips:在不知道全连接层接收的参数到底为多少的情况下(比如,这里的8800),我们可以注释掉 self.fc() 的调用,然后构造和训练数据集一个 mini-batch 相同大小的数据作为 input,得到的输出很轻易能告诉你答案。

六、实验结果

完整代码参见:dense_net.py
DenseNet 参数量不大,相同深度比 ResNet 的参数还少,这是参数共享的好处,但不断堆叠的特征导致了内存消耗很大,训练要很久。

下面是我用 Tesla T4 GPU 在 MNIST 数据集上训练 10 个 epochs 的实验结果,训练精度 0.96,后面的在测试集上的测试精度也是 0.96,至少说明没有过拟合,盲猜再多训练 10 个 epochs 能达到最佳。

  • 训练精度和 loss 曲线
    0
    1

  • 预测结果
    2


  1. Huang, Gao, et aI. “Densely Connected Convolutional Networks.” IEEE Conference on Computer Vision Pattern Recognition (2017): 2261-69. ↩︎

  2. DenseNet算法详解 ↩︎

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
下面是一个基于Keras的一维DenseNet网络模型,其中包含了防止梯度消失和梯度爆炸的方法,如Batch Normalization和梯度裁剪: ```python from keras.models import Model from keras.layers import Input, Dense, Conv1D, Dropout, concatenate, GlobalAveragePooling1D, BatchNormalization from keras.optimizers import Adam from keras.utils import plot_model from keras import backend as K def dense_block(x, layers, growth_rate): for i in range(layers): x1 = BatchNormalization()(x) x1 = Conv1D(growth_rate, kernel_size=3, padding='same', activation='relu')(x1) x1 = Dropout(0.5)(x1) x = concatenate([x, x1]) return x def transition_block(x, reduction): x = BatchNormalization()(x) x = Conv1D(int(K.int_shape(x)[2] * reduction), kernel_size=1, padding='same', activation='relu')(x) x = Dropout(0.5)(x) x = GlobalAveragePooling1D()(x) return x def DenseNet(input_shape, dense_layers, dense_blocks, growth_rate, reduction): input_layer = Input(shape=input_shape) x = Conv1D(64, kernel_size=3, padding='same', activation='relu')(input_layer) for i in range(dense_blocks): x = dense_block(x, dense_layers, growth_rate) if i < dense_blocks - 1: x = transition_block(x, reduction) output_layer = Dense(1, activation='sigmoid')(x) model = Model(inputs=input_layer, outputs=output_layer) return model model = DenseNet(input_shape=(100,1), dense_layers=8, dense_blocks=4, growth_rate=12, reduction=0.5) opt = Adam(lr=0.001, clipvalue=1.0) model.compile(loss='binary_crossentropy', optimizer=opt, metrics=['accuracy']) print(model.summary()) plot_model(model, to_file='DenseNet.png', show_shapes=True) ``` 上述代码中,dense_block函数定义了一个稠密块,其中包含了Batch Normalization、Conv1D和Dropout层,用于增加网络的深度和宽度。transition_block函数则定义了一个过渡块,用于减小特征图的深度和宽度。最后,DenseNet函数整合了所有块,构建了一个完整的一维DenseNet网络模型。在编译模型时,Adam优化器中设置了梯度裁剪clipvalue参数,用于防止梯度爆炸。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Skr.B

WUHOOO~

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

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

打赏作者

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

抵扣说明:

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

余额充值