第3章:YOLOv11模型导出与ONNX转换
模型量化之前,有一道绕不过去的坎——把PyTorch模型转成ONNX。
我见过太多人在这一步翻车了。模型在PyTorch里跑得好好的,一导出就报错,或者导出来推理结果全是NaN。说白了,ONNX导出不是简单的torch.onnx.export一把梭,里面有不少门道。
这一章,咱们就把YOLOv11从PyTorch到ONNX的完整流程捋一遍。动态batch怎么设、Opset选哪个版本、导出来怎么验证——这些坑我都踩过,你就不用再踩了。
3.1 为什么非要转ONNX?
PyTorch模型直接做量化行不行?行,但有限制。
ONNX相当于一个中间表示层。它把PyTorch的动态图“冻结”成静态计算图,这样TensorRT、OpenVINO、ONNX Runtime这些推理引擎才能读懂。量化工具链也大多基于ONNX做校准和转换。
我个人的习惯是:所有部署项目,第一步先把模型转成ONNX。哪怕后面还要转别的格式,ONNX也是那个“锚点”。
核心原则:ONNX是模型部署的“通用语言”。PyTorch模型是开发态,ONNX是部署态。两者之间需要一次彻底的“翻译”。
3.2 从PyTorch导出ONNX:基础操作
YOLOv11的导出其实比V5/V8时代简单了不少。Ultralytics官方已经封装好了导出接口,但咱们得知道底层在干什么。
3.2.1 最简单的导出方式
from ultralytics import YOLO
# 加载训练好的模型
model = YOLO('yolo11n.pt')
# 导出ONNX
model.export(format='onnx', imgsz=640)
就这一行,模型就导出来了。但——这只是“能用”,离“好用”还差几步。
3.2.2 手动导出:掌握控制权
我建议你学会手动导出。为什么呢?因为自动导出有时候会隐藏一些细节,出了问题不好排查。
import torch
import torch.onnx
from ultralytics.nn.tasks import attempt_load_weights
# 加载模型权重
model = attempt_load_weights('yolo11n.pt', device='cpu')
model.eval()
# 构造虚拟输入
dummy_input = torch.randn(1, 3, 640, 640)
# 导出ONNX
torch.onnx.export(
model,
dummy_input,
'yolo11n.onnx',
input_names=['images'],
output_names=['output0'],
dynamic_axes={
'images': {0: 'batch_size'},
'output0': {0: 'batch_size'}
},
opset_version=17,
do_constant_folding=True
)
这里有几个关键参数,咱们一个一个说。
3.3 动态batch设置:灵活还是性能?
动态batch,说白了就是让ONNX模型能接受不同大小的batch输入。
举个例子:你训练时batch size是32,但线上推理可能一次只来1张图,或者有时候来8张。如果没有动态batch,模型就只能接受固定batch size,多了少了都不行。
我的建议:除非你明确知道线上batch size永远固定,否则一定要开动态batch。我在一个安防项目中吃过亏——模型导出时没设动态batch,结果线上服务端只能凑够32张图才推理,延迟直接爆炸。
动态batch的配置就在dynamic_axes参数里:
dynamic_axes = {
'images': {0: 'batch_size'}, # 输入的第0维是batch
'output0': {0: 'batch_size'} # 输出的第0维也对应batch
}
这样导出来的ONNX,batch size就是可变的。你传1张、8张、32张都行。
注意:动态batch会带来一点性能损失。因为推理引擎没法做针对固定batch的极致优化。但相比灵活性上的收益,这点损失完全可以接受。
3.4 Opset版本选择:不是越新越好
Opset是ONNX的操作集版本号。每个版本会新增、废弃或修改一些算子。
选Opset版本,我有个原则:够用就行,不追新。
| Opset版本 | PyTorch支持 | TensorRT支持 | ONNX Runtime支持 | 我的推荐 |
|---|---|---|---|---|
| 11 | ✅ 完整 | ✅ 完整 | ✅ 完整 | 兼容性首选 |
| 13 | ✅ 完整 | ✅ 完整 | ✅ 完整 | 常用版本 |
| 17 | ✅ 完整 | ⚠️ 部分支持 | ✅ 完整 | YOLOv11推荐 |
| 18+ | ⚠️ 部分支持 | ⚠️ 支持有限 | ✅ 完整 | 不推荐 |
YOLOv11里用到了SiLU激活函数和一些较新的算子,Opset 11在某些场景下会导出失败。我测试下来,Opset 17是YOLOv11的最佳选择——PyTorch原生支持,TensorRT也能转,ONNX Runtime更是没问题。
如果你要部署到TensorRT上,记得先查一下TensorRT对应版本的ONNX解析器支持到哪个Opset。我曾经因为Opset选太高,TensorRT解析不了,折腾了两天才发现是版本不匹配。
3.5 验证ONNX模型正确性
导出完了,别急着往下走。先验证一下模型对不对。
我见过最离谱的情况:模型导出来了,推理结果全是0,但没人发现,直接拿去量化,量化完又拿去上线……最后排查了三天才发现是导出时某个算子被错误折叠了。
3.5.1 用ONNX Runtime跑一遍推理
import onnxruntime as ort
import numpy as np
# 创建推理会话
session = ort.InferenceSession('yolo11n.onnx')
# 构造输入
input_name = session.get_inputs()[0].name
dummy_input = np.random.randn(1, 3, 640, 640).astype(np.float32)
# 推理
outputs = session.run(None, {input_name: dummy_input})
# 检查输出形状
print(f"输出形状: {outputs[0].shape}")
# 期望: (1, 84, 8400) 对于YOLOv11
这一步能验证两件事:
- 模型能不能正常加载和推理
- 输出形状对不对
3.5.2 数值一致性对比
光能跑还不够,还得看结果对不对。
# 用同一张图在PyTorch和ONNX上分别推理
import torch
from PIL import Image
from torchvision import transforms
# 预处理
img = Image.open('test.jpg')
transform = transforms.Compose([
transforms.Resize((640, 640)),
transforms.ToTensor(),
])
input_tensor = transform(img).unsqueeze(0)
# PyTorch推理
with torch.no_grad():
pt_output = model(input_tensor)
# ONNX推理
ort_input = input_tensor.numpy()
ort_output = session.run(None, {input_name: ort_input})
# 对比差异
diff = np.abs(pt_output.numpy() - ort_output[0])
print(f"最大差异: {diff.max():.6f}")
print(f"平均差异: {diff.mean():.6f}")
验收标准:最大差异 < 1e-3,平均差异 < 1e-5。如果差异过大,说明导出过程中有算子精度丢失或计算图被错误优化。
3.5.3 用onnxruntime的验证工具
ONNX官方提供了一个验证工具,可以自动对比PyTorch和ONNX的输出:
import onnx
from onnx.tools import check_model
# 检查模型结构
onnx_model = onnx.load('yolo11n.onnx')
check_model(onnx_model)
print("模型结构验证通过")
这个工具能检查出一些明显的结构问题,比如节点连接错误、输入输出不匹配等。
3.6 完整导出流程:一张图看懂
下面这张图总结了YOLOv11从PyTorch到ONNX的完整流程和关键决策点:
3.7 常见问题与避坑指南
导出ONNX时,有几个问题特别容易遇到。我把自己踩过的坑列出来,你遇到了直接对照排查。
问题1:导出时报错“Unsupported operator”
原因:Opset版本太低,不支持某些算子。
解决:升级Opset版本。YOLOv11建议用17。
问题2:导出成功但推理结果全是NaN
原因:通常是某个算子在ONNX中的实现有精度问题,或者输入数据范围不对。
解决:检查输入是否归一化到[0,1]或[0,255];尝试关闭do_constant_folding。
问题3:动态batch导出来,但推理引擎报错
原因:有些推理引擎对动态batch的支持有限制,比如TensorRT需要额外配置。
解决:先确认目标推理引擎是否支持动态batch;如果不支持,导出时固定batch size。
我的经验:导出ONNX时,先保证能跑通,再追求性能。第一步用最简单的参数导出,验证正确性;第二步再调优参数,比如开启do_constant_folding、调整Opset版本。两步分开做,出问题好定位。
3.8 本章小结
YOLOv11的ONNX导出,说白了就是三件事:
- 设对参数:Opset选17,动态batch按需开启
- 验证正确性:ONNX Runtime跑一遍,数值对比不能少
- 排查问题:算子不支持就升Opset,结果不对就查输入范围
这一步做好了,后面的量化工作才能顺利进行。别图快,该验证的步骤一个都不能省。我见过太多人因为导出这一步没做好,后面量化、部署全白干。