AI/Langchain RAG

02. Documnet Loaders , Text Splitter , Embeding, Vector Store

러시안블루 크레아 의 집 2024. 8. 6. 22:51

이번 chapter 에서는 LLM에 학습되지 않은 외부 문서를 Vector store(Vector db)에  임베딩 하는 과정을 다룰 것이다.

바로 본론으로 들어가자.

 

 

참고로 필자는 LangChain을 아래와 같이 구성하였다.

1. Document Loaders : PyPDFLoader, Word Document Loader, CSV Document Loader

2. Text splitter : RecursiveCharacterTextSplitter

3. Text Embedding : HuggingFaceEmbeddings

4. Vector Store : Chroma

5 - 8. Retriever : RetrievalQA ,  MultiQueryRetriever

 

 

 

 


1. Document Loaders

Document Loader는 다양한 형식의 문서를 불러오고 이를 Langchain에 결합하기 쉬운 텍스트 형태로 변환하는 기능을 한다. 이를 통해 사용자는 txt. 형식의 문서 뿐만 아니라 pdf, word, ppt, xlsx, csv 등 거의 모든 형식의 문서를 기반으로 LLM을 구동할 수 있다.

 

1) PDF Document Loader

 

from langchain.document_loaders import PyPDFLoader

loader = PyPDFLoader("/acc/AI_project/data/출장여비지급규정-20201231.pdf")

 

 

2) Word Document Loader

from langchain.document_loaders import Docx2txtLoader

loader = Docx2txtLoader("/acc/AI_project/data/동호회규정-20230419.docx")

 

 

3) CSV Document Loader

from langchain.document_loaders.csv_loader import CSVLoader

loader = CSVLoader(file_path='/acc/AI_project/data/취업규칙_1006.csv', csv_args={
    'delimiter': ',',
    'quotechar': '"',
    'fieldnames': ['ID', 'Name', 'Position', 'Height', 'Weight', 'Sponsorship Earnings', 'Shoe Sponsor', 'Career Stage', 'Age']
})

 

 

4) URL Document Loader

LangChain은 웹에 기록된 글도 텍스트 형식으로 가져와 LLM에 활용할 수 있다. 대표적인 URL Loader는 WebBaseLoader와 UnstructuredURLLoader가 있다.

rom langchain.document_loaders import WebBaseLoader

loader = WebBaseLoader("https://www.newsis.com/view/NISX20240806_0002839611")

 

 

 

 


2. Text splitter

1) RecursiveCharacterTextSplitter by Chunk size

 

RecursiveCharacterTextSplit은 재귀적으로 문서를 분할한다. 먼저, "\n\n"(줄바꿈)을 기준으로 문서를 분할하고 이렇게 나눈 청크가 여전히 너무 클 경우에 "\n"(문장 단위)을 기준으로 문서를 분할한다. 그렇게 했을 때에도 청크가 충분히 작아지지 않았다면 문장을 단어 단위로 자르게 되지만, 그렇게까지 세부적인 분할은 자주 필요하지 않다.

이런 식의 분할 방법은 문장들의 의미를 최대한 보존하는 형태로 분할할 수 있도록 만들고, 그렇기 때문에 다수의 청크를 LLM에 활용함에 있어서 맥락이 유지되도록 하기에 용이하다.

from langchain.text_splitter import RecursiveCharacterTextSplitter

# split it into chunks
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 1000,
    chunk_overlap  = 200,
    length_function = len,
)
docs = text_splitter.split_documents(pages)

 

 

 

 

2) RecursiveCharacterTextSplitter by TOKEN (토큰 단위)

 

LLM은 텍스트를 받아들일 때, 정해진 토큰 이상으로 소화할 수 없게 설계되어 있다. 따라서 글을 토큰 단위로 분할한다면 최대한 많은 글을 포함하도록 청크를 분할할 수 있다.
토큰이라는 것은, 텍스트와 달리 Transformer에서 처리하는 방식에 따라서 그 수가 달라질 수 있다. 따라서, LLM 앱을 개발하고자 한다면 앱에 얹힐 LLM의 토큰 제한을 파악하고, 해당 LLM이 사용하는 Embedder를 기반으로 토큰 수를 계산해야 한다. 예를 들어, OpenAI의 GPT 모델은 tiktoken이라는 토크나이저를 기반으로 텍스트를 토큰화한다. 따라서 tiktoken encoder를 기반으로 텍스트를 토큰화하고, 토큰 수를 기준으로 텍스트를 분할하는 것이 프로덕트 개발의 필수 요소라고 할 수 있다.

import tiktoken
from langchain.text_splitter import RecursiveCharacterTextSplitter


tokenizer = tiktoken.get_encoding("cl100k_base")

def tiktoken_len(text):
    tokens = tokenizer.encode(text)
    return len(tokens)


# split it into chunks
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=0,
    length_function = tiktoken_len
)
docs = text_splitter.split_documents(pages)

 

 

 

 


3. Text Embeddings

 

