혼동 행렬로 분류 오류 해부하기

이진·다중 클래스 혼동 행렬의 구조, TP·FP·TN·FN에서 지표 유도, 정규화 방법, 오류 패턴 분석, sklearn ConfusionMatrixDisplay 시각화까지 완전히 이해한다.

· 5 min read · PALDYN Team

지난 글에서 정확도·정밀도·재현율·F1의 공식을 배웠다. 이번에는 그 지표들이 실제로 어디서 어떻게 나오는지, 그 원천인 **혼동 행렬(Confusion Matrix)**을 집중적으로 다룬다. 숫자 하나가 아니라 행렬 전체를 읽을 수 있으면 모델이 어떤 클래스를 어떻게 헷갈리는지 훨씬 구체적으로 진단할 수 있다.

혼동 행렬의 구조

이진 분류 혼동 행렬은 2×2 표다. 행(Row)은 실제 클래스, 열(Column)은 예측 클래스를 나타낸다.

혼동 행렬 구조와 해석

sklearn의 confusion_matrix는 다음 순서로 반환한다.

[[TN  FP]
 [FN  TP]]

즉, 음성(Negative)이 행·열의 첫 번째 인덱스(0)다. 혼동하기 쉬우니 항상 tn, fp, fn, tp = cm.ravel()로 명시적으로 추출하는 것이 좋다.

완전한 구현과 시각화

from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import (confusion_matrix,
                              ConfusionMatrixDisplay,
                              classification_report)
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# 데이터 준비
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)

# 학습
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('lr',     LogisticRegression(max_iter=1000))
])
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)

# 혼동 행렬
cm = confusion_matrix(y_test, y_pred)
tn, fp, fn, tp = cm.ravel()

print(f"TP={tp}  FP={fp}")
print(f"FN={fn}  TN={tn}")
print(f"\n정밀도: {tp/(tp+fp):.4f}")
print(f"재현율: {tp/(tp+fn):.4f}")
print(f"특이도: {tn/(tn+fp):.4f}")
print(f"정확도: {(tp+tn)/(tp+fp+fn+tn):.4f}")

혼동 행렬 시각화 코드

# 정규화 혼동 행렬 (비율 표시)
cm_norm = confusion_matrix(y_test, y_pred, normalize='true')
# normalize='true': 각 행의 합이 1 (실제 클래스별 비율)
# normalize='pred': 각 열의 합이 1 (예측 클래스별 비율)
# normalize='all' : 전체 합이 1

print(f"정규화 행렬:\n{cm_norm}")
# [[0.9722  0.0278]
#  [0.0244  0.9756]]
# → 실제 악성(1)의 97.56%를 올바르게 예측

다중 클래스 혼동 행렬

클래스가 n개일 때 혼동 행렬은 n×n이 된다. 각 행은 실제 클래스이고 각 열은 예측 클래스다. 대각선 원소는 올바른 예측, 비대각선 원소는 잘못된 예측이다.

from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix

X, y = load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42)

rf = RandomForestClassifier(n_estimators=200, random_state=42)
rf.fit(X_train, y_train)
y_pred = rf.predict(X_test)

cm = confusion_matrix(y_test, y_pred)
print(f"혼동 행렬 shape: {cm.shape}")  # (10, 10)

# 가장 많이 헷갈리는 쌍 찾기
np.fill_diagonal(cm, 0)   # 대각선 제거
idx = np.unravel_index(cm.argmax(), cm.shape)
print(f"가장 많이 헷갈리는 쌍: {idx[0]}{idx[1]} ({cm[idx]}회)")

오류 패턴 분석: 어떤 샘플을 틀렸나

혼동 행렬에서 특정 오류를 보인 샘플들을 직접 확인하면 모델 개선 방향을 찾을 수 있다.

import numpy as np

# FN: 실제 양성인데 음성으로 예측한 인덱스
fn_idx = np.where((y_test == 1) & (y_pred == 0))[0]
# FP: 실제 음성인데 양성으로 예측한 인덱스
fp_idx = np.where((y_test == 0) & (y_pred == 1))[0]

print(f"False Negative {len(fn_idx)}건: 인덱스 {fn_idx[:5]}")
print(f"False Positive {len(fp_idx)}건: 인덱스 {fp_idx[:5]}")

# FN 샘플의 모델 확률 점수 확인
y_proba = pipe.predict_proba(X_test)[:, 1]
fn_proba = y_proba[fn_idx]
print(f"FN 샘플의 평균 확률: {fn_proba.mean():.4f}")
# → 모델이 음성으로 분류한 근거 점수
# 이 값들이 임계값(0.5) 근처면 경계선 케이스

# 임계값을 낮춰 FN 줄이기 (재현율 올리기)
threshold = 0.35   # 기본값 0.5에서 낮춤
y_pred_new = (y_proba >= threshold).astype(int)
cm_new = confusion_matrix(y_test, y_pred_new)
tn2, fp2, fn2, tp2 = cm_new.ravel()
print(f"임계값 {threshold} → FN: {fn}{fn2}, FP: {fp}{fp2}")

불균형 데이터에서 혼동 행렬 읽기

클래스 불균형이 심할 때는 절대 수치보다 정규화 행렬을 봐야 한다.

# 클래스 9:1 불균형 시나리오
from sklearn.datasets import make_classification

X_imb, y_imb = make_classification(
    n_samples=10000,
    n_features=20,
    weights=[0.9, 0.1],  # 음성 90%, 양성 10%
    random_state=42
)

X_tr, X_ts, y_tr, y_ts = train_test_split(
    X_imb, y_imb, test_size=0.2,
    stratify=y_imb, random_state=42)

from sklearn.ensemble import RandomForestClassifier
rf_imb = RandomForestClassifier(
    n_estimators=100,
    class_weight='balanced',  # 클래스 가중치 자동 조정
    random_state=42)
rf_imb.fit(X_tr, y_tr)
y_pred_imb = rf_imb.predict(X_ts)

cm_imb = confusion_matrix(y_ts, y_pred_imb)
print(f"원본 행렬:\n{cm_imb}")
cm_imb_norm = confusion_matrix(y_ts, y_pred_imb, normalize='true')
print(f"\n정규화 행렬:\n{cm_imb_norm.round(3)}")

절대 수치로 보면 TN이 압도적으로 크고 모든 것이 잘 돼 보이지만, normalize='true'로 보면 각 클래스별 예측 성공률을 공평하게 비교할 수 있다.


지난 글: 분류 모델 평가 지표 완전 정복: 정확도·정밀도·재현율·F1

다음 글: ROC 곡선과 AUC: 임계값 독립적 분류 성능 평가


읽어주셔서 감사합니다. 😊