🤖 AI 모델 학습 가이드

YOLOv8로
카드 결함 탐지 모델 학습

Label Studio에서 내보낸 라벨 데이터로 YOLOv8 객체 탐지 모델을 학습시키는 전체 과정입니다. 환경 설치부터 추론 API 배포까지 코드 중심으로 안내합니다.

🎯 목표 mAP50: 0.85 이상
📦 프레임워크: Ultralytics YOLOv8
💻 GPU: NVIDIA CUDA 11.8+
🗂️ 탐지 클래스: 6종 결함
🗺️

전체 학습 파이프라인

데이터 준비부터 모델 배포까지 전체 흐름을 파악합니다.

STEP 01
데이터 준비
YOLO 형식 변환
STEP 02
환경 구성
CUDA + Ultralytics
STEP 03
모델 학습
Train / Val
STEP 04
평가
mAP · F1
STEP 05
배포
ONNX / TensorRT
ℹ️

YOLOv8을 선택하는 이유

Ultralytics YOLOv8은 단일 스테이지 객체 탐지 모델로, 속도와 정확도의 균형이 뛰어납니다. Python API가 직관적이고, ONNX/TensorRT 변환이 간단하여 서비스 배포까지 동일한 프레임워크로 처리할 수 있습니다.


📐

모델 크기 선택

카드 결함 탐지 목적에 맞는 YOLOv8 모델 변형을 선택합니다.

YOLOv8n
3.2M params · Nano
CPU 환경에서도 동작. 정확도 낮음. 프로토타입용.
YOLOv8s
11.2M params · Small
초기 실험용. GPU 없이도 학습 가능.
YOLOv8m 권장
25.9M params · Medium
카드 결함 탐지 최적. 속도·정확도 균형 우수.
YOLOv8l
43.7M params · Large
고정밀 요구 시. RTX 3080+ 권장.
YOLOv8x
68.2M params · XLarge
최고 정확도. A100/H100 필요. 서비스 배포 시 부적합.
모델권장 GPU배치 크기용도
yolov8nCPU / GTX 106032~64빠른 프로토타입
yolov8sGTX 1080 이상32초기 데이터 검증
yolov8mRTX 3070 이상16~32카드 결함 탐지 권장
yolov8lRTX 3080 이상8~16고정밀 탐지 필요 시
yolov8xA100 / H1004~8최고 성능 연구용

📁

데이터셋 디렉터리 구조

YOLOv8이 요구하는 폴더 구조대로 데이터를 정리합니다.

card-defect-dataset/ # 루트 디렉터리 ├── data.yaml # 필수 — 클래스 정의 & 경로 설정 ├── train/ │ ├── images/ # 학습 이미지 (.jpg/.png) │ │ ├── PKM_001_front.jpg │ │ ├── PKM_002_front.jpg │ │ └── ... │ └── labels/ # 학습 라벨 (.txt — YOLO 형식) │ ├── PKM_001_front.txt │ ├── PKM_002_front.txt │ └── ... ├── val/ │ ├── images/ # 검증 이미지 (전체의 15~20%) │ └── labels/ └── test/ ├── images/ # 테스트 이미지 (전체의 10~15%) └── labels/

학습 / 검증 / 테스트 분리 비율

분류비율목적주의사항
Train70 ~ 75%모델 가중치 학습가장 다양한 결함 분포 포함
Val15 ~ 20%에폭별 성능 평가Train 데이터와 중복 금지
Test10 ~ 15%최종 성능 측정학습 중 절대 사용 금지
⚠️

동일 카드의 앞/뒷면 분리 주의

같은 카드의 앞면과 뒷면이 각각 Train과 Val에 들어가면 데이터 누출(Data Leakage)이 발생합니다. 카드 ID 단위로 분리하세요.

데이터 분리 Python 스크립트

pythonimport os, shutil, random
from pathlib import Path

