Accuracy 99%가 거짓말일 때 보는 Precision과 Recall
Machine Learning

Accuracy 99%가 거짓말일 때 보는 Precision과 Recall

2026.01.25.
ML 기초39편
  1. 21연쇄 법칙으로 기울기를 되돌리는 역전파
  2. 22활성화 함수는 왜 ReLU로 정착했나
  3. 23SGD에서 Adam까지, 옵티마이저는 무엇을 개선해왔나
  4. 24신경망 학습을 안정시키는 가중치 초기화와 BatchNorm, Dropout
  5. 25Accuracy 99%가 거짓말일 때 보는 Precision과 Recall읽는 중
5 / 8

신용카드 거래 1만 건 중 사기가 100건이라고 하자. 모든 거래를 “정상”이라고 찍는 모델을 만들면 Accuracy는 99%가 나온다. 사기는 한 건도 못 잡았는데 성적표는 훌륭하다.

이게 Accuracy Paradox다. 양성이 소수인 데이터에서 Accuracy는 사실상 다수 클래스를 맞힌 비율이고, 정작 궁금한 소수 클래스의 성능은 그 숫자 뒤에 숨는다. 그리고 현실의 분류 문제는 거의 다 이렇다. 스팸 메일, 질병 진단, 이상 탐지, 제조 불량 검출 전부 양성이 소수다.

Accuracy가 못 쓸 지표라는 말은 아니다. 클래스 비율이 대체로 균형 잡혀 있고 두 방향의 오분류 비용이 비슷하다면 Accuracy로 충분하다. 문제는 그런 조건이 드물다는 것이다.

그래서 필요한 건 “몇 개 맞혔나”가 아니라 “어떤 종류의 실수를 얼마나 했나”다.

혼동 행렬의 네 칸

예측 결과를 실제 클래스와 예측 클래스의 조합으로 갈라 놓은 표가 혼동 행렬(Confusion Matrix)이다. 이진 분류라면 네 칸이 나온다.

예측 → Positive Negative 실제 Positive 실제 Negative TP 진양성 FN 위음성 FP 위양성 TN 진음성 Precision Recall

암 검진으로 읽으면 이렇다. TP는 환자를 환자라고 진단한 것, TN은 정상인을 정상이라고 진단한 것이다. FN은 환자를 정상이라고 돌려보낸 것이고, FP는 정상인에게 암 소견을 준 것이다. 같은 오답이지만 무게가 전혀 다르다. FN은 치료 시기를 놓치게 하고 FP는 추가 검사 비용을 만든다.

주요 지표는 전부 이 네 칸의 조합이다. 중요한 건 각 지표가 표의 어느 방향을 읽느냐다. Precision은 예측 Positive 열을 세로로 읽고, Recall은 실제 Positive 행을 가로로 읽는다. 둘이 TP를 공유하되 분모가 다르다는 것이 이 두 지표를 헷갈리게 만드는 지점이다.

지표 정의 읽는 방향 묻는 것
Accuracy TP+TNTP+TN+FP+FN\frac{TP+TN}{TP+TN+FP+FN} 대각선 대 전체 전체 중 맞힌 비율은
Precision TPTP+FP\frac{TP}{TP+FP} 예측 Positive 열 양성이라 한 것 중 진짜는
Recall TPTP+FN\frac{TP}{TP+FN} 실제 Positive 행 진짜 양성 중 잡은 것은
Specificity TNTN+FP\frac{TN}{TN+FP} 실제 Negative 행 진짜 음성 중 걸러낸 것은

Precision은 모델이 “이건 양성이다”라고 말했을 때 그 말을 얼마나 믿을 수 있는지를 잰다. Recall은 세상에 존재하는 양성 중 몇 퍼센트를 건져 올렸는지를 잰다.

scikit-learn의 혼동 행렬은 위 그림과 축 순서가 다르다

confusion_matrix는 레이블을 오름차순(0, 1)으로 놓기 때문에 행렬이 [[TN, FP], [FN, TP]] 형태로 나온다. 교과서 그림은 보통 Positive를 먼저 놓아서 TP가 좌상단에 오는데, 코드로 뽑으면 TP가 우하단에 있다. 인덱스를 손으로 꺼내 쓸 때 이 순서를 착각하면 Precision 자리에 NPV(음성 예측의 적중률)가, Recall 자리에 Specificity(음성의 재현율)가 들어간 값을 얻게 된다.

