Ultralytics YOLO27: 加入候补名单。
Get Started
用于生产环境的 Ultralytics YOLO·选择运行时·课程 2/9
课程中级

导出为 ONNX(并验证一致性)

导出可移植的 ONNX 模型,然后在部署前验证它在边界框、置信度和 mAP 上与 PyTorch 一致。

导出后置信度偏移 0.1 并不会导致程序崩溃。它会悄悄让你部署到的每台设备上的检测结果跨过 conf 阈值,直到计数出错才会有人发现。ONNX 是部署时可移植模型的首选起点,而花五分钟检查一致性,是本课程中成本最低的保险措施。

学习成果

导出为 ONNX,用数据证明它在边界框、置信度和 mAP 上与 PyTorch 一致,并解释发现的任何差异。

快速通道
如果你已经熟悉相关内容,以下是简要版。
  1. yolo export model=yolo26n.pt format=onnx dynamic=True.

  2. 让同一张图像分别通过两个文件;比较边界框数量、类别、置信度和坐标,而不只是数量。

  3. 在两个模型上使用相同的 batch 运行 yolo val;mAP50-95 的差异应小于 0.5 个百分点。

  4. 发现差异?检查输入形状(静态还是动态)、你自己的预处理,以及导出的是哪个检测头(nms)。

实践操作

导出#

from ultralytics import YOLO

model = YOLO("yolo26n.pt")  # or runs/detect/pills_v1/weights/best.pt
model.export(format="onnx", dynamic=True)  # 在权重文件旁写入 yolo26n.onnx

日志会显示所选的 opset(opset 18,使用 onnx 1.23),以及 onnxslim 已简化计算图。以下是会改变行为的选项,详见导出模式:

选项默认何时更改
imgsz640生产环境使用另一种尺寸;静态导出会固定为该尺寸
dynamicFalse推理时批量大小或图像形状会变化
batch1推理时进行批处理(动态导出的最大批量大小)
opset最新支持版本较旧的运行时不接受该模型;请降低版本
quantizeNone(FP32)FP16 使用 16;INT8 使用 8,并提供校准数据 data
nmsNoneTrue 会在计算图中嵌入 NMS;False 会选择 YOLO26 的无 NMS 检测头

使用默认的 nms=None 时,导出结果会返回原始预测,由 Ultralytics 封装器运行 NMS,与 PyTorch 完全相同。

用已知图像检查一致性#

让同一张图像通过两个模型,并比较所有结果:

from ultralytics import YOLO

pt = YOLO("yolo26n.pt")
onnx = YOLO(pt.export(format="onnx", dynamic=True))

img = "https://ultralytics.com/images/bus.jpg"
a = pt(img, verbose=False)[0].boxes
b = onnx(img, verbose=False)[0].boxes

ia, ib = a.conf.argsort(descending=True), b.conf.argsort(descending=True)
print(f"boxes:         PyTorch {len(a)}  ONNX {len(b)}")
print(f"same classes:  {a.cls[ia].tolist() == b.cls[ib].tolist()}")
print(f"max conf diff: {(a.conf[ia] - b.conf[ib]).abs().max():.4f}")
print(f"max xyxy diff: {(a.xyxy[ia] - b.xyxy[ib]).abs().max():.2f} px")

按置信度对两组结果排序,就能将相同目标配对,因此最后两行会比较每个检测结果与其对应项。现在运行两次:一次按示例运行,另一次删除 dynamic=True:

导出边界框类别相同置信度最大差异边界框最大差异
dynamic=True5 对 5是0.00000.00 px
静态(默认)5 对 5是0.097710.11 px

静态导出没有问题。PyTorch 会将纵向的 bus.jpg 调整为满足步长要求的最小尺寸 640×480(高 × 宽),而静态的 640×640 计算图必须在图像周围填充灰色区域,使其变成正方形。填充方式不同,预测结果也会略有差异。要进行公平比较,你可以使用 dynamic=True 导出,或在 PyTorch 端使用 rect=False,使其像静态导出一样填充到完整的正方形(参见固定尺寸与最小矩形);使用 rect=False 时,静态导出的差异也会达到 0.0000。

验证两个文件#

一张图像可以发现明显错误;验证则能发现细微问题。两个模型都使用相同的 batch,以确保预处理一致:

yolo val model=yolo26n.pt data=coco8.yaml batch=1
yolo val model=yolo26n.onnx data=coco8.yaml batch=1

这里两者都报告 mAP50-95 0.654。COCO8 能验证流水线,但不能验证模型;在你自己的验证集上,任何超过 0.5 个百分点的差异都值得调查。完整检查如下:

直接调用 ONNX Runtime#

生产服务通常会跳过 Ultralytics 封装器,直接调用推理引擎。这时预处理就由你负责,而且必须与 Ultralytics 完全一致。使用 nms=False 时,YOLO26 导出模型会返回最终检测结果,因此无需编写 NMS 代码:

import cv2
import numpy as np
import onnxruntime as ort

from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download

