평가 지표 (Evaluation Metrics)¶
모델의 성능을 정량화하는 지표. 문제 유형과 비즈니스 목표에 맞는 지표 선택이 중요함.
왜 올바른 지표 선택이 중요한가¶
Accuracy 98%인 모델이 있다고 하자. 사기 탐지 문제에서 전체 거래 중 사기가 2%라면, "모두 정상"이라고 예측해도 98% Accuracy다. 이 모델은 쓸모없음.
지표 선택의 핵심 질문:
- 어떤 실수가 더 비싼가? (False Positive vs False Negative)
- 클래스 불균형이 있는가?
- 확률 예측이 필요한가, 이진 결정만 필요한가?
- 순위(ranking)가 중요한가?
분류 평가 지표¶
혼동 행렬 (Confusion Matrix)¶
모든 분류 지표의 기반.
| 용어 | 의미 | 예시 (스팸 탐지) |
|---|---|---|
| TP (True Positive) | 양성을 양성으로 | 스팸을 스팸으로 |
| TN (True Negative) | 음성을 음성으로 | 정상을 정상으로 |
| FP (False Positive) | 음성을 양성으로 | 정상을 스팸으로 (Type I) |
| FN (False Negative) | 양성을 음성으로 | 스팸을 정상으로 (Type II) |
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
# 혼동 행렬 계산
cm = confusion_matrix(y_true, y_pred)
# 시각화
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['Negative', 'Positive'])
disp.plot(cmap='Blues')
plt.show()
# 직접 계산
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
Accuracy (정확도)¶
전체 예측 중 맞은 비율.
from sklearn.metrics import accuracy_score
accuracy = accuracy_score(y_true, y_pred)
# 또는 직접 계산
accuracy = (tp + tn) / (tp + tn + fp + fn)
사용 시점: - 클래스가 균형 잡힌 경우 - 모든 오류의 비용이 동일한 경우
주의: - 불균형 데이터에서는 무의미할 수 있음 - 단독 지표로 사용하지 말 것
Precision (정밀도)¶
양성 예측 중 실제 양성의 비율. "예측이 얼마나 믿을 만한가?"
높아야 하는 경우: - FP 비용이 큰 경우 - 스팸 필터: 정상 메일을 스팸으로 분류하면 중요한 메일 손실 - 추천 시스템: 관련 없는 아이템 추천은 사용자 이탈
Recall (재현율, Sensitivity, TPR)¶
실제 양성 중 탐지된 비율. "놓친 것이 얼마나 되는가?"
높아야 하는 경우: - FN 비용이 큰 경우 - 암 진단: 암 환자를 정상으로 분류하면 생명 위험 - 사기 탐지: 사기를 놓치면 금전적 손실
Specificity (특이도, TNR)¶
실제 음성 중 올바르게 음성으로 예측된 비율.
F1 Score¶
Precision과 Recall의 조화 평균. 둘 다 높아야 F1이 높음.
조화 평균을 쓰는 이유: - 산술 평균은 한쪽이 극단적이어도 평균이 높을 수 있음 - 조화 평균은 낮은 쪽에 더 큰 가중치
Precision=0.9, Recall=0.1 일 때:
- 산술 평균: (0.9 + 0.1) / 2 = 0.5
- 조화 평균(F1): 2 * 0.9 * 0.1 / (0.9 + 0.1) = 0.18
F-beta Score¶
Precision과 Recall에 다른 가중치를 줄 때 사용.
| Beta | 의미 |
|---|---|
| 0.5 | Precision에 더 높은 가중치 |
| 1 | 동일 가중치 (F1) |
| 2 | Recall에 더 높은 가중치 |
from sklearn.metrics import fbeta_score
# Recall을 2배 중요하게
f2 = fbeta_score(y_true, y_pred, beta=2)
# Precision을 2배 중요하게
f05 = fbeta_score(y_true, y_pred, beta=0.5)
ROC Curve & AUC¶
ROC (Receiver Operating Characteristic): 임계값 변화에 따른 TPR vs FPR 곡선.
- TPR (True Positive Rate) = Recall = TP / (TP + FN)
- FPR (False Positive Rate) = FP / (FP + TN) = 1 - Specificity
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt
# 확률 예측 필요
y_proba = model.predict_proba(X_test)[:, 1]
# ROC 곡선
fpr, tpr, thresholds = roc_curve(y_true, y_proba)
# AUC (Area Under Curve)
auc = roc_auc_score(y_true, y_proba)
# 시각화
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC (AUC = {auc:.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.show()
AUC 해석:
| AUC | 의미 |
|---|---|
| 0.5 | 랜덤 (쓸모없음) |
| 0.7-0.8 | 괜찮음 |
| 0.8-0.9 | 좋음 |
| 0.9+ | 매우 좋음 |
AUC 직관적 의미: 무작위로 양성 샘플과 음성 샘플을 뽑았을 때, 모델이 양성에 더 높은 확률을 줄 확률.
Precision-Recall Curve & AP¶
불균형 데이터에서 ROC보다 유용.
from sklearn.metrics import precision_recall_curve, average_precision_score
precision, recall, thresholds = precision_recall_curve(y_true, y_proba)
ap = average_precision_score(y_true, y_proba)
plt.figure(figsize=(8, 6))
plt.plot(recall, precision, label=f'PR Curve (AP = {ap:.3f})')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall Curve')
plt.legend()
plt.show()
ROC vs PR Curve:
| 상황 | 권장 |
|---|---|
| 클래스 균형 | ROC-AUC |
| 클래스 불균형 | PR-AUC (Average Precision) |
| 양성 클래스가 중요 | PR-AUC |
다중 클래스 분류¶
from sklearn.metrics import classification_report
print(classification_report(y_true, y_pred, target_names=class_names))
# 다중 클래스 평균 방식
# macro: 클래스별 점수의 단순 평균 (클래스 가중치 동일)
# weighted: 클래스별 샘플 수로 가중 평균
# micro: 전체 TP, FP, FN으로 계산
f1_macro = f1_score(y_true, y_pred, average='macro')
f1_weighted = f1_score(y_true, y_pred, average='weighted')
f1_micro = f1_score(y_true, y_pred, average='micro')
확률 보정 지표¶
Log Loss (Cross-Entropy Loss)¶
확률 예측의 품질 측정. 자신 있게 틀리면 큰 페널티.
Brier Score¶
확률과 실제 결과의 MSE. 낮을수록 좋음.
임계값 최적화¶
기본 임계값 0.5가 항상 최적은 아니다.
from sklearn.metrics import precision_recall_curve
def find_optimal_threshold(y_true, y_proba, beta=1):
"""F-beta 최대화 임계값 찾기"""
precision, recall, thresholds = precision_recall_curve(y_true, y_proba)
# F-beta 계산 (마지막 원소 제외, precision_recall_curve 특성)
fbeta = (1 + beta**2) * precision[:-1] * recall[:-1] / (beta**2 * precision[:-1] + recall[:-1])
optimal_idx = fbeta.argmax()
optimal_threshold = thresholds[optimal_idx]
return optimal_threshold, fbeta[optimal_idx]
# 비용 기반 임계값
def find_cost_optimal_threshold(y_true, y_proba, fp_cost, fn_cost):
"""비용 최소화 임계값"""
best_threshold = 0.5
min_cost = float('inf')
for threshold in np.arange(0.1, 0.9, 0.01):
y_pred = (y_proba >= threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
total_cost = fp * fp_cost + fn * fn_cost
if total_cost < min_cost:
min_cost = total_cost
best_threshold = threshold
return best_threshold, min_cost
회귀 평가 지표¶
MAE (Mean Absolute Error)¶
해석: 예측이 평균적으로 MAE만큼 벗어남. 이상치에 강건.
MSE (Mean Squared Error)¶
해석: 큰 오차에 더 큰 페널티. 이상치에 민감.
RMSE (Root Mean Squared Error)¶
rmse = mean_squared_error(y_true, y_pred, squared=False)
# 또는
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
해석: 원래 단위와 같은 스케일. 가장 널리 사용.
MAPE (Mean Absolute Percentage Error)¶
from sklearn.metrics import mean_absolute_percentage_error
mape = mean_absolute_percentage_error(y_true, y_pred)
주의: y_true에 0이 있으면 정의되지 않음.
R² (결정 계수)¶
해석: - 1: 완벽한 예측 - 0: 평균만큼 예측 (모델이 무의미) - 음수: 평균보다 못함 (매우 나쁨)
Adjusted R²¶
특성 수 증가에 대한 페널티.
def adjusted_r2(y_true, y_pred, n_features):
r2 = r2_score(y_true, y_pred)
n = len(y_true)
return 1 - (1 - r2) * (n - 1) / (n - n_features - 1)
회귀 지표 선택 가이드¶
| 지표 | 사용 시점 |
|---|---|
| MAE | 이상치 있음, 모든 오차 동일 취급 |
| MSE/RMSE | 큰 오차가 더 중요, 일반적 |
| MAPE | 상대적 오차 중요, 스케일 무관 |
| R² | 모델 설명력 평가 |
순위 평가 지표¶
추천 시스템, 정보 검색에서 사용.
Mean Reciprocal Rank (MRR)¶
첫 번째 정답의 순위 역수 평균.
def mean_reciprocal_rank(y_true, y_pred_ranked):
"""
y_true: 정답 아이템
y_pred_ranked: 순위별 예측 리스트
"""
reciprocal_ranks = []
for true, pred_list in zip(y_true, y_pred_ranked):
for rank, pred in enumerate(pred_list, 1):
if pred == true:
reciprocal_ranks.append(1 / rank)
break
else:
reciprocal_ranks.append(0)
return np.mean(reciprocal_ranks)
Precision@K, Recall@K¶
상위 K개에서의 정밀도/재현율.
def precision_at_k(y_true, y_pred_ranked, k):
"""상위 K개 중 정답 비율"""
top_k = y_pred_ranked[:k]
relevant = sum(1 for item in top_k if item in y_true)
return relevant / k
def recall_at_k(y_true, y_pred_ranked, k):
"""정답 중 상위 K개에 포함된 비율"""
top_k = y_pred_ranked[:k]
relevant = sum(1 for item in top_k if item in y_true)
return relevant / len(y_true)
NDCG (Normalized Discounted Cumulative Gain)¶
순위를 고려한 품질 측정. 상위 순위에 더 높은 가중치.
지표 선택 가이드¶
문제 유형별¶
| 문제 | 권장 지표 |
|---|---|
| 균형 이진 분류 | Accuracy, F1, AUC |
| 불균형 이진 분류 | F1, PR-AUC, Recall |
| 다중 클래스 | Macro F1, Weighted F1 |
| 회귀 | RMSE, MAE, R² |
| 추천/검색 | NDCG, MRR, Precision@K |
비즈니스 상황별¶
| 상황 | 중요 지표 |
|---|---|
| 의료 진단 (FN 비용 큼) | Recall, F2 |
| 스팸 필터 (FP 비용 큼) | Precision, F0.5 |
| 사기 탐지 | Recall, PR-AUC |
| 균형 잡힌 성능 | F1, AUC |
| 확률 보정 필요 | Brier Score, Log Loss |
흔히 하는 실수¶
1. 불균형 데이터에서 Accuracy만 보기¶
# 나쁜 예
print(f"Accuracy: {accuracy_score(y_true, y_pred)}") # 98%라고 좋은 게 아님
# 좋은 예
print(classification_report(y_true, y_pred))
print(f"F1: {f1_score(y_true, y_pred)}")
print(f"PR-AUC: {average_precision_score(y_true, y_proba)}")
2. 테스트 세트에서 임계값 최적화¶
# 나쁜 예: 데이터 누수
threshold = find_optimal_threshold(y_test, y_proba_test)
# 좋은 예: 검증 세트에서 임계값 결정
threshold = find_optimal_threshold(y_val, y_proba_val)
y_pred_test = (y_proba_test >= threshold).astype(int)
3. 다중 클래스에서 평균 방식 무시¶
# 어떤 평균인지 명시하지 않음
f1 = f1_score(y_true, y_pred, average='binary') # 기본값, 다중 클래스에서 에러
# 명시적으로 선택
f1_macro = f1_score(y_true, y_pred, average='macro') # 클래스 균등 가중
f1_weighted = f1_score(y_true, y_pred, average='weighted') # 샘플 수 가중
4. 확률 없이 AUC 계산 시도¶
# 나쁜 예
auc = roc_auc_score(y_true, y_pred) # 이진 예측으로는 제대로 안 됨
# 좋은 예
y_proba = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_true, y_proba)
5. 단일 지표에만 의존¶
여러 지표를 함께 봐야 모델의 전체 그림이 보임.
def evaluate_classifier(y_true, y_pred, y_proba):
"""분류기 종합 평가"""
print("=== Classification Report ===")
print(classification_report(y_true, y_pred))
print("\n=== Additional Metrics ===")
print(f"Accuracy: {accuracy_score(y_true, y_pred):.4f}")
print(f"F1 Score: {f1_score(y_true, y_pred):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_true, y_proba):.4f}")
print(f"PR-AUC: {average_precision_score(y_true, y_proba):.4f}")
print(f"Log Loss: {log_loss(y_true, y_proba):.4f}")
print(f"Brier Score: {brier_score_loss(y_true, y_proba):.4f}")
6. 검증/테스트 지표 혼동¶
- 검증 지표: 모델 선택, 하이퍼파라미터 튜닝용
- 테스트 지표: 최종 성능 보고용 (한 번만 사용)