Ultralytics YOLO27: Встать в лист ожидания.
Get Started
Ultralytics YOLO в продакшене·Выбери среду выполнения·Урок 2/9
Уроксредний

Экспорт в ONNX (и проверка соответствия)

Экспортируй переносимую модель ONNX, а затем проверь, что она совпадает с PyTorch по ограничивающим рамкам, уверенности и mAP, прежде чем что-либо выпускать.

Экспорт, который сдвигает значения уверенности на 0.1, не приводит к сбою. Он незаметно меняет результаты детекции, пересекающие порог conf, на каждом устройстве, куда ты отправляешь модель, и никто этого не замечает, пока подсчёты не начинают расходиться. ONNX — отправная точка для переносимого развёртывания, а пять минут на проверку соответствия — самая недорогая страховка в этом курсе.

Результат

Экспортируй модель в ONNX, численно подтверди её соответствие PyTorch по ограничивающим рамкам, уверенности и mAP и объясни обнаруженные расхождения.

Ускоренное прохождение
Если ты уже освоился, вот краткая версия.
  1. yolo export model=yolo26n.pt format=onnx dynamic=True.

  2. Запусти оба файла на одном изображении; сравни число рамок, классы, значения уверенности и координаты, а не только количество.

  3. Запусти yolo val на обоих файлах с одним и тем же batch; 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 упростил граф. Параметры, меняющие поведение, описаны в режиме Export:

ПараметрПо умолчаниюКогда менять
imgsz640В рабочей среде используется другой размер; статический экспорт фиксирует заданный размер
dynamicFalseРазмер пакета или изображения меняется при инференсе
batch1При инференсе используется пакетная обработка (максимальный размер пакета для динамического экспорта)
opsetпоследняя поддерживаемая версияБолее старая среда выполнения не принимает модель; понизь версию
quantizeNone (FP32)16 для FP16; 8 для INT8 с калибровкой 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, или с помощью rect=False на стороне PyTorch, либо оцени статический экспорт по 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) в ONNX дважды: со статической формой и с dynamic=True, затем запусти parity.py для каждого варианта. После этого запусти yolo val на .pt и динамическом .onnx с одинаковым batch и составь отчёт о соответствии из трёх строк: максимальная разница уверенности для каждого экспорта и разница mAP50-95.

Критерии готовности
Урок считается завершённым, если выполнены все условия ниже.
  • У тебя есть файл .onnx рядом с .pt и вывод проверки соответствия для статического и динамического экспорта.

  • Ты можешь объяснить расхождение уверенности при статическом экспорте и показать, как вариант dynamic=True или rect=False устраняет его.

  • Валидационная mAP50-95 для файла ONNX отличается от показателя модели 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 указано: максимальное расхождение confidence при статическом экспорте — 0.0977 (форма letterbox, устраняется с помощью rect=False), при динамическом экспорте — 0.0000, расхождение mAP50-95 — 0.00 пункта. Выпускай динамический экспорт.

Что дальше

На оборудовании NVIDIA следующий шаг — TensorRT: FP16 и INT8 обменивают небольшую потерю точности на значительный прирост скорости.

Войди, чтобы сохранять прогресс