Ultralytics YOLO27: Tham gia danh sách chờ.
Get Started
Nền tảng Thị giác máy tính·Đọc về các chỉ số·Bài học 8/10
Bài họcngười mới bắt đầu

Chia tập dữ liệu để đánh giá trung thực

Tìm ảnh gần trùng lặp gây rò rỉ giữa tập train và validation, rồi chia tập theo đơn vị sẽ là dữ liệu mới khi triển khai.

Nếu model đạt 95% trên tập validation nhưng chỉ 60% trong môi trường production, cách chia tập thường đã đánh giá sai. Các ảnh gần như giống hệt nhau — những khung hình video liền kề, cùng camera trong cùng ngày — nằm ở cả hai phía, khiến tập validation kiểm tra khả năng ghi nhớ thay vì khả năng tổng quát hóa. Bạn sẽ tìm thấy chính xác kiểu rò rỉ đó trong một bộ dữ liệu thực tế, rồi khắc phục bằng cách chia theo nhóm.

Kết quả

Phát hiện ảnh gần trùng lặp giữa các tập, chia theo khóa nhóm và xác minh rằng mỗi nhóm chỉ xuất hiện trong một tập.

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. Tập train dùng để khớp trọng số, tập validation định hướng quyết định, tập test chỉ dùng để báo cáo một lần.

  2. Hãy kiểm tra các ảnh gần như giống hệt nhau giữa các tập trước khi tin vào bất kỳ điểm số validation nào.

  3. Tìm khóa nhóm: clip, camera, ngày hoặc đối tượng.

  4. Xáo trộn các nhóm, không phải ảnh, rồi kiểm tra để đảm bảo mọi lớp đều xuất hiện trong mỗi tập.

  5. Cố định tập test và chỉ sử dụng một lần cho mỗi thử nghiệm đã hoàn tất.

Thực hành

Ba tập dữ liệu, ba vai trò#

TậpDữ liệu dùng để huấn luyệnThời điểm xem xét
TrainModel: trọng số được khớp tại đâyMỗi epoch
ValidationBạn: hyperparameter, số epoch, kích thước model, ngưỡngSau mỗi thử nghiệm
TestKhông ai: chỉ báo cáo con số cuối cùngMột lần, sau khi mọi quyết định đã được đưa ra

Mỗi quyết định bạn đưa ra khi xem một tập dữ liệu đều làm rò rỉ một phần nhỏ dữ liệu đó vào model. Đó là vai trò của tập validation. Tinh chỉnh trên tập test sẽ khiến con số của tập test không còn là một ước tính trung thực — bạn đã overfit theo chính các lựa chọn của mình. Trong Ultralytics, model.val(split="test") chấm điểm tập test từ file YAML của dataset.

Tìm rò rỉ trong một dataset thực tế#

Ảnh thuốc trong Frame_0.jpg, Frame_4.jpg, Frame_8.jpg là các khung hình trích từ video. Đây là dấu hiệu cảnh báo: các khung hình liền kề gần như giống hệt nhau, và phép chia ngẫu nhiên sẽ đưa chúng vào cả hai tập. Hãy kiểm tra bằng cách so sánh dấu vân tay ảnh xám thu nhỏ của từng ảnh validation với từng ảnh train:

from pathlib import Path

import numpy as np
from PIL import Image
from ultralytics.data.utils import check_det_dataset

data = check_det_dataset("medical-pills.yaml")

def thumbnail(path):
    """A 32x18 grayscale fingerprint: enough to spot near-identical frames."""
    return np.asarray(Image.open(path).convert("L").resize((32, 18)), dtype=float)

train = {p.name: thumbnail(p) for p in sorted(Path(data["train"]).glob("*.jpg"))}
pairs = []
for p in sorted(Path(data["val"]).glob("*.jpg")):
    v = thumbnail(p)
    name, diff = min(((n, np.abs(v - t).mean()) for n, t in train.items()), key=lambda x: x[1])
    pairs.append((diff, p.name, name))

