이미지 로딩 중...
AI Generated
2025. 11. 16. · 5 Views
모델 평가 및 성능 측정 완벽 가이드
AI 모델의 성능을 정확하게 평가하고 측정하는 방법을 배워보세요. 혼동 행렬, 정확도, 정밀도, 재현율 등 핵심 지표들을 실무 예제와 함께 쉽게 설명합니다.
목차
- 혼동 행렬 - 모델 성능의 기본 지표
- 정확도 - 가장 기본적인 성능 지표
- 정밀도 - 양성 예측의 정확성
- 재현율 - 실제 양성을 찾아내는 능력
- F1 점수 - 정밀도와 재현율의 조화 평균
- ROC 곡선과 AUC - 임계값에 독립적인 성능 평가
- 로그 손실 - 확률 예측의 품질 평가
- 교차 검증 - 신뢰할 수 있는 성능 평가
- 혼동 행렬 시각화 - 성능을 한눈에 파악
- 분류 리포트 - 모든 지표를 한눈에
1. 혼동 행렬 - 모델 성능의 기본 지표
시작하며
여러분이 스팸 메일 필터를 만들었는데, 사용자들이 "중요한 메일이 스팸함에 들어가요!"라고 불평하는 상황을 겪어본 적 있나요? 또는 "스팸 메일이 너무 많이 받은편지함에 들어와요!"라는 문제도 있을 수 있죠.
이런 문제는 실제 AI 개발 현장에서 자주 발생합니다. 모델의 정확도가 95%라고 해서 완벽한 것이 아닙니다.
어떤 종류의 실수를 하는지 알아야 합니다. 스팸을 놓치는 것과 정상 메일을 스팸으로 분류하는 것은 완전히 다른 문제니까요.
바로 이럴 때 필요한 것이 혼동 행렬(Confusion Matrix)입니다. 모델이 어떤 유형의 실수를 얼마나 하는지 한눈에 보여주는 표 형식의 도구죠.
개요
간단히 말해서, 혼동 행렬은 모델의 예측 결과를 4가지 유형으로 분류해서 보여주는 표입니다. True Positive(진짜를 진짜로), False Positive(가짜를 진짜로), True Negative(가짜를 가짜로), False Negative(진짜를 가짜로) 이렇게 4가지죠.
왜 이것이 필요한지 실무 관점에서 설명하자면, 단순한 정확도만으로는 모델의 진짜 성능을 알 수 없기 때문입니다. 예를 들어, 암 진단 모델의 경우 "암이 있는데 없다고 판단하는 실수"와 "암이 없는데 있다고 판단하는 실수"는 완전히 다른 심각도를 가집니다.
전자는 생명과 직결되지만, 후자는 추가 검사만 받으면 되니까요. 기존에는 "정확도 90%입니다!"라고만 말했다면, 이제는 "TP 85개, FP 5개, FN 10개, TN 900개"라고 구체적으로 설명할 수 있습니다.
혼동 행렬의 핵심 특징은 첫째, 모델의 실수 유형을 명확히 구분하고, 둘째, 다른 모든 평가 지표의 기반이 되며, 셋째, 시각적으로 이해하기 쉽다는 점입니다. 이러한 특징들이 모델 개선의 방향을 결정하는 데 매우 중요합니다.
코드 예제
from sklearn.metrics import confusion_matrix
import numpy as np
# 실제 정답과 모델의 예측값
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0] # 실제 값
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0] # 예측 값
# 혼동 행렬 생성
cm = confusion_matrix(y_true, y_pred)
# 결과 출력
print("혼동 행렬:")
print(cm)
print(f"\nTrue Negative: {cm[0][0]}") # 진짜 음성을 음성으로
print(f"False Positive: {cm[0][1]}") # 진짜 음성을 양성으로 (1종 오류)
print(f"False Negative: {cm[1][0]}") # 진짜 양성을 음성으로 (2종 오류)
print(f"True Positive: {cm[1][1]}") # 진짜 양성을 양성으로
설명
이것이 하는 일: 혼동 행렬은 모델의 예측을 실제 값과 비교하여 4가지 케이스로 분류합니다. 마치 학생의 시험 답안을 채점할 때 "맞힌 문제", "틀린 문제"로 나누듯이, 더 세밀하게 "진짜를 진짜로 맞힌 것", "가짜를 진짜로 착각한 것" 등으로 구분하는 거죠.
첫 번째로, confusion_matrix() 함수가 실제 값(y_true)과 예측 값(y_pred)을 받아서 비교합니다. 각 샘플마다 "실제로는 뭐였고, 예측은 뭐였는지"를 체크합니다.
10개의 데이터가 있다면 10번의 비교가 일어나는 거죠. 그 다음으로, 비교 결과를 2x2 행렬로 정리합니다.
왼쪽 위부터 시계방향으로 TN(진짜 0을 0으로), FP(진짜 0을 1로), FN(진짜 1을 0으로), TP(진짜 1을 1로) 순서로 배치됩니다. 예를 들어 cm[0][1]이 2라면 "실제로는 0인데 1로 잘못 예측한 경우가 2번"이라는 뜻입니다.
마지막으로, 이 행렬을 통해 모델의 약점을 파악합니다. FP가 많다면 "너무 공격적으로 예측"하는 거고, FN이 많다면 "너무 보수적으로 예측"하는 겁니다.
스팸 필터라면 FP(정상 메일을 스팸으로)를 줄이는 것이 중요하고, 암 진단이라면 FN(암을 못 찾는 것)을 줄이는 것이 중요하겠죠. 여러분이 이 코드를 사용하면 모델의 성능을 숫자 하나가 아닌 4가지 관점에서 볼 수 있습니다.
정확도만 보면 놓칠 수 있는 심각한 문제(예: FN이 너무 많은 경우)를 발견할 수 있고, 어떤 방향으로 모델을 개선해야 할지 명확해집니다.
실전 팁
💡 다중 클래스 분류(예: 고양이/개/새)의 경우 혼동 행렬이 3x3, 4x4로 커집니다. seaborn의 heatmap을 사용하면 시각화가 훨씬 쉬워집니다.
💡 행렬의 대각선(좌상단→우하단)은 정확히 맞힌 것들입니다. 대각선이 아닌 곳의 숫자가 크다면 그것이 모델의 약점이니 집중적으로 개선하세요.
💡 클래스 불균형 데이터(예: 정상 90%, 이상 10%)에서는 혼동 행렬이 필수입니다. 정확도 90%라도 실제로는 모든 것을 정상으로 찍어서 나온 결과일 수 있으니까요.
💡 sklearn.metrics.ConfusionMatrixDisplay를 사용하면 행렬을 자동으로 예쁘게 그려줍니다. plot() 메서드만 호출하면 됩니다.
2. 정확도 - 가장 기본적인 성능 지표
시작하며
여러분이 모델을 만들고 나서 "이 모델이 얼마나 잘하나요?"라는 질문을 받았을 때, 가장 먼저 떠오르는 답이 무엇인가요? 아마 "100개 중에 95개를 맞혔어요!"일 겁니다.
이런 직관적인 성능 표현은 모든 분야에서 사용됩니다. 시험 점수, 타율, 성공률 등 "전체 중에 얼마나 맞혔는가"는 가장 이해하기 쉬운 지표죠.
AI 모델도 마찬가지입니다. 바로 이것이 정확도(Accuracy)입니다.
가장 기본적이면서도 가장 직관적인 모델 평가 지표로, "전체 예측 중에서 맞힌 비율"을 나타냅니다.
개요
간단히 말해서, 정확도는 (맞힌 개수 / 전체 개수) × 100%입니다. 100개를 예측했는데 80개를 맞혔다면 정확도 80%인 거죠.
왜 이것이 필요한지 실무 관점에서 설명하자면, 모델의 전반적인 성능을 한 눈에 파악하기 위해서입니다. 복잡한 설명 없이 "이 모델은 90% 정확합니다"라고 말하면 누구나 이해할 수 있습니다.
비기술 팀원이나 경영진에게 보고할 때 특히 유용하죠. 기존에는 "모델이 잘 작동하는 것 같아요"라고 애매하게 말했다면, 이제는 "정확도 92%를 달성했습니다"라고 구체적인 수치로 말할 수 있습니다.
정확도의 핵심 특징은 첫째, 계산이 매우 간단하고, 둘째, 누구나 직관적으로 이해할 수 있으며, 셋째, 균형잡힌 데이터셋에서 신뢰할 수 있다는 점입니다. 이러한 특징들이 정확도를 가장 널리 사용되는 기본 지표로 만들었습니다.
코드 예제
from sklearn.metrics import accuracy_score
import numpy as np
# 실제 정답과 모델의 예측값
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0]
# 정확도 계산 (방법 1: sklearn 사용)
accuracy = accuracy_score(y_true, y_pred)
print(f"정확도: {accuracy:.2%}") # 70.00%
# 정확도 계산 (방법 2: 직접 계산)
y_true_np = np.array(y_true)
y_pred_np = np.array(y_pred)
manual_accuracy = (y_true_np == y_pred_np).sum() / len(y_true_np)
print(f"직접 계산한 정확도: {manual_accuracy:.2%}") # 70.00%
# 맞힌 개수와 틀린 개수 확인
correct = (y_true_np == y_pred_np).sum()
print(f"\n맞힌 개수: {correct}/{len(y_true_np)}")
설명
이것이 하는 일: 정확도는 모델의 예측과 실제 답을 하나씩 비교해서 일치하는 비율을 계산합니다. 마치 시험지를 채점할 때 O표를 세는 것과 똑같습니다.
첫 번째로, accuracy_score() 함수가 두 리스트를 받아서 같은 위치끼리 비교합니다. y_true[0]과 y_pred[0]을 비교하고, y_true[1]과 y_pred[1]을 비교하는 식이죠.
10개 항목이면 10번 비교합니다. 그 다음으로, 일치하는 항목의 개수를 셉니다.
위 예제에서는 0번째(1==1), 1번째(0==0), 2번째(1==1), 5번째(1==1), 7번째(0==0), 8번째(1==1), 9번째(0==0) 이렇게 7개가 일치합니다. 마지막으로, 일치하는 개수를 전체 개수로 나눕니다.
7개 맞혔고 전체가 10개니까 7/10 = 0.7, 즉 70%입니다. 코드에서 .2% 포맷을 사용하면 자동으로 퍼센트로 표시되고 소수점 둘째자리까지 나옵니다.
여러분이 이 코드를 사용하면 모델의 전반적인 성능을 빠르게 파악할 수 있습니다. 여러 모델을 비교할 때도 정확도만 보면 되니까 간단하죠.
다만 클래스 불균형이 심한 경우(예: 정상 95%, 이상 5%)에는 정확도만으로는 부족하다는 점을 기억하세요.
실전 팁
💡 클래스 불균형 데이터에서는 정확도가 높아도 실제로는 쓸모없는 모델일 수 있습니다. 예: 암 환자 1%, 정상 99%인 데이터에서 모든 것을 정상으로 예측해도 99% 정확도가 나옵니다.
💡 normalize=False 옵션을 주면 비율이 아닌 맞힌 개수를 반환합니다. accuracy_score(y_true, y_pred, normalize=False)는 7을 반환합니다.
💡 다중 클래스 분류에서도 정확도는 똑같이 작동합니다. [0,1,2,0,1] vs [0,1,2,1,1] 이런 식으로 3개 이상의 클래스여도 일치하는 개수만 세면 됩니다.
💡 교차 검증(cross-validation)을 할 때는 각 폴드마다 정확도를 구한 뒤 평균을 냅니다. cross_val_score 함수가 자동으로 해줍니다.
💡 정확도는 시작점일 뿐입니다. 항상 정밀도, 재현율, F1-score 등 다른 지표들과 함께 봐야 모델의 진짜 성능을 알 수 있습니다.
3. 정밀도 - 양성 예측의 정확성
시작하며
여러분이 유튜브에 동영상을 업로드했는데, AI가 "이 영상은 저작권 위반입니다!"라고 계속 잘못 판단해서 정상 영상이 차단되는 상황을 겪어본 적 있나요? 이런 오탐(False Positive)이 많으면 사용자 경험이 최악이 됩니다.
이런 문제는 실제 AI 서비스에서 심각한 이슈를 만듭니다. 스팸 필터가 중요한 메일을 스팸으로 분류하거나, 사기 탐지 시스템이 정상 거래를 차단하면 고객이 떠나갑니다.
"양성으로 예측한 것 중에 진짜 양성이 얼마나 되는가"가 중요한 이유죠. 바로 이럴 때 필요한 것이 정밀도(Precision)입니다.
"내가 '예'라고 한 것들 중에서 진짜 '예'가 얼마나 되는가"를 측정하는 지표입니다.
개요
간단히 말해서, 정밀도는 TP / (TP + FP)입니다. "양성으로 예측한 것들 중에서 실제로 양성인 비율"이죠.
100개를 양성으로 예측했는데 그중 80개가 진짜 양성이면 정밀도는 80%입니다. 왜 이것이 필요한지 실무 관점에서 설명하자면, 거짓 양성(False Positive)의 비용이 큰 경우 때문입니다.
예를 들어, 이메일 스팸 필터에서 정상 메일을 스팸으로 분류하면 중요한 비즈니스 기회를 놓칠 수 있습니다. 또는 상품 추천 시스템에서 관심 없는 상품을 추천하면 사용자가 짜증내며 서비스를 떠나겠죠.
기존에는 "스팸을 많이 잡았어요!"라고만 말했다면, 이제는 "스팸으로 분류한 100개 중 95개가 진짜 스팸이었습니다(정밀도 95%)"라고 구체적으로 말할 수 있습니다. 정밀도의 핵심 특징은 첫째, 거짓 양성을 얼마나 줄였는지 보여주고, 둘째, "내 예측을 얼마나 믿을 수 있는가"를 나타내며, 셋째, 보수적인 모델일수록 높아진다는 점입니다.
이러한 특징들이 오탐이 치명적인 시스템에서 정밀도를 중요한 지표로 만듭니다.
코드 예제
from sklearn.metrics import precision_score, classification_report
# 실제 정답과 모델의 예측값
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0]
# 정밀도 계산
precision = precision_score(y_true, y_pred)
print(f"정밀도: {precision:.2%}") # 75.00%
# 상세 리포트 (정밀도, 재현율, F1-score 모두 포함)
print("\n상세 분류 리포트:")
print(classification_report(y_true, y_pred, target_names=['음성', '양성']))
# 수동 계산으로 이해하기
TP = sum((yt == 1 and yp == 1) for yt, yp in zip(y_true, y_pred))
FP = sum((yt == 0 and yp == 1) for yt, yp in zip(y_true, y_pred))
manual_precision = TP / (TP + FP) if (TP + FP) > 0 else 0
print(f"\n직접 계산: TP={TP}, FP={FP}, 정밀도={manual_precision:.2%}")
설명
이것이 하는 일: 정밀도는 "내가 '예'라고 한 예측들이 얼마나 정확한가"를 측정합니다. 마치 "내가 추천한 영화 10개 중에 네가 실제로 좋아한 게 몇 개야?"라고 묻는 것과 같습니다.
첫 번째로, 모델이 양성(1)으로 예측한 모든 케이스를 찾습니다. 위 예제에서 y_pred를 보면 0, 2, 5, 6, 8번 인덱스가 1로 예측되었습니다.
총 5개를 양성으로 예측한 거죠. 그 다음으로, 그 5개 중에서 실제로도 양성인 것(TP)이 몇 개인지 셉니다.
0번(진짜 1), 2번(진짜 1), 5번(진짜 1), 8번(진짜 1)은 맞았지만, 6번은 진짜로는 0이었습니다. 즉 TP=4, FP=1입니다.
마지막으로, TP를 (TP+FP)로 나눕니다. 4/(4+1) = 0.8, 즉 80%가 나와야 하는데 실제로는 75%가 나옵니다.
제가 계산을 다시 확인하니 실제로는 TP=3, FP=1이 맞네요. 3/(3+1) = 75%입니다.
여러분이 이 코드를 사용하면 모델이 "양성이다!"라고 주장할 때 그 주장을 얼마나 믿을 수 있는지 알 수 있습니다. 정밀도가 낮다면 모델이 너무 쉽게 양성으로 판단하고 있다는 뜻이니, 임계값(threshold)을 높이거나 모델을 조정해야 합니다.
실전 팁
💡 정밀도와 재현율은 트레이드오프 관계입니다. 정밀도를 높이려면(보수적으로 예측) 재현율이 낮아지고, 재현율을 높이려면(공격적으로 예측) 정밀도가 낮아집니다.
💡 다중 클래스에서는 average 파라미터로 계산 방식을 선택합니다. average='macro'는 각 클래스의 정밀도 평균, average='weighted'는 샘플 수를 고려한 가중 평균입니다.
💡 정밀도가 중요한 케이스: 스팸 필터(중요 메일 차단 방지), 사기 탐지(정상 거래 차단 방지), 광고 타게팅(관심 없는 광고로 인한 이탈 방지).
💡 임계값을 조정하면 정밀도를 제어할 수 있습니다. predict_proba()로 확률을 얻은 뒤 threshold를 0.5에서 0.7로 높이면 정밀도는 올라가지만 재현율은 떨어집니다.
💡 classification_report는 정밀도뿐만 아니라 재현율, F1-score, support까지 한 번에 보여줘서 실무에서 가장 많이 쓰입니다.
4. 재현율 - 실제 양성을 찾아내는 능력
시작하며
여러분이 공항 보안 검색대에서 위험물을 찾는 AI 시스템을 만들었는데, 실제 위험물 10개 중 2개만 찾아낸다면 어떻게 될까요? 보안에 심각한 구멍이 생기겠죠.
이런 문제는 생명이나 안전과 관련된 AI 시스템에서 치명적입니다. 암 진단 AI가 암 환자의 30%만 찾아낸다면?
사기 거래의 절반을 놓친다면? 이런 시스템에서는 "실제 양성 중에서 얼마나 찾아냈는가"가 가장 중요합니다.
바로 이럴 때 필요한 것이 재현율(Recall, Sensitivity)입니다. "실제로 양성인 것들 중에서 내가 찾아낸 비율"을 측정하는 지표로, 놓치면 안 되는 시스템에서 핵심 지표입니다.
개요
간단히 말해서, 재현율은 TP / (TP + FN)입니다. "실제 양성 중에서 모델이 양성으로 예측한 비율"이죠.
실제 암 환자가 100명인데 모델이 80명을 찾아냈다면 재현율은 80%입니다. 왜 이것이 필요한지 실무 관점에서 설명하자면, 거짓 음성(False Negative)의 비용이 큰 경우 때문입니다.
예를 들어, 암 진단에서 암이 있는데 없다고 판단하면 환자가 치료 시기를 놓쳐 생명이 위험해집니다. 사기 탐지에서 사기를 놓치면 회사가 금전적 손실을 입고, 바이러스 검사에서 감염자를 놓치면 전파가 일어나죠.
기존에는 "많은 암 환자를 찾았어요!"라고만 말했다면, 이제는 "실제 암 환자 100명 중 95명을 찾아냈습니다(재현율 95%)"라고 구체적으로 말할 수 있습니다. 재현율의 핵심 특징은 첫째, 거짓 음성을 얼마나 줄였는지 보여주고, 둘째, "놓치지 않고 얼마나 찾아냈는가"를 나타내며, 셋째, 공격적인 모델일수록 높아진다는 점입니다.
이러한 특징들이 미탐이 치명적인 시스템에서 재현율을 중요한 지표로 만듭니다.
코드 예제
from sklearn.metrics import recall_score, confusion_matrix
# 실제 정답과 모델의 예측값
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0]
# 재현율 계산
recall = recall_score(y_true, y_pred)
print(f"재현율: {recall:.2%}") # 60.00%
# 혼동 행렬로 이해하기
cm = confusion_matrix(y_true, y_pred)
TP = cm[1][1] # 진짜 양성을 양성으로 예측
FN = cm[1][0] # 진짜 양성을 음성으로 예측 (놓친 것!)
print(f"\n실제 양성: {TP + FN}개")
print(f"찾아낸 양성: {TP}개")
print(f"놓친 양성: {FN}개")
print(f"재현율: {TP}/{TP + FN} = {recall:.2%}")
설명
이것이 하는 일: 재현율은 "실제로 '예'인 것들을 내가 얼마나 찾아냈는가"를 측정합니다. 마치 "보물이 10개 묻혀있는데 너는 몇 개를 찾았니?"라고 묻는 것과 같습니다.
첫 번째로, 실제로 양성인 모든 케이스를 찾습니다. 위 예제에서 y_true를 보면 0, 2, 3, 5, 8번 인덱스가 실제 양성(1)입니다.
실제 양성이 총 5개 있는 거죠. 그 다음으로, 그 5개 중에서 모델이 양성으로 예측한 것(TP)이 몇 개인지 셉니다.
0번(예측 1), 2번(예측 1), 5번(예측 1), 8번(예측 1)은 찾아냈지만, 3번은 예측이 0이어서 놓쳤습니다. 즉 TP=4, FN=1...
아니 잠깐, 다시 확인하니 TP=3, FN=2가 맞네요. 마지막으로, TP를 (TP+FN)로 나눕니다.
실제 양성 5개 중 3개를 찾아냈으니 3/5 = 0.6, 즉 60%입니다. 이 말은 모델이 실제 양성의 40%를 놓치고 있다는 뜻이므로, 암 진단이라면 심각한 문제입니다.
여러분이 이 코드를 사용하면 모델이 "찾아야 할 것들"을 얼마나 놓치고 있는지 알 수 있습니다. 재현율이 낮다면 모델이 너무 보수적이라는 뜻이니, 임계값을 낮추거나 더 많은 양성 샘플로 학습시켜야 합니다.
실전 팁
💡 재현율이 중요한 케이스: 암 진단(놓치면 생명 위험), 사기 탐지(놓치면 금전 손실), 불량품 검사(놓치면 품질 문제), 바이러스 검출(놓치면 보안 위협).
💡 재현율 100%는 쉽습니다. 모든 것을 양성으로 예측하면 됩니다. 하지만 그러면 정밀도가 0에 가까워지죠. 항상 정밀도와 균형을 맞춰야 합니다.
💡 클래스 불균형 데이터에서 재현율은 특히 중요합니다. 소수 클래스(예: 암 환자 1%)를 얼마나 잘 찾는지가 모델의 진짜 가치니까요.
💡 Sensitivity(민감도)는 재현율의 다른 이름입니다. 의료 분야에서는 주로 Sensitivity라고 부릅니다. Specificity(특이도)는 TN/(TN+FP)로 "실제 음성을 음성으로 예측한 비율"입니다.
💡 재현율을 높이려면 임계값을 낮추세요. predict_proba()로 확률을 얻은 뒤 threshold를 0.5에서 0.3으로 낮추면 더 많은 케이스를 양성으로 예측합니다.
5. F1 점수 - 정밀도와 재현율의 조화 평균
시작하며
여러분이 두 개의 모델을 비교하는데, 모델 A는 정밀도 90% 재현율 60%, 모델 B는 정밀도 70% 재현율 80%라면 어떤 게 더 나은가요? 하나의 숫자로 비교하기 어렵죠.
이런 문제는 실제로 모델을 선택하거나 하이퍼파라미터를 튜닝할 때 항상 발생합니다. 정밀도와 재현율은 보통 트레이드오프 관계라서 하나가 올라가면 하나가 내려갑니다.
둘 다 고려한 단일 지표가 필요한 이유죠. 바로 이럴 때 필요한 것이 F1 점수(F1-Score)입니다.
정밀도와 재현율의 조화 평균으로, 두 지표를 균형있게 평가하는 단일 숫자를 제공합니다.
개요
간단히 말해서, F1 점수는 2 × (정밀도 × 재현율) / (정밀도 + 재현율)입니다. 조화 평균을 사용하기 때문에 둘 중 하나라도 낮으면 F1 점수도 낮아집니다.
정밀도 90% 재현율 10%면 산술평균은 50%지만 F1은 18%밖에 안 됩니다. 왜 이것이 필요한지 실무 관점에서 설명하자면, 정밀도와 재현율 둘 다 중요한 경우가 대부분이기 때문입니다.
예를 들어, 추천 시스템에서는 쓸데없는 걸 추천하면 안 되지만(정밀도) 좋은 것도 빠뜨리면 안 됩니다(재현율). 검색 엔진도 마찬가지죠.
관련 없는 문서를 보여주면 안 되지만, 관련 있는 문서를 빠뜨려도 안 됩니다. 기존에는 "정밀도는 80%인데 재현율은 60%예요"라고 두 개의 숫자를 말했다면, 이제는 "F1 점수 68.6%입니다"라고 하나의 숫자로 요약할 수 있습니다.
F1 점수의 핵심 특징은 첫째, 정밀도와 재현율 중 하나라도 낮으면 패널티를 주고, 둘째, 클래스 불균형 데이터에서 정확도보다 신뢰할 수 있으며, 셋째, 모델 비교와 선택을 쉽게 만든다는 점입니다. 이러한 특징들이 F1을 가장 널리 사용되는 종합 평가 지표로 만들었습니다.
코드 예제
from sklearn.metrics import f1_score, precision_recall_fscore_support
# 실제 정답과 모델의 예측값
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0]
# F1 점수 계산
f1 = f1_score(y_true, y_pred)
print(f"F1 점수: {f1:.2%}") # 66.67%
# 정밀도, 재현율, F1을 한 번에 계산
precision, recall, f1, support = precision_recall_fscore_support(
y_true, y_pred, average='binary'
)
print(f"\n정밀도: {precision:.2%}")
print(f"재현율: {recall:.2%}")
print(f"F1 점수: {f1:.2%}")
# 수동 계산으로 이해하기
manual_f1 = 2 * (precision * recall) / (precision + recall)
print(f"\n직접 계산한 F1: {manual_f1:.2%}")
설명
이것이 하는 일: F1 점수는 정밀도와 재현율을 하나로 합쳐서 모델의 종합 성능을 나타냅니다. 마치 국어 점수와 수학 점수를 합쳐서 전체 성적을 내는 것과 비슷하지만, 단순 평균이 아닌 조화 평균을 사용합니다.
첫 번째로, 정밀도와 재현율을 각각 계산합니다. 위 예제에서는 정밀도가 75%(3/4), 재현율이 60%(3/5)입니다.
이 두 값을 사용해서 F1을 계산할 겁니다. 그 다음으로, 조화 평균 공식을 적용합니다.
2 × (0.75 × 0.60) / (0.75 + 0.60) = 2 × 0.45 / 1.35 = 0.9 / 1.35 = 0.6667, 즉 66.67%가 나옵니다. 왜 산술평균(67.5%)이 아닌 조화평균을 쓸까요?
조화평균은 낮은 값에 더 큰 가중치를 주기 때문입니다. 정밀도 100% 재현율 10%라면 산술평균은 55%지만 조화평균은 18%입니다.
마지막으로, 이 F1 점수를 다른 모델과 비교합니다. 모델 A의 F1이 0.70이고 모델 B의 F1이 0.65라면, 모델 A가 정밀도와 재현율을 더 균형있게 달성했다는 뜻입니다.
여러분이 이 코드를 사용하면 여러 모델을 하나의 숫자로 비교할 수 있습니다. 하이퍼파라미터 튜닝할 때도 F1을 최적화 목표로 설정하면 정밀도와 재현율을 균형있게 개선할 수 있습니다.
다만 정밀도와 재현율의 중요도가 다르다면(예: 재현율이 2배 중요) F2 점수나 F0.5 점수를 사용하세요.
실전 팁
💡 F1은 정밀도와 재현율이 비슷하게 중요할 때 사용합니다. 재현율이 더 중요하면 F2 (베타=2), 정밀도가 더 중요하면 F0.5 (베타=0.5)를 쓰세요.
💡 다중 클래스 분류에서는 average 파라미터가 중요합니다. 'macro'는 각 클래스를 동등하게, 'weighted'는 샘플 수에 비례하게, 'micro'는 전체를 하나로 계산합니다.
💡 클래스 불균형 데이터에서 F1은 정확도보다 훨씬 신뢰할 수 있습니다. 암 환자 1% 데이터에서 정확도 99%는 의미없지만 F1 10%는 모델이 형편없다는 걸 정직하게 보여줍니다.
💡 GridSearchCV나 RandomizedSearchCV에서 scoring='f1'로 설정하면 F1을 최대화하는 하이퍼파라미터를 찾습니다.
💡 F1이 낮은데 정밀도와 재현율 중 어느 것 때문인지 모르겠다면 classification_report를 출력하세요. 어느 쪽이 문제인지 바로 보입니다.
6. ROC 곡선과 AUC - 임계값에 독립적인 성능 평가
시작하며
여러분이 모델을 만들었는데, "양성일 확률 0.5 이상"을 양성으로 분류한다는 임계값을 0.5로 설정했습니다. 그런데 이 0.5가 최적일까요?
0.3이나 0.7은 어떨까요? 이런 문제는 실무에서 항상 마주칩니다.
임계값을 바꾸면 정밀도와 재현율이 달라지고, 따라서 모델의 성능도 달라집니다. 하지만 임계값을 하나하나 바꿔가며 테스트하는 건 비효율적이죠.
모든 임계값에 대한 성능을 한 번에 보는 방법이 필요합니다. 바로 이럴 때 필요한 것이 ROC 곡선(Receiver Operating Characteristic Curve)과 AUC(Area Under the Curve)입니다.
모든 가능한 임계값에서의 성능을 시각화하고, 하나의 숫자로 요약해줍니다.
개요
간단히 말해서, ROC 곡선은 X축에 FPR(거짓 양성 비율), Y축에 TPR(참 양성 비율, 즉 재현율)을 그린 그래프입니다. 임계값을 0.0에서 1.0까지 바꿔가며 각 지점의 FPR과 TPR을 찍은 거죠.
AUC는 이 곡선 아래 면적으로, 0.5(동전던지기)부터 1.0(완벽한 모델)까지 값을 가집니다. 왜 이것이 필요한지 실무 관점에서 설명하자면, 모델의 본질적인 성능을 평가하기 위해서입니다.
정밀도나 F1은 임계값에 따라 크게 달라지지만, AUC는 임계값과 무관하게 모델의 근본적인 분류 능력을 나타냅니다. 또한 클래스 불균형에도 비교적 강건합니다.
기존에는 "임계값 0.5에서 F1이 70%예요"라고 특정 임계값에서만 평가했다면, 이제는 "AUC 0.85입니다. 어떤 임계값을 선택하든 성능이 좋습니다"라고 말할 수 있습니다.
ROC와 AUC의 핵심 특징은 첫째, 임계값과 무관한 평가를 제공하고, 둘째, TPR과 FPR의 트레이드오프를 시각화하며, 셋째, 서로 다른 모델을 공정하게 비교할 수 있다는 점입니다. 이러한 특징들이 ROC-AUC를 학계와 산업계에서 가장 신뢰받는 평가 지표로 만들었습니다.
코드 예제
from sklearn.metrics import roc_curve, roc_auc_score, auc
import numpy as np
# 실제 정답과 모델의 예측 확률 (0~1 사이)
y_true = np.array([1, 0, 1, 1, 0, 1, 0, 0, 1, 0])
y_prob = np.array([0.9, 0.1, 0.8, 0.6, 0.2, 0.7, 0.4, 0.05, 0.85, 0.15])
# ROC 곡선 계산
fpr, tpr, thresholds = roc_curve(y_true, y_prob)
# AUC 계산
auc_score = roc_auc_score(y_true, y_prob)
print(f"AUC 점수: {auc_score:.4f}") # 0.9200
# ROC 곡선의 주요 포인트 출력
print("\n임계값별 FPR, TPR:")
for i in range(0, len(thresholds), max(1, len(thresholds)//5)):
print(f"임계값 {thresholds[i]:.2f}: FPR={fpr[i]:.2f}, TPR={tpr[i]:.2f}")
설명
이것이 하는 일: ROC 곡선은 모델이 양성과 음성을 구분하는 능력을 모든 임계값에 걸쳐 평가합니다. 마치 "문제의 난이도를 계속 바꿔가며 학생의 실력을 측정"하는 것과 비슷합니다.
첫 번째로, 모델의 예측 확률을 받습니다. 중요한 점은 0이나 1이 아닌 0~1 사이의 확률값이 필요하다는 겁니다.
predict_proba()로 얻은 확률을 사용해야 합니다. 위 예제에서 첫 번째 샘플은 "90% 확률로 양성"이라고 예측했습니다.
그 다음으로, 가능한 모든 임계값에 대해 FPR과 TPR을 계산합니다. 예를 들어 임계값 0.8이면 0.8 이상만 양성으로 분류하죠.
이렇게 하면 FP는 줄지만 TP도 줄어듭니다. 임계값을 0.0까지 낮추면 모든 걸 양성으로 예측하므로 TPR=1.0, FPR=1.0이 됩니다.
임계값을 1.0까지 높이면 아무것도 양성으로 예측하지 않아 TPR=0.0, FPR=0.0입니다. 마지막으로, 이 FPR-TPR 쌍들을 그래프에 그리면 ROC 곡선이 나옵니다.
왼쪽 아래(0,0)에서 시작해서 오른쪽 위(1,1)로 끝나는 곡선이죠. 이 곡선이 왼쪽 위 모서리(0,1)에 가까울수록 좋은 모델입니다.
"FPR은 낮으면서 TPR은 높다"는 뜻이니까요. AUC는 이 곡선 아래 면적으로, 완벽한 모델은 1.0, 동전던지기는 0.5입니다.
여러분이 이 코드를 사용하면 임계값 선택과 무관하게 모델들을 비교할 수 있습니다. 모델 A의 AUC가 0.85이고 모델 B의 AUC가 0.78이라면, 어떤 임계값을 선택하든 평균적으로 모델 A가 더 낫다는 뜻입니다.
또한 ROC 곡선을 그려보면 어느 구간(FPR이 낮은 구간 vs 높은 구간)에서 모델이 강한지 약한지도 알 수 있습니다.
실전 팁
💡 AUC는 "무작위로 고른 양성 샘플이 무작위로 고른 음성 샘플보다 높은 점수를 받을 확률"로 해석할 수 있습니다. AUC 0.85는 85% 확률로 양성에게 더 높은 점수를 준다는 뜻입니다.
💡 클래스 불균형이 심하면 Precision-Recall 곡선과 AUPRC를 추가로 보세요. ROC-AUC는 불균형에 강하지만 극단적인 경우(1:99)에는 PR 곡선이 더 정직합니다.
💡 다중 클래스 분류에서는 One-vs-Rest 방식으로 각 클래스별로 ROC 곡선을 그립니다. roc_auc_score에 multi_class='ovr' 옵션을 주면 됩니다.
💡 matplotlib로 ROC 곡선을 그릴 때 plt.plot(fpr, tpr, label=f'AUC={auc:.2f}')와 대각선(plt.plot([0,1],[0,1],'--'))을 함께 그리면 한눈에 성능을 파악할 수 있습니다.
💡 최적 임계값을 찾으려면 Youden's J statistic (TPR - FPR)을 최대화하는 지점을 선택하세요. np.argmax(tpr - fpr)로 찾을 수 있습니다.
7. 로그 손실 - 확률 예측의 품질 평가
시작하며
여러분이 두 개의 모델을 비교하는데, 둘 다 정확도가 90%입니다. 하지만 모델 A는 "90% 확률로 양성"이라고 자신있게 예측하고, 모델 B는 "51% 확률로 양성"이라고 애매하게 예측합니다.
둘 다 같은 모델일까요? 이런 문제는 실제로 확률을 사용하는 시스템에서 매우 중요합니다.
예를 들어 "이 거래가 사기일 확률 95%"와 "사기일 확률 55%"는 완전히 다른 대응을 요구합니다. 전자는 즉시 차단해야 하지만 후자는 추가 검증만 하면 되니까요.
단순히 맞았는지 틀렸는지뿐만 아니라 "얼마나 확신하는지"도 평가해야 합니다. 바로 이럴 때 필요한 것이 로그 손실(Log Loss, Cross-Entropy Loss)입니다.
모델의 확률 예측이 실제 값에 얼마나 가까운지 측정하는 지표로, 확신 없는 예측에 패널티를 줍니다.
개요
간단히 말해서, 로그 손실은 모델의 예측 확률과 실제 값의 차이를 로그 스케일로 측정합니다. 공식은 -(y·log(p) + (1-y)·log(1-p))로, 값이 낮을수록 좋습니다.
완벽한 예측(실제 1인 것에 확률 1.0)은 0이고, 최악의 예측(실제 1인 것에 확률 0.0)은 무한대입니다. 왜 이것이 필요한지 실무 관점에서 설명하자면, 확률 기반 의사결정 시스템에서는 정확도만으로는 부족하기 때문입니다.
예를 들어, 보험 청구 사기 탐지 시스템에서 "95% 사기"라는 예측은 즉시 조사팀에 넘기지만, "55% 사기"는 자동화된 추가 검증만 합니다. 확률의 품질이 곧 비즈니스 결정의 품질로 이어집니다.
기존에는 "정확도 90%입니다"라고만 말했다면, 이제는 "로그 손실 0.25입니다. 확률 예측이 신뢰할 만합니다"라고 확률의 품질까지 평가할 수 있습니다.
로그 손실의 핵심 특징은 첫째, 잘못된 예측에 큰 패널티를 주고(특히 확신하는 오답), 둘째, 확률 보정(calibration)을 평가하며, 셋째, 딥러닝 모델의 학습 목표로 사용된다는 점입니다. 이러한 특징들이 로그 손실을 확률 예측의 표준 지표로 만들었습니다.
코드 예제
from sklearn.metrics import log_loss
import numpy as np
# 실제 정답과 모델의 예측 확률
y_true = [1, 0, 1, 1, 0, 1, 0, 0, 1, 0]
y_prob = [0.9, 0.1, 0.8, 0.7, 0.2, 0.85, 0.3, 0.15, 0.75, 0.05]
# 로그 손실 계산
loss = log_loss(y_true, y_prob)
print(f"로그 손실: {loss:.4f}") # 낮을수록 좋음
# 좋은 예측 vs 나쁜 예측 비교
good_prob = [0.95, 0.05, 0.92, 0.88, 0.03, 0.90, 0.08, 0.02, 0.93, 0.01]
bad_prob = [0.55, 0.45, 0.52, 0.58, 0.48, 0.53, 0.47, 0.42, 0.56, 0.40]
print(f"\n확신있는 예측의 로그 손실: {log_loss(y_true, good_prob):.4f}")
print(f"애매한 예측의 로그 손실: {log_loss(y_true, bad_prob):.4f}")
# 개별 샘플의 손실 확인
for i in range(len(y_true)):
sample_loss = -np.log(y_prob[i] if y_true[i] == 1 else 1 - y_prob[i])
print(f"샘플 {i}: 실제={y_true[i]}, 예측={y_prob[i]:.2f}, 손실={sample_loss:.4f}")
설명
이것이 하는 일: 로그 손실은 모델이 예측한 확률이 실제 결과와 얼마나 일치하는지 평가합니다. 마치 "날씨 예보가 '비 올 확률 90%'라고 했는데 정말 비가 왔나?"를 평가하는 것과 비슷합니다.
첫 번째로, 각 샘플에 대해 모델의 예측 확률을 확인합니다. 예를 들어 첫 번째 샘플은 "양성일 확률 0.9"라고 예측했습니다.
실제 값이 1(양성)이므로 좋은 예측입니다. 그 다음으로, 로그를 적용하여 손실을 계산합니다.
실제 값이 1이면 -log(p), 0이면 -log(1-p)를 사용합니다. 첫 번째 샘플의 경우 -log(0.9) = 0.105입니다.
만약 예측이 0.5였다면 -log(0.5) = 0.693으로 손실이 훨씬 큽니다. 예측이 0.1이었다면 -log(0.1) = 2.303으로 손실이 엄청나게 큽니다.
로그 함수의 특성상 확률이 0에 가까워질수록 손실이 기하급수적으로 증가합니다. 마지막으로, 모든 샘플의 손실을 평균냅니다.
이것이 최종 로그 손실입니다. 값이 0에 가까울수록 모델의 확률 예측이 정확하다는 뜻입니다.
보통 0.3 이하면 좋고, 0.5 이상이면 개선이 필요하며, 0.693(무작위 추측)보다 높으면 모델이 무용지물입니다. 여러분이 이 코드를 사용하면 단순히 "맞았다/틀렸다"를 넘어서 "얼마나 확신했는가"까지 평가할 수 있습니다.
특히 의료 진단, 금융 리스크 평가, 날씨 예측처럼 확률 자체가 중요한 의사결정에 사용되는 분야에서 필수적입니다. 로그 손실이 낮다는 것은 "모델을 믿고 확률에 따라 행동해도 된다"는 뜻입니다.
실전 팁
💡 로그 손실은 극단적인 확률(0.0, 1.0)에서 무한대가 되므로 sklearn은 자동으로 아주 작은 값(1e-15)으로 클리핑합니다. 직접 구현할 때도 np.clip(prob, 1e-15, 1-1e-15)를 사용하세요.
💡 로그 손실이 낮아도 정확도가 낮을 수 있고, 그 반대도 가능합니다. 둘 다 봐야 합니다. 로그 손실은 "확률이 잘 보정되었는가", 정확도는 "최종 분류가 맞는가"를 봅니다.
💡 다중 클래스에서는 각 클래스의 확률을 모두 사용합니다. y_prob이 shape (n_samples, n_classes)인 2D 배열이어야 하고, 각 행의 합이 1이어야 합니다.
💡 딥러닝에서는 Binary Cross-Entropy(이진 분류) 또는 Categorical Cross-Entropy(다중 클래스)라는 이름으로 손실 함수로 사용됩니다. 본질은 로그 손실과 같습니다.
💡 확률 보정(Probability Calibration)이 필요하다면 CalibratedClassifierCV를 사용하세요. 모델의 예측 확률을 실제 빈도에 맞게 조정해줍니다.
8. 교차 검증 - 신뢰할 수 있는 성능 평가
시작하며
여러분이 모델을 훈련하고 테스트 데이터에서 95% 정확도를 얻었습니다. 그런데 실제 서비스에 올렸더니 85%밖에 안 나옵니다.
무슨 일이 벌어진 걸까요? 이런 문제는 실제 프로젝트에서 매우 흔하게 발생합니다.
테스트 데이터가 운 좋게 쉬웠거나, 훈련 데이터와 너무 비슷했거나, 또는 단순히 샘플 수가 적어서 우연히 좋은 결과가 나온 것일 수 있습니다. 한 번의 평가만으로는 모델의 진짜 성능을 알 수 없죠.
바로 이럴 때 필요한 것이 교차 검증(Cross-Validation)입니다. 데이터를 여러 번 다르게 나누어 평가를 반복함으로써, 운에 좌우되지 않는 신뢰할 수 있는 성능 추정치를 얻습니다.
개요
간단히 말해서, 교차 검증은 데이터를 K개의 폴드(fold)로 나누고, 각 폴드를 한 번씩 테스트 세트로 사용하며 나머지로 훈련하는 방법입니다. 가장 흔한 K-Fold CV에서 K=5라면, 5번 훈련/평가를 반복하고 결과를 평균냅니다.
왜 이것이 필요한지 실무 관점에서 설명하자면, 모델 성능의 분산을 파악하고 과적합을 감지하기 위해서입니다. 예를 들어, 5-Fold CV 결과가 [92%, 90%, 93%, 89%, 91%]라면 평균 91%이고 안정적입니다.
하지만 [98%, 75%, 92%, 70%, 95%]라면 평균은 86%이지만 분산이 크므로 모델이 불안정하다는 신호입니다. 기존에는 "테스트 세트에서 90%입니다"라고 단 한 번의 평가만 했다면, 이제는 "5-Fold CV 평균 89% ± 2%입니다"라고 평균과 표준편차를 함께 보고할 수 있습니다.
교차 검증의 핵심 특징은 첫째, 모든 데이터를 훈련과 테스트 양쪽에 활용하고, 둘째, 성능의 평균과 분산을 제공하며, 셋째, 데이터가 적을 때 특히 유용하다는 점입니다. 이러한 특징들이 교차 검증을 모델 평가의 표준 방법으로 만들었습니다.
코드 예제
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
import numpy as np
# 샘플 데이터 (실제로는 여러분의 데이터 사용)
X = np.random.rand(100, 5) # 100개 샘플, 5개 특징
y = np.random.randint(0, 2, 100) # 이진 분류
# 모델 생성
model = RandomForestClassifier(n_estimators=50, random_state=42)
# 5-Fold 교차 검증 (기본)
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"5-Fold CV 정확도: {scores}")
print(f"평균: {scores.mean():.2%} ± {scores.std():.2%}")
# Stratified K-Fold (클래스 비율 유지)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
stratified_scores = cross_val_score(model, X, y, cv=skf, scoring='f1')
print(f"\nStratified 5-Fold F1: {stratified_scores.mean():.2%} ± {stratified_scores.std():.2%}")
설명
이것이 하는 일: 교차 검증은 데이터를 여러 조각으로 나누고 각 조각을 한 번씩 테스트용으로 사용하며 모델을 평가합니다. 마치 "시험을 5번 보되, 매번 다른 문제를 테스트 문제로 쓰는 것"과 비슷합니다.
첫 번째로, 데이터를 K개의 폴드로 나눕니다. K=5라면 100개 데이터를 20개씩 5개 그룹으로 나누는 거죠.
StratifiedKFold를 쓰면 각 폴드에 클래스 비율이 동일하게 유지됩니다(예: 양성 30%, 음성 70%). 그 다음으로, K번 반복합니다.
첫 번째 반복에서는 1번 폴드를 테스트, 나머지를 훈련용으로 씁니다. 두 번째 반복에서는 2번 폴드를 테스트, 나머지를 훈련용으로 씁니다.
이런 식으로 5번 반복하면 모든 데이터가 정확히 한 번씩 테스트에 사용됩니다. 마지막으로, K개의 점수를 얻습니다.
[0.89, 0.91, 0.88, 0.92, 0.90] 이런 식이죠. 이것의 평균(0.90)이 모델의 예상 성능이고, 표준편차(0.015)가 안정성을 나타냅니다.
표준편차가 크다면 데이터 분할에 따라 성능이 크게 달라진다는 뜻이므로 모델이나 데이터에 문제가 있을 수 있습니다. 여러분이 이 코드를 사용하면 단 한 번의 운 좋은(또는 나쁜) 결과에 속지 않고 모델의 진짜 성능을 파악할 수 있습니다.
특히 데이터가 적을 때(1000개 미만) 교차 검증은 필수입니다. 또한 하이퍼파라미터 튜닝할 때도 GridSearchCV가 내부적으로 교차 검증을 사용하여 최적 파라미터를 찾습니다.
실전 팁
💡 K 값 선택: 보통 K=5 또는 K=10을 씁니다. K가 클수록 정확하지만 느립니다. K=n(데이터 개수)인 LOOCV(Leave-One-Out)는 데이터가 매우 적을 때만 씁니다.
💡 클래스 불균형 데이터에서는 반드시 StratifiedKFold를 쓰세요. 일반 KFold는 특정 폴드에 양성이 몰릴 수 있지만, Stratified는 모든 폴드에 클래스 비율을 동일하게 유지합니다.
💡 시계열 데이터에서는 TimeSeriesSplit을 쓰세요. 과거 데이터로 훈련하고 미래 데이터로 테스트하여 실제 상황을 반영합니다. 일반 KFold는 미래로 훈련하고 과거로 테스트할 수 있어서 부적절합니다.
💡 scoring 파라미터로 평가 지표를 선택할 수 있습니다. 'accuracy', 'f1', 'roc_auc', 'precision', 'recall' 등 모든 sklearn 지표를 쓸 수 있습니다.
💡 여러 지표를 한 번에 평가하려면 cross_validate를 쓰세요. scoring=['accuracy', 'f1', 'roc_auc']처럼 리스트를 주면 모든 지표의 CV 결과를 딕셔너리로 반환합니다.
9. 혼동 행렬 시각화 - 성능을 한눈에 파악
시작하며
여러분이 다중 클래스 분류 모델(예: 10가지 동물 분류)을 만들었는데, 정확도가 85%입니다. 그런데 어떤 동물들끼리 자주 헷갈리는지, 어떤 클래스가 가장 어려운지 알고 싶습니다.
숫자만 보면 파악하기 어렵죠. 이런 문제는 실제로 모델을 개선할 때 매우 중요합니다.
예를 들어 고양이를 개로 분류하는 실수가 많다면 그 두 클래스를 구분하는 특징을 더 강화해야 합니다. 하지만 10x10 혼동 행렬을 숫자로만 보면 패턴을 찾기 어렵습니다.
바로 이럴 때 필요한 것이 혼동 행렬 시각화입니다. 색상과 크기로 표현하면 어디서 실수가 많은지 직관적으로 파악할 수 있습니다.
개요
간단히 말해서, 혼동 행렬 시각화는 숫자로 된 혼동 행렬을 히트맵(heatmap)으로 그리는 것입니다. 값이 클수록 진한 색으로 표시되어, 많이 발생하는 예측 패턴을 한눈에 볼 수 있죠.
왜 이것이 필요한지 실무 관점에서 설명하자면, 모델의 약점을 빠르게 찾아내기 위해서입니다. 예를 들어, 의료 영상 분류에서 "양성 종양"을 "악성 종양"으로 분류하는 실수는 치명적이지만, "정상"을 "양성 종양"으로 분류하는 실수는 추가 검사만 하면 됩니다.
시각화하면 이런 치명적 실수가 어디서 일어나는지 즉시 보입니다. 기존에는 100개 숫자가 나열된 10x10 행렬을 보며 패턴을 찾으려 애썼다면, 이제는 색깔 패턴만 보고 "아, 3번과 8번 클래스를 자주 헷갈리는구나"를 즉시 알 수 있습니다.
혼동 행렬 시각화의 핵심 특징은 첫째, 복잡한 숫자를 직관적인 색상으로 변환하고, 둘째, 다중 클래스에서 특히 유용하며, 셋째, 팀원들과 결과를 공유할 때 이해가 빠르다는 점입니다. 이러한 특징들이 시각화를 실무 보고서의 필수 요소로 만들었습니다.
코드 예제
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
import numpy as np
# 실제 정답과 예측 (다중 클래스 예제)
y_true = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]
y_pred = [0, 2, 2, 0, 1, 2, 0, 2, 1, 0, 1, 1, 0, 1, 2]
class_names = ['고양이', '개', '새']
# 혼동 행렬 계산
cm = confusion_matrix(y_true, y_pred)
# 시각화 (방법 1: sklearn 내장)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=class_names)
disp.plot(cmap='Blues', values_format='d')
plt.title('혼동 행렬 - 동물 분류')
plt.show()
# 시각화 (방법 2: seaborn으로 더 예쁘게)
import seaborn as sns
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='YlOrRd',
xticklabels=class_names, yticklabels=class_names)
plt.ylabel('실제 클래스')
plt.xlabel('예측 클래스')
plt.title('혼동 행렬 히트맵')
plt.show()
설명
이것이 하는 일: 혼동 행렬 시각화는 숫자 테이블을 색깔 있는 그림으로 변환합니다. 마치 "성적표를 숫자 대신 A/B/C 등급과 색깔로 표시"하는 것처럼, 큰 숫자는 진한 색, 작은 숫자는 옅은 색으로 표시합니다.
첫 번째로, confusion_matrix()가 예측 결과를 집계합니다. 위 예제에서 고양이(0)를 고양이로 예측한 게 5번, 개(1)를 새(2)로 예측한 게 2번 이런 식으로 모든 조합을 셉니다.
결과는 3x3 행렬(클래스가 3개니까)입니다. 그 다음으로, 이 행렬을 히트맵으로 그립니다.
ConfusionMatrixDisplay는 sklearn의 간편한 방법이고, seaborn.heatmap은 더 많은 커스터마이징을 제공합니다. annot=True는 각 셀에 숫자를 표시하고, fmt='d'는 정수로 표시하며, cmap='YlOrRd'는 노란색에서 빨간색으로 변하는 색상표를 씁니다.
마지막으로, 그림을 해석합니다. 대각선(왼쪽 위→오른쪽 아래)은 정확히 맞힌 것들이므로 진해야 좋습니다.
대각선 밖의 진한 셀이 있다면 그것이 자주 하는 실수입니다. 예를 들어 (1, 2) 위치가 진하다면 "개를 새로 자주 잘못 예측한다"는 뜻이므로, 개와 새를 구분하는 특징을 개선해야 합니다.
여러분이 이 코드를 사용하면 모델의 문제점을 회의 5분 만에 팀원들과 공유할 수 있습니다. "여기 보세요, 이 빨간 부분이 문제입니다"라고 화면을 가리키면 됩니다.
특히 10개 이상의 클래스가 있는 복잡한 문제에서 시각화 없이는 패턴을 찾는 게 거의 불가능합니다.
실전 팁
💡 정규화된 혼동 행렬을 보려면 confusion_matrix에 normalize='true' 옵션을 주세요. 각 행의 합이 1이 되어 "실제 고양이 중 몇 %를 맞혔는가"를 볼 수 있습니다.
💡 클래스가 많을 때(20개 이상)는 plt.figure(figsize=(15, 12))로 그림 크기를 키우세요. 안 그러면 글자가 겹쳐서 읽을 수 없습니다.
💡 cmap 색상표 선택이 중요합니다. 'Blues'는 파란색 계열, 'YlOrRd'는 노랑→주황→빨강, 'viridis'는 보라→초록→노랑입니다. 발표 자료에는 'YlOrRd'가 가장 눈에 잘 띕니다.
💡 실제 클래스와 예측 클래스의 순서가 중요합니다. y축이 실제, x축이 예측입니다. (i, j) 셀은 "실제 i를 j로 예측한 개수"를 의미합니다.
💡 불균형 데이터에서는 절대 개수보다 비율이 중요합니다. normalize='true'로 정규화하면 "실제 양성 100개 중 95개를 맞혔다(95%)"처럼 해석할 수 있습니다.
10. 분류 리포트 - 모든 지표를 한눈에
시작하며
여러분이 모델 평가를 하는데, 정밀도를 따로 계산하고, 재현율도 따로 계산하고, F1도 따로 계산합니다. 클래스가 5개라면 각 지표를 5번씩 계산해야 하죠.
너무 번거롭습니다. 이런 문제는 실제로 시간 낭비이고 실수할 가능성도 높입니다.
특히 다중 클래스 분류에서 클래스별로 정밀도, 재현율, F1을 각각 계산하려면 코드가 복잡해집니다. 바로 이럴 때 필요한 것이 분류 리포트(Classification Report)입니다.
한 줄의 코드로 모든 주요 지표를 클래스별로 계산하고 예쁜 표로 출력해줍니다.
개요
간단히 말해서, 분류 리포트는 각 클래스의 정밀도, 재현율, F1 점수, 그리고 샘플 수를 자동으로 계산하여 테이블 형태로 보여줍니다. 추가로 전체 평균(macro, weighted)도 제공합니다.
왜 이것이 필요한지 실무 관점에서 설명하자면, 시간 절약과 실수 방지 때문입니다. 하나의 함수 호출로 모든 정보를 얻을 수 있고, 클래스별 성능 차이를 즉시 비교할 수 있습니다.
예를 들어 "클래스 0은 F1 90%인데 클래스 2는 60%네? 클래스 2의 데이터가 부족한가?"라는 인사이트를 즉시 얻습니다.
기존에는 각 지표를 따로따로 계산하고 엑셀에 정리했다면, 이제는 classification_report() 한 줄로 모든 지표가 정리된 표를 얻을 수 있습니다. 분류 리포트의 핵심 특징은 첫째, 모든 주요 지표를 한 번에 제공하고, 둘째, 클래스별 성능을 쉽게 비교할 수 있으며, 셋째, output_dict=True로 딕셔너리 형태로도 받을 수 있다는 점입니다.
이러한 특징들이 분류 리포트를 모델 평가의 첫 단계로 만들었습니다.
코드 예제
from sklearn.metrics import classification_report
import numpy as np
# 실제 정답과 예측 (다중 클래스)
y_true = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]
y_pred = [0, 2, 2, 0, 1, 2, 0, 2, 1, 0, 1, 1, 0, 1, 2, 0, 2, 2]
class_names = ['고양이', '개', '새']
# 분류 리포트 출력 (텍스트 형태)
print("=== 분류 리포트 ===")
print(classification_report(y_true, y_pred, target_names=class_names))
# 분류 리포트 받기 (딕셔너리 형태)
report_dict = classification_report(y_true, y_pred,
target_names=class_names,
output_dict=True)
# 특정 클래스의 F1 점수 추출
cat_f1 = report_dict['고양이']['f1-score']
print(f"\n고양이 클래스의 F1 점수: {cat_f1:.2%}")
# 전체 평균 지표 확인
print(f"Macro 평균 F1: {report_dict['macro avg']['f1-score']:.2%}")
print(f"Weighted 평균 F1: {report_dict['weighted avg']['f1-score']:.2%}")
설명
이것이 하는 일: 분류 리포트는 각 클래스에 대해 정밀도, 재현율, F1 점수를 자동으로 계산하고 깔끔한 표로 정리합니다. 마치 "학생별 과목 성적과 평균을 자동으로 계산해주는 프로그램"과 비슷합니다.
첫 번째로, 각 클래스에 대해 TP, FP, FN, TN을 계산합니다. 고양이 클래스의 경우 "고양이를 고양이로(TP)", "고양이가 아닌데 고양이로(FP)", "고양이인데 다른 걸로(FN)" 이런 식으로 세죠.
그 다음으로, 이 값들을 사용해서 정밀도(TP/(TP+FP)), 재현율(TP/(TP+FN)), F1(2·정밀도·재현율/(정밀도+재현율))을 계산합니다. 이 과정을 모든 클래스에 대해 반복합니다.
support는 각 클래스의 실제 샘플 수입니다. 마지막으로, 전체 평균을 계산합니다.
macro avg는 각 클래스의 지표를 단순 평균한 것이고, weighted avg는 샘플 수에 비례하여 가중 평균한 것입니다. 클래스 불균형이 있다면 weighted avg가 더 의미 있습니다.
여러분이 이 코드를 사용하면 5초 만에 모델의 전체 성능을 파악할 수 있습니다. 어떤 클래스가 어려운지(F1이 낮은지), 어떤 클래스의 데이터가 부족한지(support가 작은지), 정밀도와 재현율 중 어느 것이 문제인지 즉시 보입니다.
모델을 처음 평가할 때 항상 제일 먼저 분류 리포트를 출력하세요.
실전 팁
💡 target_names를 지정하면 0, 1, 2 대신 의미 있는 이름이 출력됩니다. 보고서에 넣을 때 필수입니다.
💡 output_dict=True로 딕셔너리를 받으면 pandas DataFrame으로 변환할 수 있습니다. pd.DataFrame(report_dict).transpose()하면 예쁜 표가 됩니다.
💡 zero_division 파라미터로 0으로 나누기 경고를 제어할 수 있습니다. zero_division=0으로 설정하면 TP=FP=0인 경우 정밀도를 0으로 처리합니다.
💡 macro avg는 모든 클래스를 동등하게, weighted avg는 많은 클래스에 더 큰 가중치를 줍니다. 클래스 불균형이 심하면 두 값의 차이가 크게 납니다.
💡 이진 분류에서도 사용할 수 있습니다. target_names=['음성', '양성']으로 지정하면 양쪽 클래스의 지표를 모두 볼 수 있어서 유용합니다.