YOLOv8로
카드 결함 탐지 모델 학습
Label Studio에서 내보낸 라벨 데이터로 YOLOv8 객체 탐지 모델을 학습시키는 전체 과정입니다. 환경 설치부터 추론 API 배포까지 코드 중심으로 안내합니다.
전체 학습 파이프라인
데이터 준비부터 모델 배포까지 전체 흐름을 파악합니다.
YOLOv8을 선택하는 이유
Ultralytics YOLOv8은 단일 스테이지 객체 탐지 모델로, 속도와 정확도의 균형이 뛰어납니다. Python API가 직관적이고, ONNX/TensorRT 변환이 간단하여 서비스 배포까지 동일한 프레임워크로 처리할 수 있습니다.
모델 크기 선택
카드 결함 탐지 목적에 맞는 YOLOv8 모델 변형을 선택합니다.
| 모델 | 권장 GPU | 배치 크기 | 용도 |
|---|---|---|---|
yolov8n | CPU / GTX 1060 | 32~64 | 빠른 프로토타입 |
yolov8s | GTX 1080 이상 | 32 | 초기 데이터 검증 |
yolov8m ★ | RTX 3070 이상 | 16~32 | 카드 결함 탐지 권장 |
yolov8l | RTX 3080 이상 | 8~16 | 고정밀 탐지 필요 시 |
yolov8x | A100 / H100 | 4~8 | 최고 성능 연구용 |
데이터셋 디렉터리 구조
YOLOv8이 요구하는 폴더 구조대로 데이터를 정리합니다.
학습 / 검증 / 테스트 분리 비율
| 분류 | 비율 | 목적 | 주의사항 |
|---|---|---|---|
| Train | 70 ~ 75% | 모델 가중치 학습 | 가장 다양한 결함 분포 포함 |
| Val | 15 ~ 20% | 에폭별 성능 평가 | Train 데이터와 중복 금지 |
| Test | 10 ~ 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프로젝트 Export 클릭프로젝트 상단 메뉴의
Export버튼 클릭 - 2YOLO 형식 선택형식 목록에서
YOLO with images선택 - 3ZIP 다운로드이미지 + .txt 라벨 파일이 함께 압축 다운로드됨
- 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")
핵심 하이퍼파라미터 설명
| 파라미터 | 권장값 | 설명 | 조정 기준 |
|---|---|---|---|
epochs | 100~200 | 전체 데이터 반복 횟수 | Early stopping 사용 시 크게 설정 |
imgsz | 640 | 입력 이미지 해상도 | 미세 결함 많으면 1280 고려 |
batch | 16 | GPU 배치 크기 | VRAM 부족 시 8로 낮춤 |
lr0 | 0.01 | 초기 학습률 | 전이학습 시 0.001~0.005 권장 |
patience | 30 | Early stopping 대기 에폭 | val mAP 개선 없으면 중단 |
optimizer | AdamW | 최적화 알고리즘 | SGD도 가능, 기본값 auto |
weight_decay | 0.0005 | L2 정규화 강도 | 과적합 시 0.001로 증가 |
warmup_epochs | 3 | 학습률 워밍업 에폭 | 기본값 유지 권장 |
카드 결함 탐지 최적화 설정
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은 활성화하여 소규모 데이터에서 효율 극대화.
학습 모니터링
학습 진행 상황을 실시간으로 추적합니다.
학습 로그 예시
TensorBoard로 실시간 모니터링
bash# 별도 터미널에서 실행
tensorboard --logdir ./runs/card_defect_v1
# 브라우저에서 http://localhost:6006 접속
결과 파일 위치
모델 평가
학습된 모델의 성능을 테스트 데이터로 정량 평가합니다.
평가 실행
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}")
평가 지표 해석
| 지표 | 계산식 | 낮을 때 의미 | 해결 방법 |
|---|---|---|---|
| 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~2x | CPU/GPU 모두 |
| TensorRT .engine | NVIDIA GPU 최적화 | 3~5x | NVIDIA 서버 |
| OpenVINO | Intel CPU 최적화 | 2~3x | Intel 엣지 기기 |
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를 충족하는지 확인했다
- 이상 케이스(결함 없는 카드, 극단적 조명 차이)에서의 동작을 테스트했다