def split_dataset(src_dir, dst_dir, ratios=(0.75, 0.15, 0.10)):
    """카드 ID 단위로 Train/Val/Test 분리"""
    src = Path(src_dir)
    images = sorted([f for f in src.glob("images/*.jpg")])
    random.seed(42)
    random.shuffle(images)

    n = len(images)
    n_train = int(n * ratios[0])
    n_val   = int(n * ratios[1])

    splits = {
        "train": images[:n_train],
        "val":   images[n_train:n_train+n_val],
        "test":  images[n_train+n_val:]
    }

    for split, files in splits.items():
        for img_path in files:
            lbl_path = src / "labels" / img_path.with_suffix(".txt").name
            for kind, sub in [("images", img_path), ("labels", lbl_path)]:
                dst = Path(dst_dir) / split / kind
                dst.mkdir(parents=True, exist_ok=True)
                if sub.exists():
                    shutil.copy(sub, dst / sub.name)

    print(f"완료: train={len(splits['train'])}, val={len(splits['val'])}, test={len(splits['test'])}")

split_dataset("./raw_data", "./card-defect-dataset")

🔄

Label Studio → YOLO 형식 변환

Label Studio에서 내보낸 데이터를 YOLOv8 학습 형식으로 변환합니다.

Label Studio에서 YOLO 형식으로 내보내기

  1. 1
    프로젝트 Export 클릭프로젝트 상단 메뉴의 Export 버튼 클릭
  2. 2
    YOLO 형식 선택형식 목록에서 YOLO with images 선택
  3. 3
    ZIP 다운로드이미지 + .txt 라벨 파일이 함께 압축 다운로드됨
  4. 4
    압축 해제 후 확인각 이미지에 대응하는 .txt 파일이 생성되었는지 확인

YOLO 라벨 형식 이해

각 라벨 파일(.txt)의 한 행이 결함 하나를 나타냅니다.

txt# 형식: class_id  cx  cy  width  height  (모두 0~1 정규화)
# class_id: 0=Corner_Wear, 1=Scratch, 2=Crease, 3=Stain, 4=Dent, 5=Restoration

0 0.112 0.098 0.089 0.142   # 좌상단 Corner_Wear
1 0.445 0.762 0.198 0.064   # 하단 Scratch
3 0.680 0.320 0.095 0.080   # 우측 Stain

COCO JSON → YOLO 변환 (Label Studio JSON 내보내기 사용 시)

pythonimport json
from pathlib import Path

CLASS_MAP = {
    "Corner_Wear": 0, "Scratch": 1, "Crease": 2,
    "Stain": 3, "Dent": 4, "Restoration": 5
}

def ls_json_to_yolo(json_path, out_dir):
    with open(json_path) as f:
        data = json.load(f)

    Path(out_dir).mkdir(parents=True, exist_ok=True)

    for task in data:
        img_w = task["annotations"][0]["result"][0]["original_width"]
        img_h = task["annotations"][0]["result"][0]["original_height"]
        img_name = Path(task["data"]["image"]).stem
        lines = []

        for ann in task["annotations"][0]["result"]:
            if ann["type"] != "rectanglelabels":
                continue
            v = ann["value"]
            label = v["rectanglelabels"][0]
            cls_id = CLASS_MAP.get(label, -1)
            if cls_id == -1:
                continue
            # Label Studio: x,y,w,h 는 % 단위 → 정규화
            cx = (v["x"] + v["width"] / 2) / 100
            cy = (v["y"] + v["height"] / 2) / 100
            w  = v["width"]  / 100
            h  = v["height"] / 100
            lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}")

        with open(f"{out_dir}/{img_name}.txt", "w") as f:
            f.write("\n".join(lines))

ls_json_to_yolo("./export.json", "./card-defect-dataset/train/labels")

data.yaml 작성

YOLOv8 학습의 핵심 설정 파일입니다. 경로와 클래스 정보를 정의합니다.

yaml# card-defect-dataset/data.yaml

# 데이터셋 루트 경로 (절대 경로 권장)
path: /workspace/card-defect-dataset

# 상대 경로 (path 기준)
train: train/images
val:   val/images
test:  test/images

# 클래스 수
nc: 6

# 클래스 이름 (라벨 파일의 class_id 순서와 일치)
names:
  0: Corner_Wear
  1: Scratch
  2: Crease
  3: Stain
  4: Dent
  5: Restoration

⚙️

학습 환경 설치

