본문 바로가기
  • 컴공생의 공부 일기
  • 공부보단 일기에 가까운 것 같은
  • 블로그
🤖 AI/AI

🔬임베딩 모델로 데이터 의미 압축하기 : 검색 방식 조합/ 하이브리드 검색

by 정람지 2025. 1. 20.

LLM을 활용한 실전 AI 애플리케이션 개발

- 허정준


🪧검색 방식 조합해 성능 높이기

키워드 검색 방식 

동일한 키워드가 많이 포함될수록 유사도를 높게 평가

- 장 : 관련성이 높은 검색 결과가 나올 확률이 높음

- 단 : 키워드를 포함하지 않는다면 의미가 상통하더라도 검색하지 못함

 

의미 검색 방식 

의미가 가까우면 유사도를 높게 평가

- 장 : 키워드를 포함하지 않아도 의미가 상통하면 검색 가능

- 단 : 관련성이 낮은 검색 결과가 나올 확률이 있음

 

하이브리드 검색 방식  🎉

키워드 검색 방식 +의미 검색 방식 


🫔 키워드 검색 방식 (BM25)

TF-IDF 와 유사한 통계 알고리즘

(TF-IDF : 단어의 빈도를 이용해서 카테고리화 + 많은 문서에 등장하는 단어의 중요도를 작게 설정)

 

간단 / 적은 계산량 / 뛰어난 성능

 

 

<BM25 수식>

TF-IDF

출처 : 책

포화 효과

출처 : 책

문서 길이

출처 : 책

TF-IDF와의 공통점

  • 특정 문서에 해당 토큰이 많이 나오면 중요도는 높아진다
  • 전체 문서에 해당 토큰이 많이 나오면 중요도는 낮아진다

 

TF-IDF와의 차이점

  • 포화 효과 : 특정 문서 내에서 토큰이 많이 나오면 무한정 중요도가 높아지는 것이 아닌 (K+1) 값 이상으로 높아지지 않는다.
  • 문서 길이도 고려하여 중요도를 책정한다(짧은 문서일수록 토큰의 중요도 업)

🫔 상호 순위 조합 이해하기

하이브리드 검색 : 통계 기반 점수(ex BM25) + 임베딩 유사도 점수 ( ex 밀집 임베딩 방식)

두 방식의 합으로 점수를 산출해야 함

 

하지만 점수 분포가 다를 시 어느 한 쪽의 영향을 더 크게 받게 될 가능성이 존재함

 

문제 해결을 위한

"상호 순위 조합" Reciprocal Rank Fusion / RRF

: 각 점수에서의 순위를 활용해 점수를 산출


🪧하이브리드 검색 구현하기

밀집 임베딩 기반의 의미 검색BM25 검색을 더해 하이브리드 검색 구현하기


🫔 BM25 구현하기

예제 10.14 BM25 클래스 구현
import math
import numpy as np
from typing import List
from transformers import PreTrainedTokenizer
from collections import defaultdict

class BM25:
  def __init__(self, corpus:List[List[str]], tokenizer:PreTrainedTokenizer):
    self.tokenizer = tokenizer
    self.corpus = corpus
    self.tokenized_corpus = self.tokenizer(corpus, add_special_tokens=False)['input_ids']
    self.n_docs = len(self.tokenized_corpus)
    self.avg_doc_lens = sum(len(lst) for lst in self.tokenized_corpus) / len(self.tokenized_corpus)
    self.idf = self._calculate_idf()
    self.term_freqs = self._calculate_term_freqs()

  def _calculate_idf(self):
    idf = defaultdict(float)
    for doc in self.tokenized_corpus:
      for token_id in set(doc):
        idf[token_id] += 1
    for token_id, doc_frequency in idf.items():
      idf[token_id] = math.log(((self.n_docs - doc_frequency + 0.5) / (doc_frequency + 0.5)) + 1)
    return idf

  def _calculate_term_freqs(self):
    term_freqs = [defaultdict(int) for _ in range(self.n_docs)]
    for i, doc in enumerate(self.tokenized_corpus):
      for token_id in doc:
        term_freqs[i][token_id] += 1
    return term_freqs

  def get_scores(self, query:str, k1:float = 1.2, b:float=0.75):
    query = self.tokenizer([query], add_special_tokens=False)['input_ids'][0]
    scores = np.zeros(self.n_docs)
    for q in query:
      idf = self.idf[q]
      for i, term_freq in enumerate(self.term_freqs):
        q_frequency = term_freq[q]
        doc_len = len(self.tokenized_corpus[i])
        score_q = idf * (q_frequency * (k1 + 1)) / ((q_frequency) + k1 * (1 - b + b * (doc_len / self.avg_doc_lens)))
        scores[i] += score_q
    return scores

  def get_top_k(self, query:str, k:int):
    scores = self.get_scores(query)
    top_k_indices = np.argsort(scores)[-k:][::-1]
    top_k_scores = scores[top_k_indices]
    return top_k_scores, top_k_indices

get_scores() : 저장한 문서와 점수를 계산

get_top_k(): 상위 k 개의 점수와 인덱스를 추출

_calculate_idf() : 각 토큰이 몇 개의 문서에 등장하는지 집계

 

예제 10.15 BM25 점수 계산 확인해 보기
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('klue/roberta-base')

bm25 = BM25(['안녕하세요', '반갑습니다', '안녕 서울'], tokenizer)
bm25.get_scores('안녕')
# array([0.44713859, 0.        , 0.52354835])

"안녕" 토큰이 겹치는 두 문서의 유사도가 측정.

의미가 통하는 반갑습니다는 0

 