from sklearn.metrics import 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]

tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
print(tn, fp, fn, tp)   # 4 1 1 4

정밀도와 재현율은 같은 손잡이의 양 끝

Precision과 Recall은 독립적으로 올릴 수 있는 값이 아니다. 로지스틱 회귀든 부스팅이든 확률을 출력하는 모델은 그 확률을 임계값(threshold)과 비교해서 양성/음성을 정한다. 기본값은 0.5지만 이건 그냥 기본값이다. 임계값을 움직이면 두 지표가 반대 방향으로 움직인다.

음성 샘플 양성 샘플 threshold 0.25 Precision 5/8 · Recall 5/5 양성으로 예측 threshold 0.65 Precision 3/3 · Recall 3/5 양성으로 예측 모델이 출력한 양성 확률

임계값을 0.25로 내리면 양성 다섯 개를 하나도 놓치지 않는다(Recall 100%). 대신 음성 세 개가 딸려 들어와 Precision이 62.5%로 떨어진다. 0.65로 올리면 양성이라 부른 셋이 전부 진짜 양성이지만(Precision 100%), 애매한 위치의 양성 둘을 놓쳐 Recall이 60%가 된다. 두 분포가 완전히 갈라지지 않는 한 이 교환은 피할 수 없다.

그래서 지표를 고르는 일은 사실 어느 쪽 오류를 더 아프게 볼 것인가를 정하는 일이다.

도메인 더 아픈 실수 우선 지표
스팸 필터 FP. 업무 메일이 스팸함으로 사라진다 Precision
상품 추천 FP. 엉뚱한 추천이 이탈을 부른다 Precision
암 조기 검진 FN. 환자를 정상이라며 돌려보낸다 Recall
악성코드 탐지 FN. 감염이 조용히 퍼진다 Recall

임계값을 0에서 1까지 훑으면서 (Recall, Precision) 쌍을 기록하면 PR 곡선(Precision-Recall Curve)이 된다. 곡선이 우상단 꼭짓점에 붙을수록 좋은 모델이고, 이 곡선 아래 면적을 요약한 값이 AP(Average Precision), 흔히 PR-AUC라 부르는 숫자다.

F1이 조화평균인 이유

Precision과 Recall을 한 숫자로 합치고 싶을 때 산술평균을 쓰면 안 된다. Precision 1.0, Recall 0.01인 모델은 양성을 사실상 하나도 못 잡는데 산술평균은 0.505라는 멀쩡한 점수를 준다.

F1은 조화평균을 쓴다.

F1=2⋅P⋅RP+RF_1 = 2 \cdot \frac{P \cdot R}{P + R}

산술평균은 큰 값에 끌려가고 조화평균은 작은 값에 끌려간다. 두 값 중 하나가 0에 가까우면 곱 P⋅RP \cdot R이 먼저 무너지기 때문에 결과도 같이 무너진다. 위 예에서 F1은 2×0.01/1.01=0.022 \times 0.01 / 1.01 = 0.02다.

Precision Recall 산술평균 F1
0.9 0.9 0.90 0.90
0.8 0.6 0.70 0.69
0.6 0.4 0.50 0.48
1.0 0.01 0.505 0.02

두 값이 비슷하면 두 평균이 거의 같고, 벌어질수록 F1이 훨씬 인색해진다.

F1은 Precision과 Recall에 같은 무게를 준다. 한쪽이 더 중요한 상황이라면 FβF_\beta로 기울인다.

Fβ=(1+β2)⋅P⋅Rβ2P+RF_\beta = (1 + \beta^2) \cdot \frac{P \cdot R}{\beta^2 P + R}

β\beta는 Recall을 Precision보다 몇 배 중요하게 볼 것인가를 뜻한다. β=2\beta = 2면 놓치지 않는 쪽에 무게를 싣고(F2F_2, 암 검진), β=0.5\beta = 0.5면 헛경보를 줄이는 쪽에 무게를 싣는다(F0.5F_{0.5}, 스팸 필터). β=1\beta = 1이 F1이다.

from sklearn.metrics import fbeta_score

f2 = fbeta_score(y_true, y_pred, beta=2)      # 놓치지 않는 게 우선
f05 = fbeta_score(y_true, y_pred, beta=0.5)   # 헛경보를 줄이는 게 우선

