리서치

PAPERS / 11번째 글

LLM 심판 일치도 재계산: 무승부를 어떻게 세느냐가 21%p를 만든다

MT-Bench 인간 판정 3,355건으로 GPT-4 심판의 일치도를 직접 계산했다. 논문의 85%는 재현되지만 같은 데이터가 무승부 처리에 따라 66.17%에서 88.40%까지 움직인다. 우연 보정을 걸면 카파는 0.4827이다.

PALDYN Team22 MIN READ

「GPT-4 심판이 사람과 85% 일치한다」는 문장은 LLM 평가 글에서 가장 자주 인용되는 숫자 중 하나다. 출처는 Zheng 등의 「Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena」(arXiv 2306.05685v4, 2023)이고, 그 논문은 판정 데이터를 통째로 공개해 두었다. 그래서 이 주장은 API 호출 한 번 없이 다시 계산할 수 있다.

이 글이 재현하려는 주장은 하나다. "GPT-4 심판과 사람의 일치도는 85%로, 사람끼리의 일치도 81%보다 높다."

결론부터 적으면 셋이다.

  • 재현된다. 논문과 같은 방식으로 세면 1턴 85.9%, 2턴 85.3%로 논문의 85%와 1%p 안쪽이다. 사람끼리는 82.0%·83.4%로 논문의 81%·82%와 맞는다.
  • 그런데 그 85%는 무승부를 버리고 센 값이다. 같은 데이터를 무승부까지 세면 66.17%가 되고, 무승부를 절반만 인정하면 78.52%가 된다. 바뀌는 폭이 21%p인데, 논문이 다투는 차이(85% 대 81%)는 4%p다.
  • 우연 보정을 걸면 「거의 완벽」이 아니다. 코헨 카파는 무승부를 버렸을 때 0.7120, 무승부를 값으로 셌을 때 0.4827이다. 논문은 카파를 보고하지 않는다.

심판 파이프라인을 어떻게 짜는지는 LLM 평가 파이프라인: 자동화된 품질 보장이, 사람에게 선호를 받아 내는 설계는 선호 데이터 수집 — 점수를 매기지 말고 둘 중 하나를 고르게 한다가 맡는다. 이 글은 공개된 판정 3,355건을 다시 세는 일만 맡는다.

판정 데이터

두 갈래

lmsys/mt_bench_human_judgments는 두 갈래로 되어 있다.

갈래 행 수 무엇인가
human 3,355 사람 65명(전문가 58 + 저자 7)이 매긴 판정
gpt4_pair 2,400 같은 응답 쌍에 대한 GPT-4 심판의 판정

두 갈래 모두 문항 80개, 모델 6개(alpaca-13b, claude-v1, gpt-3.5-turbo, gpt-4, llama-13b, vicuna-13b-v1.2), 턴 2개다. 판정이 데이터 안에 이미 들어 있으므로 모델을 부를 일이 없다.

구조에서 눈여겨볼 것이 둘 있다.

데이터에 남은 위치 편향

첫째, gpt4_pair의 winner에는 tie (inconsistent)라는 값이 있다. LMSYS가 GPT-4를 좌우 순서를 바꿔 두 번 돌리고, 두 판정이 어긋난 항목에 붙인 표시다. 즉 위치 편향이 데이터에 이미 기록돼 있다 — 좌우를 바꿔 물었을 때 심판이 말을 바꾸는 성질이고, 아래 위치 편향 절에서 세는 것이 바로 이 라벨이다. 따로 실험할 필요가 없다.

좌우를 없앤 키

둘째, 좌우 배치가 두 갈래에서 다르다. gpt4_pair는 모델 쌍 15개가 각각 한 방향으로만(160행씩) 들어 있는데, human은 15개 쌍이 양쪽 방향 모두 들어 있다. 그래서 사람 쪽은 「먼저 보여 준 쪽을 고르는 경향」을 직접 셀 수 있다.

이 때문에 두 갈래를 맞추려면 좌우를 없애야 한다. 모델 이름을 사전순으로 세우고 승자를 lo/hi/tie로 바꾸면 배치와 무관한 키가 된다.

