모델 평가
올바른 평가 지표와 검증 방법은 머신러닝 프로젝트의 핵심입니다.
분류 평가 지표
from sklearn.metrics import (
accuracy_score,
precision_score,
recall_score,
f1_score,
classification_report,
confusion_matrix,
roc_auc_score,
roc_curve,
)
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# 데이터 준비
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=42)
model = RandomForestClassifier(random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]
# 기본 지표
print(f"정확도 (Accuracy): {accuracy_score(y_test, y_pred):.4f}")
print(f"정밀도 (Precision): {precision_score(y_test, y_pred):.4f}")
print(f"재현율 (Recall): {recall_score(y_test, y_pred):.4f}")
print(f"F1 점수: {f1_score(y_test, y_pred):.4f}")
print(f"AUC-ROC: {roc_auc_score(y_test, y_prob):.4f}")
# 상세 리포트
print("\n분류 리포트:")
print(classification_report(y_test, y_pred, target_names=["악성", "양성"]))
지표 해석
정확도 (Accuracy) = (TP + TN) / 전체
→ 클래스 불균형 시 오해 가능 (99% 악성 데이터에서 모두 "양성" 예측 시 1% 정확도)
정밀도 (Precision) = TP / (TP + FP)
→ "양성으로 예측한 것 중 실제 양성 비율"
→ 스팸 필터: 정상 메일을 스팸으로 잘못 분류하면 안 됨 → 정밀도 중요
재현율 (Recall) = TP / (TP + FN)
→ "실제 양성 중 맞게 예측한 비율"
→ 암 진단: 실제 환자를 놓치면 안 됨 → 재현율 중요
F1 = 2 × Precision × Recall / (Precision + Recall)
→ 정밀도와 재현율의 조화 평균
AUC-ROC: 0.5(랜덤) ~ 1.0(완벽)
→ 임계값 무관하게 모델 전체 성능 평가
혼동 행렬 시각화
def plot_confusion_matrix(y_true, y_pred, class_names=None):
cm = confusion_matrix(y_true, y_pred)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 절대값
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
ax=axes[0], xticklabels=class_names, yticklabels=class_names)
axes[0].set_title("혼동 행렬 (절대값)")
axes[0].set_ylabel("실제 클래스")
axes[0].set_xlabel("예측 클래스")
# 정규화 (비율)
cm_norm = cm.astype(float) / cm.sum(axis=1, keepdims=True)
sns.heatmap(cm_norm, annot=True, fmt=".2f", cmap="Blues",
ax=axes[1], xticklabels=class_names, yticklabels=class_names)
axes[1].set_title("혼동 행렬 (비율)")
axes[1].set_ylabel("실제 클래스")
axes[1].set_xlabel("예측 클래스")
plt.tight_layout()
plt.show()
plot_confusion_matrix(y_test, y_pred, class_names=["악성", "양성"])
ROC 커브
def plot_roc_curve(models_dict, X_test, y_test):
"""여러 모델의 ROC 커브 비교"""
plt.figure(figsize=(8, 6))
for name, model in models_dict.items():
y_prob = model.predict_proba(X_test)[:, 1]
fpr, tpr, _ = roc_curve(y_test, y_prob)
auc = roc_auc_score(y_test, y_prob)
plt.plot(fpr, tpr, label=f"{name} (AUC={auc:.3f})")
plt.plot([0, 1], [0, 1], "k--", label="랜덤 (AUC=0.5)")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate (Recall)")
plt.title("ROC 커브 비교")
plt.legend()
plt.grid(True)
plt.show()
회귀 평가 지표
from sklearn.metrics import (
mean_absolute_error, # MAE
mean_squared_error, # MSE
root_mean_squared_error, # RMSE
r2_score, # R² (결정계수)
mean_absolute_percentage_error, # MAPE
)
from sklearn.datasets import fetch_california_housing
from sklearn.ensemble import RandomForestRegressor
X, y = fetch_california_housing(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
reg = RandomForestRegressor(random_state=42)
reg.fit(X_train, y_train)
y_pred = reg.predict(X_test)
print(f"MAE: {mean_absolute_error(y_test, y_pred):.4f}")
print(f"RMSE: {root_mean_squared_error(y_test, y_pred):.4f}")
print(f"R²: {r2_score(y_test, y_pred):.4f}")
print(f"MAPE: {mean_absolute_percentage_error(y_test, y_pred)*100:.2f}%")
# 잔차 플롯
residuals = y_test - y_pred
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].scatter(y_pred, residuals, alpha=0.3)
axes[0].axhline(0, color="red", linestyle="--")
axes[0].set_xlabel("예측값")
axes[0].set_ylabel("잔차")
axes[0].set_title("잔차 플롯")
axes[1].hist(residuals, bins=50, edgecolor="white")
axes[1].set_title("잔차 분포")
plt.tight_layout()
plt.show()
교차 검증 심화
from sklearn.model_selection import (
StratifiedKFold,
KFold,
LeaveOneOut,
cross_validate,
learning_curve,
)
# 층화 K-Fold (분류에서 클래스 비율 유지)
skf = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
results = cross_validate(
model, X, y, cv=skf,
scoring=["accuracy", "f1", "roc_auc"],
return_train_score=True,
)
print("학습 정확도:", results["train_accuracy"].mean())
print("검증 정확도:", results["test_accuracy"].mean())
# 학습 >> 검증: 과적합, 학습 ≈ 검증 ≈ 낮음: 과소적합
# 학습 곡선 (과적합/과소적합 진단)
train_sizes, train_scores, val_scores = learning_curve(
model, X, y, cv=5,
train_sizes=np.linspace(0.1, 1.0, 10),
scoring="accuracy",
n_jobs=-1,
)
plt.figure(figsize=(8, 5))
plt.plot(train_sizes, train_scores.mean(axis=1), label="학습 정확도")
plt.plot(train_sizes, val_scores.mean(axis=1), label="검증 정확도")
plt.fill_between(train_sizes,
train_scores.mean(axis=1) - train_scores.std(axis=1),
train_scores.mean(axis=1) + train_scores.std(axis=1), alpha=0.1)
plt.fill_between(train_sizes,
val_scores.mean(axis=1) - val_scores.std(axis=1),
val_scores.mean(axis=1) + val_scores.std(axis=1), alpha=0.1)
plt.xlabel("학습 데이터 크기")
plt.ylabel("정확도")
plt.title("학습 곡선")
plt.legend()
plt.grid(True)
plt.show()
클래스 불균형 처리
from sklearn.utils.class_weight import compute_class_weight
from imblearn.over_sampling import SMOTE # pip install imbalanced-learn
from imblearn.under_sampling import RandomUnderSampler
# 클래스 가중치 자동 계산
classes = np.unique(y_train)
weights = compute_class_weight("balanced", classes=classes, y=y_train)
class_weight = dict(zip(classes, weights))
print(f"클래스 가중치: {class_weight}")
# 가중치 적용 모델
model_balanced = RandomForestClassifier(
class_weight="balanced", # 또는 class_weight=class_weight
random_state=42,
)
# SMOTE — 소수 클래스 오버샘플링
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
print(f"오버샘플링 전: {np.bincount(y_train)}")
print(f"오버샘플링 후: {np.bincount(y_resampled)}")
정리
| 문제 | 주요 지표 | 보조 지표 |
|---|---|---|
| 이진 분류 (균형) | Accuracy, F1 | AUC-ROC |
| 이진 분류 (불균형) | F1, AUC-ROC | Precision, Recall |
| 다중 분류 | Macro F1 | Confusion Matrix |
| 회귀 | RMSE, R² | MAE, MAPE |
평가 지표는 비즈니스 목표에 맞게 선택해야 합니다. 오진 비용이 높다면 Recall, 오경보 비용이 높다면 Precision을 우선합니다.