확률 0.8은 정말 80%일까? 예측 확률의 보정과 Brier score

이 카테고리 글 목록

전체글 보기

분류기가 어떤 문서에 ‘검토 필요 확률 0.8’을 내놓았다고 해서 그 문서의 판정이 보장되는 것은 아니다. 확률이 잘 보정돼 있다면 비슷한 0.8을 받은 문서를 모았을 때 실제 검토 대상의 비율이 약 80%인지를 살펴볼 수 있다. 개별 문서 하나의 정답 여부와 여러 문서에서의 확률 보정은 다른 질문이다.

이 글은 양성을 ‘원문과 기준에 따라 검토가 필요하다고 확정한 문서’로 정한다. 아래 15건은 계산을 설명하려고 만든 합성 자료이며, 실제 OCR·AI 모델을 실행하거나 확률 보정기를 학습한 결과가 아니다. 공급자가 제공한 임의의 confidence 점수를 이 양성 확률로 간주하지 않는다.

예측 확률과 실제 양성 비율을 같은 구간에서 비교한다

scikit-learn의 확률 보정 안내는 비슷한 예측 확률을 받은 사례에서 실제 양성 빈도를 비교한다. 구간의 가로값은 그 안의 평균 예측 확률이고, 세로값은 실제 양성 비율이다. 구간 중앙값을 평균 확률 대신 쓰거나, 문서 수가 다른 구간을 같은 크기의 증거로 해석하지 않도록 주의한다.

합성 문서 15건을 세 확률 구간으로 나눈 계산
예측 확률 구간 문서 수 평균 예측 확률 실제 양성 수·비율
0 이상 0.4 미만 5건 0.2 1건 / 5건 = 0.2
0.4 이상 0.7 미만 5건 0.5 2건 / 5건 = 0.4
0.7 이상 1 이하 5건 0.8 3건 / 5건 = 0.6

마지막 구간에서는 평균 0.8을 예측했지만 실제 양성 비율은 0.6이다. 이 묶음에서는 예측 확률이 관측 빈도보다 높았다. 다만 다섯 건 중 한 건의 판정만 달라져도 비율이 0.2만큼 바뀐다. 작은 표 하나로 모델이 모든 상황에서 과신한다고 확정할 수 없다. 구간 경계·건수·자료의 기간과 종류를 함께 제시하고, 비어 있는 구간은 ‘관측 없음’으로 남긴다.

Brier score는 확률과 결과 사이의 제곱오차다

이 글은 이진 분류에서 흔히 쓰는 0~1 범위의 Brier score를 사용한다. 실제 결과를 y=0 또는 1, 양성 예측 확률을 p라고 하면 BS = Σ(p−y)² / N이다. 이는 공식 Brier score 문서의 이진 정의와 같다. 여러 클래스의 오차를 모두 더하는 0~2 범위 정의와 수치를 섞어 비교하지 않는다.

위 자료에서 p=0.2인 다섯 건의 제곱오차 합은 0.8, p=0.5인 다섯 건은 1.25, p=0.8인 다섯 건은 1.4다. 따라서 BS=(0.8+1.25+1.4)/15=0.23이다. 동일한 평가 자료와 점수 정의에서는 작을수록 확률 예측의 제곱오차가 작다.

그러나 Brier는 보정만 측정하지 않는다. 공식 보정 안내는 확률의 신뢰성, 사례를 구분하는 정도, 자료 자체의 불확실성이 함께 반영된다고 설명한다. 낮은 Brier 하나로 ‘확률이 더 잘 보정됐다’고 결론 내리기보다 구간별 표도 함께 읽는다. 전체 양성 비중이나 평가 묶음이 달라진 두 수치를 그대로 모델 순위로 쓰는 것도 피한다.

본문 코드를 복사해 구간·점수·빈 구간을 검산한다

다음 코드는 (예측 양성 확률, 실제 이진 결과) 쌍을 받는다. 경계 0.4와 0.7은 오른쪽 구간에, 확률 1은 마지막 구간에 포함한다. 빈 자료의 점수와 빈 구간의 평균은 null로 출력한다. Windows의 Python 3.14.3에서 본문 HTML의 코드 텍스트를 추출·entity 해제해 실행했으며 외부 패키지나 서비스는 호출하지 않았다.

import json
import math
from bisect import bisect_right

