news 2026/9/30 6:53:17

动手学深度学习:稠密连接网络(DenseNet)原理与四框架实现指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动手学深度学习:稠密连接网络(DenseNet)原理与四框架实现指南
  • 人工智能
  • 深度学习
  • 机器学习
  • 教程

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

本篇技术指南以《动手学深度学习》(d2l-zh)仓库的 densenet.md 为骨架,系统讲解稠密连接网络(DenseNet)从数学动机到工程实现的全过程:从 ResNet 的恒等映射思维出发,推导 DenseNet 的"通道维连结"思想,并给出 MXNet、PyTorch、TensorFlow、Paddle 四个框架下稠密块、过渡层与完整 DenseNet 模型的逐行可运行代码,最后以 Fashion-MNIST 数据集上的训练实验验证网络行为。读完本文,你将掌握 DenseNet 的核心组件设计、增长率与通道数演算规则,以及如何用仓库提供的d2l工具包快速训练验证。

从 ResNet 到 DenseNet:为什么要"连结"而不是"相加"

回顾任意函数的泰勒展开式(Taylor expansion),在 $x$ 接近 0 时,它把函数分解成越来越高阶的项:

$$f(x) = f(0) + f'(0) x + \frac{f''(0)}{2!} x^2 + \frac{f'''(0)}{3!} x^3 + \ldots.$$

同样的思想也被 ResNet 继承——ResNet 将函数展开为

$$f(\mathbf{x}) = \mathbf{x} + g(\mathbf{x}).$$

也就是说,ResNet 把 $f$ 分解为两部分:一个简单的线性项 $\mathbf{x}$ 和一个复杂的非线性项 $g(\mathbf{x})$(其具体推导见仓库的 残差网络(ResNet) 一节)。

DenseNet(稠密连接网络):cite:Huang.Liu.Van-Der-Maaten.ea.2017在某种程度上是 ResNet 的逻辑扩展:如果我们不满足于只把 $f$ 展开成两项,而是想捕捉超过两项的信息呢?一种方案便是 DenseNet。

如上图所示,ResNet 与 DenseNet 的关键区别在于跨层连接的运算方式:DenseNet 的输出是连接(concatenation)(图中用 $[,]$ 表示),而不是 ResNet 的简单相加。因此,在应用越来越复杂的函数序列后,我们执行从 $\mathbf{x}$ 到其展开式的映射:

$$\mathbf{x} \to \left[ \mathbf{x}, f_1(\mathbf{x}), f_2([\mathbf{x}, f_1(\mathbf{x})]), f_3([\mathbf{x}, f_1(\mathbf{x}), f_2([\mathbf{x}, f_1(\mathbf{x})])]), \ldots\right].$$

最后,将这些展开式结合到多层感知机中,再次减少特征的数量。实现起来非常简单:不需要添加术语,而是将它们连接起来。

DenseNet 的名字源于变量之间的"稠密连接"——最后一层与之前的所有层紧密相连,依赖图变得相当稠密,稠密连接如图示:

稠密网络主要由两部分构成:

  • 稠密块(dense block):定义如何连接输入和输出;
  • 过渡层(transition layer):控制通道数量,使其不会太复杂。

稠密块(Dense Block)

基础卷积块:BN-ReLU-Conv 架构

DenseNet 使用了 ResNet 改良版的"批量规范化、激活和卷积"(BatchNorm-ReLU-Conv)架构(参见 ResNet 练习)。四个框架下的实现如下:

from d2l import mxnet as d2l from mxnet import np, npx from mxnet.gluon import nn npx.set_np() 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
#@tab pytorch from d2l import torch as d2l import torch from torch import nn 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))
#@tab tensorflow from d2l import tensorflow as d2l import tensorflow as tf class ConvBlock(tf.keras.layers.Layer): def __init__(self, num_channels): super(ConvBlock, self).__init__() self.bn = tf.keras.layers.BatchNormalization() self.relu = tf.keras.layers.ReLU() self.conv = tf.keras.layers.Conv2D( filters=num_channels, kernel_size=(3, 3), padding='same') self.listLayers = [self.bn, self.relu, self.conv] def call(self, x): y = x for layer in self.listLayers.layers: y = layer(y) y = tf.keras.layers.concatenate([x,y], axis=-1) return y
#@tab paddle from d2l import paddle as d2l import warnings warnings.filterwarnings("ignore") import paddle import paddle.nn as nn 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))

注意 TensorFlow 版本的ConvBlock在call中已经直接完成了concatenate([x, y], axis=-1)的连结操作,这是因为 TensorFlow 的通道维在最后一维(channels-last 布局),与 MXNet/PyTorch/Paddle 的axis=1(channels-first 布局)相对应。

