Ultralytics YOLO27: 대기자 명단에 등록하세요.
Get Started
컴퓨터 비전 기초·지표 읽기·레슨 8/10
강의초급

신뢰할 수 있는 분할

학습과 검증 세트 사이에 누수되는 유사 중복 이미지를 찾고, 배포 시 새로 접하게 될 단위를 기준으로 분할하세요.

모델이 검증에서 95%를 기록하고 운영 환경에서 60%에 그친다면, 대개 분할이 잘못된 것입니다. 서로 거의 똑같은 이미지, 예를 들면 인접한 비디오 프레임이나 같은 날 같은 카메라로 촬영한 이미지가 양쪽에 모두 들어가 검증이 일반화가 아니라 기억력을 평가한 것입니다. 실제 데이터셋에서 바로 이런 누수를 찾아낸 뒤 그룹 분할로 해결합니다.

학습 성과

분할 세트 사이의 유사 중복 이미지를 탐지하고, 그룹 키를 기준으로 분할한 뒤, 어떤 그룹도 둘 이상의 분할 세트에 포함되지 않았는지 확인하세요.

속성 과정
이미 사용법을 알고 계신다면, 간단히 요약해 드리겠습니다.
  1. 학습은 가중치를 맞추고, 검증은 의사결정을 돕고, 테스트는 한 번만 결과를 보고합니다.

  2. 검증 점수를 신뢰하기 전에 분할 세트 사이에 거의 똑같은 이미지가 있는지 확인하세요.

  3. 그룹 키를 찾으세요. 클립, 카메라, 날짜 또는 대상입니다.

  4. 이미지가 아니라 그룹을 섞은 다음, 모든 클래스가 각 분할 세트에 포함되어 있는지 확인하세요.

  5. 테스트 세트를 고정하고 실험을 완료할 때마다 한 번만 사용하세요.

실습형

세 가지 분할, 세 가지 역할#

분할어떤 용도로 사용되는가언제 확인하는가
학습모델: 여기서 가중치를 피팅합니다매 에폭마다
검증사용자: 하이퍼파라미터, 에폭 수, 모델 크기, 임계값매 실험 후
테스트아무도: 최종 수치를 보고합니다모든 결정을 내린 후 한 번

분할 데이터를 살펴보며 내리는 모든 결정은 해당 데이터의 일부를 모델에 조금씩 누출합니다. 이것이 검증 세트의 역할입니다. 테스트 세트로 튜닝하면 그 수치는 더 이상 정직한 추정치가 아닙니다. 즉, 자신의 선택에 과적합한 것입니다. Ultralytics에서 model.val(split="test")은 데이터셋 YAML에 지정된 테스트 분할의 점수를 계산합니다.

실제 데이터셋에서 데이터 누출 찾기#

의료용 알약 이미지는 비디오에서 추출한 프레임으로, 파일명이 Frame_0.jpg, Frame_4.jpg, Frame_8.jpg로 지정되어 있습니다. 이는 경고 신호입니다. 인접한 프레임은 거의 동일하며, 무작위 분할을 하면 양쪽 분할에 모두 포함됩니다. 모든 검증 이미지와 모든 학습 이미지의 작은 그레이스케일 지문을 비교하여 확인합니다:

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

0~255 범위에서 차이가 0.3이면 두 이미지는 사실상 같은 사진입니다. val/Frame_24.jpg과 train/Frame_308.jpg을 나란히 열면 같은 각도에서 촬영한 동일한 블리스터 팩이 보입니다. 검증 이미지 23장 중 17장에는 학습 세트에 쌍둥이 이미지가 있으므로, 여기서 검증 점수는 대부분 모델이 이미 본 이미지를 얼마나 잘 기억하는지 측정합니다. 간단한 데모 데이터셋에는 괜찮지만, 실제 배포에서는 반드시 피해야 합니다.

의료용 알약 데이터셋의 분할을 학습 80%, 검증 20%, 테스트 0%로 설정한 Ultralytics Platform의 분할 재분배 대화 상자

같은 함정은 어떤 도구에서도 발생할 수 있습니다. 이 대화 상자의 설명에도 모든 이미지를 무작위로 재할당한다고 되어 있습니다. 따라서 이미지가 서로 독립적인 사진이라면 적합하지만, 비디오 프레임이라면 데이터 누출이 발생합니다. 먼저 근접 중복 검사를 실행하고, 튜닝에 사용되지 않은 최종 수치가 필요하다면 테스트 비율을 0%보다 크게 유지하세요.