pairs.sort()
print(f"{sum(d < 5 for d, _, _ in pairs)} of {len(pairs)} val images have a near-twin in train (difference < 5)")
for diff, val_name, train_name in pairs[:3]:
    print(f"  {val_name} ~ {train_name}  difference {diff:.1f}")
17 of 23 val images have a near-twin in train (difference < 5)
  Frame_344.jpg ~ Frame_56.jpg  difference 0.3
  Frame_28.jpg ~ Frame_312.jpg  difference 0.5
  Frame_24.jpg ~ Frame_308.jpg  difference 1.8

Chênh lệch 0.3 trên thang 0–255 có nghĩa là hai ảnh gần như giống hệt nhau. Mở val/Frame_24.jpg và train/Frame_308.jpg cạnh nhau, bạn sẽ thấy cùng các vỉ thuốc từ cùng một góc chụp. 17 trong số 23 ảnh validation có ảnh trùng tương tự trong tập train, vì vậy điểm validation ở đây chủ yếu đo mức độ model ghi nhớ những ảnh đã thấy. Điều đó ổn với một dataset demo nhanh — nhưng tuyệt đối không được đưa vào sản phẩm.

Hộp thoại Redistribute Splits của Ultralytics Platform cho medical-pills, đặt tỷ lệ train là 80%, validation là 20% và test là 0%

Cạm bẫy tương tự tồn tại trong mọi công cụ. Mô tả ngay trong hộp thoại này cho biết công cụ gán lại ngẫu nhiên từng ảnh, nên rất phù hợp khi ảnh là các ảnh chụp độc lập — nhưng gây rò rỉ khi ảnh là các khung hình video. Trước tiên, hãy kiểm tra ảnh gần trùng lặp và giữ tỷ lệ tập test trên 0% nếu bạn muốn có một con số cuối cùng mà không ai dùng để tinh chỉnh.

Chia theo nhóm#

Quy tắc: các tập phải độc lập theo đơn vị sẽ là dữ liệu mới khi triển khai.

Dữ liệuChia theo
VideoĐoạn clip hoặc phiên ghi hình
Hệ thống camera cố địnhCamera hoặc địa điểm
Các cảnh thay đổi theo thời gianNgày hoặc tuần
Dữ liệu y tế, thể thao hoặc con ngườiBệnh nhân, vận động viên hoặc đối tượng
Chuỗi ảnh chụp liên tiếpSự kiện ghi hình

Tên file thường chứa khóa nhóm. Ví dụ này tạo 900 ảnh từ 6 camera và so sánh hai chiến lược:

import random
from collections import defaultdict

# File names carry the unit that must never straddle splits: camera_day_frame.jpg
images = [f"cam{c:02d}_day{d}_{f:04d}.jpg" for c in range(1, 7) for d in range(1, 4) for f in range(50)]

# WRONG: shuffle individual frames, so near-identical neighbors land in both splits
random.seed(0)
shuffled = random.sample(images, len(images))
train, val = shuffled[: int(0.8 * len(images))], shuffled[int(0.8 * len(images)) :]
shared = {i.split("_")[0] for i in train} & {i.split("_")[0] for i in val}
print(f"per-image split: {len(shared)} of 6 cameras appear in both train and val")

# RIGHT: shuffle whole cameras, then expand them back into images
groups = defaultdict(list)
for image in images:
    groups[image.split("_")[0]].append(image)
cameras = random.sample(sorted(groups), len(groups))
train = [i for cam in cameras[:5] for i in groups[cam]]
val = [i for cam in cameras[5:] for i in groups[cam]]
print(f"per-camera split: train {len(train)} images, val {len(val)} images, val cameras {cameras[5:]}")
per-image split: 6 of 6 cameras appear in both train and val
per-camera split: train 750 images, val 150 images, val cameras ['cam05']

Với cách chia theo từng ảnh, tập validation không bao giờ có camera mới. Với cách chia theo từng camera, điểm số ước tính kết quả trên một camera mà model chưa từng thấy — chính là con số bạn cần.

