Ensemble retriever

Photo of author

By mimoofdm@naver.com

ensemble retriever 에 대해서 자세히 다루고 여러 개의 문서에서 검색 키워드와 동일한 키워드를 빠르게 검색하는 bm25retriever 와 유사한 문장과 문단을 빠르게 찾는 방법에 관하여 설명드립니다.

ensemble retriever 란?

ensemble retriever 란 정확도는 낮지만 빠르게 검색하는 bm25retriever 와

정확하게 검색하지만 검색 속도가 느린 편인 FAISS retriever 검색을 결합하여 적절히 빠르면서 정확도가 평균 수준 이상으로 높은 retriever 를 만들어 보겠습니다.

이번에는 새롭게 bm25 retriever 패키지인 rank_bm25 를 설치하고 정밀한 검색을 지원하는 faisss-gpu 패키지를 설치합니다.

!pip install -q langchain pypdf sentence-transformers chromadb langchain-openai faiss-gpu langchain_community --upgrade --quiet  rank_bm25 > /dev/null

Langchain 에 포함된 langchain.retrievers 패키지에 EnsembleRetriever 가 포함되어 있습니다. Vector Search 를 할 때 사용하는 FAISS 패키지는 langchain_community.vectorstores 에 포함되어 있습니다. 임베딩을 할 때에는 HuggingFaceEmbeddings 모듈을 사용합니다.

from langchain.retrievers import BM25Retriever, EnsembleRetriever
from langchain_community.vectorstores import FAISS
from langchain.embeddings import HuggingFaceEmbeddings

한글을 지원하기 위해서 ko-sbert-nli 모델을 사용합니다.

model_name = "jhgan/ko-sbert-nli"

한국어 임베딩은 HuggingFaceEmbeddings 을 사용하고 model_name은 ko-sbert-nli 를 사용하며 normalize_embeddings 를 True로 설정하여 생성합니다.

encode_kwargs = {'normalize_embeddings': True}
ko_embedding = HuggingFaceEmbeddings(
    model_name=model_name,
    encode_kwargs=encode_kwargs
)

pdf 문서에서 읽어들인 문장을 나누기 위해서 RecursiveCharacterTextSplitter 를 사용하기 위해서 임포트 해주고 PyPDFLoader 도 불러옵니다.

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import PyPDFLoader

loaders = [
    PyPDFLoader("/content/2023-한국-부자-보고서.pdf"),
    PyPDFLoader("/content/2024-KB-부동산-보고서_최종.pdf"),
]

loaders 리스트에서 loader를 한 개씩 꺼내서 문서 단위로 나눠줍니다.

doc_list= []
for loader in loaders:
    doc_list.extend(loader.load_and_split())

문서들(docs) 에서 텍스트를 꺼내서 chunk size는 600 으로 설정하고 겹쳐지는 overlap은 200단어로 설정하여 텍스트를 나눠줍니다. 문서들의 목록인 doc_list 를 split_documents( ) 메서드에 입력하여 문서에서 텍스트를 추출합니다.

text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=200)

texts = text_splitter.split_documents(doc_list)
print(texts)

texts에 저장된 텍스트 데이터에서 키워드를 빠르게 검색하기 위해서 bm25retriever 를 생성하고 한번에 검색할 수 있는 텍스트는 4개로 지정합니다.


bm25_retriever = BM25Retriever.from_documents(texts)
bm25_retriever.k = 4




정밀한 키워드 검색을 할 때 사용하기 위해서 임베딩 방식은 한글 임베딩을 지원하는 ko_embedding 객체를 사용하여 텍스트들을 입력한 후에 벡터들을 생성하여 faiss_vectorestore 에 저장해 줍니다.

embedding = ko_embedding
faiss_vectorstore = FAISS.from_documents(texts, ko_embedding)

faiss_vectorstore에서 검색하는 역할을 수행하며 검색을 한번에 4 개 씩 해주는 retriever 를 만듭니다.

faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 4})

본격적으로 앙상블 리트리버를 만들기 시작합니다. Ensemble retriever는 bm25retriever 의 빠른 검색 기능과 faissretriever 의 느린 검색 기능을 반 반 씩 섞어서 느리지도 빠르지도 않은 적절한 속도로 검색하는 기능을 제공합니다.


ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5]
)

예제로서 검색하고자 하는 키워드는 “2023년 주택 인허가 물량” 입니다. 키워드와 유사한 것으로 검색된 문장들을 doc_list에 저장합니다. 문장들을 하나씩 꺼내서 메타데이터도 프린트해주고 페이지에 담긴 컨텐츠도 프린트해줍니다.

