multi query retriever 로 여러 문서에서 필요한 키워드와 유사한 의미를 가진 문장들을 검색하는 똑똑한 챗봇을 만드는 방법을 아주 쉽게 소개합니다. 생성형 AI를 이용해서 내가 검색을 자세하게 하고 싶은 문서들을 입력해서 키워드와 정확하게 일치하지 않아도 유사한 뜻을 담고 있는 문장과 문단을 찾아주는 챗봇을 직접 만들 수 있어요.
Table of Contents
RAG 기술에 대해서 기대하는 바는 내가 하는 질문을 정확하게 이해하고
나의 질문을 동일한 의미를 지닌 문맥으로서 이해를 하면 좋겠습니다. 여러 문서에서 키워드에 관한 단어와 문장을 검색하는 기술은 multi query retriever 로 구현이 가능합니다.
Multi Query Retriever 란 무엇인가?
multi query retriever 는 생성형 AI 에서 Query 를 한번에 여러 번 하도록 설계를 하여서 사용자가 질문한 내용을 생성형 AI 가 유사한 질문 내용을 담고 있는 여러 개의 유사 질문을 만들어서 생성형 LLM 에게 입력하여 정확한 답변을 하도록 하는 기술입니다.
생성형 AI 에서는 단어나 문장 하나의 의미 만을 따지는 것이 아니라 앞 문장의 의미와 뒷 문장의 의미를 잘 연결하는 단어와 문장을 생성해 주어야 합니다. 키워드 한 개만 가지고 거대 언어 모델에서 질문에 대한 답을 찾는 방식에서 진화하고 있어요.
llama 와 ChatGPT 같은 생성형 AI는 사용자가 채팅 창에 질문을 한 가지만 입력하였는데 내부적으로 여러 질문 문장을 생성해요. 어떤 방식으로 여러 개의 질문들을 생성하느냐면 질문 문장이나 단어들을 임베딩이라는 과정을 통해서 벡터로 변환 시킨 후에 벡터 스토어 라고 하는 벡터들이 저장되어 있는 곳이 존재합니다.
사용자가 질문한 문장을 벡터로 변환한 것과 기존에 벡터 스토어에 저장된 벡터들 간에 얼마나 비슷한지 비교해 봅니다.
가장 유사도가 높은 유사 질문 문장드을 찾아냅니다.
multi query retrieve 장점
multi query retrieve 는 retrieve augmented generation 을 여러 번 호출하여 한 개의 질문을 유사한 내용을 담은 여러 개의 질문을 vectorstore 에서 가져온 후에 생성형 AI 에게 여러 개의 세부 질문을 함으로써 다양하고 정확한 답변을 얻도록 합니다.
실제로 multi query retrieve 를 google colab 에서 ipynb 파일에 작성하고 실습을 진행합니다.
multi query retrieve 따라하기
OPENAI API 를 호출하여 뉴스 웹페이지에 게재된 문서에서 질문에 대한 정답을 찾을 때 질문을 한번을 했는데 정확하게 답변을 하는 multi query retrieve 기술을 적용해 볼 것입니다.
rag 와 다른 점은 rag 는 한번 질문을 하면 문서에서 질문의 답변에 유사한 내용을 담고 있는 문장 한 개를 찾거나 문단을 한 개를 찾습니다. 실제로 코드를 천천히 살펴보면 매우 간단하게 구현되어 있습니다.
아래에는 llama3 를 이용하여 rag 로 나만의 챗봇을 만드는 포스팅을 여러 개를 게시했습니다. 아래 포스팅의 내용을 따라서 하기만 하면 라마3 를 이용해서 쉽게 챗봇을 만들어 볼 수 있습니다.
라마3 rag 을 이용하여 똑똑한 나만의 챗봇을 만들어 보기
llama3 설치 방법 및 한국어 영문 번역 챗봇 만들기
벡터 검색을 해주는 elasticsearch 엔진 설치하는 방법 알아보기
llama3 을 다루는 기술과 elastic search 로 벡터 검색을 잘하게 되더라도 근본적으로 생성형 AI 로 돈을 버는 방법을 모르면 애쓰고 고생을 하여 RAG 와 multi query retrieve 기술을 익히더라도 시간만 낭비될 수 있습니다.
아래 포스팅에 생성형 AI로 돈을 버는 방법을 자세하게 정리하였으니 꼭 확인해 보시기 바랍니다.
multi query retrieve 패키지 설치하는 방법
일단 langchain 패키지와 chromadb 와 openai 패키지를 설치합니다. 또 langchain_community 도 벡터 데이터베이스를 생성할 때 필요하므로 설치해 줍니다. 예전에 C++ 와 C 로 임베디드 시스템에서 주로 개발을 하던 때와 비교하면 정말 패키지 설치부터 편리한 다양한 리턴 타입까지 제공해주는 python 개발 환경은 개발할 때 겪는 스트레스와 고단함을 대폭 줄여주었습니다.
!pip install -q sentence-transformers langchain chromadb openai pypdf
%pip install langchain_community
벡터 스토어가 이번 구현에서는 핵심적인 역할을 합니다. LLM 에서 단어와 문장은 임베딩 이라는 모듈을 통과시키면 벡터들의 리스트가 생성되어서 나옵니다. 고양이 =(0.001, 0.002) 와 같은 형태로 문자나 이미지 데이터를 수학적인 벡터로 표현하는 것입니다.
예를 들자면 호랑이를 벡터로 표현하였을 때 호랑이 = (0.0011, 0.0021) 이고 판다 = (0.1, 0.2) 로 표현이 되었다면 고양이는 판다보다는 호랑이와 유사도가 더 높은 것으로 판정이 됩니다.
생성형 AI에서 답변을 찾는 행위는 유사도가 가장 높은 단어들을 검출하는 과정이라고 할 수 있습니다. 처음에 학습을 단어를 벡터로 매칭 시킨 관계들을 벡터스토어에 저장을 해둡니다. 엘론 머스크가 새롭게 시작하였다는 XAI 모델도 텍스트와 동영상과 이미지를 벡터로 만들어서 모아둔 거대한 벡터 스토어를 구축하는 과정이라고 할 수 있습니다.
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain_community.document_loaders import WebBaseLoader
네이버 뉴스에서 008에 해당하는 언론사의 뉴스 페이지 중에서 0005044343 에 해당하는 데이터를 가져옵니다. 먼저 loader( ) 에서 데이터를 불러들인 후에 loader.load( ) 를 호출하여 data 를 받아오는 방식입니다.
loader = WebBaseLoader("https://n.news.naver.com/mnews/article/008/0005044343")
data = loader.load()
LLM 에서는 문장이나 문단과 같이 글의 덩어리를 chunk 라고 부릅니다. 큰 덩어리가 있고 작은 덩어리로 나누기도 합니다. 데이터를 꼼꼼하게 벡터스토어에 있는 벡터와 유사도를 비교하기 위해서 앞의 chunk와 뒤의 chunk를 일부분이 겹치도록 구성하기도 합니다. 아래 예제에서는 chunk 사이즈는 300 바이트로 설정하였고 겹치는 overlap 을 두지는 않았습니다.
text_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=0)
splits = text_splitter.split_documents(data)
기존에 만들어둔 jhgan/ko-sbert-nli 라는 버트 계열의 모델을 사용합니다. ‘normalized_embeddings’ 임베딩 기법을 사용하도록 플래그 값을 True로 설정해줍니다. 실제로 임베딩 동작을 수행하여 (임베딩된) 벡터를 만들 때 사용하는 모듈은 HuggingFaceEmbeddings( )를 사용합니다.
model_name에는 위에서 설정한 “jhgan/ko-sbert-nli”를 사용하며, 인코딩할 때 어떻게 할지 설정하는 아규먼트는 위에서 설정한 ‘normalized_embeddings’를 설정합니다.
# VectorDB
model_name = "jhgan/ko-sbert-nli"
encode_kwargs = {'normalize_embeddings': True}
ko_embedding = HuggingFaceEmbeddings(
model_name=model_name,
encode_kwargs=encode_kwargs
)
결국에는 Chroma DB를 이용해서 벡터 데이터베이스를 생성합니다. 풀어서 설명하자면, splits에 이미 문서를 로딩해서 chunk단위로 끊어놓은 문장 덩어리들과 HuggingFaceEmbeddings( ) 을 이용해서 임베딩시킨 벡터들을 함께 입력해서 벡터 데이터베이스인 vector db를 생성합니다.
vectordb = Chroma.from_documents(documents=splits, embedding=ko_embedding)
MultiQueryRetriever 패키지와 ChatOpenAI 패키지를 임포트합니다.
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain.chat_models import ChatOpenAI
question = "이공계 인재가 지속적으로 이탈하는 이유는 ?"
llm = ChatOpenAI(temperature=0, openai_api_key = "OEPN_API_KEY")
retriever_from_llm = MultiQueryRetriever.from_llm(
retriever=vectordb.as_retriever(), llm=llm
)
# Set logging for the queries
import logging
logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)
unique_docs = retriever_from_llm.get_relevant_documents(query=question)
len(unique_docs)
위 코드를 실행하고 나면 아래와 같은 유사한 질문이 3개 등장합니다.
INFO:langchain.retrievers.multi_query:Generated queries: [‘1. 왜 이공계 인재들이 계속해서 이탈하는지에 대한 이유는 무엇인가요?’, ‘2. 이공계 인재들이 지속적으로 떠나는 이유에는 무엇이 있을까요?’, ‘3. 이공계에서 일하는 사람들이 지속적으로 떠나는 이유는 무엇인가요?’]
아래 코드를 실행하면 위 3개의 질문에 대한 답변이 자세하게 소개됩니다.
unique_docs
[Document(page_content='모아달라"고 말했다. 출연연이 학계나 산업계와 비교해 특별한 장점을 제공하지 못하는 상황이 우수 인력의 이탈을 유도하는 주요 원인으로 지목됐다. 출연연이 학계가 보장하는 연구에서의 자율성도, 산업계가 제공하는 높은 연봉도 제시하지 못한다는 것이다.차진웅 표준연 양자기술연구소 선임연구원은 "연구원이 목표하는 대형과제에 밀려 개별 연구자가 하고 싶은 연구를 할 수 없는 환경이 출연연 이탈 원인 중 하나"라고 말했다. 그러면서 "연구자가 자신의 전문성을 발휘할 수 있도록 연구 자율성을 강화해야 한다"고 말했다. 정진영', metadata={'language': 'ko', 'source': 'https://n.news.naver.com/mnews/article/008/0005044343', 'title': '이공계 인재 지속된 이탈에…"출연연 자율 강화해야 우수 인력 온다"'}),
Document(page_content='연구원이 참석한 가운데 29일 대전 유성구 한국표준과학연구원(표준연)에서 열렸다. 국가과학기술연구회(NST)에 따르면 2020년부터 2023년 6월까지 출연연을 퇴사해 학계, 산업계 등으로 이직한 인원은 총 720명이다. 이직자는 2020년 195명에서 2021년 202명, 2023년 220년으로 매년 늘어난 것으로 나타났다. 이직자의 절반이 넘는 376명(52.5%)은 학계로, 82명(11.4%)은 산업계로 자리를 옮겼다. 이 차관은 이날 "출연연에 우수한 인력이 들어올 수 있도록 환경을 어떻게 개선하면 좋을지 의견을', metadata={'language': 'ko', 'source': 'https://n.news.naver.com/mnews/article/008/0005044343', 'title': '이공계 인재 지속된 이탈에…"출연연 자율 강화해야 우수 인력 온다"'}),
Document(page_content='29일 대전 유성구 한국표준과학연구원(표준연)에서 열린 \'이공계 활성화 대책 TF\' 5차 회의. /사진=과학기술정보통신부정부출연연구기관(출연연)의 인재 이탈률이 매년 증가하는 가운데, 우수 이공계 인재를 출연연으로 끌어들이기 위해선 "기관 및 연구자의 자율성 강화가 핵심"이라는 제언이 나왔다. 이번 \'이공계 활성화 대책 TF\' 5차 회의는 박상욱 과학기술수석비서관, 이창윤 과기정통부 1차관을 비롯해 한국표준과학연구원(표준연), 한국기계연구원(기계연), 한국화학연구원(화학연), 한국원자력연구원(원자력연) 등 출연연 원장 및', metadata={'language': 'ko', 'source': 'https://n.news.naver.com/mnews/article/008/0005044343', 'title': '이공계 인재 지속된 이탈에…"출연연 자율 강화해야 우수 인력 온다"'}),
Document(page_content='이공계 인재 지속된 이탈에…"출연연 자율 강화해야 우수 인력 온다"\n\n\n\n\n\n\n\n\n\n\n\n\n\n본문 바로가기\n\n\n\n\n\n\nNAVER\n\n뉴스\n\n\n연예\n\n\n\n\n스포츠\n\n\n\n\n날씨\n\n\n\n\n프리미엄\n\n\n\n\n\n\n\n\n\n\n검색\n\n\n\n\n\n\n\n\n\n\n언론사별\n\n\n정치\n\n\n경제\n\n\n사회\n\n\n생활/문화\n\n\nIT/과학\n\n\n세계\n\n\n랭킹\n\n\n신문보기\n\n\n오피니언\n\n\nTV\n\n\n팩트체크\n\n\n알고리즘 안내\n\n\n정정보도 모음\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n구독', metadata={'language': 'ko', 'source': 'https://n.news.naver.com/mnews/article/008/0005044343', 'title': '이공계 인재 지속된 이탈에…"출연연 자율 강화해야 우수 인력 온다"'}),
Document(page_content='이창윤 과학기술정보통신부 제1차관이 29일 오후 대전광역시 유성구 한국표준과학연구원에서 열린 \'이공계 활성화 대책 TF 5차 회의\' 에서 토론을 주재하고 있다. 왼쪽부터 배태민 국가과학기술인력개발원장, 이호성 한국표준과학연구원장, 이창윤 과학기술정보통신부 제1차관, 주한규 한국원자력연구원장 /사진=과기정통부출연연 연구자가 얻는 금전적 보상이 부족하다는 점도 지적됐다. 김형우 기계연 반도체장비연구센터 선임연구원은 "정해진 인건비 외엔 추가로 얻을 수 있는 학계 및 산업계에 비해 인센티브가 적다"고 말했다. 전남중 화학연 책임연구원은', metadata={'language': 'ko', 'source': 'https://n.news.naver.com/mnews/article/008/0005044343', 'title': '이공계 인재 지속된 이탈에…"출연연 자율 강화해야 우수 인력 온다"'})]
Parent Document Retriever
parent document retriever 는 길이가 큰 chunk 를 더 작은 사이즈의 chunk로 여러 개로 나눈 후에 작은 사이즈의 chunk 안에서 사용자가 원하는 키워드와 유사 키워드가 발견이 되면 parent document 로 돌아가서 다른 작은 사이즈의 chunk 에서 키워드를 검색하는 동작을 반복합니다.
이번에는 pdf 파일에서 데이터를 읽어와서 문단 크기의 big chunk 를 small chunk 로 쪼개서 작은 chunk 안에서 유사한 키워드를 포함하고 있는 문장을 추출할 것 입니다. 다행스럽게도 ParentDocumentRetriever 패키지를 langchain 패키지에서 임포트해서 사용할 수 있습니다.
from langchain.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.retrievers import ParentDocumentRetriever
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import PyPDFLoader
from langchain.storage import InMemoryStore
입력데이터는 KB 부동산에서 발생한 2023 한국 부자 보고서.pdf 파일과 2024 KB 부동산보고서.pdf 파일을 사용하였습니다.
KB 부동산 보고서에서 2024년도 부동산 전망에 대해서 물어보는 질문을 하면 Parent Document Retriever 가 정확하게 부동산 전망에 대한 문장과 문단을 찾아서 가져오는지 확인을 하려고 합니다.
google colab 에 파일을 업로딩한 후에 /content/drive/myDrive/ 디렉토리에서 파일을 읽어옵니다.
loaders = [
PyPDFLoader("/content/drive/MyDrive/2023 한국 부자 보고서.pdf"),
PyPDFLoader("/content/drive/MyDrive/2024 KB 부동산 보고서_최종.pdf"),
]
docs = []
for loader in loaders:
docs.extend(loader.load_and_split())
한글 언어 모델은 ko-sbert-nli 를 사용합니다. HuggingFaceEmbeddings( ) 모듈에 모델 이름과 아규먼트를 입력하여 한글 처리용 임베딩 모듈을 생성합니다.
model_name = "jhgan/ko-sbert-nli"
encode_kwargs = {'normalize_embeddings': True}
ko_embedding = HuggingFaceEmbeddings(
model_name=model_name,
encode_kwargs=encode_kwargs
)
텍스트를 500 바이트의 청크로 나눈 child_splitter를 생성합니다. 벡터스토어에는 Chroma 를 사용하는데 한국어 ko_embedding 을 사용합니다.
child_splitter = RecursiveCharacterTextSplitter(chunk_size=500)
vectorstore = Chroma(
collection_name="full_documents", embedding_function=ko_embedding
)
ParentDocumentRetriever( )을 생성하여 retriever 를 만듭니다. 벡터스토어와 문서를 보관하는 docstore 를 입력합니다.
# The storage layer for the parent documents
store = InMemoryStore()
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
)
retreiver 에게 문서들을 입력합니다. 또한 “부동산 전망” 이라는 키워드와 유사한 문장을 담고 있는 문서들을 retriever.get_relevant_documents( ) 를 호출하여 확보합니다.
retriever.add_documents(docs, ids=None)
sub_docs = vectorstore.similarity_search("부동산 전망")
print("길이를 표시하자면: {}\n\n".format(len(sub_docs[0].page_content)))
print(sub_docs[0].page_content)
retrieved_docs = retriever.get_relevant_documents("부동산 전망")
print("글 길이: {}\n\n".format(len(retrieved_docs[0].page_content)))
print(retrieved_docs[0].page_content)
본문의 문장의 길이가 너무 긴 경우에는 800 바이트로 나누어서 parent_splitter 에 저장합니다. 작게 나누어진 chunk는 child 문서를 만들 때 사용이 됩니다. 작은 chunk 인 child_splitter는 200 바이트로 사이즈를 지정합니다. 벡터스토어에 작은 사이즈의 chunk 를 저장하면서 함께 인덱스로 지정하여 저장합니다.
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=800)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
Chroma( )에 collection_name을 지정하고 ko_embedding을 입력 함수로 설정합니다. parent 문서들을 저장하기 위해서 InMemoryStore( ) 를 할당합니다.
vectorstore = Chroma(
collection_name="split_parents", embedding_function=ko_embedding
)
store = InMemoryStore()
retriever 를 생성할 때 ParentDocumentRetriever( ) 에 벡터스토어와 문서 스토어 및 child_splitter 와 parent_splitter 를 함께 입력하여 생성합니다.
retriever = ParentDocumentRetriever(
vectorstore=vectorstore,
docstore=store,
child_splitter=child_splitter,
parent_splitter=parent_splitter,
)
retriever에 문서들을 집어 넣어줍니다.
retriever.add_documents(docs)
검색하고자 하는 키워드인 “부동산 전망”과 관련된 유사 단어가 포함된 문서들을 벡터 스토어에서 검색합니다. 검색된 문서의 내용을 프린트해주고 문서의 길이도 출력해 줍니다.
sub_docs = vectorstore.similarity_search("부동산 전망")
print(sub_docs[0].page_content)
len(sub_docs[0].page_content)
retriever 는 “부동산 전망” 이라는 키워드와 유사한 키워드가 포함된 문서들을 획득하여 retrieved_docs 에 저장합니다.
retrieved_docs = retriever.get_relevant_documents("부동산 전망")
print(retrieved_docs[0].page_content)
len(retrieved_docs[0].page_content)
마무리
multi query retriever 와 parent retriever 검색기술에 대해서 자세히 알아보았습니다. 다음 포스팅에서는 self query retrieve 와 ensemble retriever 와 long context retreiver 에 대해서 알아보도록 하겠습니다.