재현

스크립트

pip install numpy datasets
python judge.py
import random, itertools
from collections import Counter, defaultdict
import numpy as np
from datasets import load_dataset

d = load_dataset("lmsys/mt_bench_human_judgments")
print(f"human {len(d['human'])}행 | gpt4_pair {len(d['gpt4_pair'])}행")

def canon(r):                                  # 좌우를 없애고 (문항, 모델쌍, 턴)으로 세운다
    lo, hi = sorted((r["model_a"], r["model_b"])); w = r["winner"]
    key = (r["question_id"], lo, hi, r["turn"])
    if w.startswith("tie"): return key, "tie"
    picked = r["model_a"] if w == "model_a" else r["model_b"]
    return key, ("lo" if picked == lo else "hi")

votes, who = defaultdict(list), defaultdict(list)
for r in d["human"]:
    k, v = canon(r); votes[k].append(v); who[k].append(r["judge"])
major = {k: ("tie" if len(c) > 1 and c[0][1] == c[1][1] else c[0][0])
         for k, vs in votes.items() for c in [Counter(vs).most_common()]}
gpt4 = dict(canon(r) for r in d["gpt4_pair"])
inc = sum(1 for r in d["gpt4_pair"] if r["winner"] == "tie (inconsistent)")
jd = Counter(r["judge"].split("_")[0] for r in d["human"])
print(f"판정자 {len(set(r['judge'] for r in d['human']))}명"
      f" (expert {len({r['judge'] for r in d['human'] if r['judge'].startswith('expert')})},"
      f" author {len({r['judge'] for r in d['human'] if r['judge'].startswith('author')})})"
      f" | 항목 {len(major)}개 | 평균 {np.mean([len(v) for v in votes.values()]):.2f}표")
ht = sum(1 for r in d["human"] if r["winner"].startswith("tie"))
print(f"사람 판정 중 무승부 {ht}/{len(d['human'])} = {100*ht/len(d['human']):.1f}% | "
      f"항목 다수결에서 무승부 {sum(1 for v in major.values() if v=='tie')}/{len(major)}")
print(f"GPT-4 좌우를 바꾸면 판정이 뒤집힌 항목: {inc}/{len(d['gpt4_pair'])} = {100*inc/len(d['gpt4_pair']):.2f}%\n")

def stats(pairs, labels):
    n = len(pairs); po = sum(a == b for a, b in pairs) / n
    ca, cb = Counter(a for a, _ in pairs), Counter(b for _, b in pairs)
    pe = sum(ca[l]/n * cb[l]/n for l in labels)
    return n, po, (po - pe) / (1 - pe)

indiv = [(v, gpt4[k]) for k, v in (canon(r) for r in d["human"]) if k in gpt4]
items = [(major[k], gpt4[k]) for k in sorted(set(major) & set(gpt4))]
print(f"{'집계 방식':<12}{'무승부 처리':<12}{'n':>6}{'일치율':>10}{'카파':>9}")
for nm, ps in (("개별 투표", indiv), ("항목 다수결", items)):
    n, po, k = stats(ps, ["lo", "hi", "tie"]);       print(f"{nm:<12}{'제3의 값':<12}{n:>6}{po*100:>9.2f}%{k:>9.4f}")
    n, po, k = stats([p for p in ps if "tie" not in p], ["lo", "hi"])
    print(f"{nm:<12}{'항목 제거':<12}{n:>6}{po*100:>9.2f}%{k:>9.4f}")
    half = np.mean([1.0 if a == b else (0.5 if "tie" in (a, b) else 0.0) for a, b in ps])
    print(f"{nm:<12}{'절반 인정':<12}{len(ps):>6}{half*100:>9.2f}%{'—':>9}")

hh = [(vs[i], vs[j]) for k, vs in votes.items() if len(vs) > 1
      for i, j in itertools.combinations(range(len(vs)), 2) if who[k][i] != who[k][j]]