text embedding이란 NLP(자연어 처리)에서 사용되는 기술로, text splitter 로 분할한 문서들을 기계가 이해할 수 있는 숫자, Vector로 변환하는 작업을 의미한다.

비정형 데이터를 숫자로 표현해 VectorStore에 저장하면 아래 그림과 같이 좌표 안에 위치시킬 수 있다. 텍스트 형태의 질문이 들어오면 아래의 그림과 같이 문장 간의 유사성을 비교하여 가장 가까운 벡터(L2 distance 혹은 Cosine simularity 기준)를 반환하는 원리이다.

 

 

 

 

1) Huggingface Embedding

# create the open-source embedding function
from langchain.embeddings import HuggingFaceEmbeddings

model_name = "jhgan/ko-sbert-nli"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
hf = HuggingFaceEmbeddings(
    model_name=model_name,
    model_kwargs=model_kwargs,
    encode_kwargs=encode_kwargs
)

# save to disk
db3 = Chroma.from_documents(docs, hf, persist_directory="/acc/AI_project/chroma_db")

문서들을 hf라는 huggingface 임베딩을 통해서 임베딩화 시키고 그것을 chroma 객체를 통해서 vector 저장을 한 것고 그 문장과 유사도가 높은 문장을 출력하는 것이다.

 

 

 


4. VectorStore

1) Chroma

Chroma는 대표적인 오픈소스 벡터 저장소 이다.

기본적으로 VectorStore는 벡터를 일시적으로 저장한다. 텍스트와 임베딩 함수를 지정하여 from_documents() 함수에 보내면, 지정된 임베딩 함수를 통해 텍스트를 벡터로 변환하고, 이를 임시 db로 생성한다.


###########################
### SAVE TO VECTORSTORE ###
###########################

import tiktoken
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Chroma
from langchain.document_loaders.csv_loader import CSVLoader
from langchain.document_loaders import Docx2txtLoader
from langchain.document_loaders import PyPDFLoader


# load the document and split it into chunks

loader = PyPDFLoader("/acc/AI_project/data/퇴직금지급규정-20130601.pdf")
# loader = PyPDFLoader("/acc/AI_project/data/전자결재 매뉴얼.pdf")
 
# loader = Docx2txtLoader("/acc/AI_project/data/동호회규정-20230419.docx")
# loader = CSVLoader(file_path='/acc/AI_project/data/취업규칙_1006.csv', csv_args={
    'delimiter': ',',
    'quotechar': '"',
    'fieldnames': ['ID', 'Name', 'Position', 'Height', 'Weight', 'Sponsorship Earnings', 'Shoe Sponsor', 'Career Stage', 'Age']
})


pages = loader.load_and_split()

tokenizer = tiktoken.get_encoding("cl100k_base")
def tiktoken_len(text):
    tokens = tokenizer.encode(text)
    return len(tokens)

# split it into chunks
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0, length_function = tiktoken_len)
docs = text_splitter.split_documents(pages)

# create the open-source embedding function
from langchain.embeddings import HuggingFaceEmbeddings

model_name = "jhgan/ko-sbert-nli"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
hf = HuggingFaceEmbeddings(
    model_name=model_name,
    model_kwargs=model_kwargs,
    encode_kwargs=encode_kwargs
)

# save to disk
db3 = Chroma.from_documents(docs, hf, persist_directory="/acc/AI_project/chroma_db")



#############################
### LOAD FROM VECTORSTORE ###
#############################

# load from disk
query = " [****회사] 에서 퇴직금 중간정산을 받고싶은데 회사 내규 참고해서 안내해줘"
reusult_docs = db3.similarity_search(query)
print(reusult_docs[0].page_content)


reusult_docs = db3.similarity_search_with_relevance_scores(query, k=3)
print("가장 유사한 문서:\n\n {}\n\n".format(reusult_docs[0][0].page_content))
print("문서 유사도:\n {}".format(reusult_docs[0][1]))
 

위에서 보다시피 필자는 similarity_search() 와 similarity_search_with_relevance_score() 를 사용했다.

similarity_search()   : similarity_search() 함수에 쿼리를 지정해주면 이를 바탕으로 가장 벡터 유사도가 높은 벡터를 찾고 이를 자연어 형태로 출력한다.

similarity_search_with_relevance_score()  : 쿼리와 유사한 문서(청크)를 불러올 때, 내가 얻은 유사한 문장들의 유사도를 비교할 수 있으며, 특정 유사도 이상의 문서만 출력하도록 하는 등 다양한 활용이 가능하다.

 

 

 

 


 

 

 

이상 LLM에 학습되지 않은 외부 문서를 VectorDB에  embedding 하는 과정이었다.

Retriever에 대한 기술은 다음 chapter에서 이어가겠다.

'AI > Langchain RAG' 카테고리의 다른 글

03. Retriever  (0) 2024.08.06
01. 로컬환경에(사내에) Langchain RAG 구축하기  (0) 2024.08.06