卷积神经网络13-稠密连接网络(DenseNet):从ResNe
在深度学习的发展过程中,各种网络结构不断被提出并改进。在这一篇博客中,我们将介绍一种非常有趣且高效的网络结构——稠密连接网络(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,并为你进一步探索深度学习提供一些启发!