결과 분석
| 항목 | 내용 |
|---|---|
| 핵심 지표 | mAP@50, mAP@50-95, Precision, Recall, F1 |
| 결과 파일 | runs/detect/<name>/results.csv, results.png |
| 과적합 징후 | train loss ↓ + val loss ↑ 동시 발생 |
| 분석 순서 | mAP → 클래스별 AP → Confusion Matrix → PR 곡선 → F1 |
| 철도 드론 기준 | mAP@50 > 0.7 목표, 균열·체결구 클래스별 AP 별도 확인 |
문서 탐색
Confusion Matrix 설명
Confusion Matrix(혼동 행렬)는 모델이 각 클래스를 얼마나 정확하게 분류하는지 보여줍니다.
TP / FP / FN / TN 정의
| 용어 | 전체 이름 | 의미 | 예시 (철도 균열 탐지) |
|---|---|---|---|
| TP | True Positive | 실제 있고, 모델도 맞게 탐지 | 균열 있음 → 균열로 예측 |
| FP | False Positive | 실제 없는데, 모델이 탐지 | 정상 레일 → 균열로 예측 (오탐) |
| FN | False Negative | 실제 있는데, 모델이 놓침 | 균열 있음 → 탐지 못 함 (미탐) |
| TN | True Negative | 실제 없고, 모델도 없다고 판단 | 정상 레일 → 정상으로 예측 |
철도 안전 검사에서는 FN(미탐)이 FP(오탐)보다 더 위험합니다. 실제 결함을 놓치면 사고로 이어질 수 있기 때문입니다. 따라서 Recall(재현율)을 Precision(정밀도)보다 우선시하는 경향이 있습니다.
시각적 확인법
runs/railway/finetune_v1/
├── confusion_matrix.png ← 정규화 전 혼동 행렬
└── confusion_matrix_normalized.png ← 비율로 정규화된 혼동 행렬
혼동 행렬에서 대각선 값이 클수록 좋은 모델입니다. 대각선 외 값이 크다면 해당 클래스 간 혼동이 자주 발생함을 의미합니다.
예를 들어 균열(crack)과 마모(wear) 클래스 사이에 혼동이 크다면, 두 클래스의 라벨링 기준을 명확히 재정의하거나 해당 경계 케이스 이미지를 더 수집해야 합니다.
성능 지표
| 지표 | 수식 | 의미 | 값이 높을 때 | 중요한 경우 |
|---|---|---|---|---|
| Precision | TP / (TP + FP) | 탐지한 것 중 실제 맞은 비율 | 오탐이 적음 | 오탐 비용이 높을 때 |
| Recall | TP / (TP + FN) | 실제 있는 것 중 찾아낸 비율 | 미탐이 적음 | 안전 중시 (철도 검사) |
| F1 Score | 2 × (P × R) / (P + R) | Precision과 Recall의 조화 평균 | 균형 잡힌 성능 | 전체적인 균형 평가 |
| AP | PR 곡선 아래 면적 | 단일 클래스의 종합 성능 | 해당 클래스 탐지 우수 | 특정 클래스 분석 |
| mAP@50 | IoU=0.5 기준 전 클래스 AP 평균 | 표준 객체 탐지 성능 지표 | 전반적 탐지 우수 | 일반적인 성능 비교 |
| mAP@50-95 | IoU 0.5~0.95 기준 AP 평균 | 더 엄격한 박스 정확도 요구 | 박스 위치도 정확 | 정밀한 위치 정확도 필요 시 |
IoU(Intersection over Union)란?
IoU = 예측 박스와 정답 박스의 겹치는 면적 / 두 박스의 합산 면적
IoU = 0.0 → 전혀 겹치지 않음
IoU = 1.0 → 완전히 일치
IoU ≥ 0.5 → mAP@50 기준으로 "정답"으로 간주
정상적인 학습 곡선
건강한 학습은 다음 패턴을 따릅니다.
에포크: 1 10 30 50 100
train/loss: 2.1 → 1.2 → 0.8 → 0.6 → 0.5 (꾸준히 감소)
val/loss: 2.3 → 1.4 → 0.9 → 0.7 → 0.65 (train과 함께 감소)
mAP@50: 0.1 → 0.4 → 0.6 → 0.7 → 0.75 (상승 후 안정화)
- train loss: 에포크마다 꾸준히 감소
- val loss: train loss와 비슷하게 감소 (격차가 크지 않음)
- mAP@50: 상승하다가 특정 에포크 이후 안정화
학습 후 results.png 파일을 열어 위 패턴과 비교하는 것이 첫 번째 분석 단계입니다.
이상 징후
| 징후 | 의미 | 조치 |
|---|---|---|
| val loss 증가 (train loss는 감소) | 과적합: 학습 데이터 암기 중 | 데이터 증강 강화, weight_decay 증가, 모델 크기 축소 |
| mAP@50(val) 감소 | 일반화 실패: 새 이미지에 적응 못 함 | Early stopping 적용, 라벨 품질 재검토 |
| Loss가 전혀 감소하지 않음 | 학습 불안정: 학습률 또는 데이터 문제 | lr0 낮추기 (0.001), data.yaml 경로 재확인 |
| Loss가 발산(폭증)함 | 학습률 과대 | lr0을 10배 줄이기, warmup_epochs 증가 |
| mAP@50 < 0.3 (100 에포크 후) | 데이터 또는 라벨 문제 | 라벨 파일 재검토, 클래스 수 줄이기, 이미지 품질 확인 |
지표 해석 가이드
| 증상 | 의미 | 조치 |
|---|---|---|
| mAP@50 전체가 낮음 | 모델이 전반적으로 탐지를 못 함 | 학습 에포크 추가, 데이터 증가, 모델 크기 키우기 |
| mAP@50은 높은데 mAP@50-95가 낮음 | 객체를 찾긴 하는데 박스가 부정확 | box loss 가중치 증가, imgsz 크게 설정 |
| Precision이 낮음 | 오탐(False Positive)이 많음 | confidence threshold 높이기, 배경 이미지 추가 |
| Recall이 낮음 | 미탐(False Negative)이 많음 | confidence threshold 낮추기, 해당 클래스 이미지 추가 |
| 특정 클래스 AP만 낮음 | 해당 클래스 데이터 부족 또는 라벨 오류 | 해당 클래스 이미지 추가 수집, 라벨 재검토 |
실전 분석 워크플로우
결과를 체계적으로 분석하는 5단계 순서입니다.
Step 1. mAP@50 전체 확인
results.csv에서 metrics/mAP50(B) 컬럼의 최고값을 확인합니다. 0.5 이하라면 데이터셋 또는 라벨링 문제를 먼저 해결해야 합니다.
Step 2. 클래스별 AP 확인
results.png 또는 val() 결과에서 클래스별 AP를 확인합니다. 특정 클래스만 낮다면 해당 클래스 데이터를 집중 보강합니다.
Step 3. Confusion Matrix 분석
confusion_matrix_normalized.png를 열어 어떤 클래스 쌍 사이에서 혼동이 발생하는지 확인합니다. 균열과 마모, 체결구 종류별 혼동 등을 파악합니다.
Step 4. PR 곡선 확인
PR_curve.png에서 Precision-Recall 곡선의 형태를 확인합니다. 곡선이 오른쪽 위(1,1)에 가까울수록 좋습니다.
Step 5. F1 곡선으로 임계값 결정
F1_curve.png에서 F1 점수가 최대가 되는 confidence threshold를 확인합니다. 이 값을 실제 추론 시 confidence 파라미터로 사용합니다.
검증 코드
학습 완료 후 별도로 검증 지표를 계산하고 싶을 때 사용합니다.
from ultralytics import YOLO
# 최고 성능 모델 로드
model = YOLO("runs/railway/finetune_v1/weights/best.pt")
# 검증 실행
metrics = model.val(
data="railway_dataset/data.yaml",
imgsz=640,
batch=16,
conf=0.25, # confidence threshold
iou=0.6, # NMS IoU threshold
split="val", # "val" 또는 "test"
save_json=True, # COCO 형식 JSON으로 결과 저장
plots=True # confusion matrix, PR 곡선 등 시각화 저장
)
# 주요 지표 출력
print(f"mAP@50: {metrics.box.map50:.4f}")
print(f"mAP@50-95: {metrics.box.map:.4f}")
print(f"Precision: {metrics.box.mp:.4f}")
print(f"Recall: {metrics.box.mr:.4f}")
# 클래스별 AP 출력
for i, cls_name in enumerate(model.names.values()):
ap50 = metrics.box.ap50[i]
print(f" [{cls_name}] AP@50: {ap50:.4f}")출력 예시:
mAP@50: 0.7823
mAP@50-95: 0.5412
Precision: 0.8201
Recall: 0.7456
[crack] AP@50: 0.6932
[fastener] AP@50: 0.8104
[sleeper] AP@50: 0.8433
균열(crack) AP가 낮다면 균열 이미지를 더 수집하거나, 균열 라벨링의 일관성을 재검토해야 합니다.