Ultralytics YOLO27: Trag dich in die Warteliste ein.
Get Started
Ultralytics YOLO in der Produktion·Laufzeitumgebung auswählen·Lektion 2/9
LektionFortgeschrittene

Nach ONNX exportieren (und Übereinstimmung überprüfen)

Exportiere ein portables ONNX-Modell und weise anschließend nach, dass es bei BBoxen, Konfidenzen und mAP mit PyTorch übereinstimmt, bevor du etwas bereitstellst.

Ein Export, bei dem sich Konfidenzen um 0.1 verschieben, stürzt nicht ab. Er verschiebt unbemerkt Erkennungen auf jedem Gerät, auf dem du ihn bereitstellst, über oder unter deinen conf-Schwellenwert. Das fällt erst auf, wenn die Zählwerte nicht stimmen. ONNX ist der portable Ausgangspunkt für die Bereitstellung. Fünf Minuten zur Überprüfung der Übereinstimmung sind die günstigste Absicherung in diesem Kurs.

Lernergebnis

Exportiere nach ONNX, weise mit Zahlen nach, dass der Export bei BBoxen, Konfidenzen und mAP mit PyTorch übereinstimmt, und erkläre jede Abweichung.

Schnellkurs
Wenn du dich bereits auskennst, findest du hier die Kurzfassung.
  1. yolo export model=yolo26n.pt format=onnx dynamic=True.

  2. Führe ein Bild durch beide Dateien und vergleiche die Anzahl der BBoxen, Klassen, Konfidenzen und Koordinaten – nicht nur die Anzahl.

  3. Führe yolo val für beide mit demselben batch aus; mAP50-95 sollte höchstens 0.5 Punkte abweichen.

  4. Eine Abweichung? Prüfe die Eingabeform (statisch oder dynamisch), deine eigene Vorverarbeitung und den exportierten Erkennungskopf (nms).

Praxisnah

Export#

from ultralytics import YOLO

model = YOLO("yolo26n.pt")  # oder runs/detect/pills_v1/weights/best.pt
model.export(format="onnx", dynamic=True)  # schreibt yolo26n.onnx neben die Gewichte

Das Protokoll zeigt die gewählte Opset-Version (opset 18 mit onnx 1.23) und dass onnxslim den Graphen vereinfacht hat. Diese Optionen aus dem Exportmodus ändern das Verhalten:

OptionStandardÄndern, wenn
imgsz640Die Bereitstellung erfolgt mit einer anderen Größe; ein statischer Export ist auf die angegebene Größe festgelegt
dynamicFalseBatchgröße oder Bildform variiert bei der Inferenz
batch1Du verwendest Batches bei der Inferenz (maximale Batchgröße für einen dynamischen Export)
opsethöchste unterstützte VersionEine ältere Laufzeitumgebung lehnt das Modell ab; verwende eine niedrigere Version
quantizeNone (FP32)16 für FP16; 8 für INT8 mit Kalibrierungsdaten data
nmsNoneTrue bindet NMS in den Graphen ein; False wählt den NMS-freien Erkennungskopf von YOLO26

Mit dem Standardwert nms=None gibt der Export Rohvorhersagen zurück und der Ultralytics-Wrapper führt NMS aus – genau wie bei PyTorch.

Übereinstimmung anhand eines bekannten Bildes überprüfen#

Führe dasselbe Bild durch beide Modelle und vergleiche alles:

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")

Durch das Sortieren beider Ergebnismengen nach Konfidenz lassen sich dieselben Objekte einander zuordnen. Die letzten beiden Zeilen vergleichen daher jede Erkennung mit ihrem Gegenstück. Führe den Vergleich nun zweimal aus: einmal wie gezeigt und einmal ohne dynamic=True:

ExportBBoxenGleiche KlassenMax. KonfidenzabweichungMax. BBox-Abweichung
dynamic=True5 gegenüber 5ja0.00000.00 px
statisch (Standard)5 gegenüber 5ja0.097710.11 px

Der statische Export ist nicht fehlerhaft. PyTorch passt das Hochformatbild bus.jpg an die kleinste stride-ausgerichtete Form an: 640×480 (Höhe × Breite). Ein statischer Graph mit 640×640 muss es dagegen mit grauen Balken auf ein Quadrat auffüllen. Durch die unterschiedliche Auffüllung weichen die Vorhersagen leicht ab. Für einen direkten Vergleich exportierst du entweder mit dynamic=True oder führst die PyTorch-Seite mit rect=False aus, damit sie wie beim statischen Export auf das vollständige Quadrat auffüllt (siehe feste Form gegenüber kleinstem Rechteck). Mit rect=False stimmt auch der statische Export bis auf 0.0000 überein.

