Study NoteNIST trec_eval 소스 확인, 손계산과 trec_eval 실행

Recall@100은 reranker가 살릴 수 있는 정답의 상한선이다

Recall@100의 numerator와 denominator를 qrels 기준으로 계산하고 후보 검색의 상한선, macro average, 불완전한 relevance judgment의 함정을 확인한다.

RAG 검색에서 Recall@100을 높여야 한다는 말을 자주 본다. 처음에는 상위 100개의 정확도라고 이해했다. 그러나 recall의 denominator에는 검색 결과 100개가 아니라 그 query에 관련 있다고 알려진 전체 문서가 들어간다.

Recall@100은 상위 100개 후보 안에 알려진 관련 문서를 얼마나 회수했는지 측정한다. 순서의 품질보다 candidate coverage를 보는 지표다.

식에서 denominator부터 확인한다

query qq의 관련 문서 집합을 RqR_q, 검색 결과 상위 kk개 집합을 Sq@kS_q@k라고 두면 다음과 같다.

Recall@k(q)=RqSq@kRq\operatorname{Recall}@k(q) = \frac{|R_q\cap S_q@k|}{|R_q|}

Recall@100k=100k=100인 경우다.

항목의미
numeratortop-100 안에서 찾은 relevant document 수
denominatorqrels에서 relevant로 판정된 전체 문서 수
범위0에서 1
순서 반영top-100 내부 순서는 반영하지 않음

NIST trec_evalm_recall.c도 cutoff까지 relevance threshold 이상인 문서를 세고, 그 수를 num_rel로 나눈다. cutoff보다 결과가 적으면 나머지는 nonrelevant document로 채운 것으로 본다.

같은 예제를 직접 계산했다

qrels에 관련 문서가 다섯 개 있다고 하자.

q1 0 D1 3
q1 0 D2 2
q1 0 D3 1
q1 0 D4 1
q1 0 D5 1

검색 결과는 다음과 같다.

q1 Q0 D2 1 10.0 demo
q1 Q0 X  2  9.0 demo
q1 Q0 D1 3  8.0 demo
q1 Q0 D3 4  7.0 demo
q1 Q0 Y  5  6.0 demo
q1 Q0 D4 6  5.0 demo

top-3에서는 D2, D1 두 개를 찾았다.

Recall@3=25=0.4\operatorname{Recall}@3=\frac{2}{5}=0.4

100개까지 요청했지만 검색기가 여섯 개만 반환했다. 그 안에는 D2, D1, D3, D4 네 개가 있고 D5는 없다.

Recall@100=45=0.8\operatorname{Recall}@100=\frac{4}{5}=0.8

현재 NIST trec_eval 소스를 build해 같은 파일을 실행한 결과도 0.4와 0.8이었다.

recall_3               all    0.4000
recall_100             all    0.8000

여기서 relevance가 3인 D1과 relevance가 1인 D4는 recall numerator에서 각각 한 건이다. 기본 threshold 1을 넘으면 graded relevance의 크기는 더하지 않는다. 높은 relevance를 앞에 놓았는지까지 보려면 nDCG@10 같은 지표가 필요하다.

top-100 안의 순서는 보지 않는다

관련 문서 열 개가 rank 1부터 10까지 모여 있는 system과 rank 91부터 100까지 몰려 있는 system을 생각해 봤다. qrels의 relevant document가 그 열 개뿐이면 두 system의 Recall@100은 모두 1이다.

사용자가 첫 화면만 본다면 두 결과의 품질은 전혀 같지 않다. Recall@100은 그 질문에 답하려고 만든 지표가 아니다.

Recall@100  → 후보 집합에 정답 재료가 있는가
nDCG@10     → 사용자에게 먼저 보여 준 순서가 좋은가

후보 검색과 reranking을 분리하면 이 차이가 실용적이다. reranker는 받은 후보의 순서는 바꿀 수 있지만 후보 밖의 문서를 새로 만들 수 없다.

RqSq@100=reranker cannot recover the answerR_q\cap S_q@100=\varnothing \quad\Rightarrow\quad \text{reranker cannot recover the answer}

그래서 first-stage retriever의 Recall@100은 downstream reranker가 달성할 수 있는 품질의 상한과 연결된다. 물론 candidate 안에 정답이 있다는 사실만으로 reranker가 반드시 위로 올린다는 뜻은 아니다.

cutoff 100은 자연법칙이 아니다

100은 식에서 유도된 값이 아니라 candidate budget이다. 다음 요소가 cutoff를 정한다.

  • reranker가 한 query에서 처리할 수 있는 문서 수
  • latency와 GPU memory budget
  • 문서 길이와 batch 구성
  • relevant document 수의 분포
  • 정답 하나면 되는지 여러 근거를 모아야 하는지

qrels의 relevant document가 250개라면 서로 다른 문서 100개만 반환하는 Recall@100의 이론적 최대는 0.4다.

maxRecall@100=min(100,Rq)Rq\max \operatorname{Recall}@100 = \frac{\min(100, |R_q|)}{|R_q|}

relevant document 수가 query마다 크게 다른 dataset에서 같은 cutoff를 평균내면 query별 ceiling도 달라진다. Recall@10, Recall@50, Recall@100, Recall@200을 curve로 보고 품질 증가가 비용을 넘는 지점을 찾는 편이 낫다.

전체 점수는 보통 macro average다

