《动手学深度学习》
Table Of Contents
《动手学深度学习》
Table Of Contents

稠密连接网络(DenseNet)

ResNet 中的跨层连接设计引申出了数个后续工作。这一节我们介绍其中的一个:稠密连接网络(DenseNet) [1]。 它与 ResNet 的主要区别如图 5.10 所示。

ResNet(左)与DenseNet(右)在跨层连接上的主要区别:使用相加和使用连结。

ResNet(左)与DenseNet(右)在跨层连接上的主要区别:使用相加和使用连结。

图 5.10 中将部分前后相邻的运算抽象为模块 \(A\) 和模块 \(B\)。与 ResNet 的主要区别在于,DenseNet 里模块 \(B\) 的输出不是像 ResNet 那样和模块 \(A\) 的输出相加,而是在通道维上连结。这样模块 \(A\) 的输出可以直接传入模块 \(B\) 后面的层。在这个设计里,模块 \(A\) 直接跟模块 \(B\) 后面的所有层连接在了一起。这也是它被称为“稠密连接”的原因。

DenseNet 的主要构建模块是稠密块(dense block)和过渡层(transition layer)。前者定义了输入和输出是如何连结的,后者则用来控制通道数,使之不过大。

稠密块

DenseNet 使用了 ResNet 改良版的“批量归一化、激活和卷积”结构(参见上一节练习),我们首先在conv_block函数里实现这个结构。

In [1]:
import gluonbook as gb
from mxnet import gluon, init, nd
from mxnet.gluon import nn

def conv_block(num_channels):
    blk = nn.Sequential()
    blk.add(nn.BatchNorm(), nn.Activation('relu'),
            nn.Conv2D(num_channels, kernel_size=3, padding=1))
    return blk

稠密块由多个conv_block组成,每块使用相同的输出通道数。但在前向计算时,我们将每块的输入和输出在通道维上连结。

In [2]:
class DenseBlock(nn.Block):
    def __init__(self, num_convs, num_channels, **kwargs):
        super(DenseBlock, self).__init__(**kwargs)
        self.net = nn.Sequential()
        for _ in range(num_convs):
            self.net.add(conv_block(num_channels))

    def forward(self, X):
        for blk in self.net:
            Y = blk(X)
            X = nd.concat(X, Y, dim=1)  # 在通道维上将输入和输出连结。
        return X

在下面的例子中,我们定义一个有两个输出通道数为 10 的卷积块。使用通道数为 3 的输入时,我们会得到通道数为 \(3+2\times 10=23\) 的输出。卷积块的通道数控制了输出通道数相对于输入通道数的增长,因此也被称为增长率(growth rate)。

In [3]:
blk = DenseBlock(2, 10)
blk.initialize()
X = nd.random.uniform(shape=(4, 3, 8, 8))
Y = blk(X)
Y.shape
Out[3]:
(4, 23, 8, 8)

过渡层

由于每个稠密块都会带来通道数的增加,使用过多则会带来过于复杂的模型。过渡层则用来控制模型复杂度。它通过 \(1\times1\) 卷积层来减小通道数,并使用步幅为 2 的平均池化层减半高和宽,从而进一步降低模型复杂度。

In [4]:
def transition_block(num_channels):
    blk = nn.Sequential()
    blk.add(nn.BatchNorm(), nn.Activation('relu'),
            nn.Conv2D(num_channels, kernel_size=1),
            nn.AvgPool2D(pool_size=2, strides=2))
    return blk

对上一个例子中稠密块的输出应用通道数为 10 的过渡层。此时输出的通道数减为 10,高和宽减半。

In [5]:
blk = transition_block(10)
blk.initialize()
blk(Y).shape
Out[5]:
(4, 10, 4, 4)

DenseNet 模型

我们来构造 DenseNet 模型。DenseNet 首先使用跟 ResNet 一样的单卷积层和最大池化层。

In [6]:
net = nn.Sequential()
net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3),
        nn.BatchNorm(), nn.Activation('relu'),
        nn.MaxPool2D(pool_size=3, strides=2, padding=1))

类似于 ResNet 接下来使用的四个残差块,DenseNet 使用的是四个稠密块。同 ResNet 一样,我们可以设置每个稠密块使用多少个卷积层。这里我们设成 4,跟上一节的 ResNet-18 保持一致。稠密块里的卷积层通道数(即增长率)设为 32,所以每个稠密块将增加 128 个通道。

ResNet 里通过步幅为 2 的残差块在每个模块之间减小高和宽。这里我们则使用过渡层来减半高和宽,并减半通道数。

In [7]:
num_channels, growth_rate = 64, 32  # num_channels:当前的通道数。
num_convs_in_dense_blocks = [4, 4, 4, 4]

for i, num_convs in enumerate(num_convs_in_dense_blocks):
    net.add(DenseBlock(num_convs, growth_rate))
    # 上一个稠密的输出通道数。
    num_channels += num_convs * growth_rate
    # 在稠密块之间加入通道数减半的过渡层。
    if i != len(num_convs_in_dense_blocks) - 1:
        net.add(transition_block(num_channels // 2))

同 ResNet 一样,最后接上全局池化层和全连接层来输出。

In [8]:
net.add(nn.BatchNorm(), nn.Activation('relu'), nn.GlobalAvgPool2D(),
        nn.Dense(10))

获取数据并训练

由于这里我们使用了比较深的网络,本节里我们将输入高和宽从 224 降到 96 来简化计算。

In [9]:
lr, num_epochs, batch_size, ctx = 0.1, 5, 256, gb.try_gpu()
net.initialize(ctx=ctx, init=init.Xavier())
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
train_iter, test_iter = gb.load_data_fashion_mnist(batch_size, resize=96)
gb.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, num_epochs)
training on gpu(0)
epoch 1, loss 0.5326, train acc 0.811, test acc 0.853, time 57.1 sec
epoch 2, loss 0.3135, train acc 0.885, test acc 0.888, time 52.3 sec
epoch 3, loss 0.2615, train acc 0.904, test acc 0.865, time 52.3 sec
epoch 4, loss 0.2292, train acc 0.917, test acc 0.902, time 52.4 sec
epoch 5, loss 0.2058, train acc 0.926, test acc 0.908, time 52.3 sec

小结

  • 在跨层连接上,不同于 ResNet 中将输入与输出相加,DenseNet 在通道维上连结输入与输出。
  • DenseNet 的主要构建模块是稠密块和过渡层。

练习

  • DenseNet 论文中提到的一个优点是模型参数比 ResNet 的更小,这是为什么?
  • DenseNet 被人诟病的一个问题是内存消耗过多。真的会这样吗?可以把输入形状换成 \(224\times 224\),来看看实际(GPU)内存消耗。
  • 实现 DenseNet 论文中的表 1 提出的各个 DenseNet 版本 [1]。

扫码直达讨论区

image0

参考文献

[1] Huang, G., Liu, Z., Weinberger, K. Q., & van der Maaten, L. (2017). Densely connected convolutional networks. In Proceedings of the IEEE conference on computer vision and pattern recognition (Vol. 1, No. 2).