LLM 엔지니어링7장 · 자체 임베딩과 벡터DB, 검색 품질9 / 22← 이전목차다음 →MetaContents

7장. 자체 임베딩과 벡터DB, 검색 품질

검색 품질 평가

학원에서 모의고사를 보면 점수가 나와 어느 과목이 약한지 압니다. 검색도 정답이 붙은 질문 묶음으로 시험을 봐야 무엇을 고쳐야 할지 압니다. 느낌으로 "잘 찾는 것 같다"고 넘기지 않습니다.

질문 세 개의 정답 순위가 1등, 3등, 순위 밖일 때 hit@3 과 MRR 을 계산하는 그림

질문 세 개 가운데 첫째는 정답 조각이 1등, 둘째는 3등, 셋째는 위 3개 밖입니다. hit@3 은 위 3개 안에 든 질문의 비율이라 2/3 이고, MRR 은 1, 1/3, 0 의 평균이라 0.444 입니다. MRR 은 정답이 위에 있을수록 더 높은 점수를 줍니다. 여기서 모의고사가 질문 묶음, 과목별 점수가 hit@k 와 MRR 입니다.

지표 견주기

지표계산알려 주는 것
hit@1정답이 1등인 질문의 비율첫째 조각만 붙일 때 맞는가
hit@k정답이 위 k 개 안인 질문의 비율k 개를 붙이면 LLM 이 답을 볼 수 있는가
MRR1 / 정답 순위의 평균(못 찾으면 0)정답을 얼마나 위에 올리는가

정답 표는 질문마다 "어느 조각이 정답인지"를 적은 것입니다. 이 장의 it_helpdesk_qa.jsonl 은 조각 번호 대신 정답 조각에 들어 있어야 할 글(근거)을 적어 두어, 조각을 어떻게 나누든 그 글이 든 조각을 정답으로 봅니다.

순위로 지표 계산하기

Python
def metrics(ranks, k=3):
    """ranks: 질문마다 정답의 순위(1 부터). 못 찾았으면 None"""
    n = len(ranks)
    hit1 = sum(r == 1 for r in ranks) / n
    hitk = sum(r is not None and r <= k for r in ranks) / n
    mrr = sum(1 / r for r in ranks if r is not None) / n
    return round(hit1, 3), round(hitk, 3), round(mrr, 3)

print(metrics([1, 3, None]))          # 위 그림의 세 질문
print(metrics([1, 1, 2, 1, 5]))

그림의 세 질문은 (0.333, 0.667, 0.444), 둘째 줄은 (0.6, 0.8, 0.74) 입니다.

코드에 나오는 함수

코드쉬운 뜻
ranks질문마다 정답 조각이 몇 등이었는지 적은 목록이다. 위 k 개 안에서 못 찾았으면 None 이다
sum(r == 1 for r in ranks) / n1등인 질문의 수를 세어 전체 질문 수로 나눈다. True 는 1, False 는 0 으로 더해진다. 이것이 hit@1 이다
r is not None and r <= k정답을 찾았고 그 순위가 k 이하인지 본다. None 을 먼저 걸러야 None <= 3 같은 오류가 나지 않는다
1 / r순위의 역수다. 1등은 1, 2등은 0.5, 3등은 0.333 이다
sum(...) / n (MRR 줄)찾은 질문의 역수만 더하고 전체 질문 수로 나눈다. 못 찾은 질문은 0 을 더한 것과 같다

이 코드가 뜻하는 것

모의고사 채점표로 생각합니다. 질문 하나가 문제 하나이고, 정답이 몇 등에 나왔는지가 그 문제의 답안입니다. hit@k 는 "k 등 안에만 들면 맞음"으로 채점한 점수이고, MRR 은 "1등이면 만점, 뒤로 갈수록 부분 점수"로 채점한 점수입니다. 여기서 문제지가 질문 묶음, 채점 기준이 지표입니다.

줄하는 일쉽게 말하면
hit1 = ...1등인 질문의 비율첫 답만 보고 맞힌 비율
hitk = ...k 등 안인 질문의 비율세 번 안에 맞힌 비율
mrr = ...역순위의 평균빨리 맞힐수록 점수를 더 주는 채점
metrics([1, 3, None])그림의 세 질문을 채점한다세 문제짜리 모의고사
metrics([1, 1, 2, 1, 5])다섯 질문을 채점한다다섯 문제짜리 모의고사

둘째 줄에서 5등인 질문은 hit@3 에서는 틀린 것으로 셉니다. 그래도 MRR 에서는 1/5 = 0.2 만큼 부분 점수를 받아, 1 + 1 + 0.5 + 1 + 0.2 = 3.7 을 5 로 나눈 0.74 가 됩니다.

작게 줄여서 눈으로 보기

위 코드는 순위가 이미 주어져 있습니다. 실제로는 검색 결과 목록에서 정답이 몇 번째인지 먼저 찾아야 합니다. 그 한 걸음을 따라가 봅니다.

Python
import numpy as np

ranked = ["와이파이", "계정 잠김", "보안 인쇄", "비밀번호 주기"]   # 검색이 내놓은 순서(설명용)
answer = "보안 인쇄"                                               # 정답 조각
rank = ranked.index(answer) + 1                                    # 0 부터 세므로 1 을 더한다
print("정답 순위:", rank)
print("hit@1:", rank <= 1, " hit@3:", rank <= 3)
print("역순위 1/순위:", round(1 / rank, 3))

ranks = [1, 3, None]
rr = [0 if r is None else 1 / r for r in ranks]
print("질문별 역순위:", [round(x, 3) for x in rr], " MRR:", round(float(np.mean(rr)), 3))
출력
정답 순위: 3
hit@1: False  hit@3: True
역순위 1/순위: 0.333
질문별 역순위: [1.0, 0.333, 0]  MRR: 0.444
결과읽는 법
정답 순위 3"보안 인쇄"가 목록의 세 번째에 있습니다. index 는 0 부터 세서 2 를 돌려주므로 1 을 더했습니다
hit@1 False, hit@3 True1등은 아니지만 위 3개 안에는 들었습니다
역순위 0.3333등이라 1/3 입니다
질문별 역순위와 MRR 0.444그림의 세 질문(1등, 3등, 못 찾음)을 1.0, 0.333, 0 으로 바꿔 평균을 냈습니다. 위 metrics 의 결과와 같습니다

실습 4에서는 질문마다 이 "정답이 몇 번째인가"를 구한 뒤 같은 셈을 합니다. 다만 정답 판정은 조각 이름이 아니라 근거 글이 든 조각인지로 합니다.

주의할 점

  • 질문 묶음은 실제 사용자가 할 법한 말로 만듭니다. 문서의 문장을 그대로 베낀 질문은 어떤 검색이든 잘 찾아 시험이 너무 쉬워집니다. 이 장의 질문은 문서와 낱말을 일부러 바꾼 "뜻" 질문과, 코드·이름이 그대로 든 "낱말" 질문을 섞었습니다.
  • 질문이 수십 개면 한두 개 차이는 우연일 수 있습니다. 작은 차이로 결론을 내리지 않고, 틀린 질문을 하나하나 읽어 봅니다.
← 이전 절7장 · 문서 나누기(청킹)다음 절 →7장 · 하이브리드 검색