LLM을 이용한 의미 기반 검색

쉽고 빠르게 익히는 실전 LLM (https://product.kyobobook.co.kr/detail/S000212147276)


1. Introduction

“Text Embedding” 소개

이후 섹션 소개

  • LLM을 사용한 의미 기반 검색 소개
  • 정보 검색과 분석을 위한 도구


2. 작업

전통적인 검색 엔진

  • 사용자의 입력 내용을 받아, 해당 텍스트가 포함된 링크 제공

  • 한계점) 항상 단어가 일치하지 않을 수도!


1) 비대칭적 의미 기반 검색

의미 기반 검색

  • 사용자 query의 의미/맥락을 파악 & 검색 가능한 문서의 의미/맥락과 비교 및 대조
  • 반드시 단어 일치 필요성 X

“비대칭” 의미 기반 검색

  • 비대칭: 입력 쿼리의 의미 정보 \(\neq\) 문서/정보의 의미 정보
  • 일반적으로, “쿼리”는 “문서”보다 더 적은 정보

\(\rightarrow\) 쿼리에 정확한 단어를 사용하지 않더라도, 정확한 검색 결과를 얻어야!


Note! 의미 기반 검색이 “항상” 좋은 것은 아님.

예외 사례들:

  • (1) 대문자/구두점의 차이에 민감한 case
  • (2) 현지화된 문화 지식에 의존하는 풍자/아이러니
  • (3) 구현.유지에 더 많은 비용


3. 솔루션 개요

“비대칭적 의미 기반 검색” 시스템의 일반적인 흐름:

Step 1) 문서 저장

  • Embedding된 문서를 DB에 저장
  • 추후에 query가 주어지면, DB에서 search

Step 2) 문서 검색

  • query가 주어짐
  • Embedding 유사도 기반으로 문서 검색
  • 문서의 순위 재순위화 (re-ranking)
  • 최정 검색 결과 반환


4. 구성 요소

1) 텍스트 임베더 (Text Embedder)

역할: 텍스트 문서/단어를 받아 vector로 임베딩

여러 방법 가능

  • Open source
  • Closed source => OpenAI의 Embedding 사용함

텍스트간의 유사도:

  • e.g., cosine similarity, dot product, Euclidean distance


OpenAI의 Embedding

import openai 
from openai.embeddings_utils import get_embeddings, get_enbedding

openai.api_key = os.environ.get('OPENAI_API_KEY')
ENGINE = 'text-embedding-ada-002'

embedded_text = get_enbedding('I love to be vectorized', engine=ENGINE)

len(embedded_text) == '1536'

다양한 엔진의 종류 (ENGINE)이 있음.

  • 한번에 여러 텍스트: get_embeddings()
  • 한번에 하나의 텍스트 get_embedding()


오픈 소스 대안

E.g., BERT를 이용한 Bi-encoder

  • 두개의 BERT 모델을 훈련

    • (1) 입력 텍스트 인코딩
    • (2) 출력 텍스트 인코딩

    두 쌍이 최대한 유사해지도록 동시에 훈련


Open source 대안들은 Closed source 제품보다 더 많은 맞춤와 파인튜닝을 필요로 하지만, 더 큰 유연성 제공.


Sentence Transformer 라이브러리

  • 다양한 pretrained model 제공


Example) sentence_transformer 패키지의 Bi-encoder를 사용하여 텍스트 임베딩

from sentence_transformers import SentenceTransformer

# (1) 모델 불러오기
model = SentenceTransformer(
    'sentence-transformers/multi-qa-mpnet-base-cos-v1')

# (2) 임베딩할 문서(텍스트)
docs = [
  "Around 9 million people live in London", 
  "London is known for its financial district" ]

# (3) 임베딩하기
doc_emb = model.encode(
    docs,
    batch_size=32,
    show_progress_bar=True
)

# (4) 크기 확인
doc_emb.shape == (2,768)


2) 문서 청킹 (Document Chunking)

Text Embedding 엔진 설정 이후의 고민!

\(\rightarrow\) “ 문서를 어떻게 임베딩”할까?


전체 문서를 단일 vector로? NO!

\(\rightarrow\) Document chunking을 통해 문서를 더 작고 관리가능한 청크로 나누기


최대 토큰 범위 분할 (Max Token Window Chunking)

  • 가장 쉬운/간단한 문서 청킹 방법
  • ex) token 범위 = 500:
    • 각 청크가 토큰 500개 이하
  • 한계점: 중요한 텍스트 일부가 청크 사이에 나눠질 수 있음
  • 보완 방법: ( 비록 중복이 발생할지라도 ) 청크 간의 일정의 overlap


Example) 데이터 불러온 뒤 청킹하기

import re

def overlapping_chunks(text, max_tokens = 500, overlapping_factor = 5):
  # (1) 구두점으로 텍스트 나누기
  sentences = re.split(r'[.?!]', text)

  # (2) 각 문장의 token 수
  n_tokens = [len(tokenizer.encode(" " + s)) for s in sentences]
  
  # (3) Chunking
  chunks, n_tokens_ , chunk = [], 0, []
  for s, n in zip(sentences, n_tokens):
    if n_tokens_ + n > max_tokens:
      chuncks.append(". ".join(chunk) + ".")
      if overlapping_factor > 0:
        chunk = chunk[-overlapping_factor:]
        n_tokens_ = sum([len(tokenizer:encode(c)) for c in chunk]) 
     else:
      chunk = []
      n_tokens_ = 0
  	if token > max_tokens:
      continue
     
    chunk.append(s)
    n_tokens_ += token + 1

  return chunks


