3、PyTorch模型基础:构建简单神经网络模型,理解torch.nn.Module
3.1 核心原理:为什么一切模型都从nn.Module开始
在PyTorch中,torch.nn.Module是所有神经网络模块的基类。无论是线性层、卷积层、Transformer编码器,还是你即将导出的完整ONNX模型,最终都继承自这个类。nn.Module的核心价值在于它提供了一套参数管理与前向传播的标准化框架。当你定义一个子类并实现forward()方法时,PyTorch会自动完成以下关键工作:
- 参数注册:通过
self.xxx = nn.Parameter(...)或直接使用nn.Linear等层时,参数会被自动注册到模块的parameters()迭代器中,供优化器更新。 - 子模块追踪:嵌套的
nn.Module会被记录在children()和modules()中,使得整个模型的结构可被遍历和序列化。 - 训练/评估模式切换:调用
model.train()或model.eval()会递归影响所有子模块(如Dropout、BatchNorm的行为差异)。 - 设备迁移:
model.to(device)可将所有参数和缓冲区一次性移动到GPU或CPU。
理解nn.Module的机制,是后续进行ONNX导出的前提——因为ONNX导出过程本质上是追踪forward()执行时的数据流图,而非静态解析Python代码。因此,forward()中使用的操作必须是PyTorch可追踪的(如张量运算、nn.functional函数),而不能包含Python原生控制流(如if依赖张量值、for循环依赖张量形状)。
forward(),记录所有张量操作形成计算图。这意味着forward()中的Python逻辑(如print、assert)不会出现在ONNX中,但会影响导出时的执行路径。
3.2 方法流程:构建一个用于MNIST分类的简单神经网络
下面我们构建一个包含两个全连接层和一个ReLU激活函数的简单神经网络。该模型将作为后续ONNX导出的基础示例。整个流程分为三步:定义模型结构、实例化并检查参数、执行一次前向传播验证。
第一步:定义模型类
import torch
import torch.nn as nn
import torch.nn.functional as F
class SimpleMLP(nn.Module):
"""一个简单的多层感知机,用于MNIST分类(28x28=784输入,10类输出)"""
def __init__(self):
super(SimpleMLP, self).__init__()
# 定义网络层
self.fc1 = nn.Linear(784, 128) # 输入层 -> 隐藏层
self.fc2 = nn.Linear(128, 64) # 隐藏层 -> 第二个隐藏层
self.fc3 = nn.Linear(64, 10) # 隐藏层 -> 输出层
# Dropout用于防止过拟合(训练时随机失活)
self.dropout = nn.Dropout(0.2)
def forward(self, x):
# 前向传播逻辑
x = torch.flatten(x, start_dim=1) # 将 [batch, 1, 28, 28] 展平为 [batch, 784]
x = F.relu(self.fc1(x))
x = self.dropout(x) # 训练时生效,eval()时自动关闭
x = F.relu(self.fc2(x))
x = self.fc3(x) # 输出层不加激活,留给损失函数处理
return x
第二步:实例化并检查模型结构
model = SimpleMLP()
print(model)
# 输出:
# SimpleMLP(
# (fc1): Linear(in_features=784, out_features=128, bias=True)
# (fc2): Linear(in_features=128, out_features=64, bias=True)
# (fc3): Linear(in_features=64, out_features=10, bias=True)
# (dropout): Dropout(p=0.2, inplace=False)
# )
# 统计参数数量
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"总参数量: {total_params:,} | 可训练参数量: {trainable_params:,}")
# 输出:总参数量: 109,386 | 可训练参数量: 109,386
第三步:执行前向传播验证
# 创建模拟输入(MNIST图像:1通道,28x28)
dummy_input = torch.randn(1, 1, 28, 28)
# 切换到评估模式(关闭Dropout)
model.eval()
with torch.no_grad():
output = model(dummy_input)
print(f"输入形状: {dummy_input.shape}")
print(f"输出形状: {output.shape}")
print(f"输出张量(前5个值): {output[0, :5]}")
# 输出:
# 输入形状: torch.Size([1, 1, 28, 28])
# 输出形状: torch.Size([1, 10])
# 输出张量(前5个值): tensor([-0.0231, 0.0452, 0.0128, -0.0310, 0.0189])
model.eval()。否则,Dropout层和BatchNorm层会保留训练行为,导致导出的模型推理结果不稳定。ONNX模型通常用于推理部署,因此必须确保其处于评估模式。
3.3 案例场景:理解nn.Module的三种典型用法
在实际项目中,nn.Module的使用方式直接影响模型的可维护性和ONNX导出的顺利程度。以下是三种常见场景的对比:
| 场景 | 代码模式 | ONNX导出影响 |
|---|---|---|
| 顺序容器 (快速堆叠层) |
|
导出最稳定,结构清晰,推荐用于简单模型。 |
| 自定义forward (灵活控制数据流) |
|
可导出,但需确保所有操作都是可追踪的(不使用Python原生控制流)。 |
| 多输入/多输出 (复杂模型) |
|
导出时需要指定多个dummy_input,且ONNX中需定义多个输入节点。 |
对于本课程后续的ONNX导出实战,我们推荐使用自定义forward模式,因为它最接近真实项目中的模型写法,同时也能清晰展示数据流。但要注意:forward()中应避免使用print、if x.sum() > 0这类依赖张量值的Python控制流,否则导出时会报错或产生错误图。
3.4 误区与复盘:nn.Module使用中的常见陷阱
在构建模型和准备ONNX导出时,以下四个误区最容易导致问题。我们逐一分析并给出解决方案。
如果子类的
__init__()中没有调用super().__init__(),则nn.Module的初始化逻辑不会执行,导致参数无法注册。此时调用model.parameters()会返回空迭代器,训练时优化器无参数可更新。
误区二:在forward()中使用Python原生控制流
# 错误示例:依赖张量值的if语句
def forward(self, x):
if x.mean() > 0: # 导出ONNX时会报错
x = self.fc1(x)
else:
x = self.fc2(x)
return x
# 正确做法:使用torch.where或保持静态结构
def forward(self, x):
# 使用torch.where实现条件选择(可追踪)
x = torch.where(x.mean() > 0, self.fc1(x), self.fc2(x))
return x
误区三:混淆nn.Module与nn.functional
nn.Linear是一个模块,内部包含可学习的权重参数,需要注册到模型中。F.relu是一个函数,无参数,直接调用即可。- 如果误将
F.linear当作模块使用,会导致参数无法被优化器更新。正确做法是:有参数的层用nn.xxx,无参数的激活函数用F.xxx。
误区四:导出前未切换eval()模式
以下代码展示了错误与正确的对比:
# 错误:导出前未调用eval()
torch.onnx.export(model, dummy_input, "model.onnx") # Dropout仍在训练模式
# 正确:先eval()再导出
model.eval()
torch.onnx.export(model, dummy_input, "model.onnx",
input_names=["input"], output_names=["output"],
opset_version=17)
复盘总结:构建nn.Module模型时,请始终遵循以下清单:
- 在
__init__()中先调用super().__init__()。 - 所有含参数的层(Linear、Conv2d等)用
self.xxx = nn.xxx()定义。 - 无参数的激活函数(ReLU、Sigmoid)在
forward()中用F.xxx()调用。 - 导出ONNX前,必须调用
model.eval()并配合torch.no_grad()。 - 使用
dummy_input验证前向传播输出形状是否符合预期。