Ultralytics YOLO27: Tham gia danh sách chờ.
Get Started
Xây dựng Dataset YOLO Hiệu năng Cao·Gán nhãn·Bài học 5/10
Bài họcngười mới bắt đầu

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.

Kết quả

Chạy kiểm tra nhãn tự động và rà soát các trường hợp đáng ngờ có hỗ trợ từ model, sửa những gì phát hiện được rồi ghi lại kết quả trong ghi chú QC ngắn gọn.

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. Viết script thống kê: số instance trên mỗi class, tỷ lệ ảnh nền, class không hợp lệ, dữ liệu trùng lặp và class bị thiếu trong train.

  2. Dùng model đã huấn luyện trước để đánh dấu các detection có độ tin cậy cao nhưng không có nhãn khớp, rồi kiểm tra bằng mắt.

  3. Khắc phục mất cân bằng class theo thứ tự: trước tiên thu thập thêm dữ liệu, sau đó tăng lấy mẫu, cuối cùng mới áp trọng số cho loss.

  4. Ghi lại những gì đã kiểm tra và sửa.

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 traVấn đề được phát hiệnCách phát hiện
1Nhãn bị thiếuObject mà người gán nhãn bỏ sótRà soát các trường hợp đáng ngờ bên dưới; kiểm tra mẫu trực quan
2Class sai hoặc không hợp lệClass bị hoán đổi, ID nằm ngoài danh sách classScript kiểm tra; ma trận nhầm lẫn sau lần chạy đầu tiên
3Bbox quá lỏngBbox có lề thừa hoặc bị cắt cạnhKiểm tra mẫu trực quan; script kiểm tra đánh dấu bbox quá nhỏ
4Dữ liệu trùng lặpBản sao chính xác và các khung hình gần giống hệt nhauScript 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
5Mất cân bằng classSố lượng class thấp hơn nhiều so với mục tiêu trong thông số kỹ thuậtScript kiểm tra; tab Charts trên Platform
6Độ bao phủ và rò rỉ giữa các splitClass bị thiếu trong một split; một đoạn video xuất hiện trong hai splitScript kiểm tra; chia nhóm (bài học tiếp theo)
7Tỷ lệ ảnh nềnQuá ít hoặc quá nhiều ảnh trốngScript 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.jpg

Trong 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.jpg là 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#

Tab Charts trên Ultralytics Platform cho dataset thuốc viên y tế: Split Distribution hiển thị 80 ảnh train và 20 ảnh validation, còn Top Classes cho thấy pill có 1,755 annotation và tablet có 0

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 flag

Chê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ự:

  1. 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.
  2. 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.
  3. 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.
Không bao giờ xóa ảnh khó để tăng điểm

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: yes
Dùng thử

Trỏ qc_audit.py và suspect_labels.py đến YAML dữ liệu của bạn (hoặc COCO8 và COCO128), rồi chạy chúng. Khắc phục mọi vấn đề được báo cáo, mở từng ảnh đáng ngờ và xác định đó là lỗi nhãn hay lỗi model, sau đó chạy lại cả hai script. Sản phẩm bàn giao: qc-notes.md ghi lại số liệu trước và sau khi sửa.

Đ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.
  • Script kiểm tra không báo class không hợp lệ, dòng nhãn trùng lặp hoặc class bị thiếu trong train.

  • Mọi trường hợp đáng ngờ từ bước rà soát có hỗ trợ model đều đã được mở và xử lý bằng cách sửa nhãn hoặc xác định là lỗi model.

  • Ghi chú QC ghi lại những gì đã kiểm tra, phát hiện và sửa.

Hiể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 first

Hã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.

Tiếp theo là gì

Khi nhãn đã sạch, bài học tiếp theo sẽ chia dataset sao cho điểm validation phản ánh môi trường production thay vì ghi nhớ dữ liệu.

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