Ultralytics YOLO27: 대기자 명단에 등록하세요.
Get Started
고성능 YOLO 데이터셋 구축·라벨링·레슨 5/10
강의초급

데이터셋 품질 관리

스크립트로 레이블, 클래스 균형, 중복, 분할별 포함 여부를 검사하고, 모델이 누락 가능성이 있는 레이블을 표시하도록 한 뒤, 학습 전에 발견한 문제를 수정하세요.

레이블을 수정하면 대개 하이퍼파라미터를 조정하는 것보다 정확도가 더 크게 향상됩니다. 팀은 mAP를 몇 점 높이려고 설정을 몇 주씩 조정하기도 하지만, 어느 날 오후 데이터 감사를 진행하다가 한 클래스의 일부가 처음부터 잘못 레이블링된 사실을 발견하곤 합니다. 품질 관리는 학습 실행으로 레이블 오류가 신뢰할 수 없는 수치로 바뀌기 전에 레이블링 가이드가 놓친 부분을 찾아냅니다.

학습 성과

자동 레이블 감사를 실행하고 모델 지원 의심 항목 검토를 진행한 뒤, 발견된 문제를 수정하고 결과를 간단한 QC 메모에 기록하세요.

속성 과정
이미 사용법을 알고 계신다면, 간단히 요약해 드리겠습니다.
  1. 인스턴스 클래스별 수, 배경 비율, 유효하지 않은 클래스, 중복, train에서 누락된 클래스를 스크립트로 집계하세요.

  2. 사전 학습 모델로 대응되는 레이블이 없는 신뢰도 높은 검출을 표시한 다음 눈으로 검토하세요.

  3. 클래스 불균형은 먼저 데이터를 수집하고, 다음으로 오버샘플링하고, 마지막으로 손실 가중치를 적용해 해결하세요.

  4. 무엇을 확인하고 수정했는지 기록하세요.

실습형

일곱 가지 검사 항목#

첫 학습 실행 전과 주요 레이블링 배치가 끝난 후에 일곱 가지 항목을 모두 검사하세요. 주석이 달린 데이터 전처리 가이드에서 여러 항목을 자세히 설명합니다.

#검사 항목확인할 내용확인 방법
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 차트 탭: 분할 분포에는 train 이미지 80장과 validation 이미지 20장이 표시되고, 주요 클래스에는 pill 주석 1,755개와 tablet 주석 0개가 표시됨

Ultralytics Platform의 데이터셋 차트 탭에서는 분할 분포, 클래스 수, 박스 통계를 시각화합니다(데이터셋 문서). 이 스크린샷에는 이미 QC 검사 결과가 나타나 있습니다. 클래스 목록에는 클래스가 두 개 있지만 tablet에는 주석 1,755개 중 0개만 있습니다. 레이블이 없는 클래스는 학습할 수 없습니다. 학습 전에 해당 클래스에 레이블을 지정하거나 클래스 목록에서 삭제하세요.

올바른 순서로 클래스 불균형을 해결하세요#

   forklift  ████████████████████████ 8000 instances
   person    ████████████████ 5400
   pallet    ██ 700                              ← red flag

이런 격차가 있으면 지게차는 자신 있게 검출하면서 팔레트는 놓치는 모델이 만들어집니다. 해결 방법 세 가지를 순서대로 살펴보겠습니다.

  1. 소수 클래스 데이터를 더 수집하고 레이블링하세요. 항상 가장 좋은 방법입니다. 모든 클래스에서 이미지 1,500장 이상, 인스턴스 10,000개 이상을 목표로 하세요.
  2. train에서만 오버샘플링하세요. 소수 클래스가 포함된 이미지를 학습 파일 목록에 두 번 이상 나열해 에폭마다 더 자주 사용되도록 하세요. 복사본은 val과 test에 포함하지 마세요.
  3. 손실에 가중치를 적용하세요. 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_audit.py과 suspect_labels.py이 자체 데이터 YAML(또는 COCO8 및 COCO128)을 가리키도록 지정하고 실행하세요. 보고된 문제를 모두 수정하고, 의심 이미지마다 레이블 오류인지 모델 오류인지 확인한 다음 두 스크립트를 다시 실행하세요. 결과물은 수정 전후의 집계 결과를 기록한 qc-notes.md입니다.

완료 조건
다음 조건을 모두 충족하면 레슨을 완료한 것입니다.
  • 감사 스크립트에서 유효하지 않은 클래스, 중복 레이블 행, train에서 누락된 클래스가 보고되지 않습니다.

  • 모델 지원 검토에서 발견된 모든 의심 항목을 열어 확인하고, 레이블 수정 또는 모델 오류로 판정했습니다.

  • QC 메모에 검사한 항목, 발견한 내용, 수정한 내용이 기록되어 있습니다.

정답 보기

이번 강의의 데모 실행 두 건에 대한 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를 통과하지 못합니다. 해당 클래스의 지표는 모델이 학습할 기회조차 없었던 대상을 평가하기 때문입니다.

다음 단계

레이블을 정리했다면 다음 강의에서는 데이터셋을 분할하여 검증 점수가 학습 데이터를 기억한 결과가 아니라 실제 운영 환경을 반영하도록 합니다.