深度学习

卷积神经网络13-稠密连接网络(DenseNet):从ResNe

2025-10-28  本文已影响0人  R7_Perfect

在深度学习的发展过程中,各种网络结构不断被提出并改进。在这一篇博客中,我们将介绍一种非常有趣且高效的网络结构——稠密连接网络(DenseNet)。它是ResNet的改进版,采用了不同的连接方式,使得深层网络的训练更加高效,并且能够显著减少参数量。
通过本篇博客,你将理解DenseNet的核心概念、如何构建DenseNet,以及它的优点,尤其是为什么它可以通过“稠密连接”提高网络的表现。

一、从ResNet到DenseNet

1.1 ResNet的基本思想

首先,回顾一下 ResNet(残差网络) 的基本思想:在传统的深层网络中,由于网络层数很深,训练时容易出现梯度消失的问题,导致训练困难。为了应对这一问题,ResNet提出了一种叫做“残差连接”的方法。具体来说,ResNet在每两层之间加入了“跳跃连接”——直接将输入数据加到输出上。这样,网络学习的是“残差”,而非直接拟合输出。

1.2 DenseNet的改进

在ResNet的基础上,DenseNet 进一步改进了连接方式。与ResNet将输入和输出相加不同,DenseNet通过将每一层的输出和前面所有层的输出进行连接(而不是相加)。这种方式使得网络能够更加充分地利用每一层的特征,避免了信息的丢失。
具体来说,DenseNet将网络的每一层的输出都与前面所有层的输出相连接,从而使得每一层都能直接访问前面所有层的特征。这种设计大大增强了信息流动,并且有效缓解了梯度消失问题。


image.png

二、稠密连接网络的构成

DenseNet的核心构成主要有两部分:稠密块(Dense Block) 和 过渡层(Transition Layer)。

2.1 稠密块(Dense Block)

一个稠密块由多个卷积块(Convolutional Blocks)组成。每个卷积块都有批量归一化(Batch Normalization)、ReLU激活函数、以及卷积层。关键点是,在每一个卷积块的输入和输出之间,都会在通道维度上进行连接。这意味着,随着每一层的加入,网络的特征图通道数会逐渐增多。
假设我们有一个输入图像通道数为3的特征图,并且每个卷积块输出10个通道。经过一个稠密块之后,输出的通道数是所有卷积块输出通道的累加。例如,如果有两个卷积块,每个卷积块输出10个通道,那么输出通道数将是 3+10+10=23。

import torch
from torch import nn


def conv_block(input_channels, num_channels):
    # 定义卷积块:每个卷积块包含批量归一化、ReLU激活函数和卷积操作
    return nn.Sequential(
        nn.BatchNorm2d(input_channels),
        nn.ReLU(),
        nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1)
    )


class DenseBlock(nn.Module):
    # 定义稠密块(DenseBlock):由多个卷积块组成
    def __init__(self, num_convs, input_channels, num_channels):
        super().__init__()
        layer = []
        # 创建多个卷积块,并按顺序连接
        for i in range(num_convs):
            # 每个卷积块的输入通道数等于输入通道数 + 每个卷积块输出通道数的累计和
            layer.append(conv_block(input_channels + num_channels * i, 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)  # 在通道维度上连接输入X和输出Y,增加特征的通道数
        return X

2.2 过渡层(Transition Layer)

过渡层是DenseNet中的一个特殊组件,它的作用是减少模型的复杂度。因为每个稠密块的输出会显著增加通道数,过多的通道会导致网络过于庞大,难以训练。过渡层通过卷积层减少通道数,并且使用平均池化(Average Pooling)降低图像的高和宽。

def transition_block(input_channels, num_channels):
    # 定义过渡层(TransitionBlock):用于减少特征图的通道数,并通过池化层减少空间维度(高、宽)
    return nn.Sequential(
        nn.BatchNorm2d(input_channels),
        nn.ReLU(),
        nn.Conv2d(input_channels, num_channels, kernel_size=1),  # 1x1卷积层,减少通道数
        nn.AvgPool2d(kernel_size=2, stride=2)  # 使用2x2的平均池化,步幅为2,减半高和宽
    )

通过过渡层的控制,DenseNet能够在保持高效性的同时,避免模型的过度复杂化。

三、构建DenseNet模型

DenseNet的整体架构由多个稠密块和过渡层组成,类似于ResNet的构建方式。通常,DenseNet模型从一个初始卷积层开始,然后依次堆叠多个稠密块,每个稠密块后接一个过渡层。

3.1 基本结构

假设我们要构建一个DenseNet模型,输入是一个灰度图像(1通道),并且通过4个稠密块进行处理。每个稠密块包含4个卷积层,每个卷积层的增长率(growth rate)为32。通过这种方式,我们每个稠密块会增加128个通道。

# 定义初始卷积层
# 输入通道数为1(灰度图像),输出分类为10个类别(例如Fashion-MNIST数据集)
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  # 初始通道数为64,增长率为32
num_convs_in_dense_blocks = [4, 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)),  # 自适应平均池化,将输出大小调整为1x1
    nn.Flatten(),
    nn.Linear(num_channels, 10)
)

