Skip to content

稠密连接网络(DenseNet)

一、概述

ResNet 改变了我们对深层网络参数化的思路,而稠密连接网络(DenseNet)是 ResNet 的逻辑扩展,它在 ResNet 的基础上更进一步,让网络的每一层都能用到前面所有层的信息,就像一个知识的 "大集合",后面的层可以直接获取前面所有层学到的特征,这样能更好地利用网络里的信息,提升模型的性能。

二、从 ResNet 到 DenseNet

我们可以把 ResNet 和 DenseNet 的区别用一个简单的例子理解:

  • ResNet 就像是我们在写作业的时候,把新学到的知识点和原来的知识点 "融合" 在一起,也就是把新的特征和原来的特征相加,这样新的信息是原来的信息加上新的信息。 alt text

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

  • DenseNet 就像是我们在整理笔记的时候,把新的笔记和原来的所有笔记 "拼" 在一起,也就是把新的特征和原来的所有特征在通道维度上连接起来,这样后面的层可以直接用到前面所有层的特征,就像后面的层可以看到前面所有层的 "笔记"。

从数学上来说,ResNet 把函数展开成f(x) = x + g(x),也就是原来的信息加上新的非线性信息;而 DenseNet 把函数展开成x → [x, f1(x), f2([x,f1(x)]), f3([x,f1(x),f2([x,f1(x)])]),…],也就是把原来的信息和每一步新生成的信息都拼在一起,最后再把这些信息整合起来,这样能更充分地利用网络里的信息。

三、稠密块:DenseNet 的核心组件

alt text

稠密连接。

稠密块是 DenseNet 的核心,它就像是一个 "特征生产工厂",里面有多个卷积块,每个卷积块都会生成新的特征,而且每个卷积块的输入和输出会在通道维度上连接起来,这样后面的卷积块可以用到前面所有卷积块的特征。

1. 稠密块的结构

稠密块里用了改良版的 "批量规范化、激活、卷积" 的架构,也就是先做批量规范化,再用 ReLU 激活,最后做卷积操作。每个稠密块里的卷积块都用相同数量的输出通道,这个输出通道数也被称为 "增长率",它控制了每个卷积块能生成多少新的特征。

比如我们定义一个有 2 个卷积块、增长率为 10 的稠密块,如果输入是 3 通道的特征图,那么输出的通道数就是 3 + 2×10 = 23,也就是原来的 3 个通道加上 2 个卷积块生成的 20 个通道,这样后面的层就能用到原来的特征和新生成的所有特征。

2. 代码示例(PyTorch 版)

python
import torch
from torch import nn
from d2l import torch as d2l

# 定义卷积块,使用批量规范化、ReLU、卷积的顺序
def conv_block(input_channels, num_channels):
    return nn.Sequential(
        nn.BatchNorm2d(input_channels), nn.ReLU(),
        nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1))

# 定义稠密块
class DenseBlock(nn.Module):
    def __init__(self, num_convs, input_channels, num_channels):
        super(DenseBlock, self).__init__()
        layer = []
        for i in range(num_convs):
            # 每个卷积块的输入通道数是原来的通道数加上前面i个卷积块生成的通道数
            layer.append(conv_block(input_channels + i * num_channels, num_channels))
        self.net = nn.Sequential(*layer)

    def forward(self, X):
        for blk in self.net:
            Y = blk(X)
            # 把输入和输出在通道维度上连接起来
            X = torch.cat((X, Y), dim=1)
        return X

# 测试稠密块
blk = DenseBlock(2, 3, 10)
X = torch.randn(4, 3, 8, 8)
Y = blk(X)
print(Y.shape)  # 输出 torch.Size([4, 23, 8, 8]),3 + 2*10 = 23

四、过渡层:控制模型复杂度的 "压缩器"

每个稠密块都会增加通道数,如果我们用很多稠密块,通道数会变得非常多,模型会变得很复杂,就像我们的笔记越写越多,变得很乱,这时候我们需要一个 "整理" 的步骤,过渡层就是用来做这个的。