path = YOLO("yolo26n.pt").export(format="onnx", nms=False)  # NMS-free head: output (1, 300, 6)
session = ort.InferenceSession(path, providers=["CPUExecutionProvider"])  # put CUDAExecutionProvider first on GPU
name = session.get_inputs()[0].name

# Preprocess like Ultralytics: letterbox to 640x640 with gray (114) padding, BGR->RGB, 0-1, NCHW
im = cv2.imread(safe_download("https://ultralytics.com/images/bus.jpg"))
h, w = im.shape[:2]
r = 640 / max(h, w)
nh, nw = round(h * r), round(w * r)
top, left = (640 - nh) // 2, (640 - nw) // 2
canvas = np.full((640, 640, 3), 114, np.uint8)
canvas[top : top + nh, left : left + nw] = cv2.resize(im, (nw, nh), interpolation=cv2.INTER_LINEAR)
x = canvas[..., ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255

(out,) = session.run(None, {name: x})
det = out[0][out[0][:, 4] > 0.25]  # rows: x1, y1, x2, y2, score, class
det[:, [0, 2]] = ((det[:, [0, 2]] - left) / r).clip(0, w)  # undo padding and scale
det[:, [1, 3]] = ((det[:, [1, 3]] - top) / r).clip(0, h)
for x1, y1, x2, y2, score, cls in det:
    print(f"class {int(cls):2d}  score {score:.2f}  box [{x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}]")
class  5  score 0.93  box [6, 229, 807, 749]
class  0  score 0.92  box [47, 399, 237, 902]
class  0  score 0.90  box [223, 405, 345, 863]
class  0  score 0.86  box [669, 391, 810, 880]
class  0  score 0.52  box [0, 554, 65, 874]

一个公交车(类别 5)和四个人(类别 0),与 YOLO(path)(img) 像素级一致。这里公交车的置信度为 0.93,而使用默认导出时为 0.88,因为 nms=False 选择了不同的检测头;请比较使用相同 nms 值导出的文件。

常见问题#

比较静态导出与矩形输入的 PyTorch 推理

症状:边界框相同,但置信度最多相差 0.1,非正方形图像上差异最大。解决方法:使用 dynamic=True 检查一致性,或在 PyTorch 端使用 rect=False 检查;也可以通过验证 mAP 评估静态导出模型。

手写的预处理与 Ultralytics 不一致

症状:你自己的 ONNX Runtime 代码找到的边界框比 YOLO("model.onnx") 少,或位置发生偏移。解决方法:使用 letterbox(不要拉伸),用 114 填充,将 BGR 转换为 RGB,除以 255,并像 ort_raw.py 一样撤销边界框上的填充和缩放。

混用不同的检测头

症状:即使形状匹配,每个置信度也略有不同。解决方法:只将 nms=False(无 NMS)导出结果与另一个 nms=False 导出结果比较。对于 nms=True,还要检查图中固化的 conf 和 iou 是否与你在其他地方使用的一致。

试试看

将你的模型(或 yolo26n.pt)分别以静态和 dynamic=True 方式导出为 ONNX,然后分别运行 parity.py。接着,使用相同的 batch,对 .pt 和动态的 .onnx 运行 yolo val,并写一份三行的一致性报告:列出每种导出的最大置信度差异,以及 mAP50-95 的差距。

完成条件
满足以下所有条件,即表示你已完成本课程。
  • 你有一个 .onnx 文件,以及与之对应的 .pt,并且已打印静态和动态导出的对比结果。

  • 你能解释静态导出的置信度差异,并展示如何使用 dynamic=True 或 rect=False 消除差异。

  • ONNX 文件上的验证 mAP50-95 与 PyTorch 模型相差不超过 0.5 个百分点,或者已说明差距原因。

显示解答
from ultralytics import YOLO

if __name__ == "__main__":  # validation starts worker processes on macOS and Windows
    weights = "yolo26n.pt"  # or runs/detect/pills_v1/weights/best.pt
    onnx_path = YOLO(weights).export(format="onnx", dynamic=True)

    maps = {}
    for w in (weights, onnx_path):
        maps[w] = YOLO(w).val(data="coco8.yaml", batch=1, plots=False).box.map  # or my_dataset/data.yaml
        print(f"{w}: mAP50-95 = {maps[w]:.4f}")
    print(f"gap = {abs(maps[weights] - maps[onnx_path]) * 100:.2f} points")
yolo26n.pt: mAP50-95 = 0.6540
yolo26n.onnx: mAP50-95 = 0.6540
gap = 0.00 points

yolo26n 在 bus.jpg 上的对齐报告显示:静态导出的置信度最大差异为 0.0977(letterbox 形状导致,可通过 rect=False 消除),动态导出为 0.0000,mAP50-95 差距为 0.00 个百分点。发布动态导出版本。

接下来做什么

在 NVIDIA 硬件上,下一步是使用 TensorRT:FP16 和 INT8 会牺牲少量精度来换取大幅提速。

登录以保存学习进度