2、YOLOv11模型导出:从PyTorch到ONNX,FP32与FP16

模型训练完了,权重文件躺在 runs/train/exp/weights/best.pt 里。但 PyTorch 的 .pt 文件没法直接给部署框架用——TensorRT、OpenVINO、ONNX Runtime 都不认。所以第一步,得把模型转成通用的 ONNX 格式。

我个人习惯把这一步叫「模型翻译」。PyTorch 动态图是「解释型语言」,ONNX 是「编译型中间表示」。翻译对了,后面量化、部署才顺。翻译错了,你推理结果全是 NaN,查半天都不知道哪出的问题。

本章核心目标: 把 YOLOv11 的 PyTorch 模型导出为 ONNX,同时生成 FP32(单精度)和 FP16(半精度)两个版本,并验证导出后的模型输出与原始 PyTorch 模型一致。

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 版本冲突
版本坑: 我曾经在 ONNX 1.14 上遇到导出后某些算子不兼容的问题,降级到 1.12 就正常了。如果你导出报错,先检查 ONNX 版本。

另外,确认你的 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 导出完成")
注意: 导出 FP16 模型后,一定要用 FP16 的输入去推理。如果你用 FP32 的输入去跑 FP16 的模型,ONNX Runtime 会自动做类型转换,但会有精度损失。我遇到过有人拿 FP32 输入跑 FP16 模型,结果检测框全偏了,排查了半天才发现是类型不匹配。

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 两条路径的差异。

YOLOv11 模型导出流程 best.pt (PyTorch) 选择精度模式 FP32 路径 torch.onnx.export yolov11n_fp32.onnx FP16 路径 model.half() + torch.onnx.export yolov11n_fp16.onnx 验证:PyTorch vs ONNX 输出对比 最大误差 < 0.01 则通过 onnxsim 可简化模型

2.6 常见问题与避坑

  • 导出时报错 "Unsupported operator":升级 opset_version 到 17 或 18。如果还不行,检查模型里是否有自定义算子。
  • ONNX 推理结果全是 0:大概率是输入类型不匹配。FP16 模型必须用 FP16 输入,FP32 模型用 FP32 输入。
  • 动态 batch 不生效:检查 dynamic_axes 是否写对了。我曾经漏写了 output 的 batch 维度,结果推理时 batch 大于 1 就报错。
  • 模型文件太大(>200MB):用 onnxsim 简化,或者检查是否导出了不必要的梯度信息。
我的习惯: 每次导出后,我都会保留一份导出日志,记录 opset_version、输入尺寸、是否简化等信息。这样三个月后回来查问题,还能知道当时是怎么导的。

好了,到这里你已经掌握了 YOLOv11 模型导出的完整流程。FP32 和 FP16 两个版本都拿到了,也验证了正确性。下一步,就可以拿着这两个 ONNX 文件去做量化和部署了。