포스트

2026-04-01 강의 정리

정보 검색 평가지표 (HitRate, MRR, NDCG) 이해

2026-04-01 강의 정리

목차

  1. 사용 라이브러리 및 초기 설정
  2. 검색 평가 메트릭 (Ranking Metrics)
  3. BLEU 스코어
  4. LLM-as-Judge 평가
  5. 자주 나오는 실수 / 주의사항
  6. [보충] 평가 메트릭 선택 가이드

1. 사용 라이브러리 및 초기 설정

1
2
3
4
5
6
7
8
9
10
11
12
import os
import json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from typing import List, Dict, Tuple, Any
from dotenv import load_dotenv

from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage

모델 초기화

1
2
3
4
LLM_MODEL = "gpt-4o-mini"
EMBEDDING_MODEL = "text-embedding-3-small"
llm = ChatOpenAI(model=LLM_MODEL)
embeddings_model = OpenAIEmbeddings(model=EMBEDDING_MODEL)

데이터 구조

  • 10개 문서 DB: AI/LLM 관련 주제 (트랜스포머, RAG, 임베딩, 프롬프트 엔지니어링 등)
  • 12개 QA 데이터셋: 질문 + 관련 문서 ID + ground truth 답변
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 문서 구조
documents = [
    {"doc_id": "doc_001", "title": "트랜스포머 아키텍처", "content": "..."},
    ...  # doc_001 ~ doc_010
]

# QA 데이터셋 구조
qa_dataset = [
    {
        "query_id": "q01",
        "question": "트랜스포머의 핵심 메커니즘은 무엇인가요?",
        "relevant_doc_ids": ["doc_001"],
        "ground_truth": "트랜스포머의 핵심 메커니즘은 셀프 어텐션으로..."
    },
    ...  # q01 ~ q12
]

FAISS 벡터 검색 구성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
langchain_docs = [
    Document(page_content=doc["content"],
             metadata={"doc_id": doc["doc_id"], "title": doc["title"]})
    for doc in documents
]
vectorstore = FAISS.from_documents(langchain_docs, embeddings_model)

def search_documents(query: str, k: int = 5) -> List[Dict]:
    results = vectorstore.similarity_search_with_score(query, k=k)
    return [
        {
            "doc_id": doc.metadata["doc_id"],
            "title": doc.metadata["title"],
            "content": doc.page_content,
            "score": float(score)
        }
        for doc, score in results
    ]

2. 검색 평가 메트릭 (Ranking Metrics)

RAG 시스템에서 “검색 단계”가 얼마나 잘 동작하는지 측정하는 지표들입니다.

Precision@K — 검색 결과의 정확도

“상위 K개 결과 중 실제로 관련 있는 문서 비율”

1
Precision@K = (상위 K개 중 관련 문서 수) / K
1
2
3
4
5
6
7
8
def precision_at_k(query_id, k):
    qa = next(q for q in qa_dataset if q['query_id'] == query_id)
    relevant_ids = set(qa['relevant_doc_ids'])
    retrieved = search_results_cache[query_id][:k]
    retrieved_ids = {r['doc_id'] for r in retrieved}

    relevant_retrieved = relevant_ids & retrieved_ids  # 교집합
    return len(relevant_retrieved) / k

Recall@K — 검색의 완성도

“전체 관련 문서 중 상위 K개에서 찾아낸 비율”

1
Recall@K = (상위 K개 중 관련 문서 수) / (전체 관련 문서 수)
1
2
3
4
5
6
7
8
def recall_at_k(query_id, k):
    qa = next(q for q in qa_dataset if q['query_id'] == query_id)
    relevant_ids = set(qa['relevant_doc_ids'])
    retrieved = search_results_cache[query_id][:k]
    retrieved_ids = {r['doc_id'] for r in retrieved}

    relevant_retrieved = relevant_ids & retrieved_ids
    return len(relevant_retrieved) / len(relevant_ids) if relevant_ids else 0.0

F1@K — Precision과 Recall의 균형

“Precision과 Recall의 조화평균”

1
F1 = 2 × (P × R) / (P + R)
1
2
3
4
5
6
def f1_at_k(query_id, k):
    p = precision_at_k(query_id, k)
    r = recall_at_k(query_id, k)
    if p + r == 0:
        return 0.0
    return 2 * p * r / (p + r)

