التصدير إلى ONNX (والتحقق من التطابق)
صدّر نموذج ONNX قابلًا للنقل، ثم أثبت تطابقه مع PyTorch في الصناديق الوصفية ودرجات الثقة وmAP قبل شحن أي شيء.
التصدير الذي يغيّر درجات الثقة بمقدار 0.1 لا يتسبب في انهيار. بل يغيّر بهدوء نتائج الكشف التي تتجاوز عتبة conf على كل جهاز تشحن إليه النموذج، ولا يلاحظ أحد ذلك إلا بعد أن تصبح الأعداد خاطئة. يُعدّ ONNX نقطة بداية قابلة للنقل للنشر، والتحقق من التطابق خلال خمس دقائق هو أقل وسائل الحماية تكلفةً في هذه الدورة.
عملي
التصدير#
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 الذي اختاره (opset 18 مع onnx 1.23) وأن onnxslim بسّط الرسم البياني. الخيارات التي تغيّر السلوك، من وضع التصدير:
| الخيار | افتراضي | متى تغيّره |
|---|---|---|
imgsz | 640 | يعمل الإنتاج بحجم آخر؛ ويُثبَّت التصدير الثابت على ذلك الحجم |
dynamic | False | يختلف حجم الدفعة أو شكل الصورة أثناء الاستدلال |
batch | 1 | تستخدم تجميع الدفعات أثناء الاستدلال (أقصى حجم دفعة للتصدير الديناميكي) |
opset | أحدث إصدار مدعوم | يرفض إصدار أقدم من بيئة التشغيل النموذج؛ خفّض الإصدار |
quantize | None (FP32) | 16 لـ FP16؛ و8 لـ INT8 مع data للمعايرة |
nms | None | يضمّن True NMS في الرسم البياني؛ ويختار False الرأس الخالي من NMS في YOLO26 |
باستخدام nms=None الافتراضي، يعيد التصدير التنبؤات الخام، ويتولى غلاف Ultralytics تنفيذ NMS، تمامًا كما في PyTorch.
تحقّق من التطابق على صورة معروفة#
مرّر الصورة نفسها عبر النموذجين وقارن كل شيء:
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 |
| ثابت (افتراضي) | 5 مقابل 5 | نعم | 0.0977 | 10.11 px |
التصدير الثابت ليس معطوبًا. يعيد PyTorch تحجيم الصورة العمودية bus.jpg وإضافة الحشو إليها للوصول إلى أصغر شكل يتوافق مع خطوة الشبكة، وهو 640×480 (الارتفاع × العرض)، بينما يجب على رسم بياني ثابت بحجم 640×640 إضافة حشو لتصبح الصورة مربعة، مع أشرطة رمادية. يؤدي اختلاف الحشو إلى تنبؤات مختلفة قليلًا. لإجراء مقارنة متكافئة، إما أن تصدّر باستخدام dynamic=True، أو تشغّل جانب PyTorch باستخدام rect=False ليضيف حشوًا إلى مربع كامل كما يفعل التصدير الثابت (راجع الشكل الثابت مقابل المستطيل الأدنى)؛ ومع rect=False يتطابق التصدير الثابت أيضًا بفارق 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، أو باستخدام rect=False في جانب PyTorch، أو قيّم التصدير الثابت وفق mAP التحقق.
العَرَض: يعثر كود ONNX Runtime الخاص بك على صناديق أقل أو مزاحة مقارنةً بـ YOLO("model.onnx"). الحل: استخدم letterbox (ولا تمدد الصورة)، وأضف حشوًا بقيمة 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 pointsيُظهر تقرير التكافؤ لـ yolo26n على bus.jpg ما يلي: أقصى فرق في الثقة للتصدير الثابت 0.0977 (شكل letterbox، أزاله rect=False)، والتصدير الديناميكي 0.0000، وفارق mAP50-95 يساوي 0.00 نقطة. اعتمد التصدير الديناميكي.