层和块
本文基于d2l项目内容整理,介绍深度学习中层和块的概念,以及如何在PyTorch中实现自定义模块。
1. 从单层到块的演进
1.1 深度学习框架的发展历程
Theano 库虽已于 2017 年停止维护,但其开创性的设计逻辑在深度学习发展的早期阶段,起到了重要的推动作用。后来兴起的新框架,如 TensorFlow、PyTorch 等,逐步演化为更高级的抽象。
现代框架的优势:
现代深度学习框架在保证可灵活修改底层的同时,也能快速开发模型原型,避免了重复性工作。当前人工智能领域的研究员早已不必仔细考虑单个神经元的行为,而往往以块 (block) 为单位快速设计、构建模型。
1.2 学习目标
截至目前,我们已经了解了如何从零开始,或使用高级 API 构建深度学习模型。接下来,将继续探索深度学习计算中的关键组件、更多的细节与特性。
2. 块是对层的功能性封装
2.1 从层到块的概念演进
单层 (layer) 模型特点:
- 线性回归模型:接收输入,不断更新参数,输出单个标量
- SoftMax回归模型:接收输入,不断更新参数,输出一组向量
- 功能单一:每层只完成特定的变换操作
多层网络 (MLP) 特点:
- 由多个上述类似的层叠加而来
- 组成更复杂的网络结构
- 能够学习更复杂的非线性映射关系
块 (block) 的概念:
- 由多个层构成的层组 (groups of layers)
- 通常包含卷积层、全连接层、批归一化层、激活函数等
- 按特定顺序和结构排列,组成具有特定功能的子网络
2.2 块的重要特性
块的核心特征:
- 复用性:块是复杂神经网络的基本单元,可被快速复用
- 组合性:多个块可以组合成更复杂的结构
- 递归性:块内部可以包含其他块,形成层次化结构
实际应用案例:ResNet
广泛应用的残差神经网络 (Residual Network, ResNet) 是计算机视觉任务的首选架构:
- 使用残差块 (Residual Block) 的跳跃连接 (Skip 或 Shortcut Connections) 进行架构
- 每个残差块包含 3 个卷积层
- 通过减少中间层的通道数减少计算量
2.3 PyTorch中的块实现
在面向对象程序实现上,用继承父类的方式组织块级别的抽象,以简洁的代码表示复杂的神经网络。
PyTorch块类的实现要求:
对于由 PyTorch 实现的块类,需要满足以下条件:
- 继承关系:继承自
torch.nn.Module
类 - 初始化方法:在
__init__()
方法中调用父类的构造函数以注册子模块,初始化层或参数 - 前向传播:在
forward()
方法中定义前向传播的逻辑,接受输入,执行计算并输出
父类已重载__call__()
方法以调用forward()
方法,因此前向传播会被隐式地执行。
自动化处理:
现代深度学习框架在块中自动处理了反向传播与自动微分过程,开发者只需关注前向传播的逻辑实现。
3. 使用块类实现模型
3.1 实现 MLP
回顾:Sequential的简洁实现
查看MLP的Sequential实现
与之前学习的MLP简洁实现类似,以下代码使用nn.Sequential
块实例,维护了一个有序列表,按顺序依次向前计算:
1 | import torch |
输出结果:1
2
3
4tensor([[ 0.0719, -0.1779, 0.2055, -0.1123, -0.0242, 0.1615, 0.1049, 0.0698,
0.1137, -0.0367],
[-0.0217, -0.1339, 0.2907, -0.1309, -0.1343, 0.1238, 0.2223, 0.0750,
0.0533, -0.0908]], grad_fn=<AddmmBackward0>)
自定义MLP块类实现
现在我们使用块类来实现相同的MLP模型:
1 | import torch |
输出结果:1
2
3
4tensor([[-0.4986, -0.1931, -0.2063, 0.1460, -0.1169, 0.2062, 0.0629, -0.0160,
-0.0103, 0.1804],
[-0.3573, 0.0874, -0.3731, 0.1806, 0.1149, -0.2240, 0.2753, -0.2787,
-0.3732, -0.0407]], grad_fn=<AddmmBackward0>)
3.2 实现自定义 Sequential
torch.nn.modules.container.Sequential
通常简称为nn.Sequential
,用于将多个子模块(层、激活函数等)按顺序组织在一起构建神经网络模型。我们可以使用块类实现类似的功能:
1 | import torch |
输出结果:1
2
3
4
5
6
7
8
9MySequential(
(0): Linear(in_features=20, out_features=256, bias=True)
(1): ReLU()
(2): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[ 0.1527, 0.3147, -0.1843, 0.2630, -0.3651, -0.1950, 0.3764, 0.1457,
0.1444, 0.0282],
[-0.1666, -0.2401, -0.3169, 0.6245, -0.0906, -0.0290, 0.2561, 0.1047,
-0.1085, 0.0437]], grad_fn=<AddmmBackward0>)
重要说明:
这里没有使用自定义的 Python 列表维护多个module
,是因为nn.Module
在初始化时,需要读取成员变量_modules
字典,以完成参数的初始化。
3.3 实现特定控制流的前向传播
当处理特定常数参数时,需要定义具有特定 Python 控制流的前向传播过程。
数学表达
假设某层以$\mathbf{x}$为输入、$\mathbf{w}$为参数、$\mathbf{c}$为层的权重常量,计算过程为:
代码实现
1 | import torch |
输出结果:1
tensor(0.3900, grad_fn=<SumBackward0>)
代码说明:
这样的TestBlock
类很可能不会出现在任何实际的应用中,此处仅用于展示在前向传播中使用控制流的逻辑。
TestBlock
类在实例化时,初始化成员变量cons_weight
为常量- 在反向传播中计算$L_1$范数的大小,通过除以2的方式将其控制在小于1的范围
- 最后求和返回
3.4 实现块的嵌套
块的一个重要特性是可以嵌套使用,形成更复杂的层次结构:
1 | import torch |
输出结果:1
2
3
4
5tensor([[ 0.0206, -0.1596, 0.0709, 0.0345, -0.0782, 0.0555, -0.1312, 0.0473,
0.0342, -0.0228, 0.1037, -0.1580, -0.0944, 0.0130, 0.0675, 0.1281],
[ 0.0214, -0.1608, 0.0681, 0.0350, -0.0785, 0.0535, -0.1306, 0.0468,
0.0351, -0.0210, 0.1031, -0.1583, -0.0949, 0.0136, 0.0673, 0.1277]],
grad_fn=<AddmmBackward0>)
4. 块设计的最佳实践
4.1 设计原则
模块化设计:
- 每个块应该有明确的功能定义
- 块之间的接口应该清晰简洁
- 避免块之间的强耦合
可复用性:
- 设计通用的块,可以在不同的网络中复用
- 通过参数化控制块的行为
- 避免硬编码特定的尺寸或参数
可组合性:
- 块应该能够轻松地与其他块组合
- 支持嵌套和递归结构
- 保持输入输出接口的一致性
4.2 参数管理
在自定义块时,需要注意参数的正确管理:
1 | class CustomBlock(nn.Module): |
总结
层和块是深度学习中的核心概念:
- 层:执行特定变换的基本单元
- 块:由多个层组成的功能模块,具有复用性和组合性,负责大量的内部处理,包括参数初始化和反向传播。
- 实现要点:继承
nn.Module
,实现__init__
和forward
方法 - 设计原则:模块化、可复用、可组合
通过合理设计和使用块,我们可以构建出既灵活又高效的深度学习模型。块的概念不仅简化了模型的实现,也为模型的调试、优化和扩展提供了便利。