4、ONNX基础:什么是ONNX?PyTorch模型转ONNX、ONNX算子兼容性检查
好,咱们进入第四讲。前面几章我们把目标检测的模型选型、训练流程都捋了一遍。现在模型训好了,权重文件躺在硬盘里,接下来干嘛?部署啊!
但部署有个很现实的问题:你训练用的框架是PyTorch,可生产环境可能是TensorRT、OpenVINO,或者干脆就是个手机端。总不能每换一个平台就重写一遍模型吧?
这时候,ONNX就登场了。
4.1 什么是ONNX?
ONNX,全称Open Neural Network Exchange,开放神经网络交换格式。说白了,它就是深度学习模型界的“通用语言”。
你想想看,PyTorch有它自己的模型格式(.pt/.pth),TensorFlow有它的SavedModel,Caffe有它的.prototxt。这些格式互不兼容,就像各国说各地方言。ONNX就是那个“普通话”——不管你用什么框架训练,导出成ONNX之后,任何支持ONNX的推理引擎都能读懂。
ONNX的核心价值就两点:
- 互操作性:一次导出,到处运行。从PyTorch到TensorRT,从TensorFlow到ONNX Runtime,无缝切换。
- 推理优化:ONNX本身是一个计算图描述格式。很多推理引擎(比如ONNX Runtime、TensorRT)会针对ONNX图做算子融合、内存优化,推理速度往往比原始框架快不少。
重要概念:ONNX不是推理引擎,它是模型描述格式。
你导出ONNX文件后,还需要用ONNX Runtime或者转成TensorRT引擎才能跑推理。别搞混了。
我个人习惯把ONNX看作“中间表示层”。训练用PyTorch,部署用ONNX,各司其职。我在项目中遇到过不少团队,直接在PyTorch里做推理部署,结果线上环境装了一堆Python依赖,模型加载慢、推理延迟高。后来统一走ONNX + ONNX Runtime,问题全解决了。
4.2 PyTorch模型转ONNX
好,理论说完了,咱们直接上手。把PyTorch模型转成ONNX,核心就一个函数:torch.onnx.export()。
先看一个最简单的例子。假设我们有一个训练好的分类模型:
import torch
import torchvision.models as models
# 1. 加载模型,设为eval模式
model = models.resnet18(pretrained=True)
model.eval()
# 2. 构造一个虚拟输入
dummy_input = torch.randn(1, 3, 224, 224)
# 3. 导出ONNX
torch.onnx.export(
model, # 模型
dummy_input, # 输入张量
"resnet18.onnx", # 输出文件名
export_params=True, # 导出训练好的参数
opset_version=11, # ONNX算子集版本
do_constant_folding=True, # 常量折叠优化
input_names=['input'], # 输入节点名称
output_names=['output'], # 输出节点名称
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
print("ONNX导出成功!")
这段代码看着简单,但有几个坑我得提醒你:
- 必须调用model.eval():不设成eval模式,BatchNorm和Dropout的行为会乱掉。我见过有人忘了这步,导出的ONNX推理结果跟PyTorch对不上,排查了半天。
- dummy_input的shape要跟实际输入一致:尤其是目标检测模型,输入尺寸通常是固定的(比如640x640)。如果你训练时用了动态尺寸,那就要设置dynamic_axes。
- opset_version别选太新:我一般用opset=11或12。太新的版本(比如18、19)有些推理引擎还没完全支持,导出容易报错。
小技巧:如果你不确定用哪个opset版本,可以先试试11。这是最广泛支持的版本,兼容性最好。
对于目标检测模型,比如YOLOv5或YOLOv8,导出时还要注意后处理部分。PyTorch版本的YOLO通常包含NMS(非极大值抑制)操作,但ONNX标准算子集里没有NMS算子。怎么办?
两种方案:
- 导出时不包含NMS:只导出Backbone + Neck + Head部分,输出原始的检测框和置信度。后处理在推理端用Python或C++实现。
- 使用ONNX自定义算子:把NMS封装成自定义算子,导出时带上。但这样推理端也要注册对应的算子实现,比较麻烦。
我个人建议用方案一。原因很简单:保持ONNX的纯净性。后处理代码写起来也不复杂,而且方便调试。
4.3 ONNX算子兼容性检查
模型导出成功了,是不是就万事大吉了?
别急。你导出的ONNX文件,拿到目标推理引擎上,可能跑不起来。为什么?算子不兼容。
举个例子:PyTorch里有个torch.nn.functional.grid_sample,这个操作在ONNX的opset 11里是不支持的。如果你模型里用了它,导出时要么报错,要么自动拆成多个基础算子。但拆完之后,推理引擎能不能正确执行,又是另一回事。
所以,导出之后一定要做兼容性检查。我常用的工具有两个:
4.3.1 使用ONNX Runtime检查
最简单的方法,直接用ONNX Runtime加载模型,跑一次推理:
import onnxruntime as ort
import numpy as np
# 加载ONNX模型
session = ort.InferenceSession("resnet18.onnx")
# 获取输入输出信息
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
# 构造输入数据
dummy_input = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 跑推理
outputs = session.run([output_name], {input_name: dummy_input})
print("ONNX Runtime推理成功!")
print("输出shape:", outputs[0].shape)
如果这一步没报错,说明你的ONNX模型至少能在ONNX Runtime上跑通。但注意,这只能验证ONNX Runtime的兼容性,换到TensorRT或者OpenVINO上可能还有问题。
4.3.2 使用onnx.checker检查模型结构
ONNX官方提供了一个检查工具,可以验证模型图结构的合法性:
import onnx
# 加载ONNX模型
model = onnx.load("resnet18.onnx")
# 检查模型结构
onnx.checker.check_model(model)
print("ONNX模型结构检查通过!")
这个检查会验证:节点连接是否正确、输入输出是否匹配、算子版本是否合法等。如果这里报错,说明你的ONNX文件本身就有问题,需要重新导出。
注意:onnx.checker只检查结构合法性,不检查数值精度。也就是说,模型结构没问题,但推理结果可能跟PyTorch不一致。这种情况我遇到过好几次,尤其是量化模型或者用了自定义算子的情况。
4.3.3 数值精度对比
这是最严格的一步。把PyTorch的输出和ONNX Runtime的输出做对比:
import torch
import onnxruntime
import numpy as np
# PyTorch推理
model.eval()
with torch.no_grad():
pt_output = model(torch.from_numpy(dummy_input)).numpy()
# ONNX Runtime推理
session = onnxruntime.InferenceSession("resnet18.onnx")
ort_output = session.run(None, {input_name: dummy_input})[0]
# 对比
diff = np.abs(pt_output - ort_output)
print("最大误差:", diff.max())
print("平均误差:", diff.mean())
一般来说,误差在1e-5以内都是正常的。如果误差很大,说明导出过程中某些算子被替换了,或者精度丢失了。这时候就要检查模型里有没有不支持的算子,或者考虑换一个opset版本。
4.4 常见算子兼容性问题
我在项目中踩过的坑,列出来给你参考:
| PyTorch算子 | ONNX支持情况 | 建议方案 |
|---|---|---|
| torch.nn.functional.grid_sample | opset 16+ 才支持 | 升级opset,或用自定义算子 |
| torch.nn.functional.interpolate(mode='bilinear') | opset 11+ 支持 | 一般没问题,注意align_corners参数 |
| torch.split / torch.chunk | opset 11+ 支持 | 注意split sizes参数必须是常量 |
| torch.nonzero | opset 9+ 支持 | 输出shape是动态的,注意dynamic_axes设置 |
| torch.sort / torch.topk | opset 11+ 支持 | NMS中常用,一般没问题 |
避坑指南:我曾经把一个用了torch.meshgrid的模型导出ONNX,结果在opset 11下报错。查了半天才发现,meshgrid在opset 11里只支持2个输入,而我用了3个。后来升级到opset 13才解决。所以,遇到算子问题,先查ONNX官方文档,看看你用的opset版本到底支持哪些算子。
4.5 总结
这一章我们聊了ONNX的基础知识。说白了,ONNX就是模型部署的“中间人”。它帮你把PyTorch模型转换成通用格式,然后你可以在任何支持ONNX的推理引擎上跑。
核心要点就三个:
- 导出时记得设
model.eval(),选合适的opset版本 - 导出后一定要做兼容性检查,包括结构检查和数值精度对比
- 遇到算子不兼容,优先考虑升级opset版本,或者把复杂操作拆成基础算子
下一章,我们会深入ONNX Runtime,看看怎么用它做高性能推理。到时候我会分享一些我在项目中用到的优化技巧,保证让你眼前一亮。
嗯,今天就到这儿。有问题随时找我。