def probability_report(rows):
    edges = (0.0, 0.4, 0.7, 1.0)
    buckets = [[] for _ in range(3)]
    squared_errors = []
    for p, y in rows:
        if (type(p) not in (int, float) or not math.isfinite(p)
                or not 0 <= p <= 1):
            raise ValueError("p must be a finite probability")
        if type(y) is not int or y not in (0, 1):
            raise ValueError("y must be integer 0 or 1")
        j = min(bisect_right(edges, p) - 1, 2)
        buckets[j].append((p, y))
        squared_errors.append((p - y) ** 2)
    bins = []
    for pairs in buckets:
        n = len(pairs)
        bins.append({
            "n": n,
            "mean_p": round(sum(p for p, y in pairs) / n, 6) if n else None,
            "positive_fraction": round(sum(y for p, y in pairs) / n, 6) if n else None
        })
    n = len(squared_errors)
    return {"n": n,
            "brier": round(sum(squared_errors) / n, 6) if n else None,
            "bins": bins}

toy = ([(0.2, y) for y in (1, 0, 0, 0, 0)]
       + [(0.5, y) for y in (1, 1, 0, 0, 0)]
       + [(0.8, y) for y in (1, 1, 1, 0, 0)])
for name, rows in (("toy", toy), ("endpoints", [(0, 0), (1, 1)]),
                   ("empty", [])):
    print(json.dumps({"name": name, **probability_report(rows)}))
{"name": "toy", "n": 15, "brier": 0.23, "bins": [{"n": 5, "mean_p": 0.2, "positive_fraction": 0.2}, {"n": 5, "mean_p": 0.5, "positive_fraction": 0.4}, {"n": 5, "mean_p": 0.8, "positive_fraction": 0.6}]}
{"name": "endpoints", "n": 2, "brier": 0.0, "bins": [{"n": 1, "mean_p": 0.0, "positive_fraction": 0.0}, {"n": 0, "mean_p": null, "positive_fraction": null}, {"n": 1, "mean_p": 1.0, "positive_fraction": 1.0}]}
{"name": "empty", "n": 0, "brier": null, "bins": [{"n": 0, "mean_p": null, "positive_fraction": null}, {"n": 0, "mean_p": null, "positive_fraction": null}, {"n": 0, "mean_p": null, "positive_fraction": null}]}

마지막 두 출력은 경계 처리 확인용이다. 두 건을 맞힌 결과가 모델의 완벽한 보정을 입증하지는 않는다. 이 코드는 확률의 의미와 정답의 신뢰성을 검증하지 않고 입력된 수치만 계산한다. 실제 자료에서는 문서 ID로 확률과 판정을 연결하고 누락·중복·판정 불명 건수를 따로 확인해야 한다.

보정기를 맞출 자료와 마지막 평가 자료를 구별한다

보정기를 학습하려면 원래 분류기를 학습한 자료와 독립적인 예측·판정 자료가 필요하다. 최종 성능을 보고할 자료까지 계속 보며 보정 방법과 구간을 고르면 그 자료가 개발 과정에 쓰인다. 원본·그룹·시간의 관계는 평가 데이터 누수와 분할 기준에 따라 점검하고 마지막 평가 자료를 별도로 둔다. 이 글은 보정 전후 학습 실험이나 신뢰구간을 계산하지 않았다.

확률 보정이 괜찮아도 어느 문서를 자동 통과시킬지는 오류의 영향과 검토 여력에 달려 있다. OCR 사람 검토 전환 기준과 연결해 중요한 항목·원문 품질·복구 가능성을 함께 본다. 보정 수치를 보편적인 자동처리 허가선으로 바꾸지 않는다.

자주 묻는 질문

confidence가 80이면 0.8로 나눠 넣어도 되나요?

그 점수가 어떤 사건의 확률인지 공급자 정의부터 확인해야 한다. 범위가 0~100이라는 이유만으로 나누어도 이 글의 ‘검토 필요 확률’이 되지는 않는다. 임계값 분류의 결과는 혼동행렬과 precision·recall처럼 별도로 평가할 수 있다.

구간을 많이 만들수록 정확한 평가인가요?

더 세분하면 한 구간의 문서 수가 줄고 빈 구간이 생길 수 있다. 경계를 미리 정하고 구간별 건수와 관측 기간을 공개한다. 구간을 바꿨을 때 모양이 어떻게 달라지는지도 확인하되 보기 좋은 결과만 골라 보고하지 않는다.

Brier가 0.23이면 운영에 쓸 만한가요?

이 합성 숫자에는 보편적인 합격 의미가 없다. 같은 자료의 적절한 기준 예측, 확률 구간별 결과, 중요한 오류와 실제 검토 부담을 함께 비교해야 한다. 실제 운영·비용·후속 업무의 성공은 여기서 시험하지 않았다.

이 카테고리의 다른 글

전체글 보기

댓글 남기기