
정확도 수치 파헤치기: LLM 평가 하네스 (Eval Harness) 직접 구축하기
요약
LLM의 성능을 단순히 정확도(Accuracy)로만 판단할 때 발생하는 위험성을 경고하며, 평가 하네스(Eval Harness)를 직접 구축하여 지표를 심층 분석하는 방법을 다룹니다. Banking77 데이터셋과 Qwen2.5 모델을 활용해 정밀도, 재현율, F1 스코어 등 다양한 지표의 중요성을 설명합니다.
핵심 포인트
- 단일 정확도 지표는 모델의 세부적인 실패 사례를 은폐할 수 있음
- 신뢰할 수 있는 평가를 위해 학습/서빙과 동일한 파싱 함수 사용 필수
- 결정론적 평가를 위해 탐욕적 디코딩(Greedy decoding) 활용 권장
- 정확도 외에 클래스별 정밀도, 재현율, F1 스코어 분석이 병행되어야 함
지난 시리즈에서 저는 모델을 미세 조정 (Fine-tuning)하며 **~96% 정확도 (Accuracy)**라는 자랑스러운 수치를 계속 인용했습니다. 이번 시리즈는 당시 제가 충분히 주의 깊게 하지 않았던 일, 즉 그 숫자가 실제로 무엇을 의미하는지 제대로 확인하는 과정에 관한 것입니다.
여기에 함정이 있습니다. 정확도 (Accuracy)는 가능한 정답이 많은 태스크를 하나의 숫자로 요약하려고 시도하는 수치입니다. 이는 모델이 완벽하게 맞춘 사례와 조용히 실패한 사례를 하나로 섞어서, 단 하나의 자신만만한 백분율로 돌려줍니다. 그래서 저는 evaluate나 lm-eval-harness를 사용하지 않고 처음부터 직접 (from scratch) 작은 평가 하네스 (Eval harness)를 구축했으며, 이를 베이스 (base) Qwen2.5-1.5B-Instruct 모델(미세 조정되지 않은 상태이므로 누구나 Kaggle T4에서 바로 실행 가능)에 실행해 보았습니다.
핵심은 "프레임워크가 나쁘다"는 것이 아닙니다. 루프 (Loop)를 직접 작성해 보면, 이러한 프레임워크들이 여러분을 위해 정확히 무엇을 하고 있는지, 그리고 왜 단일 지표 (Single metric)가 고장 난 모델을 숨길 수 있는지 정확히 이해하게 된다는 점입니다.
태스크와 루프 (The task and the loop)
저는 미세 조정 시리즈에서 사용했던 Banking77 의도 분류 (Intent-classification) 데이터셋(77개의 고객 지원 의도)과 동일한 parse_prediction() 헬퍼 함수를 재사용했습니다. 따라서 여기서의 파싱 (Parsing)은 제가 학습 및 서빙 (Serving)할 때와 동일합니다. 서빙할 때와 다른 파서를 사용하여 평가하는 것은 아무런 의미가 없는 숫자를 만들어내는 전형적인 방식입니다.
N_EVAL = 400
LABELS_BLOCK = ', '.join(label_names) # 베이스 모델에게 레이블 공간을 제공
...
탐욕적 디코딩 (Greedy decoding, do_sample=False)을 통해 평가를 결정론적 (Deterministic)으로 유지합니다. 이제 재미있는 부분입니다. **동일한 예측값 (Predictions)**을 다섯 가지 다른 방식으로 점수화해 보겠습니다.
지표 #1 — 정확도 (Accuracy, 모두가 인용하는 수치)
from sklearn.metrics import accuracy_score
acc = accuracy_score(y_true, y_pred)
# Accuracy: 50.0%
...
50%. 평범하지만 "기능적"인 수준입니다. 그냥 기록하고 넘어가기 쉬운 종류의 숫자죠. 그리고 주목할 점은, **파싱 불가능한 결과가 0%**였다는 것입니다. 모든 예측은 깔끔하고 유효한 의도 레이블이었습니다. 겉으로 보이는 모든 검사 결과, 모델은 괜찮아 보였습니다.
지표 #2 — 클래스별 정밀도 (Precision) / 재현율 (Recall) / F1
이제 동일한 예측값을 의도별로 나누어 보겠습니다:
from sklearn.metrics import classification_report
report = classification_report(y_true, y_pred, labels=label_names,
output_dict=True, zero_division=0)
가장 성능이 낮은 10개 의도(intent)는 F1 = 0.0이고 support = 0.0을 보였습니다. 이는 모델이 해당 의도를 문자 그대로 단 한 번도 예측하지 않았다는 것을 의미합니다. 단순히 틀린 것이 아니라, 존재하지 않는 것처럼 취급하고 있었던 것입니다.
지표 #3 — Macro 대 Micro F1
여기서 핵심적인 문제가 드러납니다:
from sklearn.metrics import f1_score
micro = f1_score(y_true, y_pred, labels=label_names, average='micro', zero_division=0) # 50.0%
macro = f1_score(y_true, y_pred, labels=label_names, average='macro', zero_division=0) # 7.5%
...
Micro F1: 50%. Macro F1: 7.5%. 이 42.5%의 격차가 바로 핵심적인 이야기입니다. Micro는 흔한 클래스(common classes)가 지배하도록 허용하는 반면, macro는 모든 의도에 동일한 가중치를 부여하기 때문에 버려진 클래스들(abandoned classes)이 점수를 크게 떨어뜨립니다. micro가 macro보다 훨씬 클 경우, 모델은 몇몇 흔한 케이스만 처리하고 나머지 의도는 무시하고 있다는 의미입니다.
지표 #4 — 혼동 행렬 (Confusion Matrix)
정확도(Accuracy)는 '얼마나 자주'를 말해줍니다. 반면, 혼동 행렬은 '무엇이 무엇으로 오인되는지'를 보여주며—모델이 77개의 의도를 몇몇 선호하는 소수 그룹으로 축소하고 있다는 것을 보여주는 유일한 시각화입니다:
from sklearn.metrics import confusion_matrix
import seaborn as sns
cm = confusion_matrix(y_true, y_pred, labels=label_names)
...
해당 행렬에는 대각선 성분이 거의 없었으며, 밝은 수직선(모델이 선호하는 기본 레이블로, 많은 실제 의도(intents)를 흡수함)과 거의 비어 있는 하단 절반(수십 개의 의도가 전혀 예측되지 않음)이 나타났습니다. 또한, 저는 실제로 조치 가능한(actionable) 결과물을 얻기 위해, 가장 심각한 혼동(confusions) 사례들을 평이한 영어로 명명하고자 대각선 이외의 셀(off-diagonal cells)들을 순위 매겼습니다.