print()
n, po, k = stats(hh, ["lo", "hi", "tie"]);  print(f"{'사람-사람':<12}{'제3의 값':<12}{n:>6}{po*100:>9.2f}%{k:>9.4f}")
n, po, k = stats([p for p in hh if "tie" not in p], ["lo", "hi"])
print(f"{'사람-사람':<12}{'항목 제거':<12}{n:>6}{po*100:>9.2f}%{k:>9.4f}")

for t in (1, 2):                               # 논문 Table 4와 같은 축(턴별·개별 투표)
    p = [(v, gpt4[k]) for k, v in (canon(r) for r in d["human"]) if k in gpt4 and k[3] == t]
    nt = [x for x in p if "tie" not in x]
    print(f"turn {t}: S1 {100*sum(a==b for a,b in p)/len(p):.1f}% (n={len(p)})  "
          f"S2 {100*sum(a==b for a,b in nt)/len(nt):.1f}% (n={len(nt)})")

rng = random.Random(0); qs = sorted({k[0] for k in major}); by = defaultdict(list)
for i, k in enumerate(sorted(set(major) & set(gpt4))): by[k[0]].append(i)
def boot(sel, lab):
    out = []
    for _ in range(1000):
        ix = [i for q in (rng.choice(qs) for _ in qs) for i in by[q]]
        ps = [items[i] for i in ix]
        out.append(stats([p for p in ps if sel(p)], lab)[2])
    return np.percentile(out, [2.5, 97.5])
lo3, hi3 = boot(lambda p: True, ["lo", "hi", "tie"])
lo2, hi2 = boot(lambda p: "tie" not in p, ["lo", "hi"])
print(f"\n부트스트랩 1,000회 (문항 {len(qs)}개 재표집) 카파 95% 구간")
print(f"  무승부=제3의 값  [{lo3:.4f}, {hi3:.4f}]\n  무승부 제거     [{lo2:.4f}, {hi2:.4f}]")

fp = [r["winner"] == "model_a" for r in d["human"] if not r["winner"].startswith("tie")]
print(f"\n사람이 '먼저 보여 준 쪽'을 고른 비율: {100*np.mean(fp):.2f}% (n={len(fp)}), "
      f"z={(np.mean(fp)-0.5)/np.sqrt(0.25/len(fp)):.2f}")

그대로 나온 출력

돌린 결과 전문이다.

human 3355행 | gpt4_pair 2400행
판정자 65명 (expert 58, author 7) | 항목 1814개 | 평균 1.85표
사람 판정 중 무승부 780/3355 = 23.2% | 항목 다수결에서 무승부 552/1814
GPT-4 좌우를 바꾸면 판정이 뒤집힌 항목: 380/2400 = 15.83%

집계 방식       무승부 처리           n       일치율       카파
개별 투표       제3의 값         3355    66.17%   0.4827
개별 투표       항목 제거         2129    85.63%   0.7120
개별 투표       절반 인정         3355    78.52%        —
항목 다수결      제3의 값         1814    67.20%   0.5047
항목 다수결      항목 제거         1078    88.40%   0.7670
항목 다수결      절반 인정         1814    80.15%        —

사람-사람       제3의 값         2367    65.53%   0.4699
사람-사람       항목 제거         1550    82.71%   0.6534
turn 1: S1 66.6% (n=1689)  S2 85.9% (n=1060)
turn 2: S1 65.7% (n=1666)  S2 85.3% (n=1069)

부트스트랩 1,000회 (문항 80개 재표집) 카파 95% 구간
  무승부=제3의 값  [0.4548, 0.5489]
  무승부 제거     [0.7217, 0.8130]

사람이 '먼저 보여 준 쪽'을 고른 비율: 50.21% (n=2575), z=0.22

측정 환경

측정 환경은 Ubuntu 24.04.4 LTS, Linux 6.18 x86_64, Intel Xeon 2.10GHz 4코어, Python 3.11.15, numpy 2.4.6, datasets 5.0.1, huggingface_hub 1.28.0이다. 데이터셋은 lmsys/mt_bench_human_judgments이고 받은 날은 2026-08-26이다. 전체 실행 4.4초로, 부트스트랩 2,000회를 포함한 값이다.

