Study NoteJärvelin·Kekäläinen 원문과 NIST trec_eval 소스 확인, 손계산과 trec_eval 실행

nDCG@10은 상위 열 개의 순서를 어떻게 점수로 바꾸는가

nDCG@10의 gain과 discount, ideal ranking, 정규화를 손으로 계산하고 trec_eval의 실제 구현 차이와 검색 평가에서 놓치기 쉬운 조건을 확인한다.

검색 논문을 읽으면 nDCG@10이 자주 등장한다. 처음에는 상위 열 개 중 정답 비율이라고 생각했다. 그 설명은 precision에 더 가깝다.

nDCG@10은 세 가지를 함께 본다.

  • 관련성이 높은 문서를 더 가치 있게 본다.
  • 같은 관련 문서라도 앞에서 찾으면 더 높은 점수를 준다.
  • 현재 query에서 가능한 이상적인 순서와 비교해 0에서 1 사이로 정규화한다.

이 글에서는 Recall@100과 무엇이 다른지, Pyserini에서 어떤 명령으로 계산하는지까지 연결한다.

이름부터 네 조각으로 나눈다

nDCG@10n, D, C, G와 cutoff 10으로 나눠 읽을 수 있다.

조각하는 일
GGainrelevance label을 문서의 가치로 바꾼다.
CCumulative1위부터 현재 rank까지 gain을 더한다.
DDiscounted뒤에 나온 문서의 gain을 줄인다.
nnormalized실제 DCG를 이상적인 DCG로 나눈다.
@10cutoff상위 열 개만 계산한다.

먼저 query qq에서 rank ii 문서의 relevance를 relirel_i라고 둔다. gain 함수를 gg라고 하면 cutoff kk의 DCG는 다음처럼 쓸 수 있다.

DCG@k=i=1kg(reli)log2(i+1)\operatorname{DCG}@k = \sum_{i=1}^{k}\frac{g(rel_i)}{\log_2(i+1)}

rank 1의 denominator는 log22=1\log_2 2=1이므로 gain을 그대로 받는다. 뒤로 갈수록 denominator가 커져 같은 문서도 덜 기여한다.

rank iidiscount 1/log2(i+1)1/\log_2(i+1)
11.0000
20.6309
30.5000
40.4307
100.2891

Järvelin과 Kekäläinen의 원식은 rank 1을 따로 두고 rank 2부터 reli/log2irel_i/\log_2 i를 사용했다. 원식에서는 rank 2도 discount가 1이다. 위에서 사용한 현재 trec_eval 식은 denominator를 log2(i+1)\log_2(i+1)로 옮겨 rank 2부터 약 0.6309를 적용한다. 두 식은 표기만 다른 동치식이 아니라 실제 점수가 달라지는 variant다.

정규화에는 query별 이상적인 순서가 필요하다

DCG만 비교하면 query마다 relevant document 수와 relevance 분포가 다른 문제가 남는다. grade 3 문서가 열 개 있는 query와 grade 1 문서가 한 개뿐인 query의 가능한 총 gain은 같지 않다.

그래서 qrels의 문서를 relevance 내림차순으로 놓은 ideal ranking을 만든다. 그 순서의 DCG가 IDCG다.

nDCG@k=DCG@kIDCG@k\operatorname{nDCG}@k = \frac{\operatorname{DCG}@k}{\operatorname{IDCG}@k}

실제 top-kk가 ideal top-kk와 같으면 1이다. 관련 문서가 뒤로 밀리거나 낮은 grade 문서가 먼저 나오면 1보다 작아진다.

정규화는 model score를 0에서 1로 바꾸는 작업이 아니다. BM25 score나 cosine similarity의 scale은 계산에 들어가지 않는다. run file의 score는 순서를 정하는 데 사용되고, 순서가 정해진 뒤에는 qrels의 relevance가 gain을 만든다.

한 query를 손으로 계산했다

다음 qrels에는 관련 문서가 다섯 개 있다. relevance는 1에서 3까지다.

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

검색기는 다음 순서로 여섯 개를 반환했다. XY는 qrels에 없는 문서다.

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

NIST trec_evalndcg_cut과 같은 선형 gain g(rel)=relg(rel)=rel을 사용했다.

rank문서relevancediscounted gain
1D222.0000
2X00.0000
3D131.5000
4D310.4307
5Y00.0000
6D410.3562

상위 열 개 중 반환하지 않은 rank 7부터 10까지는 gain 0으로 본다.

DCG@10=2+3log24+1log25+1log27=4.2869\operatorname{DCG}@10 =2+\frac{3}{\log_2 4}+\frac{1}{\log_2 5}+\frac{1}{\log_2 7} =4.2869

ideal ranking의 relevance 순서는 [3,2,1,1,1][3,2,1,1,1]이다.

IDCG@10=3+2log23+1log24+1log25+1log26=5.5794\operatorname{IDCG}@10 =3+\frac{2}{\log_2 3}+\frac{1}{\log_2 4} +\frac{1}{\log_2 5}+\frac{1}{\log_2 6} =5.5794

따라서 최종 값은 다음과 같다.

nDCG@10=4.28695.5794=0.7683\operatorname{nDCG}@10 =\frac{4.2869}{5.5794} =0.7683

같은 파일을 현재 NIST trec_eval 소스에서 직접 build해 실행했다.

trec_eval -m ndcg_cut.10 -m recall.3,100 qrels.txt run.txt
recall_3               all    0.4000
recall_100             all    0.8000
ndcg_cut_10            all    0.7683

손계산과 실행 결과가 소수 넷째 자리까지 같았다.

nDCG라는 이름만으로 gain 함수를 알 수 없다

