Ultralytics YOLO27: Iscriviti alla lista d'attesa.
Get Started
Ultralytics YOLO in produzione·Scegli un runtime·Lezione 2/9
Lezioneintermedio

Esportare in ONNX (e verificare la parità)

Esporta un modello ONNX portabile, poi dimostra che corrisponde a PyTorch per bounding box, confidence e mAP prima di distribuire qualsiasi cosa.

Un'esportazione che sposta le confidence di 0,1 non genera errori. Sposta silenziosamente le rilevazioni oltre la soglia conf su ogni dispositivo su cui distribuisci il modello, e nessuno se ne accorge finché i conteggi non risultano errati. ONNX è il punto di partenza portabile per la distribuzione e cinque minuti di verifica della parità sono la forma di garanzia meno costosa di questo corso.

Esito

Esporta in ONNX, dimostra con i numeri che corrisponde a PyTorch per bounding box, confidence e mAP, e spiega qualsiasi differenza riscontrata.

Percorso rapido
Se sai già come muoverti, ecco la versione breve.
  1. yolo export model=yolo26n.pt format=onnx dynamic=True.

  2. Esegui un'immagine con entrambi i file; confronta il numero di bounding box, le classi, le confidence e le coordinate, non solo il conteggio.

  3. Esegui yolo val su entrambi usando lo stesso batch; mAP50-95 dovrebbe differire al massimo di 0,5 punti.

  4. Hai riscontrato una differenza? Controlla la forma dell'input (statica o dinamica), il preprocessing personalizzato e quale head (nms) hai esportato.

Pratico

Esportazione#

from ultralytics import YOLO

model = YOLO("yolo26n.pt")  # oppure runs/detect/pills_v1/weights/best.pt
model.export(format="onnx", dynamic=True)  # scrive yolo26n.onnx accanto ai pesi

Il log mostra l'opset scelto (opset 18 con onnx 1.23) e conferma che onnxslim ha semplificato il grafo. Ecco le opzioni che modificano il comportamento, tratte dalla modalità Export:

OpzionePredefinitaModificalo quando
imgsz640In produzione usi un'altra dimensione; un'esportazione statica è vincolata a quella dimensione
dynamicFalseLa dimensione del batch o dell'immagine varia durante l'inferenza
batch1Usi il batching durante l'inferenza (batch massimo per un'esportazione dinamica)
opsetultima versione supportataUn runtime meno recente rifiuta il modello; riduci questo valore
quantizeNone (FP32)16 per FP16; 8 per INT8 con calibrazione data
nmsNoneTrue incorpora NMS nel grafo; False seleziona la head senza NMS di YOLO26

Con il valore predefinito nms=None, l'esportazione restituisce le predizioni grezze e il wrapper Ultralytics esegue NMS, esattamente come PyTorch.

Verifica la parità su un'immagine nota#

Esegui la stessa immagine con entrambi i modelli e confronta ogni aspetto:

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

Ordinando entrambi gli insiemi per confidence, abbini gli stessi oggetti; le ultime due righe confrontano quindi ogni rilevazione con la corrispondente. Ora esegui il test due volte, una come mostrato e una rimuovendo dynamic=True:

EsportazioneBounding boxStesse classiDifferenza max. di confidenceDifferenza max. delle bounding box
dynamic=True5 vs 5sì0.00000.00 px
statica (predefinita)5 vs 5sì0.097710.11 px

L'esportazione statica non è difettosa. PyTorch adatta l'immagine verticale bus.jpg alla forma minima allineata allo stride, 640×480 (altezza × larghezza), mentre un grafo statico 640×640 deve aggiungere padding grigio fino a ottenere un quadrato. Il padding diverso produce predizioni leggermente diverse. Per confrontare condizioni equivalenti, esporta con dynamic=True oppure esegui la parte PyTorch con rect=False, in modo che aggiunga padding fino al quadrato completo come fa l'esportazione statica (vedi forma fissa e rettangolo minimo); con rect=False, anche l'esportazione statica corrisponde con una differenza di 0.0000.

Convalida entrambi i file#

Una sola immagine permette di rilevare gli errori più evidenti; la convalida individua quelli più sottili. Usa lo stesso batch per entrambi, così il preprocessing corrisponde:

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

Entrambi riportano mAP50-95 0.654 in questo caso. COCO8 verifica la pipeline, non il modello; sul tuo set di convalida, esamina qualsiasi differenza superiore a 0,5 punti. Ecco la verifica completa:

Chiama direttamente ONNX Runtime#

Spesso i servizi in produzione saltano il wrapper Ultralytics e chiamano direttamente il motore di inferenza. In tal caso, il preprocessing è responsabilità tua e deve corrispondere esattamente a quello di Ultralytics. Con nms=False, l'esportazione YOLO26 restituisce le rilevazioni finali, quindi non serve codice 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]

Un autobus (classe 5) e quattro persone (classe 0), con YOLO(path)(img) corrispondente al pixel. In questo caso l'autobus ha una confidence di 0,93, mentre con l'esportazione predefinita è 0,88, perché nms=False seleziona una head diversa; confronta file esportati con lo stesso valore di nms.

Insidie#

Confronto tra un'esportazione statica e l'inferenza PyTorch rettangolare

Sintomo: stessi riquadri, confidenze diverse fino a 0.1, soprattutto sulle immagini non quadrate. Soluzione: verifica la parità con dynamic=True o con rect=False sul lato PyTorch, oppure valuta l'esportazione statica in base alla mAP di validazione.

Pre-elaborazione scritta a mano diversa da quella di Ultralytics

Sintomo: il tuo codice ONNX Runtime trova meno riquadri, o riquadri spostati, rispetto a YOLO("model.onnx"). Soluzione: usa il letterbox (non deformare l'immagine), applica un padding con 114, converti da BGR a RGB, dividi per 255 e annulla il padding e il ridimensionamento dei riquadri, esattamente come fa ort_raw.py.

Mescolare le teste

Sintomo: tutte le confidenze sono leggermente diverse, anche con forme corrispondenti. Soluzione: confronta un'esportazione nms=False (senza NMS) solo con un'altra esportazione nms=False. Con nms=True, verifica anche che conf e iou incorporati nel grafo corrispondano a quelli usati altrove.

Provalo

Esporta due volte il tuo modello (o yolo26n.pt) in ONNX, una volta in modo statico e una con dynamic=True, quindi esegui parity.py su entrambe. Poi esegui yolo val su .pt e sull'esportazione dinamica .onnx con lo stesso batch e scrivi un report di parità in tre righe: la differenza massima di confidence per ciascuna esportazione e la differenza di mAP50-95.

Criteri di completamento
La lezione è completata quando tutte le seguenti condizioni sono soddisfatte.
  • Hai un file .onnx accanto a .pt e un riepilogo della parità per le esportazioni statica e dinamica.

  • Sai spiegare la differenza di confidence dell'esportazione statica e mostrare come la rimuove la correzione dynamic=True o rect=False.

  • Il mAP50-95 di validazione sul file ONNX è entro 0,5 punti da quello del modello PyTorch, oppure la differenza è spiegata.

Mostra la soluzione
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

Un report di parità per yolo26n su bus.jpg riporta: differenza massima di confidenza dell'esportazione statica pari a 0.0977 (forma letterbox, eliminata da rect=False), esportazione dinamica 0.0000, differenza mAP50-95 di 0.00 punti. Distribuisci l'esportazione dinamica.

Prossimi passi

Su hardware NVIDIA, il passaggio successivo è TensorRT, dove FP16 e INT8 sacrificano un po' di precisione per ottenere molta più velocità.

Accedi per salvare i progressi