YOLOv8 학습에 필요한 Python 패키지와 GPU 드라이버를 설치합니다.

1. 가상환경 생성

bash# conda 환경 생성 (Python 3.10 권장)
conda create -n yolo-card python=3.10 -y
conda activate yolo-card

# 또는 venv
python3 -m venv yolo-env
source yolo-env/bin/activate

2. PyTorch + CUDA 설치

bash# CUDA 11.8 기준 (CUDA 버전에 맞게 조정)
pip install torch torchvision torchaudio \
    --index-url https://download.pytorch.org/whl/cu118

# CUDA 설치 확인
python -c "import torch; print(torch.cuda.is_available(), torch.version.cuda)"

3. Ultralytics 설치

bashpip install ultralytics

# 설치 확인
yolo checks

4. 추가 패키지

bashpip install \
    albumentations   \   # 데이터 증강
    mlflow           \   # 실험 관리
    matplotlib       \   # 결과 시각화
    seaborn          \   # confusion matrix
    opencv-python        # 이미지 처리
💡

GPU 없는 환경에서 테스트하려면

CUDA 없이 CPU만으로도 소규모 학습이 가능합니다. pip install torch torchvision으로 CPU 버전을 설치하고, 학습 시 device='cpu'를 지정하세요. 단, 학습 속도가 GPU 대비 10~50배 느립니다.


🚀

모델 학습 실행

YOLOv8 모델 학습을 실행하는 다양한 방법을 설명합니다.

방법 A — CLI (명령줄)

bash# 기본 학습 명령 (YOLOv8m, 100 에폭)
yolo detect train \
    data=card-defect-dataset/data.yaml \
    model=yolov8m.pt \
    epochs=100 \
    imgsz=640 \
    batch=16 \
    device=0 \
    name=card_defect_v1 \
    project=./runs

방법 B — Python API 권장

pythonfrom ultralytics import YOLO

# 사전학습 가중치로 시작 (전이 학습)
model = YOLO("yolov8m.pt")

# 학습 실행
results = model.train(
    data    = "card-defect-dataset/data.yaml",
    epochs  = 100,
    imgsz   = 640,
    batch   = 16,
    device  = 0,              # GPU 0번. CPU는 'cpu'
    workers = 4,
    name    = "card_defect_v1",
    project = "./runs",
    exist_ok= True,
    verbose = True,
)

print(f"최적 모델: {results.save_dir}/weights/best.pt")

핵심 하이퍼파라미터 설명

파라미터권장값설명조정 기준
epochs100~200전체 데이터 반복 횟수Early stopping 사용 시 크게 설정
imgsz640입력 이미지 해상도미세 결함 많으면 1280 고려
batch16GPU 배치 크기VRAM 부족 시 8로 낮춤
lr00.01초기 학습률전이학습 시 0.001~0.005 권장
patience30Early stopping 대기 에폭val mAP 개선 없으면 중단
optimizerAdamW최적화 알고리즘SGD도 가능, 기본값 auto
weight_decay0.0005L2 정규화 강도과적합 시 0.001로 증가
warmup_epochs3학습률 워밍업 에폭기본값 유지 권장

카드 결함 탐지 최적화 설정

pythonresults = model.train(
    data      = "card-defect-dataset/data.yaml",
    epochs    = 150,
    imgsz     = 1280,          # 미세 결함 대응 위한 고해상도
    batch     = 8,             # 고해상도 학습 시 배치 감소
    device    = 0,
    lr0       = 0.005,         # 전이학습 — 낮은 초기 학습률
    lrf       = 0.01,          # 최종 학습률 = lr0 * lrf
    patience  = 30,            # early stopping
    optimizer = "AdamW",
    weight_decay = 0.0005,
    label_smoothing = 0.1,   # 라벨 불확실성 보정
    cos_lr    = True,          # cosine 학습률 스케줄
    name      = "card_defect_v1",
    project   = "./runs",
)

카드 결함 전용 데이터 증강 설정

카드 이미지는 촬영 환경이 통제되므로 증강 범위를 제한합니다. 과도한 증강은 오히려 성능을 낮춥니다.

