ONNX로 내보내기(및 패리티 검증)
이식 가능한 ONNX 모델로 내보낸 다음, 배포 전에 바운딩 박스, 신뢰도, mAP에서 PyTorch와 일치하는지 검증하세요.
신뢰도 값이 0.1만큼 달라지는 내보내기 결과는 오류를 발생시키지 않습니다. 대신 배포하는 모든 장치에서 탐지가 conf 임곗값을 넘나들게 만들고, 개수가 잘못될 때까지 아무도 알아채지 못할 수 있습니다. ONNX는 배포를 위한 이식성 높은 출발점이며, 5분간의 패리티 검사는 이 과정에서 가장 저렴한 보험입니다.
실습형
내보내기#
from ultralytics import YOLO
model = YOLO("yolo26n.pt") # or runs/detect/pills_v1/weights/best.pt
model.export(format="onnx", dynamic=True) # weights와 같은 위치에 yolo26n.onnx를 기록합니다로그에서 선택된 opset(opset 18 및 onnx 1.23)과 onnxslim을 통한 그래프 단순화 여부를 확인할 수 있습니다. 내보내기 모드에서 동작에 영향을 주는 옵션은 다음과 같습니다:
| 옵션 | 기본값 | 변경하는 경우 |
|---|---|---|
imgsz | 640 | 프로덕션에서 다른 크기로 실행한다면 static 내보내기는 해당 크기로 고정됩니다. |
dynamic | False | 추론 시 배치 크기 또는 이미지 형태가 달라집니다. |
batch | 1 | 추론 시 배치 처리를 합니다(dynamic 내보내기의 최대 배치 크기). |
opset | 최신 지원 버전 | 구버전 런타임이 모델을 거부한다면 버전을 낮추세요. |
quantize | None (FP32) | FP16에는 16을 사용하고, 보정 data를 사용하는 INT8에는 8을 사용합니다. |
nms | None | True은 그래프에 NMS를 포함하고, False은 YOLO26의 NMS-free 헤드를 선택합니다. |
기본 nms=None을 사용하면 내보내기에서 원시 예측값이 반환되고 Ultralytics 래퍼가 PyTorch와 동일하게 NMS를 실행합니다.
알려진 이미지로 패리티 확인하기#
동일한 이미지를 두 모델에 입력하고 모든 항목을 비교하세요:
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 |
| static(기본값) | 5 대 5 | 예 | 0.0977 | 10.11 px |
static 내보내기에 문제가 있는 것은 아닙니다. PyTorch는 세로형 bus.jpg을 stride에 맞는 가장 작은 형태인 640×480(높이 × 너비)으로 레터박스 처리하지만, static 640×640 그래프에서는 회색 여백을 넣어 정사각형으로 패딩해야 합니다. 패딩이 다르면 예측값도 조금 달라집니다. 동일한 조건에서 비교하려면 dynamic=True을 사용해 내보내거나, PyTorch 쪽에서 rect=False를 사용해 static 내보내기처럼 전체 정사각형으로 패딩하세요(고정 형태와 최소 직사각형 비교 참고). rect=False를 사용하면 static 내보내기도 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")보다 적은 수의 박스를 찾거나 박스 위치가 이동합니다. 해결 방법: 이미지를 늘이지 말고 레터박싱하고, 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입니다(레터박스 형태이며 rect=False로 제거됨). 동적 내보내기는 0.0000이고, mAP50-95 차이는 0.00포인트입니다. 동적 내보내기를 배포하세요.