정확도만 보면 놓치는 오류: 혼동행렬과 precision·recall 계산

이 카테고리 글 목록

전체글 보기

문서 분류의 정확도가 90%여도 사람 검토가 필요한 문서를 하나도 찾지 못할 수 있다. 검토 대상이 전체의 10%인데 모든 문서를 ‘검토 불필요’로 보내면 나머지 90%는 맞히기 때문이다. 정확도만 보기 전에 실제 검토 대상과 분류 결과를 네 칸으로 나누어야 한다.

아래 문서 100건과 분류 결과는 설명용 합성 자료다. 실제 OCR·AI 모델의 성능을 측정한 결과가 아니다. 문서마다 정답과 예측이 하나씩 있고, 두 값 모두 ‘검토 필요’ 또는 ‘검토 불필요’로 빠짐없이 판정됐다고 가정한다. 보류·판정 불명 문서가 있다면 따로 집계하고 이 분모에 조용히 섞지 않는다.

먼저 양성을 ‘검토 필요’로 고정한다

여기서 양성은 좋은 결과라는 뜻이 아니라 관심 대상으로 정한 클래스다. 원문과 업무 기준을 대조해 사람이 검토해야 한다고 판정한 문서를 실제 양성, 분류기가 검토 대상으로 보낸 문서를 예측 양성이라고 부른다. 사람이 붙인 정답도 모호하거나 틀릴 수 있으므로 판정 기준과 근거를 함께 남긴다.

합성 문서 100건: 행은 실제 판정, 열은 분류기의 예측
실제 판정 예측: 검토 필요 예측: 검토 불필요 합계
실제: 검토 필요 TP 8건: 제대로 보냄 FN 2건: 놓침 10건
실제: 검토 불필요 FP 18건: 불필요하게 보냄 TN 72건: 제대로 제외 90건
합계 26건 74건 100건

FN은 필요한 검토를 놓친 경우이고, FP는 불필요한 검토 부담을 만든 경우다. 이 업무에서는 FN 두 건이 위험 문서를 자동 통과시켰을 가능성이 있으므로 그 내용을 확인해야 한다. 모든 FN의 피해가 같거나, 모든 FP가 같은 시간을 쓰게 한다는 가정은 하지 않는다.

precision과 recall은 서로 다른 분모를 쓴다

scikit-learn의 precision 문서는 precision을 TP/(TP+FP), 연결된 recall 설명은 TP/(TP+FN)으로 정의한다. 이 예에 적용하면 다음과 같다.

같은 네 칸으로 계산한 서로 다른 질문
지표 묻는 질문 합성 계산
정확도 accuracy 전체 중 맞힌 비율은? (8+72)/100 = 80%
정밀도 precision 검토 대상으로 보낸 것 중 실제 대상은? 8/(8+18) ≈ 30.77%
재현율 recall 실제 검토 대상 중 찾아낸 것은? 8/(8+2) = 80%

재현율 80%는 검토가 필요한 열 건 중 여덟 건을 찾았다는 뜻이다. 정밀도 약 30.77%는 검토함에 들어온 26건 중 실제 대상이 여덟 건이라는 뜻이다. 두 비율이 다른 것은 계산 오류가 아니라 분모가 다른 결과다. 검토 대기량은 별도로 26건이며, 이 중 FP는 18건이다.

정확도가 더 높은 ‘전부 제외’와 비교한다

같은 100건을 전부 검토 불필요로 분류하면 TP=0, FN=10, FP=0, TN=90이다. 정확도는 90%로 올라가지만 재현율은 0%다. 실제 검토 대상 열 건을 모두 놓쳤다. 양성 비중이 낮은 업무에서는 이렇게 전체 정확도가 관심 오류를 가릴 수 있다.

이때 precision은 0/0으로 정의되지 않는다. 검토 대상으로 보낸 문서가 없어서 그 안의 적중 비율을 계산할 수 없기 때문이다. scikit-learn은 zero_division 설정으로 이런 경우의 반환을 정한다. 보고서에는 사용한 설정을 밝혀야 하며, 아래 직접 작성한 코드는 정의되지 않은 값을 null로 남긴다. ‘정밀도 0%를 관측했다’와 구별하기 위한 선택이다.

본문 코드를 복사해 분모와 경계값을 확인한다