논문 수치와의 대질

S1과 S2

논문 §4.2는 두 가지 셈법을 갈라 놓았다. S1은 무승부와 불일치를 모두 세고, S2는 양쪽 다 비긴 판정이 아닐 때만 센다. 논문이 정의하는 일치도는 「각 유형에서 무작위로 (서로 다른) 개인을 하나씩 뽑았을 때 둘이 같은 판정을 내릴 확률」이므로, 사람 쪽을 다수결로 뭉치지 않고 개별 투표로 세는 것이 논문의 방식이다.

항목 논문 (Table 4) 우리 재계산
GPT-4 ↔ 사람, 1턴, S2 85% (859표) 85.9% (1,060)
GPT-4 ↔ 사람, 2턴, S2 85% (864표) 85.3% (1,069)
GPT-4 ↔ 사람, 1턴, S1 70% (1,138표) 66.6% (1,689)
GPT-4 ↔ 사람, 2턴, S1 70% (1,161표) 65.7% (1,666)
사람 ↔ 사람, 1턴, S2 81% (479표) 82.0% (772)
사람 ↔ 사람, 2턴, S2 82% (474표) 83.4% (778)

재현된 줄과 어긋난 줄

S2는 재현된다. 네 줄 모두 1.4%p 안쪽이고, 「GPT-4가 사람끼리보다 더 사람과 맞는다」는 핵심 대소 관계도 그대로다(85.9 대 82.0, 85.3 대 83.4).

S1은 3~4%p 낮게 나온다. 표본 수도 우리 쪽이 크다(1,689 대 1,138). 원인은 무승부·불일치를 세는 규칙일 것으로 보인다 — 논문은 GPT-4의 tie (inconsistent)를 무승부와 다르게 취급했을 수 있고, 공개된 데이터에는 그 둘이 이미 하나의 라벨로 합쳐져 들어와 있어 우리 쪽에서 되돌릴 수 없다. 이 차이는 확정하지 못했고 가설로 남긴다.

여기서 재현이 성공했다는 것보다 중요한 것은 논문 표 안에서 S1과 S2가 이미 15%p 벌어져 있다는 사실이다. 그리고 인용될 때는 대개 큰 쪽만 간다.

무승부 처리

세 가지 규칙

같은 판정 데이터를 세 가지 규칙으로 세어 보면 이렇게 갈린다.

무승부 처리 개별 투표 항목 다수결
제3의 값으로 센다 66.17% 67.20%
무승부가 낀 항목을 버린다 85.63% 88.40%
한쪽만 무승부면 0.5점 78.52% 80.15%

가장 낮은 66.17%와 가장 높은 88.40% 사이가 22.23%p다. 심판을 바꾼 것도, 데이터를 바꾼 것도 아니다. 집계 규칙 두 개(무승부를 어떻게 셀 것인가, 사람 여러 명을 어떻게 뭉칠 것인가)를 바꿨을 뿐이다.

버려지는 항목

이게 왜 이렇게 크냐면 무승부가 흔하기 때문이다. 사람 판정 3,355건 중 780건, 23.2%가 무승부다. 항목 다수결로 뭉쳐도 1,814개 중 552개가 무승부로 남고, 여기에 GPT-4 쪽 무승부까지 더하면 어느 한쪽이라도 무승부인 항목이 736개(1,814 − 1,078)다. 항목의 40.6%가 「버린다」에서 사라진다. 그리고 사라지는 항목은 무작위가 아니라 두 응답의 품질이 비슷해 판정이 어려운 항목이다. 어려운 문제를 빼고 채점하면 점수가 오르는 것은 당연하고, 그 오름폭이 22%p다.

집계 방식의 기여분도 따로 보인다. 개별 투표에서 항목 다수결로 바꾸면 85.63%가 88.40%로 오른다. 다수결이 개별 판정자의 흔들림을 지워 주기 때문이다. 2.77%p가 여기서 나온다.

코헨 카파

일치율만으로는 부족하다. 선택지가 셋뿐이라 아무렇게나 찍어도 상당히 맞는다. 코헨 카파는 그 우연분을 빼는 지표다.

