本文基于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 实现的块类,需要满足以下条件:

  1. 继承关系:继承自torch.nn.Module
  2. 初始化方法:在__init__()方法中调用父类的构造函数以注册子模块,初始化层或参数
  3. 前向传播:在forward()方法中定义前向传播的逻辑,接受输入,执行计算并输出

父类已重载__call__()方法以调用forward()方法,因此前向传播会被隐式地执行。

自动化处理:

现代深度学习框架在块中自动处理了反向传播与自动微分过程,开发者只需关注前向传播的逻辑实现。


3. 使用块类实现模型

3.1 实现 MLP

回顾:Sequential的简洁实现

查看MLP的Sequential实现

与之前学习的MLP简洁实现类似,以下代码使用nn.Sequential块实例,维护了一个有序列表,按顺序依次向前计算:

1
2
3
4
5
6
7
8
9
import torch
from torch import nn

MLP = nn.Sequential(nn.Linear(in_features=20, out_features=256), nn.ReLU(), # 全连接隐藏层
nn.Linear(in_features=256, out_features=10)) # 输出层

if __name__ == '__main__':
mlp = MLP(torch.rand(2, 20))
print(mlp)

输出结果:

1
2
3
4
tensor([[ 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import torch
from torch import nn
from torch.nn.functional import relu


class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hide = nn.Linear(in_features=20, out_features=256) # 全连接隐藏层
self.out = nn.Linear(in_features=256, out_features=10) # 输出层

def forward(self, data):
return self.out(relu(self.hide(data)))


if __name__ == '__main__':
mlp = MLP()
print(mlp(torch.randn(2, 20)))

输出结果:

1
2
3
4
tensor([[-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
from torch import nn


class MySequential(nn.Module):
def __init__(self, *modules):
super().__init__()
for i, module in enumerate(modules):
self._modules[str(i)] = module # 成员变量 _modules 继承自 Module 类,是一个有序字典 OrderedDict

def forward(self, data):
for module in self._modules.values():
data = module(data)
return data


if __name__ == '__main__':
mlp = MySequential(
nn.Linear(20, 256), nn.ReLU(),
nn.Linear(256, 10)
)
print(mlp)
print(mlp(torch.randn(2, 20)))

输出结果:

1
2
3
4
5
6
7
8
9
MySequential(
(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
from torch import nn
import torch.nn.functional as F


class TestBlock(nn.Module):
def __init__(self):
super().__init__()
self.cons_weight = torch.rand((20, 20), requires_grad=False) # 常量。不参与梯度计算,不被更新
self.linear = nn.Linear(20, 20)

def forward(self, data):
data = self.linear(data) # 全连接层
data = F.relu(data @ self.cons_weight + 1) # 激活函数
data = self.linear(data) # 复用全连接层,共享参数
while data.abs().sum() > 1: # 控制流
data /= 2
return data.sum()


if __name__ == '__main__':
net = TestBlock()
print(net(torch.rand(2, 20)))

输出结果:

1
tensor(0.3900, grad_fn=<SumBackward0>)

代码说明:

这样的TestBlock类很可能不会出现在任何实际的应用中,此处仅用于展示在前向传播中使用控制流的逻辑。

  • TestBlock类在实例化时,初始化成员变量cons_weight为常量
  • 在反向传播中计算$L_1$范数的大小,通过除以2的方式将其控制在小于1的范围
  • 最后求和返回

3.4 实现块的嵌套

块的一个重要特性是可以嵌套使用,形成更复杂的层次结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from torch import nn


class Chimera(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
nn.Linear(64, 32), nn.ReLU())
self.linear = nn.Linear(32, 16)

def forward(self, data):
return self.linear(self.net(data))


if __name__ == '__main__':
net = nn.Sequential(
Chimera(),
nn.Linear(16, 20),
Chimera()
)
print(net(torch.rand(2, 20)))

输出结果:

1
2
3
4
5
tensor([[ 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CustomBlock(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
# 正确注册参数
self.linear1 = nn.Linear(input_size, hidden_size)
self.linear2 = nn.Linear(hidden_size, output_size)

# 自定义参数
self.custom_weight = nn.Parameter(torch.randn(hidden_size, hidden_size))

# 常量(不参与训练)
self.register_buffer('constant', torch.ones(hidden_size))

def forward(self, x):
x = self.linear1(x)
x = torch.matmul(x, self.custom_weight) + self.constant
x = self.linear2(x)
return x

总结

层和块是深度学习中的核心概念:

  1. :执行特定变换的基本单元
  2. :由多个层组成的功能模块,具有复用性和组合性,负责大量的内部处理,包括参数初始化和反向传播。
  3. 实现要点:继承nn.Module,实现__init__forward方法
  4. 设计原则:模块化、可复用、可组合

通过合理设计和使用块,我们可以构建出既灵活又高效的深度学习模型。块的概念不仅简化了模型的实现,也为模型的调试、优化和扩展提供了便利。