第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的完整流程和关键决策点:

YOLOv11 PyTorch → ONNX 导出流程 加载YOLOv11模型 设置导出参数 imgsz | opset_version | dynamic_axes torch.onnx.export() 验证ONNX模型 通过? 是 ✅ 进入量化流程 否 ❌ 排查导出参数 关键参数:opset_version=17 | dynamic_axes={0: 'batch_size'} | do_constant_folding=True

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,结果不对就查输入范围

这一步做好了,后面的量化工作才能顺利进行。别图快,该验证的步骤一个都不能省。我见过太多人因为导出这一步没做好,后面量化、部署全白干。