稠密块:逐块连结输入与输出

一个稠密块由多个卷积块组成,每个卷积块使用相同数量的输出通道。在前向传播中,我们将每个卷积块的输入和输出在通道维上连结(concatenate):

class DenseBlock(nn.Block): def __init__(self, num_convs, num_channels, **kwargs): super().__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 = np.concatenate((X, Y), axis=1) return X
#@tab pytorch class DenseBlock(nn.Module): def __init__(self, num_convs, input_channels, num_channels): super(DenseBlock, self).__init__() layer = [] for i in range(num_convs): layer.append(conv_block( num_channels * i + input_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
#@tab tensorflow class DenseBlock(tf.keras.layers.Layer): def __init__(self, num_convs, num_channels): super(DenseBlock, self).__init__() self.listLayers = [] for _ in range(num_convs): self.listLayers.append(ConvBlock(num_channels)) def call(self, x): for layer in self.listLayers.layers: x = layer(x) return x
#@tab paddle class DenseBlock(nn.Layer): def __init__(self, num_convs, input_channels, num_channels): super(DenseBlock, self).__init__() layer = [] for i in range(num_convs): layer.append( conv_block(num_channels * i + input_channels, num_channels)) self.net = nn.Sequential(*layer) def forward(self, X): for blk in self.net: Y = blk(X) # 连接通道维度上每个块的输入和输出 X = paddle.concat(x=[X, Y], axis=1) return X

从实现细节可以看出框架间的差异:PyTorch/Paddle 版本的DenseBlock在构建层列表时就显式地累加输入通道数(第 $i$ 个卷积块的输入通道为num_channels * i + input_channels),而 MXNet/TensorFlow 版本利用框架的惰性通道推断,无需手工计算。

增长率(Growth Rate)与通道数演算

下面验证稠密块的通道增长行为。定义一个有 2 个卷积块、输出通道数为 10 的DenseBlock,使用通道数为 3 的输入,将会得到通道数为 $3+2\times 10=23$ 的输出:

blk = DenseBlock(2, 10) blk.initialize() X = np.random.uniform(size=(4, 3, 8, 8)) Y = blk(X) Y.shape
#@tab pytorch blk = DenseBlock(2, 3, 10) X = torch.randn(4, 3, 8, 8) Y = blk(X) Y.shape
#@tab tensorflow blk = DenseBlock(2, 10) X = tf.random.uniform((4, 8, 8, 3)) Y = blk(X) Y.shape
#@tab paddle blk = DenseBlock(2, 3, 10) X = paddle.randn([4, 3, 8, 8]) Y = blk(X) Y.shape

各框架运行后均得到(4, 23, 8, 8)(TensorFlow 为 channels-last 布局的(4, 8, 8, 23))。这里的 10 即增长率(growth rate):卷积块的通道数控制了输出通道数相对于输入通道数的增长量。在 DenseNet 论文中,增长率通常取 12、24 或 32 等较小值,因为特征被反复复用,不需要为每一层学习冗余的表示。

过渡层(Transition Layer)

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

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
#@tab pytorch 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))
#@tab tensorflow class TransitionBlock(tf.keras.layers.Layer): def __init__(self, num_channels, **kwargs): super(TransitionBlock, self).__init__(**kwargs) self.batch_norm = tf.keras.layers.BatchNormalization() self.relu = tf.keras.layers.ReLU() self.conv = tf.keras.layers.Conv2D(num_channels, kernel_size=1) self.avg_pool = tf.keras.layers.AvgPool2D(pool_size=2, strides=2) def call(self, x): x = self.batch_norm(x) x = self.relu(x) x = self.conv(x) return self.avg_pool(x)
#@tab paddle 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))

对上例稠密块的输出(23 通道)使用通道数为 10 的过渡层,输出的通道数减为 10,高和宽均减半:

blk = transition_block(10) blk.initialize() blk(Y).shape
#@tab pytorch, paddle blk = transition_block(23, 10) blk(Y).shape
#@tab tensorflow blk = TransitionBlock(10) blk(Y).shape

运行后得到(4, 10, 4, 4)(TensorFlow 为(4, 4, 4, 10)),即通道数 23 → 10,空间尺寸 8×8 → 4×4。这里为什么要用平均汇聚层而不是最大汇聚层?因为过渡层的作用是压缩信息、平滑地降低空间分辨率与通道冗余,平均汇聚能保留整体特征响应,而最大汇聚只保留局部最强的单一激活,在信息压缩场景下平均汇聚对特征传递更温和。

