4、ONNX导出基础:使用torch.onnx.export导出第一个ONNX模型
核心原理:PyTorch模型如何“翻译”为ONNX
在深入代码之前,必须先理解 torch.onnx.export 的本质。它并不是“转换”PyTorch模型文件,而是执行一次静态的符号追踪(symbolic tracing)。导出器会创建一个虚拟输入(dummy input),让输入张量真正“流过”模型的每一个算子,同时记录下每个算子的计算图结构,再将这些算子逐一映射为ONNX标准算子。因此,导出过程不依赖模型内部的Python控制流(如 if、for 循环),而是依赖实际执行过的张量路径。
这一机制带来两个关键推论:其一,导出的ONNX模型是数据流图,与PyTorch的Python对象生命周期无关;其二,动态控制流无法被直接追踪,若模型包含依赖输入数据的条件分支,导出器只会记录当前输入所走的那一条路径。理解这一点,是后续处理动态轴、自定义算子等高级话题的基础。
核心记忆点:导出过程 = 虚拟输入前向传播 + 算子映射 + 静态图生成。模型权重会被固化到ONNX文件中,因此ONNX模型不再依赖PyTorch环境即可推理。
方法流程:五步完成首次导出
下面以一个包含卷积、批归一化、ReLU和全连接层的简单分类模型为例,演示完整的导出流程。该模型结构虽小,但覆盖了CNN中最常见的算子组合,足以验证导出链路是否通畅。
步骤1:准备模型与虚拟输入
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=3, padding=1),
nn.BatchNorm2d(16),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
)
self.classifier = nn.Sequential(
nn.Flatten(),
nn.Linear(16 * 16 * 16, 128),
nn.ReLU(inplace=True),
nn.Linear(128, num_classes)
)
def forward(self, x):
return self.classifier(self.features(x))
model = SimpleCNN()
model.eval() # 切换到评估模式,固定BatchNorm统计量
# 虚拟输入:batch=1, 3通道, 32x32
dummy_input = torch.randn(1, 3, 32, 32)
关键细节:导出前必须调用
model.eval()。若遗漏,BatchNorm层会使用当前batch的统计量而非训练累积的全局统计量,导致导出的ONNX模型推理结果与PyTorch不一致。
步骤2:调用导出函数
torch.onnx.export(
model, # 待导出的PyTorch模型
dummy_input, # 虚拟输入,用于追踪计算图
"simple_cnn.onnx", # 输出文件路径
export_params=True, # 是否导出权重参数,默认True
opset_version=17, # ONNX算子集版本
input_names=["input"],
output_names=["output"],
dynamic_axes=None # 本节先不使用动态轴
)
print("导出完成!")
步骤3:验证导出文件
import onnx
# 加载并检查模型
onnx_model = onnx.load("simple_cnn.onnx")
onnx.checker.check_model(onnx_model) # 结构完整性校验
print(f"算子版本: {onnx_model.opset_import[0].version}")
print(f"输入节点: {onnx_model.graph.input[0].name}")
print(f"输出节点: {onnx_model.graph.output[0].name}")
步骤4:对比推理结果
import onnxruntime as ort
import numpy as np
# PyTorch推理
with torch.no_grad():
torch_output = model(dummy_input).numpy()
# ONNX Runtime推理
ort_session = ort.InferenceSession("simple_cnn.onnx")
ort_output = ort_session.run(
None, {"input": dummy_input.numpy()}
)[0]
# 比较最大误差
max_diff = np.abs(torch_output - ort_output).max()
print(f"最大数值误差: {max_diff:.2e}")
assert max_diff < 1e-4, "推理结果不一致!"
步骤5:可视化计算图(可选)
# 使用Netron打开(图形化界面)
# 或在Python中打印节点信息
for node in onnx_model.graph.node[:5]:
print(f"算子: {node.op_type:10s} 输入: {list(node.input)}")
案例:完整导出脚本与输出解读
将上述步骤整合为完整脚本,执行后控制台输出如下:
导出完成!
算子版本: 17
输入节点: input
输出节点: output
最大数值误差: 3.72e-06
模型文件大小: 0.3 MB
最大误差在 1e-5 量级,属于浮点运算顺序差异导致的正常误差范围。若误差超过 1e-3,则需检查模型是否包含导出不支持的算子或未正确设置 eval 模式。
下表总结了导出过程中各参数的作用与推荐设置:
| 参数 | 作用 | 推荐设置 |
|---|---|---|
opset_version |
决定可用的ONNX算子集合 | ≥ 15,推荐17或更高 |
input_names |
为输入张量命名,便于推理时指定 | 使用有意义的名称 |
dynamic_axes |
允许输入/输出的维度可变 | 需要动态batch时设置 |
export_params |
是否将权重固化到ONNX文件 | 保持默认 True |
误区与复盘:常见导出陷阱
- 误区一:忘记调用
model.eval()。这会导致BatchNorm和Dropout层行为异常。Dropout在训练模式下会随机丢弃神经元,导致导出的计算图包含随机性,推理结果不可复现。 - 误区二:使用依赖Python控制流的动态结构。例如
for循环遍历可变长度序列,或if条件判断张量属性。导出器只追踪实际执行路径,未执行的分支不会出现在ONNX图中。 - 误区三:忽略输入张量的具体数值。虽然导出使用虚拟输入,但输入的形状、数据类型会影响算子选择。例如,
int64与float32输入可能映射到不同的ONNX算子。 - 误区四:直接使用
model.state_dict()手动赋值。导出过程会自动处理权重,无需也不应手动干预。
复盘建议:每次导出后,务必执行“三步验证”——
onnx.checker 检查结构、PyTorch与ONNX Runtime输出对比、Netron可视化检查图结构。三步全部通过,才能确认导出结果可靠。