랭킹 모델 평가: NDCG·MAP·MRR 이해하기
검색·추천 시스템의 랭킹 품질을 평가하는 NDCG·MAP·MRR의 계산 원리, 각각의 적용 시나리오, Python 구현 코드를 완전히 이해한다.
지난 글에서 회귀 모델을 평가하는 방법을 배웠다. 이번에는 검색 엔진, 추천 시스템, 광고 랭킹처럼 결과의 순서가 중요한 시스템을 어떻게 평가하는지 살펴본다. 이 분야에서는 분류나 회귀 지표로는 랭킹 품질을 제대로 측정할 수 없다. 결과 목록에서 어떤 문서가 몇 번째에 오느냐가 핵심이기 때문이다.
왜 랭킹 전용 지표가 필요한가
사용자가 “파이썬 입문”이라고 검색했을 때, 검색 엔진은 수천 개의 결과 중 상위 10개를 보여준다. 이때 관련도 높은 문서가 1위5위에 오는 모델과 6위10위에 오는 모델은 단순 정밀도(관련 문서가 10개 중 몇 개인지)가 같아도 사용자 경험은 완전히 다르다. 높은 순위에 좋은 결과가 올수록 더 좋은 시스템이다. 랭킹 지표는 이 순서의 질을 수치화한다.
NDCG: 등급별 관련도와 순위 가중치
**NDCG(Normalized Discounted Cumulative Gain)**는 세 단계로 계산된다.
1. DCG (Discounted Cumulative Gain): 각 위치의 관련도 점수에 순위 할인을 적용해 합산한다.
DCG@k = Σᵢ₌₁ᵏ (2^relᵢ - 1) / log₂(i + 1)
분모 log₂(i+1)이 순위 할인을 적용한다. 1위 분모는 log₂(2)=1, 2위는 log₂(3)≈1.58, 5위는 log₂(6)≈2.58로 순위가 낮아질수록 커진다.
2. IDCG (Ideal DCG): 관련도를 내림차순으로 정렬했을 때의 이상적인 DCG다.
3. NDCG = DCG / IDCG: 0~1 사이로 정규화. 1.0이면 완벽한 순위.
import numpy as np
def dcg_at_k(relevances, k):
"""DCG@k 계산"""
relevances = np.array(relevances[:k], dtype=float)
if len(relevances) == 0:
return 0.0
gains = (2 ** relevances - 1)
discounts = np.log2(np.arange(2, len(relevances) + 2))
return (gains / discounts).sum()
def ndcg_at_k(relevances, k):
"""NDCG@k 계산"""
dcg = dcg_at_k(relevances, k)
idcg = dcg_at_k(sorted(relevances, reverse=True), k)
if idcg == 0:
return 0.0
return dcg / idcg
# 예시: 관련도 [3, 2, 3, 0, 1]
rel = [3, 2, 3, 0, 1]
print(f"DCG@5: {dcg_at_k(rel, 5):.4f}")
print(f"NDCG@5: {ndcg_at_k(rel, 5):.4f}")
# sklearn으로 직접 계산
from sklearn.metrics import ndcg_score
y_true = np.array([[3, 2, 3, 0, 1]]) # 실제 관련도
y_score = np.array([[0.9, 0.8, 0.7, 0.4, 0.3]]) # 모델 예측 점수
ndcg = ndcg_score(y_true, y_score, k=5)
print(f"sklearn NDCG@5: {ndcg:.4f}")
MAP: 여러 쿼리에 걸친 전반적 랭킹 품질
**MAP(Mean Average Precision)**는 여러 쿼리에 대한 AP(Average Precision)의 평균이다. AP는 관련 문서가 나타날 때마다의 Precision을 평균한 것이다.
AP = (1/R) Σₖ P@k × rel(k)
여기서 R은 전체 관련 문서 수, P@k는 k번째까지의 정밀도, rel(k)는 k번째 결과의 관련 여부(0 또는 1)다.
def average_precision(relevant_mask):
"""
relevant_mask: [True, False, True, False, True]
True = 관련 문서, False = 무관련 문서
"""
relevant_mask = np.array(relevant_mask, dtype=bool)
n_relevant = relevant_mask.sum()
if n_relevant == 0:
return 0.0
precisions = []
hits = 0
for i, rel in enumerate(relevant_mask):
if rel:
hits += 1
precisions.append(hits / (i + 1))
return np.mean(precisions)
def mean_average_precision(queries_results):
"""
queries_results: [[True,False,True,...], ...]
각 쿼리별 관련 문서 마스크 리스트
"""
aps = [average_precision(r) for r in queries_results]
return np.mean(aps)
# 예시: 3개 쿼리
results = [
[True, False, True, False, True], # 쿼리 1
[False, True, False, True, False], # 쿼리 2
[True, True, False, False, False], # 쿼리 3
]
for i, r in enumerate(results):
ap = average_precision(r)
print(f"쿼리 {i+1} AP: {ap:.4f}")
map_score = mean_average_precision(results)
print(f"MAP: {map_score:.4f}")
MRR: 첫 번째 정답의 순위
**MRR(Mean Reciprocal Rank)**는 각 쿼리에서 첫 번째 관련 문서의 순위 역수를 평균한다.
MRR = (1/|Q|) Σₖ 1 / rank_k
1위에 관련 문서가 있으면 1.0, 2위면 0.5, 3위면 0.33, k위면 1/k을 기여한다. 관련 문서가 전혀 없으면 0을 기여한다.
def reciprocal_rank(relevant_mask):
"""첫 번째 관련 문서의 순위 역수"""
for i, rel in enumerate(relevant_mask):
if rel:
return 1.0 / (i + 1)
return 0.0
def mean_reciprocal_rank(queries_results):
"""MRR 계산"""
rrs = [reciprocal_rank(r) for r in queries_results]
return np.mean(rrs)
queries = [
[True, False, False, False], # 1위 정답 → RR=1.0
[False, False, True, False], # 3위 정답 → RR=0.33
[False, True, False, False], # 2위 정답 → RR=0.5
]
for i, q in enumerate(queries):
rr = reciprocal_rank(q)
print(f"쿼리 {i+1}: 첫 관련={q.index(True)+1}위 RR={rr:.4f}")
mrr = mean_reciprocal_rank(queries)
print(f"MRR: {mrr:.4f}") # (1.0 + 0.333 + 0.5) / 3 = 0.611
세 지표 비교와 선택 기준
# 실전 비교: 두 모델의 랭킹 성능
import numpy as np
from sklearn.metrics import ndcg_score
# 실제 관련도 (0~3 등급)
y_true = np.array([[3, 0, 2, 1, 3, 0, 2, 1, 0, 3]])
# 모델 A: 상위에 고관련 문서 배치
scores_a = np.array([[0.95, 0.10, 0.88, 0.60,
0.92, 0.15, 0.75, 0.55, 0.20, 0.90]])
# 모델 B: 고관련 문서가 중간에 분산
scores_b = np.array([[0.80, 0.40, 0.70, 0.60,
0.50, 0.30, 0.85, 0.55, 0.20, 0.45]])
for k in [3, 5, 10]:
ndcg_a = ndcg_score(y_true, scores_a, k=k)
ndcg_b = ndcg_score(y_true, scores_b, k=k)
print(f"NDCG@{k}: A={ndcg_a:.4f} B={ndcg_b:.4f}")
| 지표 | 최적 사용 시나리오 | 관련 문서 수 | 등급 구분 |
|---|---|---|---|
| NDCG | 검색 엔진, 추천 시스템 | 다수 | 있음 (0~5) |
| MAP | 정보 검색, 문서 랭킹 | 다수 | 없음 (이진) |
| MRR | QA 시스템, 단답형 검색 | 소수(주로 1개) | 없음 |
지난 글: 회귀 모델 평가 지표: MAE·MSE·RMSE·R² 완전 이해
다음 글: 클러스터링 평가 지표: 실루엣·DB·칼린스키-하라바시
읽어주셔서 감사합니다. 😊