PyTorch05-PyTorch模型定义
五、PyTorch模型定义
我们会在不同场合看到多种不同的模型架构,比如 CNN(Convolutional Neural Network,卷积神经网络)、RNN(Recurrent Neural Network,循环神经网络)、GNN(Graph Neural Network,图神经网络),它们是深度学习领域针对不同数据结构设计的神经网络架构,分别用来解决网格化结构数据(如二维图像、视频帧)的局部特征提取与空间感知问题、序列数据(如文本、语音、时间序列)的前后关联与时序依赖问题、以及非欧几里得图结构数据(如社交网络、分子结构、知识图谱)的复杂关系建模问题。
在实际应用中,每种宏观架构下都有诸多经典的具体模型实现。在学习或使用这些模型时,通常需要先理解其内部的运行机制。本章节将基于 PyTorch 这一深度学习框架,从粗略地角度来探索与学习下模型的代码实现与构造方式。
本章节学习的内容包括:使用PyTorch定义模型的三种经典方式。 ## 5.1
必要的知识回顾 在PyTorch
中,torch.nn.Module是一个基本构造类,所有神经网络层(如 nn.Linear、nn.Conv2d)以及自定义的大模型,本质上都是它的子类。
当我们基于torch.nn.Module来自定义模型时,一般就是要重写自定义类的__init__与forward两个方法。
在基于torch.nn.Module来构建自定义模型组成与执行流程的具体实现时,可以借助Sequential或
ModuleList或ModuleDict
这三个类(它们是官方提供且最常用的,但并不是 PyTorch
定义模型子模块的“唯三”方式)。其他的常用的方式还有:给自定义类的属性赋值然后在forward方法中调用。通常在编写较复杂的网络时,是将“直接类属性定义”与
Sequential、ModuleList、ModuleDict
混合配合使用的。
以下分别对这三个类及使用进行精简介绍并给出示例。 ## 5.2 定义模型子模块的常用三个类
5.1.2 Sequential
nn.Sequential 是 PyTorch
中最简单的模型容器。它按照层添加的顺序依次执行前向传播,无需手动编写
forward()
函数。因此它无法应对条件分支、跳连、多分支等复杂结构,只适合于结构简单、顺序执行的模型。
它可以接收一系列子模块或子模块的有序字典(OrderedDict),从而给模型逐一添加
Module 的实例,且按它们出现的顺序作为模型前向传播时的执行顺序。
直接添加子模块与子模块的有序字典(OrderedDict)的用法大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 方式一:直接传入多个 nn.Module(匿名层,无名称)
net1 = nn.Sequential(
nn.Linear(20, 128), # 第 0 层:输入 20 → 输出 128
nn.ReLU(), # 第 1 层:ReLU 激活
nn.Linear(128, 10), # 第 2 层:输入 128 → 输出 10
nn.Softmax(dim=1), # 第 3 层:Softmax 概率分布
)
# 方式二:使用 OrderedDict 给每层命名(推荐)
net2 = nn.Sequential(collections.OrderedDict([
('fc1', nn.Linear(20, 128)), # 命名:fc1(第一个全连接层)
('relu1', nn.ReLU()), # 命名:relu1
('fc2', nn.Linear(128, 10)), # 命名:fc2
('softmax', nn.Softmax(dim=1)), # 命名:softmax
]))
完整可执行的示例如下:
1 | |
使用Sequential虽然方便,但也会使得模型定义丧失灵活性,如需在模型中间加入一个外部输入,就不适合用Sequential的方式实现。
5.1.3 ModuleList
nn.ModuleList 接收一个子模块(或层,要是nn.Module类)的列表作为输入,但nn.ModuleList并没有直接定义一个完整的模型(无法自动进行前向传播),它只是将不同的子模块或层储存在一起,并将子模块或层的权重也会自动添加到模型中。要想让它像模型一样工作还需手动遍历其中所有层或子模块(即列表中所有元素)并执行它们。
1 | |
对于一个nn.ModuleList 实例,可以操作Python中普通的列表List一样对其增加、扩展、删除、索引与修改元素。
1
2
3# append:像普通 list 一样往 ModuleList 末尾追加一个层
net.append(nn.Softmax(dim=1)) # 在末尾追加 Softmax,将输出转为概率分布
print(f"追加 Softmax 后,ModuleList 共包含 {len(net)} 个层")
完整的可执行示例如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196# -*- coding: utf-8 -*-
"""
02-使用ModuleList定义模型.py
============================
ModuleList 是 PyTorch 提供的一个「列表式」模块容器。
它的作用类似于 Python 的 list,专门用来存放多个 nn.Module 子层。
与普通 list 的核心区别:
- 普通 list 存 nn.Module 不会被 PyTorch 自动识别为模型参数,
导致 model.parameters() 无法正确收集这些层的权重。
- ModuleList 会正确注册所有子模块,参数可以被自动收集。
本示例:
1. 直接使用 ModuleList 构建一个简单的两层全连接网络
2. 用随机数据跑一次前向传播,观察输出形状
3. 对比 ModuleList 和普通 list 在参数收集上的区别
"""
import torch
import torch.nn as nn
# ============================================================
# 第一部分:直接使用 ModuleList(不定义类)
# ============================================================
print("=" * 50)
print("第一部分:直接使用 ModuleList")
print("=" * 50)
# 用 ModuleList 定义两个全连接层 + 一个激活函数
# 注意:这里只存了层,还没有指定前向传播的顺序
net = nn.ModuleList([
nn.Linear(20, 128), # 第一层:输入 20 维 → 输出 128 维
nn.ReLU(), # 激活函数:逐元素应用 ReLU
nn.Linear(128, 10), # 第二层:输入 128 维 → 输出 10 维(10 个类别)
])
# append:像普通 list 一样往 ModuleList 末尾追加一个层
net.append(nn.Softmax(dim=1)) # 在末尾追加 Softmax,将输出转为概率分布
print(f"追加 Softmax 后,ModuleList 共包含 {len(net)} 个层")
# 索引访问:像普通 list 一样通过下标取层
last_layer = net[-1]
print(f"最后一层是:{last_layer}")
# ModuleList 本身只负责管理子模块,不负责前向传播的顺序。
# 前向传播时,需要手动遍历 ModuleList 中的每一层。
print("\n--- 前向传播 ---")
# 构造一个 batch 的随机输入:batch_size=4,每个样本 20 维
dummy_input = torch.randn(4, 20)
print(f"输入形状:{dummy_input.shape}")
params = sum(p.numel() for p in net.parameters())
print(f" net 的可训练参数数量:{params} ")
# 手动遍历每一层,依次执行前向计算
x = dummy_input
for i, layer in enumerate(net):
x = layer(x)
print(f" 经过第 {i} 层 {type(layer).__name__:12s} 后,形状变为:{tuple(x.shape)}")
print(f"最终输出形状:{x.shape}") # 期望:(4, 10)
# ============================================================
# 第二部分:在 nn.Module 子类中使用 ModuleList
# 这是 ModuleList 最典型的用法
# ============================================================
print("\n" + "=" * 50)
print("第二部分:在 nn.Module 中使用 ModuleList")
print("=" * 50)
class MyModel(nn.Module):
"""
一个使用 ModuleList 的两层全连接分类模型。
网络结构:
输入(20维) → Linear(20→128) → ReLU → Linear(128→10) → Softmax
"""
def __init__(self):
super().__init__() # 必须调用父类 nn.Module 的构造函数
# 将所有层放入 ModuleList
self.layers = nn.ModuleList([
nn.Linear(20, 128), # 隐藏层:20 → 128
nn.ReLU(), # 激活
nn.Linear(128, 10), # 输出层:128 → 10
nn.Softmax(dim=1), # 输出概率分布
])
def forward(self, x):
"""
前向传播:手动遍历 self.layers,逐层执行。
"""
for layer in self.layers:
x = layer(x)
return x
model = MyModel()
print(f"模型结构:\n{model}")
# 用随机数据跑一次前向传播
sample = torch.randn(4, 20)
output = model(sample)
print(f"\n输入形状:{tuple(sample.shape)}")
print(f"输出形状:{tuple(output.shape)}") # 期望:(4, 10)
print(f"输出示例(第一个样本):{output[0].detach().cpu().numpy().round(4)}")
print(f"概率之和(应≈1.0):{output.sum(dim=1).detach().cpu().numpy().round(4)}")
# ============================================================
# 第三部分:ModuleList vs 普通 list(参数收集对比)
# ============================================================
print("\n" + "=" * 50)
print("第三部分:ModuleList vs 普通 list — 参数收集对比")
print("=" * 50)
class ModelWithPlainList(nn.Module):
"""使用普通 Python list 存放子模块(不推荐)"""
def __init__(self):
super().__init__()
# 错误示范:普通 list 不会被 PyTorch 正确注册
self.wrong_list = [
nn.Linear(20, 128),
nn.ReLU(),
nn.Linear(128, 10),
]
def forward(self, x):
for layer in self.wrong_list:
x = layer(x)
return x
class ModelWithModuleList(nn.Module):
"""使用 ModuleList 存放子模块(推荐)"""
def __init__(self):
super().__init__()
self.right_list = nn.ModuleList([
nn.Linear(20, 128),
nn.ReLU(),
nn.Linear(128, 10),
])
def forward(self, x):
for layer in self.right_list:
x = layer(x)
return x
wrong = ModelWithPlainList()
right = ModelWithModuleList()
# model.parameters() 会递归收集所有可训练参数(权重 + 偏置)
wrong_params = sum(p.numel() for p in wrong.parameters())
right_params = sum(p.numel() for p in right.parameters())
print(f"普通 list 模型的可训练参数数量:{wrong_params} (应为 0,层没被注册)")
print(f"ModuleList 模型的可训练参数数量:{right_params} (两个 Linear 层权重+偏置)")
# ============================================================
# 第四部分:ModuleList 常见操作速查
# ============================================================
print("\n" + "=" * 50)
print("第四部分:ModuleList 常见操作速查")
print("=" * 50)
mylist = nn.ModuleList([
nn.Linear(10, 20),
nn.ReLU(),
])
# 追加单个层
mylist.append(nn.Linear(20, 5))
print(f"append 后长度:{len(mylist)}")
# 扩展多个层
mylist.extend([nn.ReLU(), nn.Softmax(dim=1)])
print(f"extend 后长度:{len(mylist)}")
# 弹出最后一个(ModuleList.pop() 需要传入索引,不像普通 list 无参弹出)
popped = mylist.pop(-1)
print(f"pop 弹出:{type(popped).__name__},剩余长度:{len(mylist)}")
# 插入到指定位置
mylist.insert(1, nn.Dropout(0.5))
print(f"insert 后第 1 层:{type(mylist[1]).__name__}")
# 删除指定位置
del mylist[1]
print(f"del 后第 1 层:{type(mylist[1]).__name__}")
print("\nModuleList 示例执行完毕!")
5.1.4 ModuleDict
nn.ModuleDict和nn.ModuleList的作用类似,只是nn.ModuleDict的结构是一个加强版的Dict,能够给神经网络的模块或层设置名称(其实就是Dict中Key),也支持一系列Dict中常见操作如添加、更新、删除等。
1
2
3
4
5
6
7
8
9
10
11
12
13# 创建 ModuleDict,用 dict 初始化(方式一:传入字典)
net = nn.ModuleDict({
'linear': nn.Linear(20, 128), # 键 'linear' 对应一个全连接层
'act': nn.ReLU(), # 键 'act' 对应一个激活层
})
# 动态添加新的层(方式二:像普通 dict 一样用 [] 赋值)
net['output'] = nn.Linear(128, 10) # 添加键 'output'
# 手动指定前向顺序:linear → act → output
x = net['linear'](x)
x = net['act'](x)
x = net['output'](x)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267# -*- coding: utf-8 -*-
"""
03-使用ModuleDict定义模型.py
=============================
ModuleDict 是 PyTorch 提供的一个「字典式」模块容器。
它的作用类似于 Python 的 dict,专门用来存放多个 nn.Module 子层,并允许通过名称(字符串)来访问。
与普通 dict 的核心区别:
- 普通 dict 存 nn.Module 不会被 PyTorch 自动识别为模型参数,
导致 model.parameters() 无法正确收集这些层的权重。
- ModuleDict 会正确注册所有子模块,参数可以被自动收集。
ModuleDict 的典型使用场景:
- 需要动态选择某几层(例如:根据不同条件选用不同的卷积层)
- 需要像查字典一样通过名字获取某层(例如 net['backbone'])
- 需要保存/加载时通过名称管理模块(例如:保存为 .pt 文件时方便按名称恢复)
本示例:
1. 构建一个简单的两层网络
2. 演示 ModuleDict 的基本操作(增/查/删/遍历)
3. 用随机数据跑前向传播
4. 对比 ModuleDict 和普通 dict 在参数收集上的区别
"""
import torch
import torch.nn as nn
# ============================================================
# 第一部分:ModuleDict 基础用法
# ============================================================
print("=" * 50)
print("第一部分:ModuleDict 基础用法")
print("=" * 50)
# 创建 ModuleDict,用 dict 初始化(方式一:传入字典)
net = nn.ModuleDict({
'linear': nn.Linear(20, 128), # 键 'linear' 对应一个全连接层
'act': nn.ReLU(), # 键 'act' 对应一个激活层
})
# 动态添加新的层(方式二:像普通 dict 一样用 [] 赋值)
net['output'] = nn.Linear(128, 10) # 添加键 'output'
print(f"ModuleDict 共包含 {len(net)} 个模块")
print(f"各模块的键:{list(net.keys())}")
print(f"各模块的值:{list(net.values())}")
# 通过名称访问某层
print(f"\n通过 net['linear'] 访问:{net['linear']}")
print(f"通过 net.output 访问:{net.output}")
# 前向传播:ModuleDict 不自动按顺序执行,需要在 forward() 中手动调用
print("\n--- 前向传播 ---")
dummy_input = torch.randn(4, 20)
print(f"输入形状:{dummy_input.shape}")
# 手动指定前向顺序:linear → act → output
x = net['linear'](dummy_input)
x = net['act'](x)
x = net['output'](x)
print(f"最终输出形状:{x.shape}") # 期望:(4, 10)
# ============================================================
# 第二部分:在 nn.Module 子类中使用 ModuleDict(推荐方式)
# 这是 ModuleDict 最典型的用法
# ============================================================
print("\n" + "=" * 50)
print("第二部分:在 nn.Module 中使用 ModuleDict")
print("=" * 50)
class MyModel(nn.Module):
"""
一个使用 ModuleDict 的两层全连接分类模型。
网络结构:
输入(20维) → linear(20→128) → ReLU → output(128→10) → Softmax
"""
def __init__(self):
super().__init__()
# 将各层放入 ModuleDict,通过名称管理
self.layers = nn.ModuleDict({
'hidden': nn.Linear(20, 128), # 隐藏层
'act': nn.ReLU(), # 激活层
'out': nn.Linear(128, 10), # 输出层
})
def forward(self, x):
# 通过名称逐层调用
x = self.layers['hidden'](x)
x = self.layers['act'](x)
x = self.layers['out'](x)
return x
model = MyModel()
print(f"模型结构:\n{model}")
# 前向传播
sample = torch.randn(4, 20)
output = model(sample)
print(f"\n输入形状:{tuple(sample.shape)}")
print(f"输出形状:{tuple(output.shape)}")
# ============================================================
# 第三部分:ModuleDict 常见操作速查
# ============================================================
print("\n" + "=" * 50)
print("第三部分:ModuleDict 常见操作速查")
print("=" * 50)
md = nn.ModuleDict()
md['a'] = nn.Linear(10, 20) # 添加:md['key'] = module
print(f"添加 'a' 后,长度 = {len(md)}")
md.update({'b': nn.ReLU()}) # 此处更新其实为添加
print(f"update {{'b':...}} 后,长度 = {len(md)}")
print(f"md['a']:{md['a']}") # 访问:md['key']
print(f"md.a:{md.a}") # 也可用属性方式访问
# 遍历所有键值对
print("\n遍历所有模块:")
for name, module in md.items():
print(f" {name}: {module}")
# 删除模块
del md['a']
print(f"\n删除 'a' 后,键列表 = {list(md.keys())}")
# pop 删除并返回模块
popped = md.pop('b')
print(f"pop 弹出 'b':{type(popped).__name__}")
print(f"pop 后长度 = {len(md)}")
# clear 清空所有模块
md.clear()
print(f"clear 后长度 = {len(md)}")
# ============================================================
# 第四部分:ModuleDict vs 普通 dict — 参数收集对比
# ============================================================
print("\n" + "=" * 50)
print("第四部分:ModuleDict vs 普通 dict — 参数收集对比")
print("=" * 50)
class ModelWithPlainDict(nn.Module):
"""使用普通 Python dict 存放子模块(不推荐)"""
def __init__(self):
super().__init__()
# 错误示范:普通 dict 不会被 PyTorch 正确注册
self.wrong_dict = {
'linear': nn.Linear(20, 128),
'act': nn.ReLU(),
'out': nn.Linear(128, 10),
}
def forward(self, x):
x = self.wrong_dict['linear'](x)
x = self.wrong_dict['act'](x)
x = self.wrong_dict['out'](x)
return x
class ModelWithModuleDict(nn.Module):
"""使用 ModuleDict 存放子模块(推荐)"""
def __init__(self):
super().__init__()
self.right_dict = nn.ModuleDict({
'linear': nn.Linear(20, 128),
'act': nn.ReLU(),
'out': nn.Linear(128, 10),
})
def forward(self, x):
x = self.right_dict['linear'](x)
x = self.right_dict['act'](x)
x = self.right_dict['out'](x)
return x
wrong = ModelWithPlainDict()
right = ModelWithModuleDict()
wrong_params = sum(p.numel() for p in wrong.parameters())
right_params = sum(p.numel() for p in right.parameters())
print(f"普通 dict 模型的可训练参数数量:{wrong_params} (应为 0,层没被注册)")
print(f"ModuleDict 模型的可训练参数数量:{right_params} (两个 Linear 层权重+偏置)")
# ============================================================
# 第五部分:ModuleDict 实战示例——按名称选择不同层
# 这是 ModuleDict 最强大的用法:
# 可以用字符串动态决定使用哪一层
# ============================================================
print("\n" + "=" * 50)
print("第五部分:ModuleDict 实战——动态选择分支")
print("=" * 50)
class MultiBranchModel(nn.Module):
"""
一个具有多分支结构的模型,使用 ModuleDict 管理分支。
场景:输入有两种处理方式(简单路径 / 复杂路径),
根据输入特征动态选择走哪条路径。
"""
def __init__(self):
super().__init__()
# 两个分支用 ModuleDict 管理
self.branches = nn.ModuleDict({
'simple': nn.Sequential( # 简单分支:单层
nn.Linear(20, 10),
),
'complex': nn.Sequential( # 复杂分支:两层 + 激活
nn.Linear(20, 64),
nn.ReLU(),
nn.Linear(64, 10),
),
})
def forward(self, x, branch_name='complex'):
# 根据传入的 branch_name 字符串,动态选择使用哪个分支
if branch_name not in self.branches:
raise ValueError(f"未知分支 '{branch_name}',可选:{list(self.branches.keys())}")
return self.branches[branch_name](x)
mb = MultiBranchModel()
print(f"可选分支:{list(mb.branches.keys())}")
sample = torch.randn(4, 20)
# 走简单分支
out_simple = mb(sample, branch_name='simple')
print(f"\n简单分支输出形状:{tuple(out_simple.shape)}")
# 走复杂分支
out_complex = mb(sample, branch_name='complex')
print(f"复杂分支输出形状:{tuple(out_complex.shape)}")
# ============================================================
# 总结
# ============================================================
print("\n" + "=" * 50)
print("ModuleDict 使用要点总结")
print("=" * 50)
summary = """
1. nn.ModuleDict() 创建一个字典式的模块容器。
2. 通过字符串键名访问模块:md['name'] 或 md.name。
3. 支持字典操作:md['key'] = module、update()、pop()、clear()、del 等。
4. 适合需要「按名称动态选择/管理模块」的场景。
5. ModuleDict 本身不自动执行前向传播,需在 forward() 中手动调用各层。
6. 适用场景:
- 多分支网络(根据条件选不同分支)
- 需要保存/加载时按名称恢复模块
- 可插拔的层结构(如更换 backbone)
"""
print(summary)
print("ModuleDict 示例执行完毕!")
5.3 三种方法的比较与适用场景
nn.Sequential
适用于结构简单、单线顺序执行的网络或子网络。它的优势在于能自动处理层与层之间的数据传递,无需在
forward
中逐层手写连接逻辑;ß但无法直接支持跳连、多分支和动态条件判断。
nn.ModuleList 和 nn.ModuleDict
则适用于需要动态选择、遍历堆叠或分支切换的复杂网络结构。它们主要起
“安全注册参数”和“容器管理”
的作用,具体的跳连、分支以及数据流向逻辑,必须在自定义模型的
forward() 中手动编写实现。
| 容器类型 | 自动执行 forward | 适用场景 | 关键局限性 |
|---|---|---|---|
nn.Sequential |
是 | 单线流水线结构、无分支/无跳连、快速搭建局部子网络(如 MLP、经典 CNN 骨干) | 无法支持多输入/多输出、条件分支、跳连(如 ResNet) |
nn.ModuleList |
否 | 需要按索引访问层、循环堆叠大量相同 Block(如 Transformer 编码器层)、动态选择执行某些层 | 必须在自定义 forward 中用 for
循环手动调用每一层 |
nn.ModuleDict |
否 | 需要根据名称(字符串 Key)动态调用不同分支(如多任务学习中的不同 Head、多数据集适配器) | 必须在自定义 forward 中通过 Key
手动查找并调用对应模块 |