pythonresults = model.train(
    # ... 기본 파라미터 ...

    # ── 권장 증강 (카드 이미지 특성 반영) ──
    hsv_h    = 0.01,   # 색조 변화 ±1% (카드 색상 보존)
    hsv_s    = 0.5,    # 채도 변화 ±50%
    hsv_v    = 0.3,    # 밝기 변화 ±30%
    degrees  = 5,      # 회전 ±5° (과도한 회전 금지)
    translate= 0.1,   # 이동 ±10%
    scale    = 0.3,    # 스케일 ±30%
    fliplr   = 0.5,    # 좌우 반전 50%
    flipud   = 0.0,    # 상하 반전 비활성 (카드 방향 고정)
    mosaic   = 0.5,    # Mosaic 증강 50% (작은 결함 탐지 강화)
    mixup    = 0.1,    # MixUp 10%
    blur     = 0.01,   # 블러 1% (촬영 흔들림 시뮬레이션)

    # ── 비활성화 ──
    perspective = 0.0, # 원근 변환 비활성 (고정 지그 촬영)
    shear       = 0.0, # 전단 변환 비활성
)

증강 전략 요약

밝기·채도 변화는 적극 활용 (조명 환경 차이 대응). 기하학적 변환은 최소화 (고정 지그 촬영 환경). Mosaic은 활성화하여 소규모 데이터에서 효율 극대화.


📈

학습 모니터링

학습 진행 상황을 실시간으로 추적합니다.

학습 로그 예시

Epoch box_loss cls_loss dfl_loss mAP50 mAP50-95
1/100 1.842 2.317 1.521 0.243 0.098
20/100 1.102 1.284 1.241 0.612 0.354
50/100 0.784 0.892 1.091 0.768 0.492
100/100 ★ 0.612 0.671 0.983 0.871 0.584

TensorBoard로 실시간 모니터링

bash# 별도 터미널에서 실행
tensorboard --logdir ./runs/card_defect_v1

# 브라우저에서 http://localhost:6006 접속

결과 파일 위치

runs/card_defect_v1/ ├── weights/ │ ├── best.pt # ★ 최고 mAP 시점의 가중치 (배포용) │ └── last.pt # 마지막 에폭 가중치 ├── results.csv # 에폭별 loss/mAP 수치 ├── results.png # 학습 곡선 그래프 ├── confusion_matrix.png # 클래스별 혼동 행렬 ├── PR_curve.png # Precision-Recall 곡선 ├── F1_curve.png # F1 곡선 └── val_batch0_pred.jpg # 검증셋 예측 시각화

🎯

모델 평가

학습된 모델의 성능을 테스트 데이터로 정량 평가합니다.

평가 실행

pythonfrom ultralytics import YOLO

# 최적 가중치 로드
model = YOLO("./runs/card_defect_v1/weights/best.pt")

# 테스트 데이터로 평가
metrics = model.val(
    data   = "card-defect-dataset/data.yaml",
    split  = "test",      # 테스트셋으로 평가
    imgsz  = 1280,
    conf   = 0.25,        # 신뢰도 임계값
    iou    = 0.7,         # IoU 임계값
    device = 0,
)

print(f"mAP50     : {metrics.box.map50:.4f}")
print(f"mAP50-95  : {metrics.box.map:.4f}")
print(f"Precision : {metrics.box.mp:.4f}")
print(f"Recall    : {metrics.box.mr:.4f}")

평가 지표 해석

mAP50
평균 정밀도 (IoU=0.5)
목표: 0.85 이상
mAP50-95
엄격한 평균 정밀도
목표: 0.55 이상
Precision
탐지의 정확성
False Positive 제어
Recall
결함 탐지율
False Negative 제어
지표계산식낮을 때 의미해결 방법
Precision TP / (TP + FP) 결함 없는 영역을 결함으로 탐지 (오탐) conf 임계값 높이기 / 데이터 품질 개선
Recall TP / (TP + FN) 실제 결함을 놓치는 경우 (미탐) conf 임계값 낮추기 / 데이터 추가
mAP50 AP의 클래스 평균 전체 탐지 성능 부족 더 많은 데이터 / 모델 크기 확대
F1 2 × P × R / (P+R) Precision과 Recall 불균형 클래스별 개별 분석 필요