ROC 곡선과 AUC

ROC(Receiver Operating Characteristic) 곡선도 임계값을 훑으면서 그리지만 세로축과 가로축이 다르다. 세로축은 TPR로 Recall과 같은 값이고, 가로축은 FPR, 즉 실제 음성 중 양성으로 잘못 분류한 비율이다.

TPR=TPTP+FN,FPR=FPFP+TN=1−Specificity\text{TPR} = \frac{TP}{TP+FN}, \qquad \text{FPR} = \frac{FP}{FP+TN} = 1 - \text{Specificity}

임계값 1에서 시작하면 아무것도 양성으로 부르지 않으니 원점 (0, 0)이고, 임계값 0이면 전부 양성으로 부르니 (1, 1)이다. 그 사이를 어떻게 지나가느냐가 모델의 실력이다.

ROC 곡선과 AUC 면적 1.0 0.5 0 0 0.5 1.0 TPR (Recall) FPR (1 - Specificity) AUC 0.85 ROC 곡선 무작위 기준선 (AUC 0.5)

AUC는 이 곡선 아래 면적이다. 무작위로 찍는 분류기는 대각선을 그리므로 AUC가 0.5이고, 완벽한 분류기는 좌상단 꼭짓점을 지나 1이 된다.

AUC에는 깔끔한 확률적 해석이 있다. 양성 샘플 하나와 음성 샘플 하나를 무작위로 뽑았을 때, 모델이 양성 쪽에 더 높은 점수를 줄 확률이 곧 AUC다. AUC 0.85는 그 순서 맞히기를 100번 중 85번 성공한다는 뜻이다. 임계값을 하나로 고정하지 않고 순위 매기는 능력만 보기 때문에, 임계값을 정하기 전 모델끼리 비교하는 데 편하다.

from sklearn.metrics import roc_curve, roc_auc_score

fpr, tpr, thresholds = roc_curve(y_true, y_scores)
auc = roc_auc_score(y_true, y_scores)

불균형에서는 PR 곡선을 봐라

AUC가 임계값에 독립적이라는 장점은 그대로 함정이 되기도 한다. 음성이 압도적으로 많으면 FPR의 분모가 커져서 어지간한 오탐으로는 눈금이 움직이지 않는다.

음성 9,900개, 양성 100개인 사기 탐지를 보자. 모델이 200건을 사기로 지목했고 그중 90건이 진짜였다고 하자.

TPR=90100=0.90,FPR=1109900≈0.011\text{TPR} = \frac{90}{100} = 0.90, \qquad \text{FPR} = \frac{110}{9900} \approx 0.011

ROC 평면에서 (0.011, 0.90)은 좌상단에 바짝 붙은 훌륭한 점이다. 그런데 같은 결과를 Precision으로 보면 90/200=0.4590 / 200 = 0.45다. 사기라고 경보를 울린 것의 절반 이상이 헛것이다. 조사 인력을 배정해야 하는 현장에서는 이쪽이 진실에 가깝다.

차이의 원인은 하나다. FPR은 분모에 TN을 쓰고 Precision은 쓰지 않는다. 음성이 많을수록 TN이 FP를 희석해서 ROC 곡선을 낙관적으로 만든다.

기준 ROC 곡선 PR 곡선
축 FPR 대 TPR Recall 대 Precision
TN 사용 쓴다(FPR의 분모) 쓰지 않는다
무작위 기준선 대각선, AUC 0.5 양성 비율 높이의 수평선
불균형 데이터 낙관적으로 보인다 성능 저하가 그대로 드러난다

무작위 기준선이 다르다는 점도 실무에서 자주 걸린다. ROC-AUC 0.5는 어떤 데이터에서도 무작위지만, PR-AUC의 기준선은 양성 비율 그 자체다. 양성이 1%인 데이터에서 PR-AUC 0.4는 기준선 0.01의 40배이므로 나쁘지 않은 성적이다. 절대값만 보고 낮다고 판단하면 안 된다.

from sklearn.metrics import roc_auc_score, average_precision_score

print(f"ROC-AUC: {roc_auc_score(y_true, y_scores):.3f}")
print(f"PR-AUC : {average_precision_score(y_true, y_scores):.3f}")

다중 클래스에서 평균 내는 세 가지 방법

