Một cửa hàng muốn biết trên kệ còn bao nhiêu chai, lon và hộp, và chúng nằm ở đâu. Mô hình phân loại ảnh ở bài trước chỉ trả lời "ảnh này thuộc lớp nào", còn bài toán kiểm kê cần câu trả lời cho từng vật thể: loại gì và nằm ở khung nào. Đó là phát hiện đối tượng (object detection), và YOLO (You Only Look Once) là họ mô hình được dùng nhiều nhất cho việc này nhờ tốc độ. Bài này huấn luyện một mô hình YOLO trên bộ dữ liệu riêng, đo bằng mAP, và so sánh hai cách khởi tạo: tinh chỉnh từ trọng số COCO và học từ đầu, với 200 ảnh và chỉ 20 ảnh.
Bài nối tiếp Phân loại ảnh với CNN và transfer learning trong PyTorch, dùng cùng tư duy tái sử dụng tri thức đã học, và dùng các khái niệm precision, recall ở Đánh giá mô hình phân loại: precision, recall, ROC và dữ liệu lệch. Mọi con số dưới đây là output thật khi chạy trên máy Apple Silicon (GPU Metal, backend mps) với ultralytics 8.0.230, phiên bản phát hành tháng 12/2023.
Phát hiện đối tượng khác phân loại ảnh ở đâu
Phân loại cho mỗi ảnh một nhãn, còn YOLO trả về nhiều hộp. Phát hiện đối tượng cho mỗi ảnh một danh sách, mỗi phần tử gồm hộp bao (bounding box), nhãn lớp và độ tin cậy (confidence). Số vật thể trong ảnh không biết trước, nên mô hình vừa phải định vị vừa phải phân loại. Trước YOLO, các hệ thống như R-CNN chạy hai giai đoạn: đề xuất vùng rồi phân loại từng vùng, chính xác nhưng chậm.
Bài báo gốc You Only Look Once: Unified, Real-Time Object Detection của Redmon và cộng sự (2015) đặt bài toán lại thành hồi quy: một mạng duy nhất nhìn toàn bộ ảnh một lần và dự đoán trực tiếp hộp bao cùng xác suất lớp. Bản YOLO đầu tiên đạt 45 khung hình/giây, bản nhỏ Fast YOLO đạt 155 khung hình/giây, đổi lại định vị kém chính xác hơn các hệ thống hai giai đoạn thời đó. Ý tưởng "một lần nhìn" vẫn là lõi của mọi phiên bản YOLO sau này.
YOLOv8: kiến trúc và các cỡ mô hình
Ta dùng YOLOv8 của Ultralytics, ra mắt ngày 10/01/2023. Theo tài liệu YOLOv8, đầu dự đoán là dạng không dùng anchor (anchor-free): mô hình dự đoán trực tiếp tâm và kích thước hộp thay vì chỉnh các hộp mẫu định sẵn, nên bớt một nhóm siêu tham số phải điều chỉnh theo bộ dữ liệu. YOLO có năm cỡ, từ n (nano) tới x:
| Cỡ | Tham số (triệu) | FLOPs (tỉ, ảnh 640) |
|---|---|---|
| YOLOv8n | 3,2 | 8,7 |
| YOLOv8s | 11,2 | 28,6 |
| YOLOv8m | 25,9 | 78,9 |
| YOLOv8l | 43,7 | 165,1 |
| YOLOv8x | 68,2 | 257,8 |
Bảng lấy từ tài liệu chính thức. Bài này dùng cỡ n: nhỏ nhất, hợp với RAM 16 GB dùng chung và đủ cho bài toán ba lớp. Với ảnh 320 và 3 lớp, mô hình có 3.011.433 tham số, đúng như log huấn luyện báo. Trọng số yolov8n.pt của YOLO đã huấn luyện trên bộ COCO (80 lớp, mAP50-95 37,3 theo tài liệu), nên là điểm khởi đầu cho transfer learning, giống ResNet18 trên ImageNet ở bài trước.
Định dạng dữ liệu YOLO và bộ dữ liệu của bài
Theo hướng dẫn dataset phát hiện, mỗi ảnh có một file .txt cùng tên, mỗi dòng là một vật thể: class x_center y_center width height, toạ độ chuẩn hoá về 0–1 (chia cho chiều rộng/cao ảnh), lớp đánh số từ 0. Khi huấn luyện YOLO, Ultralytics tìm nhãn của một ảnh bằng cách thay /images/ cuối cùng trong đường dẫn bằng /labels/. Cấu trúc thư mục và file data.yaml của bài:
dataset/
images/train/0000.jpg ... labels/train/0000.txt ...
images/val/0000.jpg ... labels/val/0000.txt ...
data.yaml # path, train, val, names: {0: hop, 1: chai, 2: lon}Để thí nghiệm YOLO lặp lại được và không dính bản quyền ảnh, ta sinh dữ liệu tổng hợp: ảnh 320x320 có nền thay đổi, nhiễu, làm mờ nhẹ, trên đó vẽ 2–5 vật thể thuộc ba lớp "hộp", "chai", "lon" bằng PIL, nhãn tính thẳng từ toạ độ vẽ (make_dataset.py). Có 200 ảnh huấn luyện (704 vật thể: 232 hộp, 240 chai, 232 lon) và 50 ảnh kiểm định (181 vật thể: 58 hộp, 59 chai, 64 lon). Đây là điểm cần nói thẳng: ảnh vẽ tay dễ hơn ảnh kệ hàng thật rất nhiều, nên các con số tuyệt đối dưới đây không dự báo độ chính xác trên ảnh thật; giá trị của thí nghiệm là so sánh tương đối giữa các cách huấn luyện, và quy trình thì giống hệt khi thay bằng ảnh thật đã gán nhãn.
Mô hình COCO gốc chưa biết "chai" của bạn
Trước khi huấn luyện, thử YOLO gốc để thấy vì sao cần dữ liệu riêng (zero_shot.py):
số lớp COCO: 80 | ví dụ: ['bottle', 'cup', 'vase']
ảnh val/0000.jpg: [('cell phone', 0.77), ('cell phone', 0.62), ('cell phone', 0.56)]Ảnh có một chai và ba lon, YOLO gốc báo ba "điện thoại". COCO có lớp bottle nhưng không có "hộp" hay "lon" theo cách ta định nghĩa, và các hình vẽ này khác hẳn ảnh tự nhiên. Đây là lý do phải tinh chỉnh YOLO trên dữ liệu riêng: đổi tập nhãn và điều chỉnh mô hình theo miền ảnh thật của bạn.
Huấn luyện YOLO trên dữ liệu riêng: chỉ vài dòng
Theo tài liệu chế độ train, device="mps" chạy trên GPU Apple, pretrained=True là mặc định, và khởi tạo từ file .yaml thay vì .pt cho mô hình ngẫu nhiên. Mã huấn luyện YOLO của bài (train.py, rút gọn):
from ultralytics import YOLO
model = YOLO("yolov8n.pt") # hoặc "yolov8n.yaml": học từ đầu
model.train(data="dataset/data.yaml", epochs=30, imgsz=320, batch=16,
device="mps", workers=0, seed=0, project="runs", name=pre200)Log báo Transferred 319/355 items from pretrained weights: 319 trong 355 tensor được nạp từ COCO, phần còn lại là đầu phân loại có kích thước khác vì ta chỉ có 3 lớp thay cho 80. YOLO qua Ultralytics tự chọn optimizer (ở đây AdamW, lr 0,001429) và tự tăng cường dữ liệu (mosaic, lật, đổi màu). Tám lần chạy của bài: hai cách khởi tạo (COCO / ngẫu nhiên) x hai cỡ dữ liệu (200 / 20 ảnh) x hai hạt giống, đều 30 epoch, ảnh 320. Lưu ý kỹ thuật: bản 8.0.230 gọi torch.load theo hành vi cũ, nên trên các bản PyTorch mới hơn cần nới weights_only (file safe_load.py). Chỉ làm vậy với checkpoint chính thức hoặc do chính bạn huấn luyện, vì file .pt là pickle và có thể chạy mã tuỳ ý nếu lấy từ nguồn lạ.
Đo YOLO bằng IoU, precision, recall và mAP
Một dự đoán tính là đúng khi cùng lớp và hộp đủ trùng với hộp thật. Độ trùng đo bằng IoU (Intersection over Union): diện tích giao chia diện tích hợp, 1,0 là khớp hoàn hảo. Theo hướng dẫn chỉ số hiệu năng, mAP50 là Average Precision trung bình các lớp tại ngưỡng IoU 0,5 (chỉ cần hộp "tạm đúng"), còn mAP50-95 lấy trung bình qua các ngưỡng từ 0,5 tới 0,95, nên phạt nặng hộp lệch. Precision là tỉ lệ dự đoán đúng, recall là tỉ lệ vật thể thật được tìm thấy. Để thấy IoU bằng số thật, iou_demo.py đổi nhãn chuẩn hoá về pixel và so với dự đoán của mô hình tinh chỉnh trên ảnh val/0007.jpg:
ảnh val/0007.jpg: 5 nhãn thật, 5 dự đoán
thật hop (106,6,191,72) -> dự đoán hop conf=0.99 IoU=0.978
thật chai (169,161,207,264) -> dự đoán chai conf=0.96 IoU=0.970
thật chai (111,88,156,187) -> dự đoán chai conf=0.98 IoU=0.973
thật hop (21,5,80,75) -> dự đoán hop conf=0.98 IoU=0.954
thật hop (238,88,294,162) -> dự đoán hop conf=0.99 IoU=0.949Cả năm hộp đều có IoU trên 0,94, tức vượt mọi ngưỡng của mAP50-95 từ 0,50 tới 0,90 và bốn hộp còn vượt cả ngưỡng 0,95. Khi mọi hộp đều như vậy, mAP50 nằm sát trần 0,995 ở nhiều mô hình khác nhau, còn mAP50-95 mới phân biệt được mô hình YOLO nào định vị tốt hơn.
Kết quả: tinh chỉnh từ COCO so với học từ đầu
Lệnh evaluate.py chạy model.val() trên 50 ảnh kiểm định. Bảng dưới là mAP50-95 của hai hạt giống (seed 0 / seed 1), kèm mAP50 và recall của seed 0:
| Cách huấn luyện | mAP50-95 (seed 0 / 1) | mAP50 (seed 0) | Recall (seed 0) |
|---|---|---|---|
| Từ COCO, 200 ảnh | 0,984 / 0,991 | 0,995 | 1,000 |
| Từ đầu, 200 ảnh | 0,904 / 0,901 | 0,995 | 0,971 |
| Từ COCO, 20 ảnh | 0,894 / 0,869 | 0,983 | 0,665 |
| Từ đầu, 20 ảnh | 0,000 / 0,000 | 0,000 | 0,000 |
Output thật của dòng đầu (eval_output.txt):
[pre200] mAP50=0.995 mAP50-95=0.984 precision=0.999 recall=1.000
lớp hop AP50=0.995 AP50-95=0.991
lớp chai AP50=0.995 AP50-95=0.984
lớp lon AP50=0.995 AP50-95=0.978Có bốn điều đáng đọc về YOLO. Thứ nhất, với 200 ảnh cả hai cách đều đạt mAP50 gần 0,995, nghĩa là phát hiện đúng vật thể; khác biệt nằm ở độ chính xác của hộp: mAP50-95 của mô hình tinh chỉnh cao hơn khoảng 0,08–0,09, ổn định qua hai hạt giống. Thứ hai, mô hình học từ đầu sau 10 epoch mới bắt đầu có mAP đáng kể (0,198), trong khi mô hình COCO đã có 0,813 ở epoch 3 và 0,929 ở epoch 10 (hạt giống 0), theo results.csv của lần chạy hạt giống 0.
Thứ ba, với 20 ảnh mô hình học từ đầu hoàn toàn không phát hiện được gì (mAP 0 ở cả hai hạt giống, loss hộp còn 3,88 ở epoch cuối so với 0,41 của mô hình COCO), còn mô hình COCO vẫn đạt mAP50 0,983 và 0,916.
Thứ tư, và là điều dễ bị bỏ qua: recall của mô hình 20 ảnh chỉ 0,665. Precision 0,992 cao nhưng nó bỏ sót một phần ba vật thể. Đếm số hộp dự đoán trên 50 ảnh kiểm định (181 vật thể thật) theo ngưỡng confidence bằng conf_check.py cho thấy lý do:
[pre200] conf>=0.05: 182 | conf>=0.25: 181 | conf>=0.5: 181 | conf>=0.8: 181 | nhãn thật 181
[pre20] conf>=0.05: 128 | conf>=0.25: 65 | conf>=0.5: 35 | conf>=0.8: 11 | nhãn thật 181
[pre20_s1] conf>=0.05: 129 | conf>=0.25: 68 | conf>=0.5: 42 | conf>=0.8: 24 | nhãn thật 181YOLO huấn luyện 200 ảnh tự tin: gần như mọi vật thể được tìm thấy ngay cả ở ngưỡng 0,8. YOLO huấn luyện 20 ảnh "chưa chắc chắn": ở ngưỡng mặc định 0,25 chỉ ra 65–68 hộp trên 181 vật thể, và ngay ở ngưỡng thấp 0,05 cũng chưa tới 130. Nghĩa là bổ sung dữ liệu có ích không chỉ vì hộp chuẩn hơn mà còn vì mô hình dám khẳng định.
Thời gian huấn luyện 30 epoch trên 200 ảnh với mô hình COCO là 0,038 giờ (khoảng 2,3 phút) trong log hạt giống 1, nên thử lại nhanh trên laptop là việc khả thi. Thời gian của các lần học từ đầu dao động (0,057 giờ ở seed 0, 0,275 giờ ở seed 1) vì máy chạy song song nhiều tác vụ khác, nên không dùng chúng để so tốc độ.
Suy luận và đọc kết quả dự đoán
Sau khi huấn luyện YOLO, trọng số tốt nhất nằm ở runs/<tên>/weights/best.pt. Dự đoán một ảnh:
m = YOLO("runs/pre200/weights/best.pt")
res = m.predict("dataset/images/val/0000.jpg", imgsz=320, conf=0.25, device="mps")[0]
for b in res.boxes:
x1, y1, x2, y2 = [round(v) for v in b.xyxy[0].tolist()]
print(m.names[int(b.cls)], float(b.conf), (x1, y1, x2, y2))ảnh val/0000.jpg: dự đoán 4 vật thể | nhãn thật 4
chai conf=0.98 box=(233,192,277,302)
lon conf=0.97 box=(54,64,95,136)
lon conf=0.96 box=(97,189,137,263)
lon conf=0.93 box=(169,116,217,178)Đối chiếu với nhãn thật (một chai, ba lon), cả bốn đều đúng lớp. xyxy là toạ độ pixel góc trên-trái và dưới-phải; xywhn cho dạng chuẩn hoá khớp định dạng nhãn. Ngưỡng conf là núm vặn nghiệp vụ: kiểm kê hàng hoá ưu tiên recall nên hạ ngưỡng, cảnh báo tự động ưu tiên precision nên nâng ngưỡng. Hãy chọn ngưỡng trên tập kiểm định của bạn, không giữ mặc định.
Khi nào không nên dùng YOLO, và giấy phép
- Chỉ cần biết ảnh thuộc lớp nào: không cần YOLO, dùng phân loại như bài trước, rẻ hơn nhiều và gán nhãn đơn giản hơn (một nhãn cho cả ảnh, không phải vẽ hộp).
- Cần ranh giới chính xác tới từng pixel (đo diện tích, cắt nền): phân đoạn (segmentation) phù hợp hơn hộp bao.
- Vật thể rất nhỏ hoặc che khuất nặng: cần ảnh độ phân giải cao hơn nhiều (ta dùng 320), và dữ liệu có đủ trường hợp đó.
- Giấy phép: theo kho mã Ultralytics, thư viện phát hành theo AGPL-3.0 và có giấy phép doanh nghiệp riêng cho dùng thương mại. Với sản phẩm đóng nguồn, hãy đọc kỹ điều khoản và hỏi bộ phận pháp lý trước khi đưa vào hệ thống của khách hàng.
Lỗi thường gặp khi huấn luyện YOLO
- Nhãn sai định dạng: dùng pixel thay vì toạ độ chuẩn hoá, hoặc đưa góc trái-trên thay cho tâm. Mô hình vẫn chạy nhưng không học được. Hãy vẽ lại hộp lên vài ảnh để kiểm tra bằng mắt.
- Sai đường dẫn nhãn: thư mục không có
/images/và/labels/song song, Ultralytics báo không tìm thấy nhãn và coi ảnh là nền. - Đánh giá trên chính tập đã huấn luyện hoặc tập kiểm định có ảnh gần trùng: mAP phồng. Chia theo cảnh hoặc theo ngày chụp, không chia ngẫu nhiên ảnh liên tiếp của cùng một video.
- Tin vào mAP50 trần: ở thí nghiệm trên, mAP50 (seed 0) đều là 0,995 ở cả YOLO tinh chỉnh lẫn YOLO học từ đầu với 200 ảnh; chỉ mAP50-95 mới tách được chúng.
- Chọn mô hình tốt nhất trên cùng tập kiểm định dùng để báo cáo: Ultralytics lưu
best.pttheo kết quả kiểm định, nên số trên tập đó hơi lạc quan. Dự án thật cần thêm một tập test tách riêng. - Học từ đầu với ít dữ liệu: kết quả 0,000 ở trên là bằng chứng. Hãy bắt đầu YOLO từ trọng số tiền huấn luyện.
- Dùng dữ liệu tổng hợp rồi kỳ vọng ngang ảnh thật: nó hữu ích để kiểm thử quy trình, không thay được ảnh chụp thật ở đúng điều kiện triển khai (ánh sáng, góc, vật cản).
Checklist huấn luyện YOLO trên dữ liệu riêng
- Nhãn: một
.txtcho mỗi ảnh,class xc yc w hchuẩn hoá, lớp từ 0; kiểm tra bằng mắt trên mẫu ngẫu nhiên. - Khởi tạo YOLO từ
yolov8n.pt(hoặc cỡ lớn hơn nếu đủ tài nguyên); chỉ học từ đầu khi có hàng nghìn ảnh. - Báo cáo mAP50-95, precision, recall từng lớp, không chỉ mAP50; ghi số ảnh và số vật thể kiểm định.
- Chạy ít nhất 2 hạt giống; với 181 vật thể một vài hộp lệch đã đổi số ở chữ số thập phân thứ hai.
- Chọn ngưỡng
conftheo mục tiêu nghiệp vụ (recall hay precision) trên dữ liệu tách riêng. - Ghi phiên bản
ultralyticsvà PyTorch (xemrequirements.txt) cùng giấy phép AGPL-3.0 vào tài liệu dự án.
Toàn bộ mã YOLO của bài nằm trong code/s7-24: make_dataset.py, train.py, evaluate.py, zero_shot.py, iou_demo.py, conf_check.py, safe_load.py cùng các file *_output.txt và requirements.txt. Chạy run_all.sh cho hạt giống 0 và thêm SEED=1 trước lệnh train.py cho hạt giống 1; yolov8n.pt được Ultralytics tải về ở lần chạy đầu. Số liệu là output thật nhưng trên dữ liệu tổng hợp và 50 ảnh kiểm định, chỉ dùng để so sánh tương đối.
Bài tiếp theo
Phân loại văn bản tiếng Việt: tách từ, TF-IDF và mô hình tuyến tính. Sau hai bài thị giác máy tính, bài sau chuyển sang ngôn ngữ: biến văn bản tiếng Việt thành vector, tách từ đúng và dựng bộ phân loại cơ sở mạnh trước khi nghĩ tới mô hình lớn.