클래스별 성능 분석

python# 클래스별 AP 출력
class_names = ["Corner_Wear", "Scratch", "Crease",
               "Stain", "Dent", "Restoration"]

print("\n클래스별 AP@0.5:")
for i, (name, ap) in enumerate(zip(class_names, metrics.box.ap50)):
    status = "✅" if ap >= 0.8 else ("⚠️" if ap >= 0.6 else "❌")
    print(f"{status} {name:18s}: {ap:.4f}")

🔍

추론 (Inference)

학습된 모델로 새 카드 이미지의 결함을 탐지합니다.

단일 이미지 추론

pythonfrom ultralytics import YOLO
import cv2

model = YOLO("./runs/card_defect_v1/weights/best.pt")

# 추론 실행
results = model.predict(
    source = "card_sample.jpg",
    conf   = 0.35,      # 신뢰도 0.35 이상만 탐지
    iou    = 0.45,      # NMS IoU 임계값
    imgsz  = 1280,
    save   = True,       # 결과 이미지 저장
    show   = False,
)

# 탐지 결과 파싱
for r in results:
    boxes   = r.boxes.xyxy.cpu().numpy()   # [x1,y1,x2,y2]
    classes = r.boxes.cls.cpu().numpy()    # class id
    confs   = r.boxes.conf.cpu().numpy()   # 신뢰도

    CLASS_NAMES = ["Corner_Wear","Scratch","Crease",
                   "Stain","Dent","Restoration"]
    for box, cls, conf in zip(boxes, classes, confs):
        print(f"결함: {CLASS_NAMES[int(cls)]:15s} | 신뢰도: {conf:.3f} | 위치: {box.astype(int)}")

배치 추론 (폴더 전체)

python# 디렉터리 내 모든 이미지 일괄 처리
results = model.predict(
    source = "./new_cards/",  # 폴더 경로
    conf   = 0.35,
    imgsz  = 1280,
    save   = True,
    save_txt = True,          # YOLO 형식 .txt 저장
    save_conf= True,          # 신뢰도 포함
    project= "./inference_results",
)

print(f"처리 완료: {len(results)}장")

FastAPI 추론 서버 구성

python# inference_server.py
from fastapi import FastAPI, UploadFile, File
from ultralytics import YOLO
import cv2, numpy as np

app = FastAPI(title="카드 결함 탐지 API")
model = YOLO("./runs/card_defect_v1/weights/best.pt")

CLASS_NAMES = ["Corner_Wear","Scratch","Crease",
               "Stain","Dent","Restoration"]

@app.post("/detect")
async def detect_defects(file: UploadFile = File(...)):
    img_bytes = await file.read()
    np_arr = np.frombuffer(img_bytes, np.uint8)
    img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)

    results = model.predict(img, conf=0.35, imgsz=1280)
    defects = []

    for r in results:
        for box in r.boxes:
            defects.append({
                "type":       CLASS_NAMES[int(box.cls)],
                "confidence": round(float(box.conf), 4),
                "bbox":       box.xyxy[0].tolist(),
            })

    return {"filename": file.filename, "defects": defects}

# 실행: uvicorn inference_server:app --host 0.0.0.0 --port 8000

📦

모델 배포 형식 변환

서비스 환경에 맞는 형식으로 모델을 변환합니다.

형식용도속도권장 환경
PyTorch .pt개발/연구기준Python 서버
ONNX범용 배포1.5~2xCPU/GPU 모두
TensorRT .engineNVIDIA GPU 최적화3~5xNVIDIA 서버
OpenVINOIntel CPU 최적화2~3xIntel 엣지 기기
pythonfrom ultralytics import YOLO

model = YOLO("./runs/card_defect_v1/weights/best.pt")

# ONNX 변환 (범용 — 권장)
model.export(format="onnx", imgsz=1280, simplify=True)

# TensorRT 변환 (NVIDIA GPU 서버 — 최고 속도)
model.export(
    format   = "engine",
    imgsz    = 1280,
    half     = True,     # FP16 — 속도 2배, 메모리 절반
    device   = 0,
    workspace= 4,        # GB
)