가장 중요하게 바로잡은 부분이다. nDCG 식은 하나로 고정돼 있지 않다.

자주 만나는 다른 형태는 relevance 차이를 지수로 벌린다.

g(rel)=2rel1g(rel)=2^{rel}-1

같은 예제에 이 gain을 적용하면 DCG는 7.2869, IDCG는 10.2103, nDCG는 0.7137이 된다. trec_eval 결과 0.7683과 다르다.

Järvelin과 Kekäläinen의 원문도 relevance label을 그대로 쓰는 방법만 고정하지 않았다. 실험에서 0, 1, 2, 3뿐 아니라 0, 1, 4, 100, 1, 10, 100 같은 gain weight를 비교했다. 높은 relevance를 몇 배 더 가치 있게 볼지는 평가 설계의 일부였다.

NIST trec_evalm_ndcg_cut.c는 gain이 qrels의 relevance 값이라고 명시하고, 실제 합에서도 gain / log2(rank + 1)을 사용한다. 따라서 결과를 기록할 때 nDCG@10만 남기면 부족하다.

metric implementation
gain function
discount function
cutoff
qrels relevance scale

논문 표의 nDCG@10을 다른 library로 다시 계산할 때 gain 정의를 확인하지 않으면 model이 아니라 evaluator 차이를 성능 차이로 오해할 수 있다.

Recall@100과 보는 실패가 다르다

관련 문서가 top-100 안에 들어오기만 하면 Recall@100의 numerator에 포함된다. rank 1에 있든 rank 100에 있든 한 건이다.

반면 nDCG는 앞쪽 순서를 구분한다. relevant document가 하나뿐인 binary qrels를 생각하면 그 문서가 rank 1에 있을 때 nDCG는 1이고 rank 10에 있을 때는 약 0.289다. 두 경우의 Recall@100은 모두 1이다.

이 차이 때문에 two-stage retrieval에서는 두 지표의 역할을 나눠 볼 수 있다.

first-stage retriever  → Recall@100으로 후보 누락 확인
reranker output        → nDCG@10으로 상위 순서 확인

reranker가 nDCG@10을 높였는데 Recall@100이 그대로인 것은 이상하지 않다. 후보 문서를 새로 찾지 않고 기존 후보의 순서만 바꿨기 때문이다.

반대로 nDCG@10=1도 관련 문서를 모두 찾았다는 뜻은 아니다. relevant document가 100개여도 가장 좋은 열 개만 정확히 위에 놓으면 nDCG@10은 1이 될 수 있다. 이때 top-10만 반환했다면 recall은 0.1이다.

평가 결과를 왜곡하는 조건

Unjudged document를 0으로 보는 문제

trec_eval의 표준 경로는 qrels에 없는 문서를 대부분의 지표에서 nonrelevant처럼 처리한다. 새 retriever가 기존 pooling system이 찾지 못한 관련 문서를 가져와도 판정 전에는 gain 0이 될 수 있다.

따라서 dense나 hybrid system이 lexical baseline과 다른 문서를 많이 찾는다면 nDCG@10과 함께 top-kk의 judged 비율을 확인해야 한다. 점수가 낮은 이유가 ranking 실패인지 judgment coverage 부족인지 분리하기 위해서다.

Query 평균이 실패 유형을 숨기는 문제

보통 최종 all 값은 query별 nDCG를 평균낸 macro average다. 쉬운 query가 많으면 특정 업무 유형의 실패가 가려질 수 있다.

identifier query
procedure query
symptom query
long-form question

전체 평균을 유지하되 query type별 분포와 낮은 점수의 실제 결과도 같이 봐야 한다.

Relevance label이 모호한 문제

grade 3과 grade 2의 차이를 annotator가 일관되게 판단하지 못하면 지수 gain은 그 작은 불일치를 크게 증폭할 수 있다. label 설명, 중복 판정, disagreement 처리 없이 graded metric만 정교하게 계산해도 평가 자체가 정교해지지는 않는다.

Pyserini에서 계산하는 명령

qrels와 TREC run file이 있으면 다음처럼 계산한다.

python -m pyserini.eval.trec_eval \
  -q -c \
  -m ndcg_cut.10 \
  qrels.txt run.txt

-q는 query별 값도 출력한다. -c는 qrels에 있지만 run에 빠진 query를 0으로 포함한다. 이 옵션이 없으면 결과를 하나도 반환하지 못한 query가 평균에서 빠질 수 있다.

비교 실험에서는 다음 항목을 함께 고정한다.

  • 동일한 qrels와 query 집합
  • 동일한 cutoff 10
  • 동일한 evaluator version
  • 동일한 relevance scale과 gain 정의
  • 동일한 unjudged document 처리
  • query별 결과와 macro average

내가 기억할 한 문장

nDCG@10관련 문서를 얼마나 많이 찾았는가보다 가치가 높은 문서를 상위 열 개 안에서 얼마나 이상적으로 정렬했는가에 답한다.

숫자를 읽기 전에는 gain 함수와 qrels 생성 과정을 먼저 확인한다. 첫 단계 후보가 정답을 놓쳤는지는 Recall@100으로 따로 본다.

References

  1. Kalervo Järvelin, Jaana Kekäläinen. Cumulated gain-based evaluation of IR techniques. ACM TOIS, 2002.
  2. Tampere University. 저자 공개 원문 PDF.
  3. NIST. trec_evalndcg_cut 구현.
  4. NIST. trec_eval의 qrels와 run 결합 구현.
  5. Castorini. Pyserini trec_eval wrapper.

원문과 소스는 2026-07-21에 확인했다.

대화

댓글

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