데이터셋 품질 관리
스크립트로 레이블, 클래스 균형, 중복, 분할별 포함 여부를 검사하고, 모델이 누락 가능성이 있는 레이블을 표시하도록 한 뒤, 학습 전에 발견한 문제를 수정하세요.
레이블을 수정하면 대개 하이퍼파라미터를 조정하는 것보다 정확도가 더 크게 향상됩니다. 팀은 mAP를 몇 점 높이려고 설정을 몇 주씩 조정하기도 하지만, 어느 날 오후 데이터 감사를 진행하다가 한 클래스의 일부가 처음부터 잘못 레이블링된 사실을 발견하곤 합니다. 품질 관리는 학습 실행으로 레이블 오류가 신뢰할 수 없는 수치로 바뀌기 전에 레이블링 가이드가 놓친 부분을 찾아냅니다.
실습형
일곱 가지 검사 항목#
첫 학습 실행 전과 주요 레이블링 배치가 끝난 후에 일곱 가지 항목을 모두 검사하세요. 주석이 달린 데이터 전처리 가이드에서 여러 항목을 자세히 설명합니다.
| # | 검사 항목 | 확인할 내용 | 확인 방법 |
|---|---|---|---|
| 1 | 누락된 레이블 | 어노테이터가 건너뛴 객체 | 아래의 의심 항목 검토 및 시각적 샘플 검사 |
| 2 | 잘못되었거나 유효하지 않은 클래스 | 뒤바뀐 클래스, 클래스 목록 범위를 벗어난 ID | 감사 스크립트, 첫 실행 후 혼동 행렬 |
| 3 | 여유가 너무 큰 박스 | 여백이 있거나 가장자리가 잘린 박스 | 시각적 샘플 검사, 감사에서 표시된 작은 박스 |
| 4 | 중복 | 완전히 동일한 복사본과 거의 동일한 프레임 | 완전히 동일한 복사본은 감사 스크립트로 확인하고, 거의 중복되는 항목은 분할별로 색상을 지정한 Ultralytics Platform의 클러스터링 뷰에서 확인합니다. |
| 5 | 클래스 불균형 | 사양의 목표치에 크게 못 미치는 클래스 | 감사 스크립트, Platform의 차트 탭 |
| 6 | 분할별 포함 여부 및 누수 | 특정 분할에서 누락된 클래스, 두 분할에 포함된 동일 클립 | 감사 스크립트, 그룹 기반 분할(다음 강의) |
| 7 | 배경 비율 | 빈 이미지가 너무 적거나 많음 | 감사 스크립트, 목표치 0~10% |
집계를 자동화하세요#
검사 항목 2, 4, 5, 6, 7은 집계 작업이므로 스크립트로 자동화하세요. 이 감사 도구는 train:과 val:이 이미지 폴더를 가리키는 객체 검출 데이터 YAML을 읽습니다. 분할별 클래스 인스턴스 수와 배경 이미지 수를 집계하고, 유효하지 않은 클래스 ID, 너무 작은 박스, 중복 레이블 행, 바이트 단위로 동일한 이미지를 표시하며, val에는 있지만 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']마지막 줄이 검사 결과입니다. person은 val에 인스턴스가 10개 있지만 train에는 없으므로, 모델이 학습한 적 없는 클래스를 val에서 평가하고 있습니다. COCO8은 8장으로 구성된 스모크 테스트 세트이므로 이 경우는 예상된 결과입니다. 하지만 자체 데이터셋에서 이런 결과가 나오면 학습 전에 분할을 수정하거나 데이터를 더 수집해야 합니다. 문제가 있으면 해당 분할 아래에 ! 000000000025.jpg: class 99 is not in names 또는 ! 000000000009.jpg: duplicate label rows로 표시됩니다.
모델로 의심스러운 레이블을 찾아보세요#
레이블 누락은 가장 큰 피해를 주는 오류이자, 이미지를 스크롤하며 찾아내기 가장 어려운 오류입니다. 사전 학습 모델을 사용하면 누락 가능성이 있는 항목을 찾을 수 있습니다. 주변에 레이블이 없는 신뢰도 높은 검출은 레이블 누락이거나 모델 오류이므로 어느 쪽이든 확인할 가치가 있습니다. 다음은 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이미지 128장 중 스크립트가 검토 대상을 세 장으로 좁혀 줍니다. 해당 이미지를 열어 보면 두 유형의 결과를 모두 확인할 수 있습니다.
000000000438.jpg은 도넛 12개가 있는 박스이며, 신뢰도 높은 검출 두 개가 레이블된 도넛과 겹치지 않습니다. 레이블 누락 또는 위치 오류의 후보입니다.000000000531.jpg은 테니스 코트에 비친 사진작가의 그림자를 "person"으로 표시합니다. 레이블은 맞고 모델이 틀렸습니다. 따라서 의심 항목을 자동으로 추가하지 않고 사람이 하나씩 검토해야 합니다.
검토 범위를 넓히려면 conf의 값을 낮추세요. 첫 번째 파인튜닝 모델을 만들었다면 자체 모델로 교체하세요. 해당 모델은 자체 클래스에 대해 알고 있기 때문입니다.
데이터셋 차트를 확인하세요#