전이 학습 전략

적은 데이터로 높은 성능을 내기 위한 전이 학습 방법입니다.

단계별 동결 학습 (Freeze Training)

데이터가 부족할 때(클래스당 500장 미만) 효과적입니다. 초기에는 Backbone을 동결하고 Head만 학습한 후, 이후 전체를 미세 조정합니다.

python# Phase 1: Head만 학습 (Backbone 동결) — 10~20 에폭
model = YOLO("yolov8m.pt")
results1 = model.train(
    data    = "card-defect-dataset/data.yaml",
    epochs  = 20,
    freeze  = 10,   # 첫 10개 레이어 동결
    lr0     = 0.01,
    name    = "phase1_freeze",
)

# Phase 2: 전체 미세 조정 — 80~100 에폭
model2 = YOLO("./runs/phase1_freeze/weights/best.pt")
results2 = model2.train(
    data    = "card-defect-dataset/data.yaml",
    epochs  = 100,
    freeze  = 0,    # 전체 레이어 학습
    lr0     = 0.001, # 더 낮은 학습률
    name    = "phase2_finetune",
)

🔧

문제 해결

학습 중 자주 발생하는 문제와 해결 방법입니다.

CUDA Out of Memory

RuntimeError: CUDA out of memory

GPU VRAM 부족. batch 크기를 절반으로 줄이거나 imgsz를 640으로 낮추세요. --batch=-1로 자동 배치 크기 탐색도 가능합니다.

python# 자동 배치 크기 탐색 (VRAM의 60% 사용)
results = model.train(..., batch=-1)

mAP가 수렴하지 않음

⚠️

50 에폭 이후에도 mAP가 0.3 미만

① 라벨 파일 경로 오류 여부 확인. ② 클래스 분포 불균형(특정 결함 데이터 부족). ③ 학습률이 너무 높거나 낮음. ④ data.yaml 경로 오류.

python# 데이터셋 검증
from ultralytics.data.utils import check_det_dataset
info = check_det_dataset("card-defect-dataset/data.yaml")
print(info)

특정 클래스 AP가 매우 낮음

⚠️

예: Restoration AP = 0.12

해당 클래스 데이터가 부족하거나 라벨 품질이 낮습니다. 해당 클래스 이미지를 추가 수집하고 라벨을 재검토하세요. 임시 방편으로 Class-weighted Loss 적용도 가능합니다.

과적합 (Overfitting)

Train loss는 낮아지는데 Val loss가 상승하면 과적합입니다.

python# 과적합 방지 설정
results = model.train(
    dropout        = 0.2,   # Dropout 추가
    weight_decay   = 0.001, # L2 정규화 강화
    label_smoothing= 0.1,  # 라벨 스무딩
    mixup          = 0.2,   # MixUp 증강 강화
    copy_paste     = 0.1,   # Copy-Paste 증강
)

최종 체크리스트

모델을 서비스에 배포하기 전 확인 사항입니다.

데이터 준비

  • data.yaml의 경로(path, train, val, test)가 실제 디렉터리와 일치한다
  • 클래스 수(nc)와 names 목록의 순서가 라벨 파일과 일치한다
  • Train/Val/Test가 카드 ID 단위로 분리되어 데이터 누출이 없다
  • 클래스당 최소 300장 이상 학습 데이터가 확보되었다

학습 완료 후

  • best.pt (최고 mAP 가중치)를 안전한 위치에 백업했다
  • Test 데이터셋으로 최종 mAP50이 0.85 이상 달성되었다
  • confusion_matrix.png를 확인하여 혼동이 많은 클래스를 파악했다
  • 클래스별 AP@0.5를 모두 확인하여 낮은 클래스를 식별했다

배포 전

  • ONNX 또는 TensorRT 변환 후 추론 결과가 .pt와 동일한지 확인했다
  • conf/iou 임계값을 서비스 요구사항(Precision vs Recall 균형)에 맞게 조정했다
  • 추론 속도(FPS)가 서비스 SLA를 충족하는지 확인했다
  • 이상 케이스(결함 없는 카드, 극단적 조명 차이)에서의 동작을 테스트했다