R-Precision — 관련 문서 개수 기준 Precision

“관련 문서가 R개라면, 상위 R개 결과에서의 Precision”

1
2
3
4
5
6
7
def r_precision(query_id):
    qa = next(q for q in qa_dataset if q['query_id'] == query_id)
    relevant_ids = set(qa['relevant_doc_ids'])
    R = len(relevant_ids)
    if R == 0:
        return 0.0
    return precision_at_k(query_id, R)

F-Beta@K — 가중치를 부여한 F1

“Precision과 Recall 중 어느 쪽을 더 중요하게 볼지 조절”

1
F_beta = (1 + beta²) × (P × R) / (beta² × P + R)
1
2
3
4
5
6
7
def f_beta_at_k(query_id, k, beta):
    p = precision_at_k(query_id, k)
    r = recall_at_k(query_id, k)
    beta_sq = beta ** 2
    if beta_sq * p + r == 0:
        return 0.0
    return (1 + beta_sq) * p * r / (beta_sq * p + r)
beta 값의미
beta < 1Precision을 더 중요시
beta = 1F1 (동일 가중치)
beta > 1Recall을 더 중요시

실험 결과

K 값에 따른 평균 메트릭:

KP@KR@KF1@K
11.00.9580.972
30.3330.9580.492
50.2171.00.353
1
2
3
4
핵심 해석:
- K↑ → Precision↓, Recall↑ (항상 이 방향으로 움직임)
- K=1일 때 P@K=1.0 → 1위 결과가 항상 정확함
- K=5일 때 R@K=1.0 → 5개 검색하면 모든 관련 문서를 찾음

F-Beta 비교 (K=3):

betaF_beta@3
0.50.382
1.00.492
2.00.693
1
→ beta 증가 = Recall 중시 = 더 많이 찾는 것에 점수 보상

3. BLEU 스코어

생성된 텍스트의 품질을 참조 텍스트와 비교하여 측정하는 자동 평가 메트릭입니다.

동작 원리

1
2
3
4
5
6
7
BLEU = BP × exp( Σ log(Precision_n) / N )

BP (Brevity Penalty): 짧은 출력에 패널티
- hypothesis 길이 >= reference 길이 → BP = 1.0
- hypothesis 길이 < reference 길이  → BP = exp(1 - |ref|/|hyp|)

Precision_n: N-gram 단위 겹침 비율 (Clipped)

구현 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from collections import Counter

def get_ngrams(tokens, n):
    return Counter(tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1))

def bleu_score(reference, hypothesis, max_n):
    ref_tokens = reference.split()
    hyp_tokens = hypothesis.split()

    # Brevity Penalty
    c, r = len(hyp_tokens), len(ref_tokens)
    bp = 1.0 if c >= r else (np.exp(1 - r/c) if c > 0 else 0.0)

    # N-gram Precision 계산
    precision = {}
    for n in range(1, max_n + 1):
        ref_ngrams = get_ngrams(ref_tokens, n)
        hyp_ngrams = get_ngrams(hyp_tokens, n)
        if len(hyp_ngrams) == 0:
            precision[n] = 0.0
            continue
        # Clipped: reference에 있는 만큼만 인정
        clipped = sum(min(cnt, ref_ngrams.get(ng, 0))
                      for ng, cnt in hyp_ngrams.items())
        precision[n] = clipped / sum(hyp_ngrams.values())

    # 기하평균
    log_avg, valid_n = 0.0, 0
    for n in range(1, max_n + 1):
        if precision[n] > 0:
            log_avg += np.log(precision[n])
            valid_n += 1

    bleu = bp * np.exp(log_avg / valid_n) if valid_n > 0 else 0.0
    return {'bleu': round(bleu, 4), 'bp': round(bp, 4)}

실행 예시

1
2
3
4
5
ref = "트랜스포머의 핵심 메커니즘은 셀프 어텐션으로, 입력 시퀀스의 모든 위치 간 관계를 병렬로 처리합니다."
hyp = "트랜스포머는 셀프 어텐션 메커니즘을 사용하여 시퀀스 내 모든 위치 간 관계를 병렬로 처리합니다."