예제 10.16 BM25 검색 결과의 한계
# BM25 검색 준비
bm25 = BM25(klue_mrc_dataset['context'], tokenizer)

query = "이번 연도에는 언제 비가 많이 올까?"
_, bm25_search_ranking = bm25.get_top_k(query, 100)

for idx in bm25_search_ranking[:3]:
  print(klue_mrc_dataset['context'][idx][:50])

# 출력 결과
# 갤럭시S5 언제 발매한다는 건지언제는 “27일 판매한다”고 했다가 “이르면 26일 판매한다 (오답)
# 인구 비율당 노벨상을 세계에서 가장 많이 받은 나라, 과학 논문을 가장 많이 쓰고 의료 특 (오답)
# 올여름 장마가 17일 제주도에서 시작됐다. 서울 등 중부지방은 예년보다 사나흘 정도 늦은  (정답)
예제 10.17 BM25 검색 결과의 장점
query = klue_mrc_dataset[3]['question']  # 로버트 헨리 딕이 1946년에 매사추세츠 연구소에서 개발한 것은 무엇인가?
_, bm25_search_ranking = bm25.get_top_k(query, 100)

for idx in bm25_search_ranking[:3]:
  print(klue_mrc_dataset['context'][idx][:50])

# 출력 결과
# 미국 세인트루이스에서 태어났고, 프린스턴 대학교에서 학사 학위를 마치고 1939년에 로체스 (정답)
# ;메카동(メカドン)                                                      (오답)
# :성우 : 나라하시 미키(ならはしみき)
# 길가에 버려져 있던 낡은 느티나
# ;메카동(メカドン)                                                      (오답)
# :성우 : 나라하시 미키(ならはしみき)

키워드 중심으로 의미가 통하는 건 잘 찾고 / 키워드가 적은 정답은 못 찾고


🫔 상호 순위 조합 구현하기

BM25의 순위와 의미 검색 순위를 조합하는 상호 순위 조합 함수 구현

예제 10.18 상호 순위 조합 함수 구현
from collections import defaultdict

def reciprocal_rank_fusion(rankings:List[List[int]], k=5):
    rrf = defaultdict(float)
    for ranking in rankings:
        for i, doc_id in enumerate(ranking, 1):
            rrf[doc_id] += 1.0 / (k + i)
    return sorted(rrf.items(), key=lambda x: x[1], reverse=True)

 

각각의 순위 리스트를 순회하며

각각의 문서 인덱스에 1/(k+순위)의 점수 더하기

점수를 종합한 딕셔너리를 점수에 따라 높은 순으로 정렬해 반환함

rankings = [[1, 4, 3, 5, 6], [2, 1, 3, 6, 4]]
reciprocal_rank_fusion(rankings)

# [(1, 0.30952380952380953),
#  (3, 0.25),
#  (4, 0.24285714285714285),
#  (6, 0.2111111111111111),
#  (2, 0.16666666666666666),
#  (5, 0.1111111111111111)]

1은 1/6 + 1/7

2는 1/8 + 1/8

...


🫔 하이브리드 검색 구현하기

예제 10.20 하이브리드 검색 구현하기
def dense_vector_search(query:str, k:int):
  query_embedding = sentence_model.encode([query])
  distances, indices = index.search(query_embedding, k)
  return distances[0], indices[0]

def hybrid_search(query, k=20):
  _, dense_search_ranking = dense_vector_search(query, 100)
  _, bm25_search_ranking = bm25.get_top_k(query, 100)

  results = reciprocal_rank_fusion([dense_search_ranking, bm25_search_ranking], k=k)
  return results

입력받은 쿼리 문장을

1. 의미 검색, 2.BM25 키워드 검색 을 수행한 뒤

상호 순위 조합을 사용해 순위를 조합하고 결과를 반환함

 

예제 10.21 예시 데이터에 대한 하이브리드 검색 결과 확인
query = "이번 연도에는 언제 비가 많이 올까?"
print("검색 쿼리 문장: ", query)
results = hybrid_search(query)
for idx, score in results[:3]:
  print(klue_mrc_dataset['context'][idx][:50])

print("=" * 80)
query = klue_mrc_dataset[3]['question'] # 로버트 헨리 딕이 1946년에 매사추세츠 연구소에서 개발한 것은 무엇인가?
print("검색 쿼리 문장: ", query)

results = hybrid_search(query)
for idx, score in results[:3]:
  print(klue_mrc_dataset['context'][idx][:50])

# 출력 결과
# 검색 쿼리 문장:  이번 연도에는 언제 비가 많이 올까?
# 올여름 장마가 17일 제주도에서 시작됐다. 서울 등 중부지방은 예년보다 사나흘 정도 늦은  (정답)
# 갤럭시S5 언제 발매한다는 건지언제는 “27일 판매한다”고 했다가 “이르면 26일 판매한다  (오답)
# 연구 결과에 따르면, 오리너구리의 눈은 대부분의 포유류보다는 어류인 칠성장어나 먹장어, 그 (오답)
# ================================================================================
# 검색 쿼리 문장:  로버트 헨리 딕이 1946년에 매사추세츠 연구소에서 개발한 것은 무엇인가?
# 미국 세인트루이스에서 태어났고, 프린스턴 대학교에서 학사 학위를 마치고 1939년에 로체스 (정답)
# 1950년대 말 매사추세츠 공과대학교의 동아리 테크모델철도클럽에서 ‘해커’라는 용어가 처음 (오답)
# 1950년대 말 매사추세츠 공과대학교의 동아리 테크모델철도클럽에서 ‘해커’라는 용어가 처음 (오답)