构建完整的 DenseNet 模型

与 ResNet 的构造过程类似,DenseNet 整体由四段构成:入口卷积块 + 4 个稠密块(含过渡层)+ 全局汇聚与输出层。

第一步:入口卷积与最大汇聚

DenseNet 首先使用同 ResNet 一样的单卷积层和最大汇聚层(卷积 64 通道、7×7 核、步幅 2、padding 3):

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))
#@tab pytorch 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))
#@tab tensorflow def block_1(): return tf.keras.Sequential([ tf.keras.layers.Conv2D(64, kernel_size=7, strides=2, padding='same'), tf.keras.layers.BatchNormalization(), tf.keras.layers.ReLU(), tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')])
#@tab paddle 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))

第二步:堆叠 4 个稠密块与过渡层

类似于 ResNet 使用的 4 个残差块模块,DenseNet 使用 4 个稠密块。与 ResNet 类似,我们可以设置每个稠密块使用多少个卷积层,这里设成 4,从而与 ResNet-18 保持一致(见 ResNet)。稠密块里的卷积层通道数(即增长率)设为 32,所以每个稠密块将增加 $4 \times 32 = 128$ 个通道。

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

# num_channels为当前的通道数 num_channels, growth_rate = 64, 32 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: num_channels //= 2 net.add(transition_block(num_channels))
#@tab pytorch # num_channels为当前的通道数 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
#@tab tensorflow def block_2(): net = block_1() # num_channels为当前的通道数 num_channels, growth_rate = 64, 32 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: num_channels //= 2 net.add(TransitionBlock(num_channels)) return net
#@tab paddle # num_channels为当前的通道数 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

这段循环的通道数演算值得细读:初始num_channels = 64,每个稠密块之后num_channels += num_convs * growth_rate(即 +128),随后除了最后一个稠密块外,过渡层又把通道数整除 2。以 PyTorch 版本为例,实际通道序列为:64 → 192 → 96 → 224 → 112 → 240 → 120 → 248,最后一个稠密块后不做减半,最终以 248 通道进入输出层。

第三步:全局汇聚与输出层

与 ResNet 类似,最后接上全局汇聚层和全连接层来输出结果:

net.add(nn.BatchNorm(), nn.Activation('relu'), nn.GlobalAvgPool2D(), nn.Dense(10))
#@tab pytorch net = nn.Sequential( b1, *blks, nn.BatchNorm2d(num_channels), nn.ReLU(), nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(num_channels, 10))
#@tab tensorflow def net(): net = block_2() net.add(tf.keras.layers.BatchNormalization()) net.add(tf.keras.layers.ReLU()) net.add(tf.keras.layers.GlobalAvgPool2D()) net.add(tf.keras.layers.Flatten()) net.add(tf.keras.layers.Dense(10)) return net
#@tab paddle net = nn.Sequential( b1, *blks, nn.BatchNorm2D(num_channels), nn.ReLU(), nn.AdaptiveMaxPool2D((1, 1)), nn.Flatten(), nn.Linear(num_channels, 10))

全局汇聚(PyTorch 用AdaptiveAvgPool2d((1,1)),Paddle 的示例中使用了AdaptiveMaxPool2D)把每个通道的特征图压缩为 1×1,从而与输入尺寸无关地接入全连接层;因为 DenseNet 的最后一层稠密连接了前面所有层,全连接层之前必须通过这种聚合把可变大小的稠密特征张量固定下来。

训练模型:在 Fashion-MNIST 上验证

由于 DenseNet 是较深的网络,本节将输入高和宽从 224 降到 96 来简化计算,学习率设为 0.1(ResNet 一节中为 0.05,稠密连接让梯度流动更充分,可以采用稍大的学习率):

#@tab all 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())

这里依赖的d2l工具函数都可以在仓库的 d2l 目录源码中找到:

  • load_data_fashion_mnist(如 d2l/torch.py):下载并加载 Fashion-MNIST 数据集,支持resize参数把输入图像调整到指定尺寸,返回训练与测试DataLoader;
  • train_ch6(如 d2l/torch.py):第 6 章的通用训练流程,内部完成 Xavier 初始化、SGD 优化器、交叉熵损失、逐 epoch 的训练/测试评估与动画可视化,最后打印 loss、train acc、test acc 与吞吐量;
  • try_gpu(如 d2l/torch.py):存在 GPU 时返回cuda:i设备,否则回退到 CPU。