Ultralytics Platform의 데이터셋 차트 탭에서는 분할 분포, 클래스 수, 박스 통계를 시각화합니다(데이터셋 문서). 이 스크린샷에는 이미 QC 검사 결과가 나타나 있습니다. 클래스 목록에는 클래스가 두 개 있지만 tablet에는 주석 1,755개 중 0개만 있습니다. 레이블이 없는 클래스는 학습할 수 없습니다. 학습 전에 해당 클래스에 레이블을 지정하거나 클래스 목록에서 삭제하세요.
올바른 순서로 클래스 불균형을 해결하세요#
forklift ████████████████████████ 8000 instances
person ████████████████ 5400
pallet ██ 700 ← red flag이런 격차가 있으면 지게차는 자신 있게 검출하면서 팔레트는 놓치는 모델이 만들어집니다. 해결 방법 세 가지를 순서대로 살펴보겠습니다.
- 소수 클래스 데이터를 더 수집하고 레이블링하세요. 항상 가장 좋은 방법입니다. 모든 클래스에서 이미지 1,500장 이상, 인스턴스 10,000개 이상을 목표로 하세요.
- train에서만 오버샘플링하세요. 소수 클래스가 포함된 이미지를 학습 파일 목록에 두 번 이상 나열해 에폭마다 더 자주 사용되도록 하세요. 복사본은 val과 test에 포함하지 마세요.
- 손실에 가중치를 적용하세요.
cls_pw학습 인수(기본값은 0.0이며 역빈도 가중치를 완전히 적용하면 최대 1.0)는 희귀 클래스의 분류 손실 가중치를 높입니다. 1번과 2번을 적용한 뒤에만 사용하세요.
검증 세트에서 흐리거나 복잡하거나 판단하기 어려운 이미지를 제거하면 mAP는 올라가지만 모델이 나아지는 것은 아니며, 평가의 정직성만 떨어집니다. 배포 환경에서 실제로 촬영된 이미지라면 남겨 두고 라벨을 수정하세요. 실제 이상치(실수 9)와 완전히 중복된 이미지만 제거합니다.
QC 메모를 작성하세요#
몇 줄이면 충분합니다. 고객이나 감사자가 몇 달 뒤 "무엇을 확인했나요?"라고 물었을 때 답할 수 있습니다.
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정답 보기
이번 강의의 데모 실행 두 건에 대한 QC 메모:
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마지막 줄을 확인하세요. val에만 나타나는 클래스가 있는 데이터셋은 개별 레이블이 모두 정확하더라도 QC를 통과하지 못합니다. 해당 클래스의 지표는 모델이 학습할 기회조차 없었던 대상을 평가하기 때문입니다.