ensemble retriever 에 대해서 자세히 다루고 여러 개의 문서에서 검색 키워드와 동일한 키워드를 빠르게 검색하는 bm25retriever 와 유사한 문장과 문단을 빠르게 찾는 방법에 관하여 설명드립니다.
Table of Contents
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 을 이용해서 정확한 답변을 해주는 챗봇을 만드는 방법