AI 기초

GUIDE / 중급

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

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

PALDYN Team5 MIN READ

지난 글에서 정확도·정밀도·재현율·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: 임계값 독립적 분류 성능 평가


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

KEEP READING

이어 읽기

AI 기초2026.07.29

검증자 모델: 답을 고르는 눈을 따로 기른다

규칙 검증기, 결과 보상 모델(ORM), 과정 보상 모델(PRM), 자기 검증의 특성과 한계를 비교하고, 여러 후보 중 정답을 골라내는 검증 계층을 실무에서 어떻게 구성할지 코드와 함께 정리합니다.

중급16 MIN
AI 기초2026.07.29

사고 예산 제어: 얼마나 생각하게 할 것인가

사고 예산과 출력 상한의 관계, 난도에 따른 적응형 예산 배분, 예산 소진으로 답변이 잘리는 실패 모드와 그 방어책을 실무 코드와 운영 지표 중심으로 정리합니다.

중급14 MIN