다음 JavaScript는 별도 패키지 없이 실행한다. Windows의 Node.js v24.15.0에서 이 HTML의 코드 텍스트를 추출·entity 해제해 검산했다. 출력의 비율은 0~1 단위이며 소수 넷째 자리까지 반올림한다.

function metrics({tp, fn, fp, tn}) {
  const counts = [tp, fn, fp, tn];
  if (!counts.every(x => Number.isSafeInteger(x) && x >= 0)) {
    throw new Error("Counts must be nonnegative safe integers");
  }
  const total = tp + fn + fp + tn;
  if (!Number.isSafeInteger(total)) throw new Error("Total is too large");
  const ratio = (a, b) => b === 0 ? null : Number((a / b).toFixed(4));
  return {
    total,
    accuracy: ratio(tp + tn, total),
    precision: ratio(tp, tp + fp),
    recall: ratio(tp, tp + fn)
  };
}
const cases = {
  reviewRule: {tp: 8, fn: 2, fp: 18, tn: 72},
  allNegative: {tp: 0, fn: 10, fp: 0, tn: 90},
  noActualPositive: {tp: 0, fn: 0, fp: 2, tn: 8},
  empty: {tp: 0, fn: 0, fp: 0, tn: 0}
};
for (const [name, counts] of Object.entries(cases)) {
  console.log(JSON.stringify({name, ...metrics(counts)}));
}
{"name":"reviewRule","total":100,"accuracy":0.8,"precision":0.3077,"recall":0.8}
{"name":"allNegative","total":100,"accuracy":0.9,"precision":null,"recall":0}
{"name":"noActualPositive","total":10,"accuracy":0.8,"precision":0,"recall":null}
{"name":"empty","total":0,"accuracy":null,"precision":null,"recall":null}

실제 양성이 없는 세 번째 사례에서는 recall이 정의되지 않는다. 반면 예측 양성 두 건은 모두 틀렸으므로 precision은 0이다. 문서가 전혀 없는 마지막 사례에서는 정확도도 계산할 수 없다. 누락된 측정을 0으로 채우면 ‘평가하지 못함’과 ‘평가해 모두 놓침’을 혼동하게 된다.

지표와 함께 놓친 문서·검토량을 읽는다

검토 기준을 완화하거나 강화했을 때 무엇을 바꿀지는 놓친 문서의 중요도와 사람이 감당할 검토량에 달려 있다. 한 지표를 올리는 것만을 보편적인 합격 조건으로 삼지 않는다. 기준·평가 묶음·양성 정의를 고정하고 TP/FN/FP/TN 원래 건수와 결과를 함께 비교한다. 이 100건의 비율이 다른 기간이나 다른 종류의 문서에도 그대로 적용된다는 보장은 없다.

OCR 사람 검토 전환 기준은 어떤 항목과 위험을 검토 대상으로 삼을지 설명한다. 이 글은 그 기준으로 만든 분류를 네 칸으로 계산하는 데 초점을 둔다. 기준을 바꿀 때는 같은 입력으로 회귀 평가하는 절차에 따라 중요한 새 누락도 따로 살핀다.

자주 묻는 질문

양성을 ‘검토 불필요’로 정해도 되나요?

가능하지만 관심 클래스와 지표의 의미가 바뀐다. 같은 표에서 양성·음성 이름만 바꾸고 이전 precision·recall과 같은 의미로 비교하면 안 된다. 보고서에 양성의 업무 정의를 먼저 적는다.

정밀도가 낮으면 분류기를 버려야 하나요?

이 숫자만으로 결정할 수 없다. 놓치는 오류의 피해, 검토 처리량, 다른 기준의 결과를 함께 본다. 위 합성 예는 실제 시스템의 채택 여부를 검증하지 않았다.

보류 결과도 음성으로 세면 되나요?

보류가 ‘검토 불필요’와 같은 업무 결과인지부터 정해야 한다. 보류·실패·판정 불명 건수를 따로 공개하고 이진 평가에 포함한 범위를 밝히면 분모가 바뀌는 문제를 줄일 수 있다.

이 카테고리의 다른 글

전체글 보기

“정확도만 보면 놓치는 오류: 혼동행렬과 precision·recall 계산”에 대한 2개의 생각

댓글 남기기