Kiểm soát Chất lượng Dataset
Dùng script để kiểm tra nhãn, cân bằng class, dữ liệu trùng lặp và độ bao phủ của các tập chia; để model chỉ ra những nhãn có khả năng bị thiếu, rồi sửa các vấn đề tìm được trước khi huấn luyện.
Sửa nhãn thường giúp tăng độ chính xác nhiều hơn so với tinh chỉnh hyperparameter. Các nhóm thường dành hàng tuần để thử các thiết lập nhằm tăng một hoặc hai điểm mAP, rồi chỉ trong một buổi kiểm tra lại phát hiện một phần của một class đã bị gán nhãn sai từ trước. Kiểm soát chất lượng phát hiện những gì tài liệu hướng dẫn gán nhãn còn bỏ sót trước khi một lần huấn luyện biến lỗi nhãn thành con số không đáng tin.
Thực hành
Bảy bước kiểm tra#
Thực hiện cả bảy bước trước lần huấn luyện đầu tiên và sau mỗi batch gán nhãn lớn. Hướng dẫn tiền xử lý dữ liệu đã gán nhãn trình bày chi tiết một số bước.
| # | Bước kiểm tra | Vấn đề được phát hiện | Cách phát hiện |
|---|---|---|---|
| 1 | Nhãn bị thiếu | Object mà người gán nhãn bỏ sót | Rà soát các trường hợp đáng ngờ bên dưới; kiểm tra mẫu trực quan |
| 2 | Class sai hoặc không hợp lệ | Class bị hoán đổi, ID nằm ngoài danh sách class | Script kiểm tra; ma trận nhầm lẫn sau lần chạy đầu tiên |
| 3 | Bbox quá lỏng | Bbox có lề thừa hoặc bị cắt cạnh | Kiểm tra mẫu trực quan; script kiểm tra đánh dấu bbox quá nhỏ |
| 4 | Dữ liệu trùng lặp | Bản sao chính xác và các khung hình gần giống hệt nhau | Script kiểm tra bản sao chính xác; chế độ xem Clustering trên Ultralytics Platform, tô màu theo split, để tìm các bản trùng gần giống |
| 5 | Mất cân bằng class | Số lượng class thấp hơn nhiều so với mục tiêu trong thông số kỹ thuật | Script kiểm tra; tab Charts trên Platform |
| 6 | Độ bao phủ và rò rỉ giữa các split | Class bị thiếu trong một split; một đoạn video xuất hiện trong hai split | Script kiểm tra; chia nhóm (bài học tiếp theo) |
| 7 | Tỷ lệ ảnh nền | Quá ít hoặc quá nhiều ảnh trống | Script kiểm tra; mục tiêu 0–10% |
Tự động hóa việc thống kê#
Các bước kiểm tra 2, 4, 5, 6 và 7 đều là bài toán thống kê nên hãy viết script cho chúng. Quy trình kiểm tra này đọc file YAML của dataset detection, trong đó train: và val: trỏ đến các thư mục ảnh. Quy trình đếm instance theo class và ảnh nền trong từng split, đánh dấu ID class không hợp lệ, bbox quá nhỏ, các dòng nhãn trùng lặp và ảnh giống hệt nhau theo từng byte, đồng thời liệt kê các class có trong val nhưng không có trong train:
import hashlib
from collections import Counter
from pathlib import Path
from ultralytics.data.utils import IMG_FORMATS, check_det_dataset, img2label_paths
data = check_det_dataset("coco8.yaml") # downloads COCO8 on first run; swap in your data YAML
names, seen = data["names"], {}
for split in ("train", "val"):
images = sorted(p for p in Path(data[split]).rglob("*.*") if p.suffix[1:].lower() in IMG_FORMATS)
instances, hashes, problems, background = Counter(), Counter(), [], 0
for img, label in zip(images, img2label_paths([str(p) for p in images])):
hashes[hashlib.md5(img.read_bytes()).hexdigest()] += 1
rows = [r.split() for r in Path(label).read_text().splitlines() if r.strip()] if Path(label).exists() else []
background += not rows
if len(rows) != len({tuple(r) for r in rows}):
problems.append(f"{img.name}: duplicate label rows")
for r in rows:
cls, w, h = int(r[0]), float(r[3]), float(r[4])
if cls not in names:
problems.append(f"{img.name}: class {cls} is not in names")
elif w * h < 1e-4:
problems.append(f"{img.name}: tiny {names[cls]} box")
instances[cls] += 1
seen[split] = set(instances)
duplicates = sum(n - 1 for n in hashes.values())
print(f"{split}: {len(images)} images, {background} background, {duplicates} duplicate images")
for cls, n in instances.most_common():
print(f" {names.get(cls, cls):>12} {n}")
for problem in problems:
print(" !", problem)
print("in val but never in train:", sorted(names[c] for c in seen["val"] - seen["train"] if c in names))train: 4 images, 0 background, 0 duplicate images
orange 4
bowl 3
giraffe 2
...
val: 4 images, 0 background, 0 duplicate images
person 10
horse 2
...
in val but never in train: ['dog', 'elephant', 'horse', 'person', 'umbrella']Dòng cuối cùng là kết quả phát hiện. person có 10 instance trong val nhưng không có instance nào trong train, nghĩa là val đang đánh giá một class mà model chưa từng được huấn luyện. COCO8 là bộ dữ liệu kiểm tra nhanh gồm 8 ảnh nên điều này là bình thường; trong dataset của bạn, điều đó có nghĩa là cần sửa split hoặc thu thập thêm dữ liệu trước khi huấn luyện. Khi có vấn đề, chúng sẽ được in bên dưới split tương ứng dưới dạng ! 000000000025.jpg: class 99 is not in names hoặc ! 000000000009.jpg: duplicate label rows.
Dùng model để chỉ ra nhãn đáng ngờ#
Nhãn bị thiếu là lỗi gây hại nhiều nhất và khó phát hiện nhất khi cuộn xem ảnh. Model đã huấn luyện trước có thể giúp chỉ ra các trường hợp này: một detection có độ tin cậy cao nhưng không có nhãn nào ở gần có thể là nhãn bị thiếu hoặc lỗi của model; cả hai trường hợp đều đáng để kiểm tra. Ví dụ này chạy trên COCO128:
from pathlib import Path
import torch
from ultralytics import YOLO
from ultralytics.data.utils import check_det_dataset, img2label_paths
from ultralytics.utils.metrics import box_iou
from ultralytics.utils.ops import xywh2xyxy
data = check_det_dataset("coco128.yaml") # swap in your data YAML
model = YOLO("yolo26n.pt") # a pretrained model, or your own first fine-tune
images = sorted(Path(data["train"]).glob("*.jpg"))
suspects = []
for result, label in zip(model(images, conf=0.6, stream=True, verbose=False), img2label_paths(map(str, images))):
rows = [r.split() for r in Path(label).read_text().splitlines() if r.strip()] if Path(label).exists() else []
boxes = xywh2xyxy(torch.tensor([[float(v) for v in r[1:5]] for r in rows]).reshape(-1, 4))
iou = box_iou(result.boxes.xyxyn, boxes) # model boxes vs labeled boxes
for j, (cls, conf) in enumerate(zip(result.boxes.cls.int().tolist(), result.boxes.conf.tolist())):
if not rows or iou[j].max() < 0.3: # confident detection with no label anywhere near it
suspects.append((conf, Path(result.path).name, model.names[cls]))
print(f"{len(suspects)} confident detections with no matching label; top 5:")
for conf, name, cls in sorted(suspects, reverse=True)[:5]:
print(f" {conf:.2f} {cls:<12} {name}")4 confident detections with no matching label; top 5:
0.77 donut 000000000438.jpg
0.75 person 000000000531.jpg
0.71 donut 000000000438.jpg
0.66 bottle 000000000436.jpgTrong số 128 ảnh, script thu hẹp phạm vi rà soát còn ba ảnh. Mở các ảnh đó sẽ thấy cả hai dạng phát hiện:
000000000438.jpglà một bbox chứa khoảng một tá bánh donut, trong đó hai detection có độ tin cậy cao không khớp với nhãn donut nào: có thể nhãn bị thiếu hoặc đặt sai vị trí.000000000531.jpgđánh dấu một "người" thực chất là bóng của nhiếp ảnh gia trên sân tennis. Nhãn đúng nhưng model sai; chính vì vậy, một người cần kiểm tra từng trường hợp đáng ngờ thay vì tự động thêm nhãn.
Giảm conf để mở rộng phạm vi tìm kiếm, và thay bằng model fine-tune đầu tiên của bạn khi đã có, vì model đó hiểu các class của bạn.
Đọc biểu đồ dataset#