doc_list = ensemble_retriever.invoke("2023년 주택 인허가 물량")

for doc in doc_list:
  print(doc.metadata)
  print(":")
  print(doc.page_content)
  print("#"*70)

FAISS Retriever 만들기

이번에는 정밀한 검색을 해주는 FAISS 를 이용해서 faiss_retriever 를 만들어 보겠습니다.

faiss_vectorstore = FAISS.from_documents(texts, ko_embedding)

faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 4})

아래의 faiss_retriever 에 “원리금 상환액” 이라는 키워드를 입력해서 검색을 시키면 키워드를 포함하고 있는 문서의 페이지들이 검색되어서 리턴됩니다. 각각의 문서의 메타데이터를 출력하고 페이지의 컨텐츠를 출력해 준 후에 각 페이지 마다 “#####” 이라는 페이지 경계 구분선을 출력해 줍니다.

한마디로 아래 코드가 무슨 일을 해주는지 요약하자면 “원리금 상환액” 이 포함된 문장을 벡터 검색 방식인 FAISS 패키지를 사용해서 pdf 문서에서 여러 페이지들을 찾아주고 각 페이지 별로 검색된 내용을 프린트해준다는 의미입니다.

docs = faiss_retriever.invoke("원리금 상환액")
for i in docs:

  print(i.metadata)
  print(":")
  print(i.page_content)
  print("#"*100)

OpenAI의 api 를 사용하기 위해서 유료로 결제한 OPENAI_API_KEY를 입력해줍니다. OPENAI_API_KEY는 1회 호출 할 때 마다 토큰이 여러 개가 차감이 됩니다. OPENAI_API_KEY 를 발급 받으시려면 아래에서 발급 받으실 수 있습니다.

아래 코드들을 직접 실행해보려면 google colab 에 가입해서 로그인을 하셔야 합니다.

import os

os.environ["OPENAI_API_KEY"] = 'OPENAI_API_KEY'

from langchain.chains import RetrievalQA

from langchain_openai import ChatOpenAI

ChatGPT 모델은 토큰 당 사용 비용이 저렴한 gpt-3.5-turbo 를 사용하도록 설정하였습니다. 생성형 AI의 문제점으로 지적되는 환각 현상 ( hallucination ) 을 방지하기 위해서 temperature = 0 으로 설정하였습니다. 만일 다양하고 창의적인 답변을 원하신다면 temperature = 1로 설정하시면 됩니다.

openai = ChatOpenAI(model_name="gpt-3.5-turbo", temperature = 0)

Large Language Model 로서 openai 를 사용하고 langchain 의 타입을 stuff 로 지정하였습니다. 이때 사용하는 검색 엔진은 위에서 우리가 직접 코딩했던 ensemble_retriever 로 설정해 줍니다. 타겟으로 하는

QA = RetrievalQA.from_chain_type(llm = openai,
                                 chain_type = "stuff",
                                 retriever = ensemble_retriever,
                                 return_source_documents = True)


query = "주택담보 대출"
result = QA(query)

print(result['result'])

검색 키워드를 “주택담보 대출” 로 설정해서 검색하는 쿼리문을 생성해서 전송합니다. 응답으로 돌아온 result에 담겨있는 내용을 살펴보기 위해서 한 개 씩 프린트를 해줍니다.

키워드를 담고 있는 검색 결과의 메타데이터와 검색된 문단의 컨텐츠를 프린트해서 눈으로 확인할 수 있도록 해줍니다.

for i in result['source_documents']:
  print(i.metadata)
  print("#"*80)
  print(i.page_content)
  print("#"*80)

Faiss Retriever 만드는 방법

정밀한 검색을 할 때 사용하는 FAISS retriever 를 만들어봅시다. 한글로 작성된 pdf 문서에서 키워드를 검색하도록 할 것 입니다. 앞에서 보았던 bm25 retriever 와 달라진 점은 faiss Retreiver 는 느리지만 꼼꼼하게 키워드와 유사한 단어와 유사한 표현들이 담겨있는 문단과 페이지를 찾아준다는 점입니다.

요즘에는 로톡에서 만든 법률 AI 인 빅케이스 라는 인공지능 법률 서비스나 엘박스 라고 하는 법률 인공지능 서비스가 등장하였습니다. 법률 AI에 사용되는 초거대 언어 모델은 temperature=0 으로 설정하고 각 법률 모델을 제작한 회사에서 제공한 판례 450만 개와 조문과 학술 논문 문서 안에서만 검색하도록 설계가 되어 있습니다. 자세한 법률 AI 에 관한 설명은 아래 글을 읽어보시면 자세하게 무료로 사용하는 방법까지 소개가 되어 있습니다.

