Ultralytics YOLO27: Inscreve-te na lista de espera.
Get Started
Ultralytics YOLO em produção·Escolha um runtime·Aula 2/9
Aulaintermediário

Exportar para ONNX (e verificar a paridade)

Exporta um modelo ONNX portátil e confirma que corresponde ao PyTorch em caixas, confianças e mAP antes de disponibilizares qualquer coisa.

Uma exportação que altera as confianças em 0.1 não causa uma falha. Altera silenciosamente as deteções que ultrapassam o teu limiar conf em todos os dispositivos para os quais fazes a implantação, e ninguém repara até as contagens estarem erradas. O ONNX é o ponto de partida portátil para a implantação, e cinco minutos a verificar a paridade são o seguro mais barato deste curso.

Resultado

Exporta para ONNX, prova com números que corresponde ao PyTorch em caixas, confianças e mAP, e explica qualquer diferença que encontrares.

Via rápida
Se já sabes orientar-te, aqui está a versão resumida.
  1. yolo export model=yolo26n.pt format=onnx dynamic=True.

  2. Executa uma imagem nos dois ficheiros; compara o número de caixas, as classes, as confianças e as coordenadas, não apenas a contagem.

  3. Executa yolo val em ambos com o mesmo batch; o mAP50-95 deverá ficar a menos de 0.5 pontos de diferença.

  4. Encontraste uma diferença? Verifica a forma de entrada (estática ou dinâmica), o teu pré-processamento e qual o cabeçalho (nms) que exportaste.

Prático

Exportação#

from ultralytics import YOLO

model = YOLO("yolo26n.pt")  # or runs/detect/pills_v1/weights/best.pt
model.export(format="onnx", dynamic=True)  # grava yolo26n.onnx ao lado dos pesos

O registo mostra o opset escolhido (opset 18 com onnx 1.23) e que o onnxslim simplificou o grafo. As opções que alteram o comportamento, no modo Export:

OpçãoPadrãoQuando alterar
imgsz640A produção usa outro tamanho; uma exportação estática fica fixa nesse tamanho
dynamicFalseO tamanho do lote ou a forma da imagem varia durante a inferência
batch1Fazes batching durante a inferência (o lote máximo para uma exportação dinâmica)
opsetmais recente suportadoUm runtime mais antigo rejeita o modelo; reduz este valor
quantizeNone (FP32)16 para FP16; 8 para INT8 com calibração data
nmsNoneTrue incorpora NMS no grafo; False seleciona o cabeçalho sem NMS do YOLO26

Com o valor predefinido nms=None, a exportação devolve previsões em bruto e o wrapper da Ultralytics executa NMS, tal como no PyTorch.

Verifica a paridade numa imagem conhecida#

Executa a mesma imagem nos dois modelos e compara tudo:

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

Ordenar ambos os conjuntos por confiança emparelha os mesmos objetos, por isso as duas últimas linhas comparam cada deteção com a sua correspondente. Agora executa-o duas vezes, uma como mostrado e outra sem dynamic=True:

ExportaçãoCaixasMesmas classesDiferença máxima de confiançaDiferença máxima das caixas
dynamic=True5 vs 5sim0.00000.00 px
estático (predefinido)5 vs 5sim0.097710.11 px

A exportação estática não está avariada. O PyTorch ajusta a imagem vertical bus.jpg à menor forma alinhada pela passada, 640×480 (altura × largura), enquanto um grafo estático de 640×640 tem de a preencher até formar um quadrado, com barras cinzentas. O preenchimento diferente produz previsões ligeiramente diferentes. Para comparar em condições equivalentes, exporta com dynamic=True ou executa o lado do PyTorch com rect=False para que preencha até ao quadrado completo, tal como a exportação estática (consulta forma fixa vs retângulo mínimo); com rect=False, a exportação estática também corresponde, com uma diferença de 0.0000.

Valida ambos os ficheiros#

Uma imagem deteta erros grosseiros; a validação deteta os mais subtis. Usa o mesmo batch para ambos, para que o pré-processamento corresponda:

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

Ambos indicam mAP50-95 0.654 neste caso. O COCO8 valida o pipeline, não o modelo; no teu próprio conjunto de validação, investiga qualquer diferença superior a 0.5 pontos. A verificação completa:

Chama diretamente o ONNX Runtime#

Os serviços de produção muitas vezes dispensam o wrapper da Ultralytics e chamam diretamente o motor de inferência. Nesse caso, o pré-processamento fica a teu cargo e tem de corresponder exatamente ao da Ultralytics. Com nms=False, a exportação do YOLO26 devolve as deteções finais, por isso não é necessário código 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]

Um autocarro (classe 5) e quatro pessoas (classe 0), correspondendo ao YOLO(path)(img) até ao píxel. O autocarro tem uma pontuação de 0.93 neste caso e de 0.88 com a exportação predefinida, porque nms=False seleciona um cabeçalho diferente; compara ficheiros exportados com o mesmo valor de nms.

Armadilhas#

Comparação de uma exportação estática com inferência retangular em PyTorch

Sintoma: mesmas caixas, mas confianças com diferença de até 0.1, principalmente em imagens não quadradas. Solução: verifique a paridade com dynamic=True, ou com rect=False no lado do PyTorch, ou avalie a exportação estática pela mAP de validação.

Pré-processamento manual diferente do Ultralytics

Sintoma: seu próprio código ONNX Runtime encontra menos caixas ou caixas deslocadas em comparação com YOLO("model.onnx"). Solução: use letterbox (não estique), preencha com 114, converta BGR para RGB, divida por 255 e desfaça o preenchimento e o redimensionamento das caixas, exatamente como faz ort_raw.py.

Misturar cabeças

Sintoma: todas as confianças são ligeiramente diferentes, mesmo com formatos correspondentes. Solução: compare uma exportação nms=False (sem NMS) somente com outra exportação nms=False. Com nms=True, confira também se conf e iou incorporados ao grafo correspondem aos que você usa em outros lugares.

Experimente

Exporta o teu modelo (ou yolo26n.pt) para ONNX duas vezes, uma estática e outra com dynamic=True, e executa parity.py em cada uma. Depois, executa yolo val no .pt e no .onnx dinâmico, com o mesmo batch, e escreve um relatório de paridade de três linhas: a diferença máxima de confiança para cada exportação e a diferença de mAP50-95.

Concluído quando
Você concluiu a aula quando todos estes critérios forem atendidos.
  • Tens um ficheiro .onnx junto do teu .pt e uma impressão dos resultados de paridade para as exportações estática e dinâmica.

  • Consegues explicar a diferença de confiança da exportação estática e mostrar a correção dynamic=True ou rect=False que a elimina.

  • O mAP50-95 de validação no arquivo ONNX está a menos de 0,5 pontos do modelo PyTorch, ou a diferença está explicada.

Mostrar solução
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

Um relatório de paridade para yolo26n em bus.jpg indica: diferença máxima de confiança na exportação estática de 0,0977 (formato letterbox, removido por rect=False), exportação dinâmica de 0,0000, diferença de mAP50-95 de 0,00 pontos. Usa a exportação dinâmica.

Próximos passos

Em hardware NVIDIA, o próximo passo é o TensorRT, em que FP16 e INT8 trocam um pouco de precisão por muito mais velocidade.

Entre para salvar seu progresso