3、ONNX基础:ONNX格式介绍、PyTorch模型导出为ONNX、ONNX模型可视化与调试
好,咱们进入第三章。这一章聊的是ONNX,一个在模型部署里绕不开的中间格式。
说实话,我刚开始接触部署时,对ONNX也是一头雾水。心想:我PyTorch模型跑得好好的,干嘛非要转一手?后来踩了几个坑才明白——ONNX就是那个让模型在不同框架、不同硬件之间自由穿梭的“通用语言”。
3.1 ONNX格式介绍:它到底是什么?
ONNX,全称Open Neural Network Exchange,开放神经网络交换格式。说白了,它就是一个模型的中转站。
你想想看,PyTorch训练出来的模型是.pt或.pth,TensorFlow的是.pb或.h5。这些格式彼此不认,但ONNX都能吃进去,也能吐出来。这就是它的价值所在。
ONNX的核心优势:
- 框架互通:PyTorch→ONNX→TensorFlow,反过来也行
- 硬件适配:TensorRT、OpenVINO、ONNX Runtime都原生支持ONNX
- 计算图标准化:ONNX定义了一套统一的算子集,比如Conv、Relu、Gemm
我个人习惯把ONNX看作“模型界的PDF”。你写Word文档,最终导出PDF给别人看,不管对方用WPS还是Office,打开都一样。ONNX也是这个道理。
ONNX的存储结构其实很简单。它用Protobuf序列化,里面包含:
- 计算图结构(节点、边、张量)
- 模型参数(权重、偏置)
- 输入输出元信息(名称、数据类型、形状)
嗯,这里要注意:ONNX本身不负责推理加速,它只是定义了一个标准。真正的加速,要靠TensorRT这类引擎去解析ONNX,然后做优化。
3.2 PyTorch模型导出为ONNX:实操环节
这部分我直接上代码。咱们用一个简单的分类模型做演示。
import torch
import torch.nn as nn
# 定义一个简单的CNN
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 16, 3)
self.relu = nn.ReLU()
self.pool = nn.MaxPool2d(2)
self.fc = nn.Linear(16 * 13 * 13, 10)
def forward(self, x):
x = self.pool(self.relu(self.conv1(x)))
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
model = SimpleCNN()
model.eval() # 记得切到eval模式
# 构造一个虚拟输入
dummy_input = torch.randn(1, 3, 28, 28)
# 导出ONNX
torch.onnx.export(
model, # 模型
dummy_input, # 输入张量
"simple_cnn.onnx", # 输出文件名
export_params=True, # 导出参数
opset_version=11, # 算子集版本
input_names=['input'], # 输入名称
output_names=['output'], # 输出名称
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
print("ONNX导出成功!")
这段代码里,有几个地方我特别想强调一下:
我的经验之谈:
- eval模式必须开:我见过有人忘了调model.eval(),结果导出的ONNX里带着Dropout和BatchNorm的训练行为,推理结果完全不对。
- opset_version别乱选:我建议用11或更高。太老的版本不支持某些算子,太新的版本可能TensorRT还没跟上。
- dynamic_axes很有用:如果你需要处理不同batch size的输入,一定要加上这个。否则导出的ONNX只认固定形状。
导出成功后,你会得到一个simple_cnn.onnx文件。它大概几百KB,里面包含了模型的结构和权重。
3.3 ONNX模型可视化与调试:看清你的计算图
模型导出只是第一步。更关键的是,你得确认导出的ONNX对不对。我见过太多人导出后直接扔给TensorRT,结果报错一脸懵。
可视化工具,我推荐两个:
| 工具 | 特点 | 适用场景 |
|---|---|---|
| Netron | 网页版/桌面版,拖拽即用 | 快速查看模型结构 |
| ONNX GraphSurgeon | Python库,可编程操作 | 调试和修改计算图 |
Netron用起来很简单。你打开它的网页,把.onnx文件拖进去,就能看到完整的计算图。每个节点都可以点开看属性,比如卷积的kernel size、stride、padding。
我个人习惯先用Netron扫一眼,确认输入输出对不对,有没有奇怪的算子。
如果发现有问题,比如某个算子TensorRT不支持,那就得用ONNX GraphSurgeon来修了。
import onnx
from onnx import helper, TensorProto
# 加载ONNX模型
model = onnx.load("simple_cnn.onnx")
# 打印输入输出信息
print("输入:", model.graph.input)
print("输出:", model.graph.output)
# 遍历所有节点
for node in model.graph.node:
print(f"节点名: {node.name}, 算子类型: {node.op_type}")
# 修改节点属性(示例:修改卷积的padding)
for node in model.graph.node:
if node.op_type == "Conv":
for attr in node.attribute:
if attr.name == "pads":
# 把padding从(0,0,0,0)改成(1,1,1,1)
attr.ints[:] = [1, 1, 1, 1]
# 保存修改后的模型
onnx.save(model, "simple_cnn_modified.onnx")
我曾经踩过的坑:
有一次我导出一个带Reshape操作的模型,Netron里看着没问题,但TensorRT死活报错。后来用GraphSurgeon一查,发现Reshape的shape输入是个常量节点,TensorRT不认识。我手动把这个常量节点替换成ONNX的Constant算子,问题就解决了。
所以我的建议是:导出ONNX后,先用Netron看结构,再用GraphSurgeon做一次“体检”。别嫌麻烦,这一步能省你后面大量调试时间。
3.4 常见问题与避坑指南
最后,我总结几个导出ONNX时容易翻车的地方:
- 动态控制流:如果你的模型里有if-else或for循环,ONNX可能导不出来。因为ONNX要求计算图是静态的。
- 不支持的操作:比如torch.einsum、某些高级索引操作。遇到这种情况,要么换实现方式,要么用ONNX自定义算子。
- 形状推断失败:有时候导出的ONNX里某些张量的形状是unknown。这通常是因为用了动态shape,但没配好dynamic_axes。
嗯,关于ONNX的基础就聊这么多。下一章我们会正式进入TensorRT的世界,到时候你会发现,ONNX就是连接PyTorch和TensorRT的那座桥。桥搭好了,后面就顺了。