두 query를 만들었다.

  • qA에는 relevant document가 1개이고 system이 그 문서를 찾았다.
  • qB에는 relevant document가 9개이고 system은 하나도 찾지 못했다.

query별 recall은 1과 0이다. trec_eval의 전체 Recall@100은 두 값을 같은 비중으로 평균내 0.5가 됐다.

recall_100             qA     1.0000
recall_100             qB     0.0000
recall_100             all    0.5000

전체 relevant document를 한 번에 합친 micro recall은 1/10=0.11/10=0.1이다. 둘은 같은 집계가 아니다.

NIST 소스의 설명도 recall은 single topic measure로는 좋지만 평균이 잘 작동하지 않을 수 있다고 경고한다. macro average가 틀렸다는 뜻보다, relevant document가 적은 query와 많은 query를 같은 한 표로 압축하면 해석이 달라진다는 뜻으로 읽었다.

운영 평가에서는 전체 macro average와 함께 다음을 남기는 편이 안전하다.

  • query type별 recall
  • relevant document 수 구간별 recall
  • recall 분포와 최저 구간
  • zero-recall query 비율
  • cutoff별 recall curve

qrels가 recall의 세계를 결정한다

식의 RqR_q는 세상에 실제로 존재하는 모든 관련 문서가 아니다. 평가 qrels에 relevant로 기록된 문서 집합이다.

모든 query-document 쌍을 사람이 판정할 수 없어서 보통 여러 retrieval system의 상위 결과를 pooling한 뒤 판정한다. 기존 lexical system이 후보를 만들었다면 lexical overlap이 낮은 관련 문서가 qrels에 없을 수 있다.

새 dense retriever가 그런 문서를 찾아도 qrels에 없으면 recall numerator에는 들어가지 않는다. 따라서 보고서의 Recall@100=0.82는 더 정확히 다음처럼 읽어야 한다.

qrels에서 관련 있다고 알려진 문서 중 82%가 top-100에 있었다.

실제 관련 문서 전체의 82%를 찾았다고 단정할 수는 없다. 새 system의 top result에서 unjudged 비율이 높다면 추가 judgment를 만들거나 여러 retriever의 pool을 합쳐 평가셋을 보완해야 한다.

Graded qrels는 threshold에 따라 binary로 바뀐다

trec_eval은 기본적으로 relevance 1 이상을 relevant로 센다. -l 옵션으로 최소 relevance level을 바꿀 수 있다.

trec_eval -l 2 -m recall.100 qrels.txt run.txt

이 명령에서는 grade 2와 3만 numerator와 denominator에 들어간다. 같은 run이라도 threshold를 1에서 2로 바꾸면 다른 질문에 답한다.

-l 1  → 조금이라도 관련된 문서의 coverage
-l 2  → 높은 관련성 문서만의 coverage

실험 기록에 Recall@100만 쓰지 말고 relevance threshold도 남겨야 한다.

빠진 query를 평균에서 빼면 점수가 오른다

trec_eval은 기본 설정에서 qrels와 run에 모두 있는 query의 교집합을 평가한다. system이 어떤 query에서 결과를 하나도 쓰지 않았다면 그 query가 평균에서 빠질 수 있다.

-c는 qrels의 전체 query를 기준으로 평균내고 누락된 run query를 0으로 처리한다.

python -m pyserini.eval.trec_eval \
  -q -c \
  -m recall.100 \
  qrels.txt run.txt

batch failure나 filter 오류로 결과가 비어도 품질 지표에 반영하려면 -c를 명시하는 편이 안전하다.

Recall@100이 올라가도 서비스가 나빠질 수 있다

후보 수를 늘리거나 dense 후보를 합치면 Recall@100은 오를 수 있다. 동시에 다음 문제가 생길 수 있다.

  • 상위 열 개가 noisy document로 밀린다.
  • reranker 처리량과 latency가 늘어난다.
  • 긴 문서가 많아져 token budget을 초과한다.
  • 중복 chunk가 후보를 차지한다.
  • unjudged document가 늘어 offline score 해석이 불안정해진다.

그래서 Recall@100 하나를 최적화 목표로 두지 않는다.

단계최소한 같이 볼 값
first-stage retrievalRecall@k, zero-recall rate, latency, candidate 중복률
rerankingnDCG@10, MRR 또는 Hit@k, latency
RAG answeranswer correctness, citation support, abstention failure

retrieval recall은 generation correctness가 아니다. 관련 chunk가 후보에 있어도 prompt에서 잘리거나 generator가 사용하지 않을 수 있다.

내가 기억할 한 문장

Recall@100상위 100개 후보 안에 알려진 정답 문서를 얼마나 빠뜨리지 않았는가에 답한다.

순서의 품질, unknown relevant document, answer correctness까지 설명하지는 않는다. reranker를 평가하기 전에 first-stage의 후보 상한을 확인하는 지표로 사용하고, 최종 상위 결과는 nDCG@10으로 따로 본다.

References

  1. NIST. trec_eval의 Recall at Cutoffs 구현.
  2. NIST. trec_eval 실행 옵션과 표준 평가 절차.
  3. NIST. trec_eval의 qrels 집계와 relevance threshold 처리.
  4. Castorini. Pyserini trec_eval wrapper.

소스와 실행 결과는 2026-07-21에 확인했다.

대화

댓글

0
댓글을 불러오는 중입니다.