
📓Google Colab에서 실습하기
이 레슨은 PyTorch/GPU가 필요합니다. 노트북을 다운로드 후 Google Colab에서 열어주세요.
학습 내용
모델 평가
학습 목표
- •검출 모델의 핵심 평가 지표(IoU, mAP)를 이해한다
- •분류 모델의 혼동 행렬과 F1-score를 계산한다
- •Precision-Recall 곡선과 ROC 곡선을 그려본다
- •번호판 인식 시스템 전체의 성능을 종합적으로 평가한다
왜 평가가 중요한가?
비유: 요리사가 맛을 보지 않고 음식을 내놓을 수 있을까요?
모델을 학습시키는 것은 요리를 만드는 과정이고, 평가는 맛을 보는 과정입니다. 평가 없이 배포하는 것은 눈을 감고 운전하는 것과 같습니다. 학습 데이터에서는 잘 작동하지만 실제 환경에서 엉망인 경우가 매우 흔하기 때문입니다.
검출 모델 평가: IoU와 mAP
IoU (Intersection over Union)
비유: 두 사람이 각각 "여기가 번호판이야"라고 영역을 표시했을 때, 두 영역이 얼마나 겹치는지 측정하는 것이 IoU입니다.
IoU는 예측한 바운딩 박스와 실제 정답 박스가 얼마나 일치하는지를 0에서 1 사이 값으로 나타냅니다.
| IoU 값 | 의미 | 판정 |
|---|---|---|
| 0.0 | 전혀 겹치지 않음 | 완전 실패 |
| 0.5 | 절반 정도 겹침 | 최소 기준 |
| 0.75 | 대부분 겹침 | 좋은 검출 |
| 1.0 | 완벽히 일치 | 이상적 |
pythonimport numpy as np def calculate_iou(box1, box2): """IoU 계산 (box 형식: [x1, y1, x2, y2])""" x1 = max(box1[0], box2[0]) y1 = max(box1[1], box2[1]) x2 = min(box1[2], box2[2]) y2 = min(box1[3], box2[3]) intersection = max(0, x2 - x1) * max(0, y2 - y1) area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]) union = area1 + area2 - intersection return intersection / union if union > 0 else 0 # 예시: 두 박스의 IoU 계산 gt_box = [100, 100, 300, 200] # 정답 박스 pred_box = [120, 90, 310, 210] # 예측 박스 iou = calculate_iou(gt_box, pred_box) print(f"IoU: {iou:.3f}") for t in [0.5, 0.7, 0.9]: print(f" IoU >= {t}: {'SUCCESS' if iou >= t else 'FAIL'}")
Precision과 Recall
검출 결과를 분류하면 네 가지 경우가 생깁니다.
| 구분 | 설명 | 번호판 인식 예시 |
|---|---|---|
| TP (True Positive) | 있는 걸 있다고 함 | 번호판을 정확히 검출 |
| FP (False Positive) | 없는 걸 있다고 함 | 광고판을 번호판으로 오인 |
| FN (False Negative) | 있는 걸 없다고 함 | 번호판을 놓침 |
| TN (True Negative) | 없는 걸 없다고 함 | 배경을 배경으로 판단 |
pythonimport numpy as np # 번호판 검출 결과 예시 TP = 90 # 번호판을 정확히 검출 FP = 5 # 광고판을 번호판으로 오인 FN = 10 # 번호판을 놓침 precision = TP / (TP + FP) recall = TP / (TP + FN) f1 = 2 * precision * recall / (precision + recall) print(f"TP: {TP}, FP: {FP}, FN: {FN}") print(f"Precision: {precision:.3f} (high = few false alarms)") print(f"Recall: {recall:.3f} (high = few missed)") print(f"F1-score: {f1:.3f} (balance of both)")
mAP (mean Average Precision)
비유: 시험에서 "평균 점수"를 내는 것처럼, mAP는 다양한 기준에서의 검출 성능을 하나의 숫자로 요약합니다.
mAP는 Precision-Recall 곡선 아래의 면적(AP)을 모든 클래스에 대해 평균낸 값입니다. 번호판 검출은 클래스가 하나(번호판)이므로 AP와 mAP가 같습니다.
| 지표 | 의미 | YOLO 기본 출력 |
|---|---|---|
| mAP@0.5 | IoU 0.5 기준 AP | map50 |
| mAP@0.5:0.95 | IoU 0.5부터 0.95까지 평균 AP | map |
| mAP@0.75 | IoU 0.75 기준 AP (엄격) | map75 |
python# YOLO 모델 평가 (자동으로 mAP 계산) from ultralytics import YOLO model = YOLO("best.pt") results = model.val(data="plate_dataset.yaml") print(f"mAP@0.5: {results.box.map50:.3f}") print(f"mAP@0.5:0.95: {results.box.map:.3f}") print(f"Precision: {results.box.mp:.3f}") print(f"Recall: {results.box.mr:.3f}")
분류 모델 평가: 혼동 행렬과 F1-score
혼동 행렬 (Confusion Matrix)
비유: 학생들의 시험 답안을 채점할 때, "어떤 문제를 어떤 답으로 틀렸는지" 표로 정리하는 것이 혼동 행렬입니다. 예를 들어 "7"을 "1"로 자주 틀린다면, 그 패턴을 알아야 개선할 수 있습니다.
혼동 행렬의 행은 실제 클래스, 열은 예측 클래스를 나타냅니다. 대각선은 정확한 예측이고, 비대각선은 혼동(오류)입니다. 이것을 시각화해보겠습니다.
pythonimport numpy as np import matplotlib.pyplot as plt # 혼동 행렬 데이터 (번호판 문자 인식) class_names = ["0", "1", "2", "3", "ga", "na"] cm = np.array([ [28, 0, 1, 5, 0, 0], [ 0, 30, 4, 0, 1, 0], [ 1, 2, 31, 0, 0, 1], [ 2, 0, 0, 33, 0, 0], [ 0, 1, 0, 0, 29, 3], [ 0, 0, 1, 0, 4, 28] ]) fig, ax = plt.subplots(1, 1, figsize=(8, 6)) im = ax.imshow(cm, interpolation="nearest", cmap="Blues") ax.set_title("Confusion Matrix - Character Recognition", fontsize=14) plt.colorbar(im, ax=ax) # 축 설정 tick_marks = np.arange(len(class_names)) ax.set_xticks(tick_marks) ax.set_xticklabels(class_names, fontsize=12) ax.set_yticks(tick_marks) ax.set_yticklabels(class_names, fontsize=12) ax.set_xlabel("Predicted", fontsize=12) ax.set_ylabel("Actual", fontsize=12) # 셀에 숫자 표시 thresh = cm.max() / 2 for i in range(len(class_names)): for j in range(len(class_names)): color = "white" if cm[i, j] > thresh else "black" ax.text(j, i, str(cm[i, j]), ha="center", va="center", color=color, fontsize=14) plt.tight_layout() plt.show() print("Diagonal = correct predictions") print("Off-diagonal = confused character pairs")
F1-Score
Precision과 Recall 중 하나만 높으면 안 됩니다. F1-score는 둘의 조화평균으로, 둘 다 높아야 높은 값이 나옵니다.
비유: 축구에서 공격(Recall)만 잘하고 수비(Precision)가 약하면 이길 수 없습니다. F1-score는 공격과 수비의 균형을 측정하는 지표입니다.
pythonimport numpy as np # 앞의 혼동 행렬에서 F1 계산 cm = np.array([ [28, 0, 1, 5, 0, 0], [ 0, 30, 4, 0, 1, 0], [ 1, 2, 31, 0, 0, 1], [ 2, 0, 0, 33, 0, 0], [ 0, 1, 0, 0, 29, 3], [ 0, 0, 1, 0, 4, 28] ]) names = ["0", "1", "2", "3", "ga", "na"] print(f"{'Class':>6} {'Precision':>10} {'Recall':>10} {'F1':>10}") print("-" * 38) f1_scores = [] for i in range(len(names)): tp = cm[i, i] fp = cm[:, i].sum() - tp fn = cm[i, :].sum() - tp p = tp/(tp+fp) if tp+fp > 0 else 0 r = tp/(tp+fn) if tp+fn > 0 else 0 f1 = 2*p*r/(p+r) if p+r > 0 else 0 f1_scores.append(f1) print(f"{names[i]:>6} {p:>10.3f} {r:>10.3f} {f1:>10.3f}") print("-" * 38) print(f"{'Avg':>6} {'':>10} {'':>10} {np.mean(f1_scores):>10.3f}")
Precision-Recall 곡선과 ROC 곡선
신뢰도 임계값을 바꾸면 Precision과 Recall이 달라집니다. 이 관계를 곡선으로 그려보겠습니다.
pythonimport numpy as np import matplotlib.pyplot as plt np.random.seed(42) pos_scores = np.random.beta(5, 2, size=150) # 양성: 높은 신뢰도 neg_scores = np.random.beta(2, 5, size=150) # 음성: 낮은 신뢰도 scores = np.concatenate([pos_scores, neg_scores]) labels = np.concatenate([np.ones(150), np.zeros(150)]) thresholds = np.linspace(0.01, 0.99, 50) precisions, recalls, fprs, tprs = [], [], [], [] for t in thresholds: pred = (scores >= t).astype(int) tp = np.sum((pred == 1) & (labels == 1)) fp = np.sum((pred == 1) & (labels == 0)) fn = np.sum((pred == 0) & (labels == 1)) tn = np.sum((pred == 0) & (labels == 0)) precisions.append(tp/(tp+fp) if tp+fp > 0 else 1.0) recalls.append(tp/(tp+fn) if tp+fn > 0 else 0.0) tprs.append(tp/(tp+fn) if tp+fn > 0 else 0) fprs.append(fp/(fp+tn) if fp+tn > 0 else 0) ap = abs(np.trapz(precisions, recalls)) auc_val = abs(np.trapz(tprs, fprs)) fig, axes = plt.subplots(1, 2, figsize=(14, 5)) axes[0].plot(recalls, precisions, "b-", linewidth=2) axes[0].set_xlabel("Recall"); axes[0].set_ylabel("Precision") axes[0].set_title(f"PR Curve (AP={ap:.3f})") axes[0].set_xlim([0, 1.05]); axes[0].set_ylim([0, 1.05]) axes[0].grid(True, alpha=0.3) axes[1].plot(fprs, tprs, "r-", linewidth=2, label=f"AUC={auc_val:.3f}") axes[1].plot([0, 1], [0, 1], "k--", alpha=0.5, label="Random") axes[1].set_xlabel("FPR"); axes[1].set_ylabel("TPR") axes[1].set_title("ROC Curve") axes[1].set_xlim([0, 1.05]); axes[1].set_ylim([0, 1.05]) axes[1].legend(); axes[1].grid(True, alpha=0.3) plt.tight_layout() plt.show() print("PR: top-right = better | ROC: top-left = better")
번호판 인식 시스템 전체 평가
번호판 인식은 검출과 인식 두 단계로 구성됩니다. 전체 시스템 평가도 단계별로 해야 합니다.
단계별 평가 지표
| 단계 | 핵심 지표 | 목표 |
|---|---|---|
| 번호판 검출 | mAP@0.5 | 0.95 이상 |
| 문자 분류 | F1-score (macro) | 0.98 이상 |
| 전체 파이프라인 | 번호판 완전 일치율 | 0.90 이상 |
전체 시스템 정확도 계산
pythonimport numpy as np # 전체 파이프라인 정확도 = 검출 x (문자 정확도 ^ 글자수) det_acc = 0.96 plate_len = 7 # 12가3456 print("Impact of per-character accuracy on full plate accuracy:") print(f"(Detection accuracy: {det_acc:.0%}, Plate length: {plate_len})") print() for ca in [0.99, 0.98, 0.97, 0.95, 0.90]: total = det_acc * (ca ** plate_len) print(f" Char {ca:.0%} -> Full plate {total:.1%}")
조건별 성능 분석
다양한 환경에서 모델이 어떻게 작동하는지 분석해야 합니다.
| 환경 | 정확도 | 판정 |
|---|---|---|
| 주간 | 96% | 목표 달성 |
| 야간 | 82% | 주의 필요 |
| 비/안개 | 78% | 개선 필요 |
| 역광 | 71% | 개선 필요 |
| 먼 거리 | 85% | 목표 달성 |
| 기울어짐 | 88% | 목표 달성 |
약한 조건(야간, 비, 역광)을 파악하고 해당 데이터를 보강하면 전체 성능이 올라갑니다.
평가 결과를 개선에 활용하기
평가는 그 자체가 목적이 아니라, 개선 방향을 찾기 위한 도구입니다.
| 평가 결과 | 원인 분석 | 개선 방법 |
|---|---|---|
| 검출 mAP 낮음 | 학습 데이터 부족 | 데이터 증강, 추가 라벨링 |
| 특정 문자 F1 낮음 | 비슷한 문자 혼동 | 해당 클래스 데이터 보강 |
| 야간 정확도 낮음 | 조명 조건 다양성 부족 | 야간 데이터 추가, 전처리 개선 |
| 속도 느림 | 모델이 너무 큼 | 경량화 (pruning, distillation) |
개선 사이클
평가와 개선은 한 번으로 끝나지 않습니다. "평가 -> 분석 -> 개선 -> 재평가"를 반복하며 성능을 올려갑니다.
이것이 실제 AI 프로젝트의 핵심 루프입니다. 논문에서 보는 높은 정확도는 이 사이클을 수십 번 반복한 결과입니다.
핵심 정리
| 지표 | 대상 | 의미 |
|---|---|---|
| IoU | 검출 | 예측 박스와 정답 박스의 겹침 정도 |
| mAP | 검출 | 다양한 임계값에서의 종합 검출 성능 |
| Confusion Matrix | 분류 | 어떤 클래스를 어떻게 혼동하는지 |
| F1-score | 분류 | Precision과 Recall의 조화평균 |
| PR Curve | 종합 | 임계값에 따른 정밀도-재현율 변화 |
| ROC / AUC | 종합 | 전체적인 분류 성능 요약 |
- •IoU: 바운딩 박스의 겹침 정도, 0.5 이상이면 검출 성공으로 판정
- •mAP: 검출 모델의 종합 점수, YOLO가 자동 계산해줌
- •혼동 행렬: 어떤 문자를 어떻게 헷갈리는지 한눈에 파악 가능
- •F1-score: Precision과 Recall의 균형을 하나의 숫자로 요약
- •조건별 분석: 약한 환경을 찾아 데이터를 보강하는 것이 핵심 전략
레슨 정보
- 레벨
- Level 9: 종합 프로젝트
- 예상 소요 시간
- 60분
- 참고 영상
- YouTube 링크
💡실습 환경 안내
이 레벨은 PyTorch/GPU가 필요하여 Google Colab 사용을 권장합니다.
Colab은 무료 GPU를 제공하여 PyTorch, CNN, Transformer 등을 실행할 수 있습니다.