Ultralytics YOLO27: Tham gia danh sách chờ.
Get Started
Ultralytics YOLO trong môi trường production·Chọn runtime·Bài học 2/9
Bài họctrung cấp

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.

Kết quả

Xuất sang ONNX, dùng số liệu để chứng minh model khớp với PyTorch về bounding box, độ tin cậy và mAP, đồng thời giải thích mọi chênh lệch phát hiện được.

Lộ trình cấp tốc
Nếu bạn đã nắm rõ cách thực hiện, đây là phiên bản tóm tắt.
  1. yolo export model=yolo26n.pt format=onnx dynamic=True.

  2. Chạy một ảnh qua cả hai file; so sánh số lượng box, class, độ tin cậy và tọa độ, chứ không chỉ so sánh số lượng.

  3. Chạy yolo val trên cả hai với cùng batch; mAP50-95 nên chênh không quá 0.5 điểm.

  4. Có chênh lệch? Hãy kiểm tra shape đầu vào (tĩnh hay động), bước tiền xử lý của riêng bạn và head (nms) đã xuất.

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ọnMặc địnhThay đổi khi nào
imgsz640Mô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 đó
dynamicFalseKích thước batch hoặc ảnh thay đổi khi inference
batch1Bạn xử lý theo batch khi inference (batch tối đa của bản xuất động)
opsetmới nhất được hỗ trợRuntime cũ hơn từ chối model; hãy giảm phiên bản opset
quantizeNone (FP32)16 cho FP16; 8 cho INT8 với dữ liệu calibration data
nmsNoneTrue 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 modelBoxCùng classChênh lệch độ tin cậy tối đaChênh lệch box tối đa
dynamic=True5 so với 5có0.00000.00 px
tĩnh (mặc định)5 so với 5có0.097710.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=1

Cả 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#

So sánh bản export tĩnh với suy luận PyTorch có hình chữ nhật

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.

Tiền xử lý viết thủ công khác với Ultralytics

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.

Trộn các head

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.

Dùng thử

Xuất model (hoặc yolo26n.pt) sang ONNX hai lần: dạng tĩnh và dynamic=True, rồi chạy parity.py cho từng bản. Tiếp theo, chạy yolo val trên .pt và bản động .onnx với cùng batch, rồi viết báo cáo tính tương đương gồm ba dòng: chênh lệch độ tin cậy tối đa của từng bản xuất và mức chênh mAP50-95.

Điều kiện hoàn thành
Bạn đã hoàn thành bài học khi tất cả các điều kiện sau đều đúng.
  • Bạn có file .onnx đặt cạnh .pt và kết quả in ra về tính tương đương cho cả bản xuất tĩnh lẫn động.

  • Bạn có thể giải thích chênh lệch độ tin cậy của bản xuất tĩnh và trình bày cách khắc phục bằng dynamic=True hoặc rect=False.

  • mAP50-95 trên file ONNX nằm trong phạm vi chênh lệch 0,5 điểm so với model PyTorch, hoặc có giải thích cho khoảng chênh lệch.

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 points

Bá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.

Tiếp theo là gì

Trên phần cứng NVIDIA, bước tiếp theo là TensorRT, nơi FP16 và INT8 đánh đổi một chút độ chính xác để lấy tốc độ cao hơn nhiều.

Đăng nhập để lưu tiến độ