3.2 输出结果

DenseNet模型通过多个稠密块和过渡层的堆叠,使得网络能够学习到更多层次的特征。最终,经过全局平均池化和全连接层后,输出最终的分类结果。


image.png
Sequential(
  (0): Sequential(
    (0): Conv2d(1, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3))
    (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU()
    (3): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  )
  (1): DenseBlock(
    (net): Sequential(
      (0): Sequential(
        (0): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(64, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (1): Sequential(
        (0): BatchNorm2d(96, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(96, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (2): Sequential(
        (0): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (3): Sequential(
        (0): BatchNorm2d(160, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(160, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
    )
  )
  (2): Sequential(
    (0): BatchNorm2d(192, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (1): ReLU()
    (2): Conv2d(192, 96, kernel_size=(1, 1), stride=(1, 1))
    (3): AvgPool2d(kernel_size=2, stride=2, padding=0)
  )
  (3): DenseBlock(
    (net): Sequential(
      (0): Sequential(
        (0): BatchNorm2d(96, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(96, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (1): Sequential(
        (0): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(128, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (2): Sequential(
        (0): BatchNorm2d(160, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(160, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (3): Sequential(
        (0): BatchNorm2d(192, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(192, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
    )
  )
  (4): Sequential(
    (0): BatchNorm2d(224, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (1): ReLU()
    (2): Conv2d(224, 112, kernel_size=(1, 1), stride=(1, 1))
    (3): AvgPool2d(kernel_size=2, stride=2, padding=0)
  )
  (5): DenseBlock(
    (net): Sequential(
      (0): Sequential(
        (0): BatchNorm2d(112, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(112, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (1): Sequential(
        (0): BatchNorm2d(144, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(144, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (2): Sequential(
        (0): BatchNorm2d(176, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(176, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (3): Sequential(
        (0): BatchNorm2d(208, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(208, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
    )
  )
  (6): Sequential(
    (0): BatchNorm2d(240, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (1): ReLU()
    (2): Conv2d(240, 120, kernel_size=(1, 1), stride=(1, 1))
    (3): AvgPool2d(kernel_size=2, stride=2, padding=0)
  )
  (7): DenseBlock(
    (net): Sequential(
      (0): Sequential(
        (0): BatchNorm2d(120, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(120, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (1): Sequential(
        (0): BatchNorm2d(152, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(152, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (2): Sequential(
        (0): BatchNorm2d(184, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(184, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (3): Sequential(
        (0): BatchNorm2d(216, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
        (1): ReLU()
        (2): Conv2d(216, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
    )
  )
  (8): BatchNorm2d(248, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (9): ReLU()
  (10): AdaptiveAvgPool2d(output_size=(1, 1))
  (11): Flatten(start_dim=1, end_dim=-1)
  (12): Linear(in_features=248, out_features=10, bias=True)
)

四、训练DenseNet模型

DenseNet模型的训练过程与其他深度学习模型类似。为了提高计算效率,可以使用GPU进行训练。以下是训练模型的基本步骤:

import d2l

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())
image.png image.png

五、小结

通过本文的学习,我们了解了稠密连接网络(DenseNet)的核心原理和实现方式。DenseNet相比于ResNet,采用了更加高效的稠密连接方式,能够显著提高信息流动和梯度的传播,减少梯度消失问题,同时有效减少网络参数。
DenseNet的主要构成是稠密块和过渡层,其中稠密块负责在网络中进行特征连接,而过渡层则控制模型的复杂度,使得网络更加高效。通过这种设计,DenseNet在多个任务上都表现出了优异的性能。
希望这篇文章能够帮助你理解DenseNet,并为你进一步探索深度学习提供一些启发!

上一篇 下一篇

猜你喜欢

热点阅读