클래스가 셋 이상이면 Precision, Recall, F1은 클래스마다 하나씩 나온다. 이걸 대표값 하나로 합치는 방식이 세 가지다.

방식 계산 성격
Macro 클래스별 지표를 단순 평균 클래스마다 같은 무게. 샘플 열 개짜리 소수 클래스가 점수를 크게 흔든다
Micro 전체 TP, FP, FN을 먼저 합산한 뒤 계산 샘플마다 같은 무게. 샘플이 많은 클래스가 점수를 거의 정한다
Weighted 클래스별 지표를 샘플 수로 가중 평균 Micro와 Macro 사이. 다수 클래스 쪽으로 기운다

샘플 하나가 정확히 한 레이블을 갖는 보통의 다중 분류에서는 Micro Precision, Micro Recall, Micro F1이 전부 Accuracy와 같은 값이 된다. 틀린 예측 하나는 자기가 지목한 클래스에 FP를, 정답 클래스에 FN을 하나씩 남기므로 ∑FP=∑FN\sum FP = \sum FN이고, 그래서 Micro Precision과 Micro Recall이 같아진다. 두 값이 같으면 조화평균인 Micro F1도 같은 값이다. 게다가 샘플마다 예측이 정확히 하나씩이니 ∑TP+∑FP=n\sum TP + \sum FP = n이고, 결국 ∑TP/n\sum TP / n, 곧 Accuracy다. 단일 레이블 문제에서 Micro F1을 보고하는 건 Accuracy를 다른 이름으로 부르는 셈이다.

소수 클래스가 중요한 문제라면 Macro F1을 본다. 클래스마다 같은 표를 주기 때문에 소수 클래스가 망가지면 점수가 바로 내려간다.

지표 고르기

상황 지표
클래스가 대체로 균형이고 두 오류 비용이 비슷하다 Accuracy
헛경보가 비싸다 Precision, F0.5F_{0.5}
놓치는 게 비싸다 Recall, F2F_2
둘 다 챙겨야 한다 F1
양성이 극소수다 PR-AUC
임계값을 정하기 전 모델끼리 비교한다 ROC-AUC
클래스가 여럿이고 소수 클래스가 중요하다 Macro F1

주 지표 하나에 보조 지표 두엇을 붙여라

의사결정 기준은 하나여야 실험이 굴러가지만, 보고에는 반대편 지표를 반드시 같이 적는다. 암 검진 모델의 주 지표를 Recall로 잡았다면 Precision이 어디까지 내려갔는지 함께 본다. 헛경보가 너무 잦으면 의료진이 알림 자체를 무시하기 시작해서, 장부상의 Recall이 현장에서 실현되지 않는다.

불균형 데이터라면 보고서에 양성 비율을 같이 적는 습관도 도움이 된다. 그 숫자가 없으면 PR-AUC를 읽는 사람이 기준선을 모르고, Accuracy를 읽는 사람은 99%가 어디서 왔는지 모른다.

마치며

분류 지표를 고르는 일은 통계 문제가 아니라 비용 문제다. 혼동 행렬의 네 칸 중 FP와 FN 가운데 무엇이 더 비싼지를 먼저 정하면 지표는 거의 자동으로 따라온다. 그 판단 없이 F1이나 AUC를 습관처럼 집으면, 실제로는 아무도 원하지 않는 균형점에서 모델을 최적화하게 된다.

임계값이 하이퍼파라미터라는 사실도 같이 기억할 만하다. 학습이 끝난 모델의 성능은 고정된 하나의 숫자가 아니라 임계값을 따라 움직이는 곡선이고, Precision과 Recall은 그 곡선 위의 서로 다른 두 좌표다. 배포 직전에 검증 데이터로 임계값을 다시 고르는 것만으로 재학습 없이 원하는 균형을 만들 수 있는 경우가 많다.

다음 글에서는 연속값을 예측하는 회귀 모델을 다룬다. 맞았다와 틀렸다로 나눌 수 없는 예측을 어떤 자로 재는지가 주제다.

함께 보면 좋은 글

  • 로지스틱 회귀 : 임계값과 비교할 확률 출력이 어디서 나오는지
  • 결정 경계 : 임계값을 움직이면 경계가 어느 쪽으로 밀리는지
  • 교차 검증 : 이 지표들을 흔들리지 않게 재는 방법

참고자료

Thank You for Visiting My Blog, Have a Good Day 😆