Beide Dateien validieren#

Ein Bild deckt grobe Fehler auf; die Validierung findet subtile Abweichungen. Verwende für beide dasselbe batch, damit die Vorverarbeitung übereinstimmt:

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

Beide melden hier mAP50-95 0.654. COCO8 überprüft die Pipeline, nicht das Modell. Untersuche auf deinem eigenen Validierungsdatensatz jede Abweichung von mehr als 0.5 Punkten. Die gesamte Prüfung:

ONNX Runtime direkt aufrufen#

Produktionsdienste umgehen oft den Ultralytics-Wrapper und rufen die Inferenz-Engine direkt auf. Dann bist du für die Vorverarbeitung zuständig und musst sie exakt an Ultralytics anpassen. Mit nms=False gibt der YOLO26-Export die endgültigen Erkennungen zurück, sodass kein NMS-Code erforderlich ist:

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]

Ein Bus (Klasse 5) und vier Personen (Klasse 0); die BBoxen stimmen pixelgenau mit YOLO(path)(img) überein. Der Bus erreicht hier 0.93, beim Standardexport dagegen 0.88, weil nms=False einen anderen Erkennungskopf auswählt. Vergleiche Dateien, die mit demselben Wert für nms exportiert wurden.

Stolperfallen#

Einen statischen Export mit rechteckiger PyTorch-Inferenz vergleichen

Symptom: Dieselben Boxen, aber Konfidenzwerte weichen um bis zu 0,1 ab, am stärksten bei nicht quadratischen Bildern. Lösung: Prüfe die Übereinstimmung mit dynamic=True oder auf der PyTorch-Seite mit rect=False, oder bewerte den statischen Export anhand der Validierungs-mAP.

Von Ultralytics abweichende, selbst geschriebene Vorverarbeitung

Symptom: Dein eigener ONNX-Runtime-Code findet weniger oder verschobene Boxen als YOLO("model.onnx"). Lösung: Verwende Letterboxing (nicht strecken), fülle mit 114 auf, wandle BGR in RGB um, teile durch 255 und mache Auffüllung und Skalierung der Boxen rückgängig – genau wie ort_raw.py.

Klassifikationsköpfe mischen

Symptom: Jeder Konfidenzwert ist leicht unterschiedlich, selbst bei übereinstimmenden Formen. Lösung: Vergleiche einen nms=False-Export (ohne NMS) nur mit einem anderen nms=False-Export. Bei nms=True prüfe außerdem, ob die in den Graphen eingebetteten Werte conf und iou mit den anderweitig verwendeten Werten übereinstimmen.

Ausprobieren

Exportiere dein Modell (oder yolo26n.pt) zweimal nach ONNX: einmal statisch und einmal mit dynamic=True. Führe parity.py anschließend für beide aus. Führe dann yolo val für .pt und den dynamischen .onnx mit demselben batch aus und erstelle einen dreizeiligen Bericht zur Übereinstimmung: maximale Konfidenzabweichung für jeden Export und die mAP50-95-Abweichung.

Abgeschlossen, wenn
Du hast die Lektion abgeschlossen, wenn alle folgenden Punkte zutreffen.
  • Dir liegt eine Datei .onnx neben deiner .pt sowie ein Ausdruck der Übereinstimmungsprüfung für beide Exporte – den statischen und den dynamischen – vor.

  • Du kannst die Konfidenzabweichung des statischen Exports erklären und zeigen, wie sie sich mit dynamic=True oder rect=False beheben lässt.

  • Der Validierungs-mAP50-95-Wert der ONNX-Datei liegt höchstens 0,5 Punkte unter dem PyTorch-Modell, oder die Abweichung wird erklärt.

Lösung anzeigen
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

Ein Paritätsbericht für yolo26n auf bus.jpg lautet: maximale Konfidenzabweichung beim statischen Export 0.0977 (Letterbox-Form, durch rect=False entfernt), beim dynamischen Export 0.0000, mAP50-95-Abweichung 0.00 Punkte. Stelle den dynamischen Export bereit.

Wie es weitergeht

Auf NVIDIA-Hardware ist TensorRT der nächste Schritt: FP16 und INT8 tauschen etwas Genauigkeit gegen deutlich mehr Geschwindigkeit ein.