Các lỗi thường gặp#

Xáo trộn ngẫu nhiên trên dữ liệu có tương quan

Triệu chứng: kết quả validation tốt hơn nhiều so với lần thử nghiệm thực địa đầu tiên. Cách khắc phục: tìm khóa nhóm — thường ẩn trong tên file hoặc metadata ghi nhận — chia tách theo khóa đó và chạy kiểm tra ảnh gần trùng lặp để xác nhận.

Một lớp bị thiếu trong validation

Triệu chứng: một lớp hiếm không có AP, hoặc AP chỉ dựa trên hai đối tượng. Cách khắc phục: sau khi chia theo nhóm, đếm số lượng từng lớp trong mỗi tập và hoán đổi toàn bộ nhóm cho đến khi mọi lớp xuất hiện với tỷ lệ tương tự. sklearn.model_selection.StratifiedGroupKFold tự động hóa việc này.

Tinh chỉnh trên tập test

Triệu chứng: điểm test tăng dần qua các thử nghiệm. Cách khắc phục: cố định tập test, lấy mẫu từ các điều kiện gần với production và chỉ báo cáo một lần cho mỗi thử nghiệm đã hoàn tất. Với dataset nhỏ, hãy tính trung bình trên nhiều lần chia tách bằng kiểm định chéo k-fold thay vì dùng lại một lần chia tách.

Chia Dataset đúng cách trình bày tỷ lệ chia và cách áp dụng tất cả điều này cho toàn bộ dataset.

Dùng thử

Chạy kiểm tra ảnh gần trùng lặp trên dataset của bạn (hoặc medical-pills). Ghi lại số ảnh validation có ảnh gần trùng trong tập train. Sau đó xác định khóa nhóm, chia lại theo khóa đó và chạy kiểm tra rò rỉ trong phần lời giải trên các danh sách file mới. Kết quả cần bàn giao là số lượng ảnh gần trùng trước và sau, cùng với kiểm tra rò rỉ in ra một danh sách rỗng.

Đ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ó số lượng ảnh gần trùng trong cách chia hiện tại.

  • Bạn đã xác định khóa nhóm và chia lại theo khóa đó.

  • Kiểm tra rò rỉ in ra danh sách rỗng cho các tập mới.

Hiển thị lời giải

Một bước kiểm tra rò rỉ hoạt động với mọi danh sách tên file, khi được cung cấp hàm trích xuất khóa nhóm:

def leaked_groups(splits, key):
    """Return every group key that appears in more than one split."""
    owner, leaks = {}, set()
    for split, files in splits.items():
        for name in files:
            group = key(name)
            if owner.setdefault(group, split) != split:
                leaks.add(group)
    return sorted(leaks)

# Thay các danh sách này bằng tên file của bạn, ví dụ: [p.name for p in Path("images/train").glob("*.jpg")]
splits = {
    "train": ["cam01_day1_0001.jpg", "cam01_day1_0002.jpg", "cam02_day1_0001.jpg"],
    "val": ["cam03_day2_0001.jpg", "cam01_day1_0003.jpg"],
    "test": ["cam04_day3_0001.jpg"],
}
print(leaked_groups(splits, key=lambda name: name.split("_")[0]))  # nhóm theo camera
['cam01']

cam01 xuất hiện trong cả train và val, vì vậy cách chia này bị rò rỉ. Chuyển mọi ảnh cam01 vào cùng một tập và chạy lại cho đến khi in ra []. Với medical-pills, nơi các khung hình từ một video quay lại cùng một góc nhìn, tên file không có khóa nhóm rõ ràng: hãy ghi hình các video validation mới với các vỉ thuốc khác, hoặc phân cụm ảnh theo dấu vân tay gần trùng lặp rồi gán toàn bộ từng cụm vào một tập.

Tiếp theo là gì

Sau khi đã có các tập chia trung thực, tiếp theo bạn sẽ đọc các metric mà chúng tạo ra.

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