Xuất sang ONNX (và xác minh tính tương đương)
Xuất model ONNX có tính di động, sau đó xác minh model khớp với PyTorch về bounding box, độ tin cậy và mAP trước khi triển khai.
Một bản xuất làm lệch độ tin cậy 0.1 sẽ không gây crash. Bản xuất đó âm thầm khiến các detection vượt qua ngưỡng conf trên mọi thiết bị bạn triển khai, và không ai nhận ra cho đến khi số liệu bị sai. ONNX là điểm khởi đầu có tính di động cho việc triển khai, và năm phút kiểm tra tính tương đương là biện pháp bảo vệ ít tốn kém nhất trong khóa học này.
Thực hành
Xuất model#
from ultralytics import YOLO
model = YOLO("yolo26n.pt") # hoặc runs/detect/pills_v1/weights/best.pt
model.export(format="onnx", dynamic=True) # ghi yolo26n.onnx cạnh các tệp trọng sốLog cho biết opset đã chọn (opset 18 với onnx 1.23) và onnxslim đã đơn giản hóa graph. Các tùy chọn làm thay đổi hành vi, từ chế độ Export:
| Tùy chọn | Mặc định | Thay đổi khi nào |
|---|---|---|
imgsz | 640 | Môi trường production chạy ở kích thước khác; bản xuất tĩnh bị cố định ở kích thước đó |
dynamic | False | Kích thước batch hoặc ảnh thay đổi khi inference |
batch | 1 | Bạn xử lý theo batch khi inference (batch tối đa của bản xuất động) |
opset | mới nhất được hỗ trợ | Runtime cũ hơn từ chối model; hãy giảm phiên bản opset |
quantize | None (FP32) | 16 cho FP16; 8 cho INT8 với dữ liệu calibration data |
nms | None | True nhúng NMS vào graph; False chọn head không cần NMS của YOLO26 |
Với nms=None mặc định, bản xuất trả về các dự đoán thô và wrapper Ultralytics chạy NMS, giống hệt PyTorch.
Kiểm tra tính tương đương trên một ảnh đã biết#
Chạy cùng một ảnh qua cả hai model và so sánh mọi thứ:
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")Sắp xếp cả hai tập theo độ tin cậy sẽ ghép các object giống nhau, vì vậy hai dòng cuối so sánh từng detection với bản tương ứng. Bây giờ chạy hai lần: một lần như minh họa và một lần khi bỏ dynamic=True:
| Xuất model | Box | Cùng class | Chênh lệch độ tin cậy tối đa | Chênh lệch box tối đa |
|---|---|---|---|---|
dynamic=True | 5 so với 5 | có | 0.0000 | 0.00 px |
| tĩnh (mặc định) | 5 so với 5 | có | 0.0977 | 10.11 px |
Bản xuất tĩnh không bị lỗi. PyTorch letterbox ảnh dọc bus.jpg thành shape nhỏ nhất căn chỉnh theo stride, 640×480 (chiều cao × chiều rộng), trong khi graph tĩnh 640×640 phải đệm ảnh thành hình vuông bằng các dải màu xám. Phần đệm khác nhau tạo ra các dự đoán hơi khác biệt. Để so sánh tương đương, hãy xuất với dynamic=True, hoặc chạy phía PyTorch với rect=False để đệm thành hình vuông đầy đủ như bản xuất tĩnh (xem shape cố định và hình chữ nhật tối thiểu); với rect=False, bản xuất tĩnh cũng khớp với sai số 0.0000.
Đánh giá cả hai file#
Một ảnh giúp phát hiện lỗi lớn; validation giúp phát hiện lỗi tinh vi. Dùng cùng batch cho cả hai để đảm bảo bước tiền xử lý khớp nhau:
yolo val model=yolo26n.pt data=coco8.yaml batch=1
yolo val model=yolo26n.onnx data=coco8.yaml batch=1Cả hai đều báo cáo mAP50-95 0.654 ở đây. COCO8 xác minh pipeline chứ không phải model; trên tập validation của riêng bạn, hãy tìm hiểu mọi chênh lệch trên 0.5 điểm. Toàn bộ quy trình kiểm tra:
Gọi trực tiếp ONNX Runtime#
Các dịch vụ production thường bỏ qua wrapper Ultralytics và tự gọi inference engine. Khi đó, bạn chịu trách nhiệm tiền xử lý và phải thực hiện chính xác như Ultralytics. Với nms=False, bản xuất YOLO26 trả về các detection cuối cùng, nên không cần code 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]Một xe buýt (class 5) và bốn người (class 0), khớp chính xác đến từng pixel với YOLO(path)(img). Xe buýt có độ tin cậy 0.93 ở đây và 0.88 với bản xuất mặc định vì nms=False chọn head khác; hãy so sánh các file được xuất với cùng giá trị nms.
Các lỗi thường gặp#
Triệu chứng: box giống nhau nhưng confidence lệch tới 0.1, rõ nhất ở ảnh không vuông. Cách khắc phục: kiểm tra tính tương đương bằng dynamic=True, hoặc bằng rect=False ở phía PyTorch, hoặc đánh giá bản export tĩnh dựa trên mAP validation.
Triệu chứng: mã ONNX Runtime tự viết tìm thấy ít box hơn hoặc box bị dịch chuyển so với YOLO("model.onnx"). Cách khắc phục: letterbox (không kéo giãn), padding bằng 114, chuyển BGR sang RGB, chia cho 255, rồi hoàn tác padding và tỷ lệ trên các box, chính xác như ort_raw.py.
Triệu chứng: mọi giá trị confidence đều hơi khác, ngay cả khi shape khớp nhau. Cách khắc phục: chỉ so sánh bản export nms=False (không dùng NMS) với một bản export nms=False khác. Với nms=True, hãy kiểm tra thêm xem conf và iou được tích hợp vào graph có khớp với các giá trị bạn dùng ở nơi khác hay không.
Hiển thị lời giải
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 pointsBáo cáo đối chiếu cho yolo26n trên bus.jpg ghi: chênh lệch confidence tối đa của bản export tĩnh là 0.0977 (hình dạng letterbox, được loại bỏ bằng rect=False), bản export động là 0.0000, chênh lệch mAP50-95 là 0.00 điểm. Triển khai bản export động.