过渡层的作用有两个:

  1. 用 1×1 的卷积层减少通道数,就像把很多零散的笔记整理成几大类,减少笔记的数量。

  2. 用步幅为 2 的平均汇聚层把特征图的高度和宽度减半,进一步降低模型的复杂度。

1. 代码示例(PyTorch 版)

python
def transition_block(input_channels, num_channels):
    return nn.Sequential(
        nn.BatchNorm2d(input_channels), nn.ReLU(),
        nn.Conv2d(input_channels, num_channels, kernel_size=1),
        nn.AvgPool2d(kernel_size=2, stride=2))

# 测试过渡层
blk = transition_block(23, 10)
print(blk(Y).shape)  # 输出 torch.Size([4, 10, 4, 4]),通道数变成10,尺寸减半

五、DenseNet 模型:把组件拼起来

DenseNet 的模型结构和 ResNet 有点类似,就像用积木搭房子一样,我们把前面的稠密块和过渡层拼起来:

  1. 首先是一个卷积层和最大汇聚层,用来提取图像的初始特征,就像我们看一张图片的时候,先看整体的轮廓。

  2. 然后是 4 个稠密块,每个稠密块之间用过渡层连接,用来生成更多的特征,同时控制模型的复杂度。

  3. 最后是批量规范化、ReLU 激活、全局平均汇聚层和全连接层,用来把特征整合起来,输出分类结果。

1. 代码示例(PyTorch 版)

python
# 定义DenseNet模型
def densenet():
    # 初始的卷积层和最大汇聚层
    b1 = nn.Sequential(
        nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
        nn.BatchNorm2d(64), nn.ReLU(),
        nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
    
    # 定义稠密块和过渡层
    num_channels, growth_rate = 64, 32
    num_convs_in_dense_blocks = [4, 4, 4, 4]
    blks = []
    for i, num_convs in enumerate(num_convs_in_dense_blocks):
        blks.append(DenseBlock(num_convs, num_channels, growth_rate))
        # 更新通道数,加上当前稠密块生成的通道数
        num_channels += num_convs * growth_rate
        # 除了最后一个稠密块,后面都加过渡层,把通道数减半
        if i != len(num_convs_in_dense_blocks) - 1:
            blks.append(transition_block(num_channels, num_channels // 2))
            num_channels = num_channels // 2
    
    # 最后的全局汇聚层和全连接层
    net = nn.Sequential(
        b1, *blks,
        nn.BatchNorm2d(num_channels), nn.ReLU(),
        nn.AdaptiveAvgPool2d((1, 1)),
        nn.Flatten(),
        nn.Linear(num_channels, 10))
    return net

# 创建模型
net = densenet()

六、训练模型

因为 DenseNet 是比较深的网络,为了简化计算,我们把输入图像的尺寸从 224×224 调整为 96×96,这样训练的时候速度会更快。我们用 Fashion-MNIST 数据集来训练模型,学习率设为 0.1,训练 10 轮,批量大小为 256。

1. 训练代码

python
lr, num_epochs, batch_size = 0.1, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

2. 训练结果

训练完成后,大概能得到这样的结果:训练准确率在 0.95 左右,测试准确率在 0.88 左右,在 GPU 上训练的速度大概是每秒 5000 多个样本,说明模型的训练效果还是不错的。

七、小结

  1. DenseNet 和 ResNet 的最大区别是跨层连接的方式:ResNet 是把输入和输出相加,DenseNet 是把输入和输出在通道维度上连接,这样后面的层可以用到前面所有层的特征。

  2. DenseNet 的核心组件是稠密块和过渡层:稠密块用来生成新的特征,并且把新特征和原来的特征连接起来;过渡层用来控制模型的复杂度,减少通道数和特征图的尺寸。

  3. DenseNet 的模型结构和 ResNet 类似,但是通过稠密连接和过渡层的配合,能更好地利用网络里的信息,提升模型的性能。

  4. DenseNet 的优点是模型参数比 ResNet 小,因为它可以共享前面层的特征,不需要学习太多新的参数;但是它的缺点是内存或显存消耗比较多,因为要保存前面所有层的特征。

(注:文档部分内容可能由 AI 生成) 源地址

京ICP备2024093538号-1