맞춤형 구분 기호 찾기

  • e.g., 페이지 분리, 단락 사이의 새로운 줄 \(\rightarrow\) 구분하기 좋은 기준일 것!


예시) 자연 공백으로 chunking하기

import re
from collections import Counter

top_K = 5
matches = re.findall(r'[\s]{1,}', pdf)
most_common_spaces = Counter(matches).most_common(top_K)

매우 실용적이지만, 원본 문서에 대한 이해도 & 많은 지식이 필요.

(참고: chunking을 위해, 머신러닝도 활용 가능)


Clustering을 통한 의미 기반 문서 생성

\(\rightarrow\) 의미적으로 유사한 작은 정보 청크를 결합하여 새로운 문서 생성

  • ex) 유사한 문장/단락을 함꼐 그룹화 하여 새로운 문서를 형성


from sklearn.cluster inport AgglomerativeClustering
froe sklearn.metrics.pairwise inport cosine_similarity
import numpy as np

# (1) Cosine similarity matrix
cos_sim = cosine_similarity(Z)

# (2) Agglomerative clustering
clustering = AgglomerativeClustering(
    n_clusters=None
    distance_threshold=0.1
    affinity='precomputed'
    linkage='complete'
	)

# (3) Fit
clustering.fit(1-cos_sim)

# (4) Cluster labels
labels = clustering.labels_

# (5) Clustering results
cluster_idx, cluster_cnt = np.unique(labels, return_counts=True)
for idx, cnt in zip(cluster_idx, cluster_cnt):
  print(f'Cluster {idx}: {cnt} embeddings')
  • 장점) 의미적으로 더 연관성 있는 청크를 생성
  • 단점) 내용의 일부가 주변 텍스트와 맥락에서 벗어날 수 있음.

\(\rightarrow\) 즉, 청크들이 서로 관련이 없을 때 (독립적일 때) 잘 작동


(청크로 나누지 않고) 전체 문서 사용

  • 문서가 너무 길지 않을 경우에만!
  • 장/단을 잘 고려해서 사용하기


Summary

Chunking 방법 개요

  • 최대 토큰 범위 Chunking (w/o 중복)
  • 최대 토큰 범위 Chunking (w/ 중복)
  • 자연 구분자를 기준으로 Chunking
  • 의미 기반 문서 생성을 위한 클러스터링
  • 전체 문서 사용 ( Chunking X )


3) 벡터 데이터베이스 (Vector Database)

Vector Database = 벡터를 빠르게 저장 & 검색하기 위한 DB

  • 의미적으로 유사한 텍스트를 검색하는 Nearest Neighbor Search (NNS) 방법을 효율적으로 수행


4) 파인콘 (Pinecone)

Pinecone = 소/중규모 데이터셋을 위한 Vector DB (100만개 미만의 항목)

  • 무료 버전 O
  • (추가 기능/확장성을 위한) 유료 버전도 O

빠른 벡터 검색에 최적화

\(\rightarrow\) 추천 시스템, 검색 엔진, 챗봇과 같이 “낮은 대기시간”이 필요한 application에 좋음


5) 오픈 소스 대안

Pinecone외에도 여러 대안이 있음.

  • Pgvector: PostgreSQL 확장 기능
  • Weaviate: 의미 기반 검색 지원 + ML 도구와 통합도 가능
  • ANNOY: 대규모 데이터셋에 최적화된 NNS을 위한 라이브러리


6) 검색 결과 재순위화 (re-ranking)

Vector DB로부터 유사도 비교를 사용하여, 주어진 query에 대한 잠재젹 결과를 검색한 후에는…

\(\rightarrow\) 사용자에게 가장 고나련된 결과가 제시되도록 “순위를 다시 지정” ( =re-ranking)


방법 1) 크로스 인코더 (Cross Encoder)

  • What? 재순위화하는 대표적인 방법
  • How? 입력 시퀀스 쌍을 취하고, 2번째 시퀀쓰가 1번째와 얼마나 관련이 있는지 점수로 예측하는 Transformer
  • 효과: 개별 이쿼드만이 아닌, “전체 쿼리의 문맥”을 고려할 수 있음.
  • 장/단점
    • 장점: 성능 향상
    • 단점: overhead 추가, 대기시간 악화
  • ex) (앞에서 사용했던) Bi-encoder
  • 또한, 특정 데이터셋에 대해 fine-tuning도 가능!


방법 2) BM25

  • 전통적인 검색 모델
  • 문서 내 쿼리 용어의 “빈도”에 따라 결과를 순위하
  • 단순히 빈도를 사용하므로, 전체 쿼리의 “문맥 고려 X”지만, 결과의 대략적인/전반적인 관련성은 향상!


7) API

사용자가 문서에 빠르게/안전하게/쉽게 접근할 수 있도록, 위의 모든 컴포넌트를 넣을 장소가 필요함!

\(\rightarrow\) 이를 위한 API 만들기!


FastAPI

  • Python으로 API를 구축하기 위한 web framework
교재 참고


5. 통합

1단계: 문서 저장

  • 1-1) 임베딩 준비를 위한 문서 저장: Chunking
  • 1-2) 텍스트 임베딩 생성: OpenAI의 embedding
  • 1-3) 쿼리 주어졌을 떄, 검색할 수 있도록 임베딩을 DB에 저장: Pinecone

2단계: 문서 검색

  • 2-1) 사용자에게 전처리되고 정라힐 수 있는 쿼리: FastAPI
  • 2-2) 후보 문서 검색: OpenAI의 embedding + Pinecone
  • 2-3) (필요 시) 후보 문서 재순위화: 크로스 인코더
  • 2-4) 최종 결과 반환: Fast API

Categories:

Updated: