3、YOLOv11模型导出为FP32 ONNX:模型加载、ONNX导出流程、验证ONNX模型正确性
好,咱们进入实战环节。
前面两章聊了量化原理和FP16的优势,但真正动手前,得先把模型从PyTorch的“温床”里拽出来,转成ONNX这种通用格式。说白了,ONNX就是深度学习模型的“普通话”,不管你是PyTorch、TensorFlow还是别的框架训练出来的,转成ONNX后,就能在各种推理引擎(比如ONNX Runtime、TensorRT)上跑。
这一章,我就带你走一遍YOLOv11从PyTorch到FP32 ONNX的完整流程。包括怎么加载模型、怎么导出、怎么验证。每一步我都会结合项目里的实际经验来讲,尤其是那些容易踩坑的地方。
本章核心目标: 掌握YOLOv11模型导出为FP32 ONNX的标准流程,并学会验证导出后的模型是否“靠谱”。
3.1 模型加载:别小看这一步
很多人觉得加载模型不就是 torch.load() 嘛,有什么好说的?
嗯,这里要注意。YOLOv11的官方代码库(ultralytics)封装得比较深,直接 torch.load() 可能会把一些辅助模块也加载进来,导致后续导出时出问题。
我个人习惯用官方提供的 YOLO 类来加载。它会把模型结构、权重、甚至一些预处理参数都打包好,省心。
from ultralytics import YOLO
# 加载官方预训练模型(或者你自己训练好的 .pt 文件)
model = YOLO('yolo11n.pt') # 我用的是yolo11n,轻量级,演示够用
# 或者加载你自己训练的模型
# model = YOLO('runs/detect/train/weights/best.pt')
print("模型加载成功!")
print(f"模型类型: {type(model)}")
加载成功后,model 对象里包含了 model.model 这个PyTorch原生模型。我们后续导出ONNX,主要操作的就是这个 model.model。
小提示: 加载模型时,记得把模型放到正确的设备上。如果你有GPU,可以加一句 model.to('cuda')。不过导出ONNX时,我建议在CPU上操作,避免一些设备相关的算子兼容性问题。
3.2 ONNX导出流程:核心步骤拆解
导出ONNX,官方其实提供了一个非常方便的接口 model.export()。但为了让你理解背后的原理,我决定手写一遍 torch.onnx.export() 的流程。这样以后遇到自定义模型,你也能自己搞定。
整个流程可以概括为三步:
- 准备输入张量: 给模型一个“假”的输入,告诉ONNX导出器输入的形状和类型。
- 调用导出函数: 设置好参数,比如输出路径、opset版本、动态轴等。
- 检查输出: 确保导出的ONNX文件能正常加载和推理。
下面是我在项目中常用的导出脚本:
import torch
# 获取PyTorch原生模型
pt_model = model.model
# 设置模型为评估模式
pt_model.eval()
# 准备一个虚拟输入,形状为 [batch_size, 3, height, width]
# YOLOv11默认输入是640x640
dummy_input = torch.randn(1, 3, 640, 640)
# 定义ONNX导出路径
onnx_path = 'yolo11n.onnx'
# 开始导出
torch.onnx.export(
pt_model, # 要导出的模型
dummy_input, # 虚拟输入
onnx_path, # 输出路径
export_params=True, # 导出训练好的参数
opset_version=12, # ONNX算子集版本,我一般用12或13
do_constant_folding=True, # 常量折叠优化
input_names=['images'], # 输入节点名称
output_names=['output0'], # 输出节点名称
dynamic_axes={
'images': {0: 'batch_size'}, # 动态batch
'output0': {0: 'batch_size'}
}
)
print(f"ONNX模型已导出至: {onnx_path}")
这里有几个关键点,我得跟你唠叨一下:
- opset_version: 别用太低的版本(比如9),很多新算子不支持。也别用太高的(比如18),有些推理引擎还没跟上。我个人习惯用 12或13,兼容性最好。
- dynamic_axes: 这个参数很重要。它允许你导出的ONNX模型支持动态batch。我在项目中遇到过,如果这里不设置,部署时只能一次推理一张图,非常不灵活。
- do_constant_folding: 建议开启。它会把一些固定的计算(比如BN层的参数融合)在导出时就算好,减少推理时的计算量。
注意: 如果你用的是YOLOv11的官方 model.export() 方法,它内部会自动处理这些参数。但如果你自己写导出脚本,一定要记得设置 dynamic_axes,否则导出的模型是固定batch的。
3.3 验证ONNX模型正确性:别让模型“带病上岗”
导出完ONNX,别急着高兴。我曾经有一次,模型导出后没验证,直接丢到TensorRT里跑,结果推理结果全是乱的。排查了半天,才发现是导出时某个算子不兼容,被替换成了错误的实现。
所以,验证这一步,不能省。
验证主要分两个层面:
- 结构验证: 检查ONNX模型的结构是否完整,有没有错误的节点。
- 数值验证: 用同一张图,对比PyTorch模型和ONNX模型的输出结果,看误差有多大。
3.3.1 结构验证
用ONNX自带的 checker 工具,可以快速检查模型结构是否正确。
import onnx
# 加载ONNX模型
onnx_model = onnx.load(onnx_path)
# 检查模型结构
try:
onnx.checker.check_model(onnx_model)
print("ONNX模型结构验证通过!")
except onnx.checker.ValidationError as e:
print(f"ONNX模型结构验证失败: {e}")
如果这一步报错,通常是因为某些算子不兼容。你可以尝试升级ONNX版本,或者修改 opset_version。
3.3.2 数值验证
这一步才是关键。我们需要对比PyTorch和ONNX的输出差异。
import numpy as np
import onnxruntime as ort
# 准备一张测试图片(这里用随机数模拟)
test_input = np.random.randn(1, 3, 640, 640).astype(np.float32)
# ---- PyTorch推理 ----
with torch.no_grad():
pt_output = pt_model(torch.from_numpy(test_input)).numpy()
# ---- ONNX Runtime推理 ----
# 创建ONNX Runtime推理会话
ort_session = ort.InferenceSession(onnx_path)
# 获取输入输出名称
input_name = ort_session.get_inputs()[0].name
output_name = ort_session.get_outputs()[0].name
# 运行推理
ort_output = ort_session.run([output_name], {input_name: test_input})[0]
# ---- 对比结果 ----
# 计算最大误差
max_diff = np.max(np.abs(pt_output - ort_output))
print(f"PyTorch与ONNX输出最大误差: {max_diff:.6f}")
# 计算相对误差
relative_diff = np.mean(np.abs(pt_output - ort_output) / (np.abs(pt_output) + 1e-8))
print(f"平均相对误差: {relative_diff:.6f}")
一般来说,FP32的ONNX模型,最大误差应该在 1e-5 到 1e-6 这个量级。如果误差太大,说明导出过程有问题,需要排查。
我的经验: 如果误差突然变得很大(比如超过0.1),十有八九是某个算子不兼容,被ONNX Runtime用了一个精度很低的实现替代了。这时候可以尝试用 onnxruntime.quantization 工具里的 quantize_static 来定位问题算子。
3.4 本章知识结构图
为了让你更直观地理解整个流程,我画了一张SVG图。它把模型加载、ONNX导出、验证这三个核心步骤串了起来,并标注了每个环节的关键操作和注意事项。
3.5 常见问题与避坑指南
导出ONNX这件事,看起来简单,但实际项目中总会遇到各种幺蛾子。我整理了几个常见问题,希望能帮你少走弯路。
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 算子不兼容 | 导出时报错,提示某个算子不支持 | 升级ONNX版本,或降低 opset_version |
| 动态batch失效 | 推理时只能固定batch size | 检查 dynamic_axes 是否设置正确 |
| 输出误差过大 | PyTorch和ONNX结果差异明显 | 检查是否有不兼容算子,尝试用 onnxruntime.quantization 定位 |
| 模型加载失败 | ONNX Runtime加载时报错 | 用 onnx.checker.check_model() 检查结构 |
我曾经踩过的坑: 有一次导出YOLOv8(和v11结构类似)的ONNX模型,在验证时发现输出误差达到了0.5。排查了半天,发现是模型里用了一个自定义的 SiLU 激活函数,ONNX Runtime用了一个精度很低的近似实现。最后我手动把 SiLU 替换成了 ReLU,问题才解决。
所以,如果你用了自定义算子,一定要在导出前确认ONNX Runtime是否支持。如果不支持,要么换算子,要么写一个自定义的ONNX算子实现。