Trên Ultralytics Platform, tab Charts của dataset vẽ biểu đồ phân bố split, số lượng class và thống kê bbox (tài liệu dataset). Ảnh chụp màn hình này đã cho thấy một vấn đề QC: danh sách class có hai class, nhưng tablet có 0 trên tổng số 1,755 annotation. Model không thể học một class không có nhãn. Hãy gán nhãn cho class đó hoặc xóa class khỏi danh sách trước khi huấn luyện.
Khắc phục mất cân bằng class theo đúng thứ tự#
forklift ████████████████████████ 8000 instances
person ████████████████ 5400
pallet ██ 700 ← red flagChênh lệch như thế này tạo ra một model có độ tin cậy cao khi phát hiện xe nâng nhưng lại bỏ sót pallet. Có ba cách khắc phục, theo thứ tự:
- Thu thập và gán nhãn thêm dữ liệu cho class thiểu số. Đây luôn là lựa chọn tốt nhất: hướng đến ≥ 1,500 ảnh và ≥ 10,000 instance cho mọi class.
- Chỉ tăng lấy mẫu trong train. Liệt kê nhiều lần hơn các ảnh có class thiểu số trong danh sách file huấn luyện để mỗi epoch nhìn thấy chúng thường xuyên hơn. Không đưa các bản sao vào val và test.
- Gán trọng số cho loss. Argument huấn luyện
cls_pw(mặc định 0.0, tối đa 1.0 để áp trọng số nghịch đảo tần suất đầy đủ) tăng trọng số loss phân loại cho các class hiếm. Chỉ dùng cách này sau bước 1 và 2.
Xóa ảnh mờ, đông đúc hoặc khó hiểu khỏi val sẽ làm mAP tăng lên mà model không tốt hơn — chỉ kém trung thực hơn. Nếu ảnh là khung hình thực tế trong quá trình triển khai, hãy giữ lại và sửa nhãn. Chỉ xóa các ngoại lệ thực sự (lỗi 9) và ảnh trùng khớp chính xác.
Ghi chú QC#
Chỉ cần vài dòng là đủ để trả lời câu hỏi "bạn đã kiểm tra những gì?" khi khách hàng hoặc kiểm toán viên hỏi sau đó vài tháng:
Dataset: forklift-v1 Audit date: 2026-05-09
Images: 4,820 Labels: 18,442 Background: 380 (7.9%)
Audit script: 3 invalid class IDs fixed, 41 duplicate label rows removed
Suspect review: 62 flagged, 19 missing labels added, 43 model errors ignored
Visual sample: 120 images, 4 mislabeled (3.3%), fixed
Split check: grouped by camera-day, every class in every split
Imbalance: pallet 2.5x below person; collecting more next round
Ready to train: yesHiển thị lời giải
Ghi chú QC cho hai lần chạy minh họa trong bài học này:
Dataset: COCO8 + COCO128 demo Audit date: 2026-05-09
qc_audit.py (COCO8): 0 invalid classes, 0 duplicate rows, 0 duplicate images
FINDING: dog, elephant, horse, person, umbrella appear in val only
ACTION: re-split by group or collect these classes into train before training
suspect_labels.py (COCO128, conf 0.6): 4 suspects in 3 images
000000000438.jpg: 2 donuts without matching labels -> relabel the donut box
000000000531.jpg: "person" is the photographer's shadow -> model error, label kept
000000000436.jpg: bottle -> open and decide
Ready to train: no, fix the val-only classes firstHãy chú ý dòng cuối cùng. Một dataset có class chỉ xuất hiện trong val sẽ không đạt QC ngay cả khi từng nhãn đều chính xác, vì metric của class đó đo lường điều mà model chưa từng có cơ hội học.