小结

  • 在跨层连接上,不同于 ResNet 中将输入与输出相加,稠密连接网络(DenseNet)在通道维上连结输入与输出,特征被显式地复用,最后一层与之前所有层稠密相连。
  • DenseNet 的主要构建模块是稠密块(决定输入输出如何连结)和过渡层(通过 $1\times 1$ 卷积减半通道数、步幅 2 平均汇聚减半高宽,控制网络复杂度)。
  • 在构建 DenseNet 时,我们需要通过添加过渡层来控制网络的维数,从而再次减少通道的数量;增长率(growth rate)直接决定了每个稠密块通道数的增长速度。

练习与进阶思考

  1. 为什么过渡层使用平均汇聚层而不是最大汇聚层?从信息压缩的语义出发分析两种汇聚操作的差异。
  2. DenseNet 的优点之一是模型参数比 ResNet 小,为什么?结合"特征复用"与"每层只需学习少量新特征(增长率)"两个角度作答。
  3. DenseNet 被诟病的问题是内存或显存消耗过多:
    • 真的是这样吗?把输入形状换成 $224 \times 224$,观察实际的显存消耗变化;
    • 是否有另一种方法减少显存消耗?需要改变框架吗?(提示:考虑计算与存储的重新调度,如梯度检查点技术)
  4. 实现 DenseNet 论文:cite:Huang.Liu.Van-Der-Maaten.ea.2017表 1 所示的不同 DenseNet 版本(DenseNet-121/169/201/264 等),注意在 ImageNet 上使用的 bottleneck 与 compression 设置。
  5. 应用 DenseNet 的思想设计一个基于多层感知机的模型,并将其应用于 Kaggle 房价预测 任务中(即把"通道维连结"思想移植到稠密连接的 MLP 上)。

如果你想系统了解 DenseNet 的前置知识,可先阅读仓库的 残差网络(ResNet) 章节;本文全部代码(四框架版本)可在 chapter_convolutional-modern/densenet.md 中直接运行,配合 d2l 目录下的工具包即可完成端到端实验。

  • 人工智能
  • 深度学习
  • 机器学习
  • 教程

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

相关推荐

上一篇:ZeroOmega终极指南:如何轻松管理浏览器代理的完整解决方案
下一篇:eSearch滚动截屏:三步出长图,横向竖向任意方向都能拼

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 6:50:08

3步上手BaiduPCS-Go:百度网盘命令行管理与分享转存的完整指南

3步上手BaiduPCS-Go:百度网盘命令行管理与分享转存的完整指南 【免费下载链接】BaiduPCS-Go iikira/BaiduPCS-Go原版基础上集成了分享链接/秒传链接转存功能 项目地址: https://gitcode.com/GitHub_Trending/ba/BaiduPCS-Go BaiduPCS-Go 是一款用 Go 语言编写…

作者头像 李华
网站建设 2026/9/30 6:50:00

5 个命令掌握 Cog CLI:把 Python 模型项目变成可部署的容器

5 个命令掌握 Cog CLI:把 Python 模型项目变成可部署的容器 【免费下载链接】cog Containers for machine learning 项目地址: https://gitcode.com/GitHub_Trending/co/cog 想把 Python 模型变成稳定的 API 服务,你要对付三件事:环境…

作者头像 李华
网站建设 2026/9/30 6:48:46

正交的 React 组件:用正交性重构组件边界,让取数与 UI 彻底解耦

文档技术博客教程 【免费下载链接】weekly 前端精读周刊。帮你理解最前沿、实用的技术。 项目地址: https://gitcode.com/GitHub_Trending/we/weekly 点击查看 免费下载 本文是前端精读周刊对《The Benefits of Orthogonal React Components》一文的深度解读&#…

作者头像 李华
网站建设 2026/9/30 6:48:36

基于微信小程序的民宿短租系统

选题背景与研究意义近年来,民宿行业依托共享经济模式迅猛发展,成为传统酒店业的重要补充。其个性化服务、本地化体验和性价比优势吸引了大量年轻用户群体。数据显示,2022年中国在线民宿市场交易规模突破300亿元,但行业仍面临信息化…

作者头像 李华
网站建设 2026/9/30 6:48:34

大麦网抢票脚本:10 分钟改好 3 个参数,跑通自动抢购流程

大麦网抢票脚本:10 分钟改好 3 个参数,跑通自动抢购流程 【免费下载链接】Automatic_ticket_purchase 大麦网抢票脚本 项目地址: https://gitcode.com/GitHub_Trending/au/Automatic_ticket_purchase 这是一款用 Python Selenium 写的大麦网抢票…

作者头像 李华