result = bleu_score(ref, hyp, 4)
# 출력: {'bleu': 0.3955, 'bp': 1.0}
1
2
3
4
해석:
- BLEU = 0.3955 → 의미는 비슷하지만 표현이 다르면 낮은 점수
- BP = 1.0 → hypothesis 길이가 reference보다 짧지 않음
- BLEU의 한계: 의미가 같아도 표현만 다르면 낮은 점수 → LLM 평가와 함께 사용

4. LLM-as-Judge 평가

LLM을 평가자로 사용하여 의미적 관련성을 판단하는 방법입니다.

왜 쓰나?

BLEU 같은 통계 기반 메트릭은 의미적 유사성을 못 잡음 → LLM에게 평가를 맡겨 의미적으로 판단

Context Relevance 평가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def evaluate_context_relevance(question, contexts, model):
    context_text = '\n\n'.join([
        f"[컨텍스트 {i+1}] : {ctx}" for i, ctx in enumerate(contexts)
    ])

    prompt = f"""질문과 검색된 컨텍스트의 관련성을 평가하세요.

## 질문
{question}

## 검색된 컨텍스트
{context_text}

## 평가 기준
각 컨텍스트에 대해 1-5점으로 평가하세요:
- 5 : 질문에 직접적으로 답할 수 있는 핵심 정보 포함
- 4 : 질문과 매우 관련 있는 정보 포함
- 3 : 부분적으로 관련 있는 정보 포함
- 2 : 간접적으로만 관련 있음
- 1 : 거의 관련 없음

## 응답 형식 (JSON)
{{"scores": [점수1, 점수2, ...], "average": 평균점수, "reasoning": "평가 근거"}}

반드시 유효한 JSON만 출력하세요."""

    response = model.bind(response_format={'type': 'json_object'}).invoke(
        [HumanMessage(content=prompt)]
    )
    result = json.loads(response.content)
    return result

실행 결과 예시

1
2
3
4
5
6
7
8
9
# 질문: "트랜스포머의 핵심 메커니즘은 무엇인가요?"
# 검색된 컨텍스트 3개: doc_001 (트랜스포머), doc_010 (멀티모달), doc_005 (파인튜닝)

cr_result = {
    'scores': [5, 1, 1],
    'average': 2.33,
    'reasoning': '컨텍스트 1은 트랜스포머의 핵심 메커니즘에 대한 구체적인 정보를 제공하므로 5점,
                  컨텍스트 2와 3은 관련 정보가 전혀 없으므로 각각 1점.'
}
1
2
평가 흐름:
질문 + 검색된 컨텍스트 → LLM에게 전달 → 각 컨텍스트 점수(1~5) + 근거 반환

LLM-as-Judge의 장점

항목통계 메트릭 (BLEU 등)LLM-as-Judge
의미 이해XO
유연한 기준XO
설명(reasoning)XO
자동화OO
비용무료API 비용 발생

5. 자주 나오는 실수 / 주의사항

  • embed_documents()는 list를 반환함.shape 바로 쓰면 오류. np.array(doc_embeddings).shape 로 변환 후 사용
  • Precision과 Recall은 반비례 → K 크게 하면 Recall↑, Precision↓. 목적에 맞게 K 설정
  • BLEU는 의미 판단 불가 → 표현이 달라도 의미가 같으면 낮은 점수. LLM-as-Judge와 병행
  • LLM-as-Judge에서 JSON 강제 안 하면 파싱 실패response_format={'type': 'json_object'} 반드시 설정
  • F-Beta에서 beta 의미 혼동 주의 → beta > 1이면 Recall 중시 (숫자가 크면 Recall 방향)

[보충] 평가 메트릭 선택 가이드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RAG 시스템 평가 구조:

[검색 단계 평가]               [생성 단계 평가]
  Precision@K                    BLEU (자동, 빠름)
  Recall@K          →→→         LLM-as-Judge (의미 기반, 비용)
  F1@K / F-Beta@K
  R-Precision

언제 어떤 메트릭을?

- Precision 중요: 사용자에게 노출되는 결과 수가 적을 때 (K=1, K=3)
- Recall 중요: 하나도 빠뜨리면 안 되는 도메인 (의료, 법률)
- F-Beta(beta>1): Recall 중시 시스템 (검색 엔진)
- BLEU: 빠른 1차 평가
- LLM-as-Judge: 최종 품질 검증, 의미 판단 필요할 때

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그