2、YOLOv11模型导出:从PyTorch到ONNX,FP32与FP16
模型训练完了,权重文件躺在 runs/train/exp/weights/best.pt 里。但 PyTorch 的 .pt 文件没法直接给部署框架用——TensorRT、OpenVINO、ONNX Runtime 都不认。所以第一步,得把模型转成通用的 ONNX 格式。
我个人习惯把这一步叫「模型翻译」。PyTorch 动态图是「解释型语言」,ONNX 是「编译型中间表示」。翻译对了,后面量化、部署才顺。翻译错了,你推理结果全是 NaN,查半天都不知道哪出的问题。
2.1 导出前的准备工作
先确认环境。YOLOv11 官方仓库已经集成了导出脚本,但有几个依赖需要手动装一下。
# 核心依赖
pip install onnx>=1.12.0
pip install onnxsim>=0.4.33 # 用于简化 ONNX 图结构
pip install onnxruntime-gpu # 用于验证推理
# 可选但推荐
pip install protobuf==3.20.3 # 避免 protobuf 版本冲突
另外,确认你的 PyTorch 版本 ≥ 2.0。YOLOv11 用了一些较新的算子,老版本 PyTorch 的 torch.onnx.export 可能不支持。
2.2 导出 FP32 ONNX 模型
YOLOv11 官方提供了 export.py,但为了讲清楚原理,我带你手写导出流程。这样后面遇到自定义模型时,你也能自己改。
import torch
import torch.nn as nn
from ultralytics import YOLO
# 1. 加载训练好的模型
model = YOLO('runs/train/exp/weights/best.pt')
# 2. 获取原始 PyTorch 模型(去掉 ultralytics 封装)
pt_model = model.model
pt_model.eval()
# 3. 构造 dummy 输入(batch=1, channel=3, height=640, width=640)
dummy_input = torch.randn(1, 3, 640, 640).to('cuda')
# 4. 导出 ONNX
torch.onnx.export(
pt_model, # 模型
dummy_input, # 示例输入
'yolov11n_fp32.onnx', # 输出路径
opset_version=17, # 算子集版本
input_names=['images'], # 输入名称
output_names=['output0'], # 输出名称
dynamic_axes={ # 动态 batch 支持
'images': {0: 'batch_size'},
'output0': {0: 'batch_size'}
}
)
print("FP32 ONNX 导出完成")
这里有几个关键点:
- opset_version=17:我建议用 17,兼容性好。低于 15 的话,一些 reshape 和 slice 算子可能导出失败。
- dynamic_axes:让 ONNX 支持动态 batch。实际部署时,你可以一次推理 1 张图,也可以一次推理 8 张图,不用重新导出。
- dummy_input 的尺寸:必须和训练时一致。YOLOv11 默认输入是 640x640,如果你训练时用了 1280,这里也要改成 1280。
onnxsim 简化模型,去掉冗余的 Identity 和 Cast 节点,能减小 10%~20% 的文件体积。
onnxsim yolov11n_fp32.onnx yolov11n_fp32_sim.onnx
2.3 导出 FP16 ONNX 模型
FP16 模型体积是 FP32 的一半,推理速度在 TensorRT 上能快 1.5~2 倍。但精度会略微下降,一般 mAP 掉 0.1~0.3 个点,可以接受。
导出 FP16 ONNX 有两种方式:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 方式一:PyTorch 转半精度再导出 | 将模型权重转为 half(),然后导出 ONNX |
简单直接,一行代码 | 某些算子(如 BatchNorm)在 FP16 下可能不稳定 |
| 方式二:导出 FP32 ONNX 后再转 FP16 | 用 onnxconverter-common 的 float16 工具转换 |
更可控,可以指定哪些算子保留 FP32 | 多一个依赖,步骤稍多 |
我个人更常用方式一,因为简单。但要注意,导出前要把 dummy_input 也转成 FP16。
# 方式一:PyTorch 转半精度导出
pt_model.half() # 模型权重转 FP16
dummy_input_fp16 = dummy_input.half() # 输入也转 FP16
torch.onnx.export(
pt_model,
dummy_input_fp16,
'yolov11n_fp16.onnx',
opset_version=17,
input_names=['images'],
output_names=['output0'],
dynamic_axes={
'images': {0: 'batch_size'},
'output0': {0: 'batch_size'}
}
)
print("FP16 ONNX 导出完成")
2.4 验证导出模型的正确性
模型导出完了,怎么知道它没坏?最直接的方法:拿同一张图,分别用 PyTorch 模型和 ONNX 模型推理,对比输出结果。
YOLOv11 的输出是一个 [1, 84, 8400] 的张量。其中 84 是 4 个框坐标 + 80 个类别概率,8400 是三个尺度上的预测框总数。我们对比这个张量的数值差异。
import onnxruntime as ort
import numpy as np
# 1. PyTorch 推理
with torch.no_grad():
pt_output = pt_model(dummy_input_fp16).cpu().numpy()
# 2. ONNX Runtime 推理
ort_session = ort.InferenceSession('yolov11n_fp16.onnx')
ort_input = {ort_session.get_inputs()[0].name: dummy_input_fp16.cpu().numpy()}
ort_output = ort_session.run(None, ort_input)[0]
# 3. 计算差异
diff = np.abs(pt_output - ort_output)
max_diff = diff.max()
mean_diff = diff.mean()
print(f"最大误差: {max_diff:.6f}")
print(f"平均误差: {mean_diff:.6f}")
# 4. 判断标准
if max_diff < 0.01:
print("✅ 导出验证通过!模型输出一致。")
else:
print("❌ 导出验证失败,请检查模型或导出参数。")
为什么阈值设 0.01?因为 FP16 的精度有限,两个 FP16 数之间的误差在 0.001~0.01 之间是正常的。如果误差超过 0.1,那肯定有问题。
- FP32 模型:最大误差 < 1e-5
- FP16 模型:最大误差 < 0.01
- 如果误差很大,先检查
opset_version和dynamic_axes设置
2.5 本章知识结构图
下面这张图总结了从 PyTorch 到 ONNX 的完整流程,以及 FP32 和 FP16 两条路径的差异。
2.6 常见问题与避坑
- 导出时报错 "Unsupported operator":升级 opset_version 到 17 或 18。如果还不行,检查模型里是否有自定义算子。
- ONNX 推理结果全是 0:大概率是输入类型不匹配。FP16 模型必须用 FP16 输入,FP32 模型用 FP32 输入。
- 动态 batch 不生效:检查
dynamic_axes是否写对了。我曾经漏写了 output 的 batch 维度,结果推理时 batch 大于 1 就报错。 - 模型文件太大(>200MB):用
onnxsim简化,或者检查是否导出了不必要的梯度信息。
好了,到这里你已经掌握了 YOLOv11 模型导出的完整流程。FP32 和 FP16 两个版本都拿到了,也验证了正确性。下一步,就可以拿着这两个 ONNX 文件去做量化和部署了。