콘텐츠로 이동
Data Prep
상세

평가 지표 (Evaluation Metrics)

모델의 성능을 정량화하는 지표. 문제 유형과 비즈니스 목표에 맞는 지표 선택이 중요함.

왜 올바른 지표 선택이 중요한가

Accuracy 98%인 모델이 있다고 하자. 사기 탐지 문제에서 전체 거래 중 사기가 2%라면, "모두 정상"이라고 예측해도 98% Accuracy다. 이 모델은 쓸모없음.

지표 선택의 핵심 질문:

  • 어떤 실수가 더 비싼가? (False Positive vs False Negative)
  • 클래스 불균형이 있는가?
  • 확률 예측이 필요한가, 이진 결정만 필요한가?
  • 순위(ranking)가 중요한가?

분류 평가 지표

혼동 행렬 (Confusion Matrix)

모든 분류 지표의 기반.

                    예측
                Positive  Negative
실제  Positive    TP        FN
      Negative    FP        TN
용어 의미 예시 (스팸 탐지)
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 (정확도)

\[\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}\]

전체 예측 중 맞은 비율.

from sklearn.metrics import accuracy_score

accuracy = accuracy_score(y_true, y_pred)

# 또는 직접 계산
accuracy = (tp + tn) / (tp + tn + fp + fn)

사용 시점: - 클래스가 균형 잡힌 경우 - 모든 오류의 비용이 동일한 경우

주의: - 불균형 데이터에서는 무의미할 수 있음 - 단독 지표로 사용하지 말 것

Precision (정밀도)

\[\text{Precision} = \frac{TP}{TP + FP}\]

양성 예측 중 실제 양성의 비율. "예측이 얼마나 믿을 만한가?"

from sklearn.metrics import precision_score

precision = precision_score(y_true, y_pred)

높아야 하는 경우: - FP 비용이 큰 경우 - 스팸 필터: 정상 메일을 스팸으로 분류하면 중요한 메일 손실 - 추천 시스템: 관련 없는 아이템 추천은 사용자 이탈

Recall (재현율, Sensitivity, TPR)

\[\text{Recall} = \frac{TP}{TP + FN}\]

실제 양성 중 탐지된 비율. "놓친 것이 얼마나 되는가?"

from sklearn.metrics import recall_score

recall = recall_score(y_true, y_pred)

높아야 하는 경우: - FN 비용이 큰 경우 - 암 진단: 암 환자를 정상으로 분류하면 생명 위험 - 사기 탐지: 사기를 놓치면 금전적 손실

Specificity (특이도, TNR)

\[\text{Specificity} = \frac{TN}{TN + FP}\]

실제 음성 중 올바르게 음성으로 예측된 비율.

tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
specificity = tn / (tn + fp)

F1 Score

\[F1 = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}\]

Precision과 Recall의 조화 평균. 둘 다 높아야 F1이 높음.

from sklearn.metrics import f1_score

f1 = f1_score(y_true, y_pred)

조화 평균을 쓰는 이유: - 산술 평균은 한쪽이 극단적이어도 평균이 높을 수 있음 - 조화 평균은 낮은 쪽에 더 큰 가중치

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

\[F_\beta = (1 + \beta^2) \cdot \frac{\text{Precision} \cdot \text{Recall}}{(\beta^2 \cdot \text{Precision}) + \text{Recall}}\]

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)

\[\text{Log Loss} = -\frac{1}{N}\sum_{i=1}^{N}[y_i \log(p_i) + (1-y_i)\log(1-p_i)]\]

확률 예측의 품질 측정. 자신 있게 틀리면 큰 페널티.

from sklearn.metrics import log_loss

logloss = log_loss(y_true, y_proba)

Brier Score

\[\text{Brier} = \frac{1}{N}\sum_{i=1}^{N}(p_i - y_i)^2\]

확률과 실제 결과의 MSE. 낮을수록 좋음.

from sklearn.metrics import brier_score_loss

brier = brier_score_loss(y_true, y_proba)

임계값 최적화

기본 임계값 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)

\[\text{MAE} = \frac{1}{n}\sum_{i=1}^{n}|y_i - \hat{y}_i|\]
from sklearn.metrics import mean_absolute_error

mae = mean_absolute_error(y_true, y_pred)

해석: 예측이 평균적으로 MAE만큼 벗어남. 이상치에 강건.

MSE (Mean Squared Error)

\[\text{MSE} = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2\]
from sklearn.metrics import mean_squared_error

mse = mean_squared_error(y_true, y_pred)

해석: 큰 오차에 더 큰 페널티. 이상치에 민감.

RMSE (Root Mean Squared Error)

\[\text{RMSE} = \sqrt{\text{MSE}}\]
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)

\[\text{MAPE} = \frac{100}{n}\sum_{i=1}^{n}\left|\frac{y_i - \hat{y}_i}{y_i}\right|\]
from sklearn.metrics import mean_absolute_percentage_error

mape = mean_absolute_percentage_error(y_true, y_pred)

주의: y_true에 0이 있으면 정의되지 않음.

R² (결정 계수)

\[R^2 = 1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2}\]
from sklearn.metrics import r2_score

r2 = r2_score(y_true, y_pred)

해석: - 1: 완벽한 예측 - 0: 평균만큼 예측 (모델이 무의미) - 음수: 평균보다 못함 (매우 나쁨)

Adjusted R²

특성 수 증가에 대한 페널티.

\[R^2_{adj} = 1 - (1 - R^2)\frac{n-1}{n-p-1}\]
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 상대적 오차 중요, 스케일 무관
모델 설명력 평가

순위 평가 지표

추천 시스템, 정보 검색에서 사용.

Mean Reciprocal Rank (MRR)

첫 번째 정답의 순위 역수 평균.

\[\text{MRR} = \frac{1}{|Q|}\sum_{i=1}^{|Q|}\frac{1}{\text{rank}_i}\]
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)

순위를 고려한 품질 측정. 상위 순위에 더 높은 가중치.

\[\text{DCG}_k = \sum_{i=1}^{k}\frac{2^{rel_i} - 1}{\log_2(i+1)}\]
\[\text{NDCG}_k = \frac{\text{DCG}_k}{\text{IDCG}_k}\]
from sklearn.metrics import ndcg_score

ndcg = ndcg_score([y_true_relevance], [y_pred_scores], k=10)

지표 선택 가이드

문제 유형별

문제 권장 지표
균형 이진 분류 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. 검증/테스트 지표 혼동

  • 검증 지표: 모델 선택, 하이퍼파라미터 튜닝용
  • 테스트 지표: 최종 성능 보고용 (한 번만 사용)
# 올바른 흐름
# 1. 훈련 세트로 학습
# 2. 검증 세트로 모델 선택/튜닝
# 3. 테스트 세트로 최종 평가 (1회)

참고 자료