핵심 요점 (The point)
저는 정확히 동일한 예측값을 다섯 가지 방식으로 점수화했고, 다섯 가지 서로 다른 인상을 얻었습니다:
- **정확도 (Accuracy)**는 50%라고 말했습니다 — "기능적임."
- **매크로 F1 (Macro F1)**은 7.5%라고 말했습니다 — 모델이 대부분의 클래스(classes)를 포기했음을 의미합니다.
- **클래스별 F1 (Per-class F1) / 미예측 목록 (never-predicted list)**은 재현율(recall)이 0인 어떤 클래스들인지를 명시했습니다.
- **혼동 행렬 (Confusion matrix)**은 모델이 무엇을 무엇으로 뭉뚱그리는지(collapses into) 보여주었습니다.
- **0%의 파싱 불가능 (unparseable)**은 이 중 어떤 것도 표면적인 검사(surface check)에서 나타나지 않았음을 의미했습니다.
지표(metric)는 단순히 모델을 측정하는 것이 아닙니다. 지표는 당신이 무엇을 인지할 수 있는지를 결정합니다. 잘못된 지표를 선택하면, 부주의해서가 아니라 당신의 단 하나의 숫자가 결코 보여줄 수 없었기 때문에, 존재조차 몰랐던 사각지대(blind spots)를 그대로 배포하게 될 것입니다.
다음 단계 (What's next)
Part 2: 비교할 레이블이 없는 경우(문단, 요약, 지원 답변 등) — 사람들은 LLM을 채점하기 위해 LLM을 사용합니다. 저는 그 판사(judge)를 처음부터 구축하고, 그것이 실제 인간과 일치하는지 확인합니다. (스포일러: 당신이 기대하는 것만큼 자주 일치하지는 않습니다.)
📓 Kaggle의 전체 실행 가능한 노트북: https://www.kaggle.com/code/sumannath88/ep01-eval-harness-from-scratch
PyTorch + Hugging Face Transformers + scikit-learn으로 제작되었습니다. 질문이나 수정 사항은 댓글로 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기