导出为 ONNX(并验证一致性)
导出可移植的 ONNX 模型,然后在部署前验证它在边界框、置信度和 mAP 上与 PyTorch 一致。
导出后置信度偏移 0.1 并不会导致程序崩溃。它会悄悄让你部署到的每台设备上的检测结果跨过 conf 阈值,直到计数出错才会有人发现。ONNX 是部署时可移植模型的首选起点,而花五分钟检查一致性,是本课程中成本最低的保险措施。
实践操作
导出#
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 已简化计算图。以下是会改变行为的选项,详见导出模式:
| 选项 | 默认 | 何时更改 |
|---|---|---|
imgsz | 640 | 生产环境使用另一种尺寸;静态导出会固定为该尺寸 |
dynamic | False | 推理时批量大小或图像形状会变化 |
batch | 1 | 推理时进行批处理(动态导出的最大批量大小) |
opset | 最新支持版本 | 较旧的运行时不接受该模型;请降低版本 |
quantize | None(FP32) | FP16 使用 16;INT8 使用 8,并提供校准数据 data |
nms | None | True 会在计算图中嵌入 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=True | 5 对 5 | 是 | 0.0000 | 0.00 px |
| 静态(默认) | 5 对 5 | 是 | 0.0977 | 10.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 值导出的文件。
常见问题#
症状:边界框相同,但置信度最多相差 0.1,非正方形图像上差异最大。解决方法:使用 dynamic=True 检查一致性,或在 PyTorch 端使用 rect=False 检查;也可以通过验证 mAP 评估静态导出模型。
症状:你自己的 ONNX Runtime 代码找到的边界框比 YOLO("model.onnx") 少,或位置发生偏移。解决方法:使用 letterbox(不要拉伸),用 114 填充,将 BGR 转换为 RGB,除以 255,并像 ort_raw.py 一样撤销边界框上的填充和缩放。
症状:即使形状匹配,每个置信度也略有不同。解决方法:只将 nms=False(无 NMS)导出结果与另一个 nms=False 导出结果比较。对于 nms=True,还要检查图中固化的 conf 和 iou 是否与你在其他地方使用的一致。
显示解答
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 pointsyolo26n 在 bus.jpg 上的对齐报告显示:静态导出的置信度最大差异为 0.0977(letterbox 形状导致,可通过 rect=False 消除),动态导出为 0.0000,mAP50-95 差距为 0.00 个百分点。发布动态导出版本。