κ=po−pe1−pe\kappa = \frac{p_o - p_e}{1 - p_e}

비교 무승부 처리 일치율 카파 부트스트랩 95% 구간
GPT-4 ↔ 사람 제3의 값 67.20% 0.5047 [0.4548, 0.5489]
GPT-4 ↔ 사람 항목 제거 88.40% 0.7670 [0.7217, 0.8130]
사람 ↔ 사람 제3의 값 65.53% 0.4699 —
사람 ↔ 사람 항목 제거 82.71% 0.6534 —

구간은 문항 80개를 통째로 재표집하는 방식으로 냈다. 같은 문항의 판정들은 서로 독립이 아니므로 판정 하나하나를 재표집하면 구간이 실제보다 좁게 나온다.

카파를 관습적인 눈금(0.410.60 보통, 0.610.80 상당, 0.81~1.00 거의 완벽)에 대면 이렇게 읽힌다.

  • 무승부를 버린 0.7670은 「상당한 일치」이고 「거의 완벽」은 아니다. 88.40%라는 숫자만 보면 후자로 읽힌다.
  • 무승부를 세면 0.5047, 「보통」이다. 구간 [0.4548, 0.5489]이 0.6에 닿지도 않는다.
  • 사람끼리도 같은 방향으로 낮아진다(0.6534 / 0.4699). GPT-4가 사람보다 카파가 높다는 대소 관계는 두 규칙 모두에서 유지된다.

논문의 주장 — GPT-4가 사람끼리만큼, 혹은 그 이상으로 사람과 맞는다 — 은 우연 보정 뒤에도 살아남는다. 무너지는 것은 그 수치의 절대적인 크기에 대한 인상이다.

위치 편향

심판과 사람

gpt4_pair에 이미 기록돼 있다. 2,400건 중 380건, 15.83%가 좌우를 바꾸자 판정이 뒤집혔다.

사람은 어떨까. human 쪽은 같은 모델 쌍이 양쪽 배치로 다 들어 있으니 「먼저 보여 준 쪽」을 고른 비율을 셀 수 있다. 무승부가 아닌 2,575건에서 50.21%다. 50%와의 차이가 0.21%p, 정규근사 z = 0.22 — 우연과 구별되지 않는다.

위치에 따른 흔들림
GPT-4 심판 15.83%의 항목에서 순서를 바꾸면 판정이 뒤집힌다
사람 65명 먼저 보여 준 쪽을 고른 비율 50.21% (z = 0.22)

논문의 65%

논문 §3.1도 위치 편향을 따로 실험했고 기본 프롬프트에서 GPT-4의 일관성을 65.0%로 보고한다(첫 번째 위치 선호 30.0%, 두 번째 5.0%). 우리가 잰 15.83%와는 크게 다른데 같은 것을 잰 것이 아니다. 논문의 65%는 위치 편향만 보려고 따로 고른 소규모 집합에서 잰 값이고, 15.83%는 MT-bench 판정 2,400건 전체에서의 비율이다. 방향은 같고 크기는 설정에 달렸다.

실무에 옮길 것은 하나다. 심판을 쓸 때 좌우를 바꿔 두 번 돌리는 것은 선택이 아니다. 한 번만 돌리면 그중 6분의 1은 동전 던지기다.

꺾이는 지점과 한계

보고 규칙의 진폭

여기까지가 보고 규칙의 문제이고 여기서부터가 심판 성능의 문제다 — 무승부 처리와 집계 방식만으로 22.23%p가 움직이는데, 이 논문이 다투는 GPT-4와 사람의 차이는 3.9%p다.

숫자로 놓으면 이렇다.

무엇이 움직이나 크기
무승부 처리 규칙 (제3의 값 ↔ 제거, 다수결 기준) 21.20%p
사람 집계 방식 (개별 투표 ↔ 항목 다수결, 무승부 제거) 2.77%p
둘을 합친 최대 진폭 22.23%p
GPT-4 ↔ 사람과 사람 ↔ 사람의 차이 (S2, 1턴) 3.9%p

