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) # 重みと同じフォルダーにyolo26n.onnxを書き出しますログには、選択されたopset(onnx 1.23でのopset 18)と、onnxslimによってグラフが簡略化されたことが表示されています。エクスポートモードで変更できる動作に関わるオプションは次のとおりです。
| オプション | デフォルト | 変更する場合 |
|---|---|---|
imgsz | 640 | 本番環境で別のサイズを使用する場合、静的エクスポートではサイズが固定されます |
dynamic | False | 推論時にバッチサイズまたは画像形状が変わる場合 |
batch | 1 | 推論時にバッチ処理を行う場合(動的エクスポートの最大バッチサイズ) |
opset | サポートされている最新バージョン | 古いランタイムでモデルが拒否される場合は、値を下げます |
quantize | None(FP32) | FP16には16、INT8にはキャリブレーション用のdataを指定して8を使用します |
nms | None | TrueはグラフにNMSを組み込みます。FalseはYOLO26のNMSフリーヘッドを選択します |
デフォルトのnms=Noneでは、エクスポート結果として生の予測が返され、PyTorchと同様にUltralyticsラッパーが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")両方の検出結果を信頼度順に並べると同じ物体が対応するため、最後の2行で各検出結果と対応する結果を比較できます。次に、表示どおりに実行する場合と、dynamic=Trueを削除する場合の2回実行してください。
| エクスポート | ボックス | クラスが一致 | 信頼度の最大差 | ボックスの最大差 |
|---|---|---|---|---|
dynamic=True | 5対5 | はい | 0.0000 | 0.00 px |
| 静的(デフォルト) | 5対5 | はい | 0.0977 | 10.11 px |
静的エクスポートに問題があるわけではありません。PyTorchでは、縦長のbus.jpgはストライドに合わせた最小サイズの640×480(高さ×幅)にレターボックス化されます。一方、静的な640×640のグラフでは、灰色の帯で正方形になるようパディングする必要があります。パディングが異なると、予測にわずかな差が生じます。同じ条件で比較するには、dynamic=Trueを指定してエクスポートするか、PyTorch側にrect=Falseを指定して静的エクスポートと同様に正方形全体にパディングしてください(固定形状と最小矩形の比較を参照)。rect=Falseを指定すると、静的エクスポートも0.0000の差で一致します。
両方のファイルを検証する#
1枚の画像で大きなエラーを検出できますが、細かな差異を見つけるには検証が必要です。前処理を一致させるため、両方に同じ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]バス1台(クラス5)と人4人(クラス0)が検出され、YOLO(path)(img)とピクセル単位で一致しています。バスの信頼度はここでは0.93ですが、デフォルトのエクスポートでは0.88です。これはnms=Falseによって別のヘッドが選択されるためです。同じnmsの値でエクスポートしたファイル同士を比較してください。
よくある落とし穴#
症状: ボックスは同じですが、確信度に最大0.1の差があり、特に正方形でない画像で差が大きくなります。 対処: dynamic=Trueを使って、またはPyTorch側でrect=Falseを使って一致度を確認するか、検証mAPで静的エクスポートを評価します。
症状: 独自のONNX Runtimeコードでは、YOLO("model.onnx")より検出ボックスが少なくなる、または位置がずれます。 対処: ort_raw.pyとまったく同じように、レターボックス処理を行い(引き伸ばさない)、114でパディングし、BGRからRGBに変換して255で割ります。その後、ボックスのパディングとスケールを元に戻します。
症状: 形状が一致していても、すべての確信度が少しずつ異なります。 対処: 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ポイントと記されています。動的エクスポートをリリースしてください。