지난 글에서 혼동 행렬로 분류 오류를 해부하는 방법을 배웠다. 혼동 행렬과 그에서 유도된 지표들은 특정 임계값(보통 0.5)에서의 성능을 보여준다. 그런데 모델의 "진짜" 분리 능력은 임계값에 상관없이 얼마나 양성과 음성을 잘 구분하는가에 있다. ROC 곡선과 AUC는 모든 임계값에서의 성능을 하나의 숫자로 압축한다.
ROC 곡선의 두 축
ROC(Receiver Operating Characteristic) 곡선은 X축에 FPR(False Positive Rate), Y축에 TPR(True Positive Rate, 재현율)을 놓고 임계값을 0에서 1까지 변화시킬 때의 궤적을 그린다.
- TPR(민감도, Recall) = TP / (TP + FN) — 실제 양성 중 탐지한 비율
- FPR(1-특이도) = FP / (FP + TN) — 실제 음성 중 잘못 양성으로 판단한 비율
임계값을 낮추면 더 많은 것을 양성으로 선언하므로 TPR과 FPR이 모두 올라가고, 높이면 둘 다 내려간다. 이 트레이드오프의 궤적이 ROC 곡선이다.
AUC의 확률론적 의미
AUC(Area Under the ROC Curve)는 ROC 곡선 아래 면적으로, 0부터 1 사이의 값을 갖는다. 단순히 면적이 아니라 직관적인 확률로 해석할 수 있다.
AUC = 임의로 선택한 양성 샘플이 임의로 선택한 음성 샘플보다 더 높은 점수를 받을 확률
AUC=0.85라면, 아무 양성 샘플과 아무 음성 샘플을 골랐을 때 모델이 양성 샘플에 더 높은 점수를 줄 확률이 85%라는 뜻이다. AUC=0.5는 동전 던지기 수준(랜덤 분류기)이고, AUC=1.0은 완벽한 분리다.
기본 구현
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (roc_curve, roc_auc_score,
average_precision_score)
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42)
# 두 모델 비교
models = {
'LR': Pipeline([('s', StandardScaler()),
('m', LogisticRegression(max_iter=1000))]),
'RF': RandomForestClassifier(n_estimators=200, random_state=42)
}
for name, m in models.items():
m.fit(X_train, y_train)
y_score = m.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_score)
ap = average_precision_score(y_test, y_score)
fpr, tpr, thresholds = roc_curve(y_test, y_score)
print(f"{name}: ROC-AUC={auc:.4f} PR-AUC={ap:.4f}")
최적 임계값 찾기
실제 배포할 때는 어떤 임계값을 사용할지 결정해야 한다. 목적에 따라 최적 임계값이 다르다.
import numpy as np
from sklearn.metrics import roc_curve, precision_recall_curve
y_score = model.predict_proba(X_test)[:, 1]
# 방법 1: Youden's J 통계량 (TPR - FPR 최대화)
fpr, tpr, thresholds = roc_curve(y_test, y_score)
j_scores = tpr - fpr
best_idx = np.argmax(j_scores)
best_thr_youden = thresholds[best_idx]
print(f"Youden 임계값: {best_thr_youden:.4f}")
print(f" TPR={tpr[best_idx]:.4f}, FPR={fpr[best_idx]:.4f}")
# 방법 2: F1이 최대인 임계값
precisions, recalls, pr_thresholds = precision_recall_curve(
y_test, y_score)
f1_scores = (2 * precisions * recalls /
(precisions + recalls + 1e-9))
best_f1_idx = np.argmax(f1_scores[:-1])
best_thr_f1 = pr_thresholds[best_f1_idx]
print(f"F1 최적 임계값: {best_thr_f1:.4f}")
# 방법 3: FPR ≤ 0.05 제약 하에 TPR 최대화
constraint_mask = fpr <= 0.05
if constraint_mask.any():
constrained_idx = np.argmax(tpr[constraint_mask])
idx = np.where(constraint_mask)[0][constrained_idx]
print(f"FPR≤5% 제약 임계값: {thresholds[idx]:.4f}"
f" TPR={tpr[idx]:.4f}")
PR-AUC: 극심한 불균형에서의 대안
양성 클래스 비율이 매우 낮을 때(1% 미만) ROC-AUC는 지나치게 낙관적일 수 있다. FPR 계산에 TN이 크게 관여하는데, 음성 샘플이 압도적으로 많으면 TN이 크게 나와 FPR이 낮게 유지되기 때문이다. 이때는 PR(Precision-Recall) 곡선과 PR-AUC를 사용한다.
from sklearn.metrics import (precision_recall_curve,
average_precision_score)
precisions, recalls, thresholds = precision_recall_curve(
y_test, y_score)
# Average Precision: PR 곡선 아래 면적
ap = average_precision_score(y_test, y_score)
print(f"Average Precision: {ap:.4f}")
# 비율이 낮은 클래스의 기준선
baseline = y_test.mean()
print(f"랜덤 기준선 AP: {baseline:.4f}")
# AP가 기준선보다 얼마나 높은지가 중요
PR 곡선의 기준선은 y.mean()(양성 비율)이다. ROC에서 랜덤 기준선이 AUC=0.5인 것처럼, PR에서는 AP=양성비율이 랜덤 기준선이 된다.
다중 클래스 ROC-AUC
클래스가 3개 이상일 때는 One-vs-Rest(OvR) 또는 One-vs-One(OvO) 방식으로 확장한다.
from sklearn.metrics import roc_auc_score
# 다중 클래스: 각 클래스 확률 필요
y_proba_multi = model.predict_proba(X_test) # (n, n_classes)
# macro: 클래스를 동등하게
auc_macro = roc_auc_score(y_test, y_proba_multi,
multi_class='ovr',
average='macro')
# weighted: 클래스 샘플 수 가중
auc_weighted = roc_auc_score(y_test, y_proba_multi,
multi_class='ovr',
average='weighted')
print(f"Macro AUC: {auc_macro:.4f}")
print(f"Weighted AUC: {auc_weighted:.4f}")
ROC-AUC vs 다른 지표 선택 가이드
| 상황 | 권장 지표 |
|---|---|
| 클래스 균형, 임계값 고정 | F1, Accuracy |
| 임계값 선택이 유동적 | ROC-AUC |
| 양성 비율 < 5% | PR-AUC (Average Precision) |
| 비용에 민감한 임계값 | Youden's J, 비용-민감 최적화 |
| 다중 클래스 | Macro ROC-AUC |
지난 글: 혼동 행렬로 분류 오류 해부하기
다음 글: 회귀 모델 평가 지표: MAE·MSE·RMSE·R² 완전 이해
읽어주셔서 감사합니다. 😊