보고 규칙이 논점보다 5.7배 크다. 그래서 심판 일치도를 무승부 정책 없이 적은 숫자는 해석할 수가 없다. 「우리 심판은 사람과 87% 일치합니다」는 그 자체로는 아무 정보가 아니다 — 무승부를 버렸는지, 몇 %가 버려졌는지를 함께 적어야 비로소 다른 숫자와 비교된다.

표본과 셈법의 한계

축소해서 다시 풀었기 때문에 확인하지 못한 것이 넷이다.

  • 한 벤치마크, 한 심판, 여섯 모델이다. 문항 80개에 2023년의 모델 6개이고 심판은 GPT-4 하나다. 카파 0.4827이 다른 심판이나 다른 과제에서도 그 근처라는 근거가 이 데이터에는 없다.
  • 영어 데이터다. 한국어 응답을 심판시켰을 때 같은 값이 나올지는 이 실험이 말해 주지 않는다.
  • S1의 3~4%p 불일치는 원인을 확정하지 못했다. 무승부와 불일치를 가르는 논문 쪽 규칙이 공개 데이터에서 이미 합쳐진 뒤라 되돌릴 수 없다. 가설로만 적었다.
  • 논문의 표본 수와 우리 표본 수가 다르다. 사람↔사람에서 논문은 479·474표, 우리는 772·778쌍이다. 논문이 쌍을 추출하는 방식이 본문에 없어 그대로 맞추지 못했다. 비율이 1~2%p 안에서 맞으므로 같은 것을 재고 있다고 보지만, 표본 수가 다르다는 것은 셈법이 완전히 같지는 않다는 뜻이다.

지표의 한계

  • 사람의 위치 편향이 0이라고 말한 것이 아니다. 2,575건에서 z = 0.22이므로 「이 표본으로는 잡히지 않는다」가 맞다. 이 크기의 표본에서 잡히려면 대략 2%p는 넘어야 한다.
  • 카파도 완전하지 않다. 한쪽 라벨이 몰려 있으면 일치율이 높아도 카파가 낮게 나오는 성질이 있다. 이 데이터는 무승부가 23% 섞여 있어 심한 편은 아니지만, 카파 하나를 절대 눈금으로 쓰는 것은 일치율 하나를 쓰는 것과 같은 종류의 잘못이다.

무승부 정책

논문의 주장은 재현된다. GPT-4 심판은 이 데이터에서 사람만큼 사람과 맞고, 우연을 보정해도 그렇다.

다시 계산해서 얻은 것은 그 주장의 크기를 읽는 눈금이다. 85%는 데이터의 40%를 버리고 센 값이고, 버린 것은 하필 판정이 어려운 항목이며, 우연을 빼면 카파 0.7120이다. 그리고 그 심판은 좌우를 바꾸면 여섯 중 하나꼴로 말을 바꾼다.

심판 성능을 비교하려면 같은 눈금을 써야 한다. 그 눈금의 이름은 무승부 정책이다.


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

LATEST

논문의 최신 글

논문2026.08.25

Chinchilla 정독: 20토큰/파라미터는 Table 3에서 나왔다

세 접근법이 낸 예측을 표에서 직접 나눠 봤다. 토큰/파라미터가 20 근처에 머무는 것은 Approach 1 열뿐이고 파라메트릭 적합 열은 23에서 142까지 간다. 세 예측은 실험을 돌린 구간에서 7% 안이지만 예산을 열 자릿수 밀면 2.6배로 벌어진다.

27 MIN
논문2026.08.24

Attention Is All You Need 정독: Table 3의 params 칸이 요구하는 어휘는 37,000이 아니다

논문 Table 3을 HTML에서 직접 파싱해 스무 행을 복원하고, params가 적힌 열 행의 파라미터 수를 shape 산술로 다시 셌다. 논문이 §5.1에 적은 어휘 37,000으로는 어느 행도 맞지 않는다. 아홉 행은 40,932~41,160이라는 한 창에서 동시에 성립하고 big 한 행만 35,296~36,272를 요구해 교집합을 깬다.

24 MIN