로톡이 만든 법률 AI 와 인공지능 AI 서비스 엘박스 무료로 사용하는 방법 알아보기

생성형 AI 가 신기하기는 하지만 어떻게 돈을 벌어야 할지 잘 모르시는 분들을 위해서 ChatGPT 와 Gemini 와 라마3 과 같은 생성형 AI 를 이용해서 돈 버는 방법에 관하여 자세히 설명을 하였으니 꼭 확인해 보시기 바랍니다.

ChatGPT4-o 와 Gemini 와 llama3 과 같은 생성형 AI 로 돈버는 방법 자세히 알아보기

다시 정밀한 벡터 검색 기능을 제공하는 FAISS Retriever 를 구현하는 내용으로 돌아가겠습니다.

위에서 검색된 문서들 (docs ) 을 한국어로 임베딩할 수 있도록 ko_embedding을 입력하여 FAISS.from_documents( ) 메서드를 호출하면 faiss_vectorestore를 리턴합니다.

faiss_vectorestore 는 말 그대로 벡터 스토어 로서 여러 개의 한글 단어를 벡터로 만들 벡터 리스트들을 보관하고 있는 저장소 입니다. 스토리지 기능에서 한발 더 나아가서 벡터 검색까지 해주도록 기능을 부여하기 위해서 faiss_vectorstore.as_retriever( ) 메서드를 호출해 줍니다. 이때, 입력 인자로 아규먼트의 갯수는 4 로 지정을 하여 벡터 검색을 할 때 인덱스를 한번에 4개 씩 병렬로 검색하는 fais_retriever 를 생성합니다.

faiss_vectorstore = FAISS.from_documents(docs, ko_embedding)


faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 4})

이전 내용에서는 ensemble retriever 를 이용해서 langchain 을 생성하고 언어 모델은 openai 모델을 적용하였는데 벡터 검색의 성능을 확인해 보기 위해서 faiss_retriever 를 입력받고 LLM 은 Ensemble retriever와 동일하게 openai 를 사용합니다.

최종적으로 생성된 Faiss Retriever 는 QA 라는 객체로 생성됩니다. QA(query) 를 호출하여 query 에 해당하는 “주택담보 대출” 이라는 키워드를 벡터 검색 방식으로 쿼리문을 벡터 데이터베이스로 보내서 검색을 합니다.

검색된 결과는 result에 저장되며 프린트하여 result 값을 확인해 봅니다.

QA = RetrievalQA.from_chain_type(llm = openai,
                                 chain_type = "stuff",
                                 retriever = faiss_retriever,
                                 return_source_documents = True)


query = "주택담보 대출"
result = QA(query)
print(result['result'])

검색하려는 문서는 “2024-KB-부동산-보고서_최종.pdf” 입니다. 문서에서 한페이지 씩 메타데이터와 페이지의 컨텐츠를 프린트 해주고 각 페이지 마다 “####” 으로 구분선을 출력해줍니다.

for i in result["/content/2024-KB-부동산-보고서_최종.pdf"]:
  print(i.metadata)
  print("#"*80)
  print(i.page_content)
  print("#"*80)

마무리

ensemble retriever 는 bm25retriever 와 faiss retriever 를 결합하여 검색 속도가 빠르면서도 정밀한 검색 기능을 제공합니다. 예제를 통해서 문서에 내용을 추가하고 문서에서 검색 요청한 키워드를 정확하게 찾는 동작을 보여줍니다. 무료로 사용할 수 있는 ensemble retriever 를 이용해서 ChatGPT4-o 만큼 정확한 답변을 하는 챗봇을 만들 수 있습니다.

ensemble retriever 와 bm25retriever 및 faiss retriever 를 이용해서 ChatGPT 와 Gemini 와 라마 와 같은 생성형 ai 가 지니고 있는 본질적인 문제점인 환각 현상을 제거할 수 있습니다. 실제로 학교 숙제를 하거나 회사에서 주어진 업무 자료 내에서 답변을 정확하게 찾으려고 할 때 사용하면 정확한 검색 결과를 제시해 주어서 매우 유용합니다.

llama3 을 이용해서 집에서도 무료로 정확한 답변을 해주는 챗봇을 만드는데 관심 있는 분들은 아래의 글을 참고하시면 큰 도움이 되실 것입니다.

라마3 을 이용해서 정확한 답변을 해주는 챗봇을 만드는 방법

llama3 설치 방법 및 한글과 영문을 번역해 주는 챗봇을 만드는 방법

Multi Query Retriever 로 똑똑한 챗봇 만드는 방법