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

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

하이브리드 검색

맛집을 고를 때 별점만 보지 않고 리뷰의 낱말("주차 가능")도 봅니다. 둘을 함께 보면 한쪽만 볼 때 놓치는 집을 덜 놓칩니다. 하이브리드 검색은 키워드 검색과 의미 검색의 점수를 섞습니다.

키워드 점수와 의미 점수를 각각 0~1 로 맞춘 뒤 가중 합으로 섞어 순위를 정하는 흐름도

1번과 2번에서 키워드 점수(TF-IDF)와 의미 점수(임베딩)를 따로 구합니다. 두 점수는 크기가 달라서, 3번에서 질문마다 가장 작은 값을 0, 가장 큰 값을 1 로 맞춥니다. 4번에서 의미 점수에 α, 키워드 점수에 1 - α 를 곱해 더하고 높은 순서로 줄 세웁니다. 여기서 별점이 의미 점수, 리뷰 낱말이 키워드 점수입니다.

섞는 두 방법

방법섞는 것좋은 점아쉬운 점
가중 합0~1 로 맞춘 점수 × 무게α 로 비중을 조절한다α 를 질문 묶음으로 골라야 한다
RRF순위의 역수 1/(60 + 순위)점수 크기를 맞출 필요가 없다한쪽이 약하면 그 순위도 똑같이 섞인다

점수 섞어 보기

Python
import numpy as np

kw = np.array([0.30, 0.05, 0.10, 0.00])     # 조각 4개의 키워드 점수(설명용 숫자)
sem = np.array([0.80, 0.86, 0.79, 0.78])    # 의미 점수: 고만고만하게 높다

def minmax(a):
    """가장 작은 값을 0, 가장 큰 값을 1 로 맞춘다"""
    return (a - a.min()) / (a.max() - a.min())

for alpha in [0.0, 0.5, 0.7, 1.0]:
    mixed = alpha * minmax(sem) + (1 - alpha) * minmax(kw)
    print(alpha, mixed.round(2), "1등:", int(mixed.argmax()))

α 가 0 이면 키워드만 보아 0번이, 1 이면 의미만 보아 1번이 1등입니다. 0.5 에서는 0번(0.63)이 1번(0.58)을 조금 앞서고, 0.7 에서는 1번(0.75)이 0번(0.48)을 앞섭니다. α 를 어디에 두느냐에 따라 순위가 바뀌므로, 정답 표로 재서 고릅니다(실습 6).

코드에 나오는 함수

코드쉬운 뜻
kw, sem조각 4개의 키워드 점수와 의미 점수다. kw 는 keyword(키워드), sem 은 semantic(의미)의 앞 글자다
a.min(), a.max()배열에서 가장 작은 값과 가장 큰 값이다
(a - a.min()) / (a.max() - a.min())min-max 맞추기다. 가장 작은 값은 0, 가장 큰 값은 1, 나머지는 그 사이의 비율로 바뀐다
alpha * minmax(sem) + (1 - alpha) * minmax(kw)가중 합이다. 의미 점수에 α, 키워드 점수에 1 - α 만큼 무게를 준다. alpha 는 그리스 글자 α 의 이름이다
mixed.argmax()섞은 점수가 가장 큰 조각의 번호를 돌려준다

이 코드가 뜻하는 것

맛집 고르기로 보면, 별점(의미 점수)과 리뷰 낱말 점수(키워드 점수)를 같은 100점 만점으로 바꾼 뒤 "별점 70%, 리뷰 30%"처럼 섞어 1등 집을 정하는 일입니다.

줄하는 일쉽게 말하면
kw = ..., sem = ...두 검색의 점수를 적는다리뷰 점수와 별점
def minmax(a)점수를 0~1 로 맞추는 함수서로 다른 만점을 같은 만점으로 바꾼다
for alpha in [...]무게를 네 가지로 바꿔 본다별점을 얼마나 믿을지 바꿔 본다
mixed = ...무게대로 섞은 점수최종 점수
print(..., mixed.argmax())1등 조각의 번호를 찍는다오늘 갈 집

의미 점수(0.78~0.86)는 차이가 작아 그대로 더하면 키워드 점수의 차이에 묻히거나, 반대로 크기가 커서 키워드 점수를 덮습니다. 0~1 로 맞추면 두 점수가 같은 눈금에서 겨룹니다.

α 0.7 한 번만 손으로 따라가기

섞는 셈을 단계마다 찍어 봅니다.

Python
import numpy as np

kw = np.array([0.30, 0.05, 0.10, 0.00])
sem = np.array([0.80, 0.86, 0.79, 0.78])

def minmax(a):
    return (a - a.min()) / (a.max() - a.min())

print("키워드 0~1:", minmax(kw).round(3))
print("의미   0~1:", minmax(sem).round(3))
alpha = 0.7
print("의미 × 0.7 :", (alpha * minmax(sem)).round(3))
print("키워드 × 0.3:", ((1 - alpha) * minmax(kw)).round(3))
mixed = alpha * minmax(sem) + (1 - alpha) * minmax(kw)
print("합         :", mixed.round(3))
출력
키워드 0~1: [1.    0.167 0.333 0.   ]
의미   0~1: [0.25  1.    0.125 0.   ]
의미 × 0.7 : [0.175 0.7   0.088 0.   ]
키워드 × 0.3: [0.3  0.05 0.1  0.  ]
합         : [0.475 0.75  0.188 0.   ]
결과읽는 법
키워드 0~1가장 큰 0.30 이 1, 가장 작은 0.00 이 0 이 되었습니다. 0.05 는 0.167, 0.10 은 0.333 입니다
의미 0~10.78~0.86 의 좁은 차이가 0~1 로 넓어졌습니다. 1번 조각(0.86)이 1 입니다
의미 × 0.7, 키워드 × 0.3무게를 곱한 몫입니다. 의미 쪽은 최대 0.7, 키워드 쪽은 최대 0.3 까지만 보탭니다
합1번 조각이 0.75 로 1등, 0번 조각이 0.475 로 2등입니다. 위 코드의 α 0.7 줄(0.75, 0.48)과 같은 값입니다

0번 조각은 키워드에서 1등(1.0)이었지만 무게 0.3 이 곱해져 0.3 만 보탰고, 의미 쪽에서 0.175 를 더해 0.475 에 그쳤습니다. α 가 클수록 의미 검색의 1등이 이기기 쉽다는 것을 숫자로 볼 수 있습니다.

주의할 점

  • 하이브리드가 늘 낫지는 않습니다. 한쪽 검색이 이미 훨씬 좋으면, 섞어서 오히려 첫째 자리가 나빠질 수 있습니다. 섞기 전후를 같은 질문 묶음으로 잽니다.
  • α 를 고른 질문 묶음으로만 성적을 보고하면 점수가 부풀려집니다. 질문이 넉넉하면 고르는 데 쓰는 질문과 성적을 보고하는 질문을 나눕니다(1장의 검증용과 시험용).
← 이전 절7장 · 검색 품질 평가다음 절 →7장 · 재정렬