그룹별 분할#

원칙: 배포 시 새로 접하게 될 단위를 기준으로 분할을 독립적으로 구성해야 합니다.

데이터분할 기준
비디오클립 또는 녹화 세션
고정 카메라 fleet카메라 또는 현장
시간에 따라 변하는 장면일 또는 주
의료, 스포츠 또는 사람 데이터환자, 선수 또는 피험자
연속 촬영 사진촬영 이벤트

파일 이름에는 그룹 키가 포함된 경우가 많습니다. 이 예제에서는 카메라 6대에서 이미지 900장을 만들고 두 가지 전략을 비교합니다:

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']

이미지별 분할에서는 검증 세트가 새로운 카메라를 전혀 보지 않습니다. 카메라별 분할에서는 모델이 한 번도 보지 못한 카메라에서 어떤 결과가 나올지 점수로 추정합니다. 이것이 필요한 수치입니다.

주의할 점#

상관관계가 있는 데이터의 무작위 셔플

증상: 검증 결과가 첫 현장 테스트보다 훨씬 좋습니다. 해결: 파일 이름이나 캡처 메타데이터에 숨겨져 있는 경우가 많은 그룹 키를 찾아 해당 키를 기준으로 분할한 뒤, 근접 중복 검사를 실행해 확인합니다.

검증 데이터에서 누락된 클래스

증상: 희귀 클래스의 AP가 없거나 인스턴스 두 개만으로 계산된 AP가 있습니다. 해결: 그룹 분할 후 각 분할에서 클래스별 개수를 세고, 모든 클래스가 비슷한 비율로 나타날 때까지 그룹 전체를 서로 바꿉니다. sklearn.model_selection.StratifiedGroupKFold이 이 과정을 자동화합니다.

테스트 세트에서 튜닝하기

증상: 실험을 거듭할수록 테스트 점수가 조금씩 올라갑니다. 해결: 테스트 세트를 고정하고, 프로덕션과 유사한 조건에서 데이터를 추출해 실험을 완료할 때마다 한 번만 보고합니다. 소규모 데이터셋에서는 테스트 세트를 재사용하지 말고 k-fold cross-validation을 사용해 여러 분할의 평균을 구합니다.

데이터셋을 올바르게 분할하기에서는 비율과 전체 데이터셋에 이 원칙을 적용하는 방법을 다룹니다.

사용해 보기

데이터셋(또는 medical-pills)에서 근접 중복 검사를 실행하세요. 학습 세트에 근접 쌍둥이 이미지가 있는 검증 이미지 수를 기록하세요. 그런 다음 그룹 키를 지정하고 해당 키를 기준으로 다시 분할한 뒤, 새 파일 목록에 솔루션의 데이터 누출 검사를 실행하세요. 결과물에는 전후의 근접 쌍둥이 이미지 수와 빈 목록을 출력하는 누출 검사 결과가 포함되어야 합니다.

완료 조건
다음 조건을 모두 충족하면 레슨을 완료한 것입니다.
  • 현재 분할의 근접 쌍둥이 이미지 수를 확인했습니다.

  • 그룹 키를 지정하고 해당 키를 기준으로 다시 분할했습니다.

  • 새 분할에서 누출 검사가 빈 목록을 출력합니다.

정답 보기

그룹 키를 추출하는 함수가 주어졌을 때, 파일 이름 목록에 사용할 수 있는 누출 검사:

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)

# 이 목록을 파일 이름으로 바꾸세요. 예: [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]))  # 카메라별 그룹화
['cam01']

cam01이 train과 val 양쪽에 있으므로 이 분할에는 데이터 누출이 있습니다. 모든 cam01 이미지를 하나의 분할에 넣고 []가 출력될 때까지 다시 실행하세요. 하나의 비디오에서 추출한 프레임이 같은 장면을 반복해서 담는 medical-pills의 경우, 파일 이름에 명확한 그룹 키가 없습니다. 서로 다른 팩을 촬영한 새 검증 비디오를 녹화하거나, 근접 중복 지문을 기준으로 이미지를 클러스터링한 뒤 클러스터 전체를 하나의 분할에 할당하세요.

다음 단계

신뢰할 수 있는 분할이 준비되었으니, 다음으로 생성된 지표를 살펴보겠습니다.