새소식

Project

LoGO 해외로고 프로젝트 - RAG 1.

  • -

LoGO 해외로고, 해외진출을 희망하는 대한민국 기업을 위한 정보 검색 서비스에서 RAG 부분

첫번째 게시물이다.

 

https://github.com/khw11044/KT_BIGPRO_RAG

 

GitHub - khw11044/KT_BIGPRO_RAG

Contribute to khw11044/KT_BIGPRO_RAG development by creating an account on GitHub.

github.com

 

 

위 깃헙링크에서 코드를 따라하면 되겠다.

해당 게시물에서는 빈 프로젝트 폴더에서 시작해서 하나하나 코딩을 해본다.

첫번째 게시물은 수집한 데이터들을 VectorDB에 넣는 부분이다.

1. api키 등록

.env 파일을 생성하고 OpenAI api key를 넣는다.

OPENAI_API_KEY='sk-projxxxx' 

2.API key load

post1.ipynb을 만든다.

# API KEY를 환경변수로 관리하기 위한 설정 파일
from dotenv import load_dotenv
# API KEY 정보로드
load_dotenv()

이러면 앞으로 ChatOpenAI에서 모델 사용할 때 api key 설정을 따로 할 필요없이 자동으로 입력된다고 한다.

3. utils 폴더 만들기

utils 폴더를 만들고 config.py 파일을 만든다.

config라는 딕셔너리에 모델 명이나 temperature, chroma 위치 등을 적어두었다.
나중에 main 코드에서 위치를 찾고 일일이 고치는 것보다 해당 파일에서 고쳐가면서 실험하면 편하다.

# intfloat/multilingual-e5-small
config = {
    "llm_predictor": {
        "model_name": "gpt-4o",  # gpt-3.5-turbo,
        "temperature": 0
    },
    "embed_model": {
        "model_name": "text-embedding-ada-002",  # "intfloat/e5-small",
        "cache_directory": "",
    },
    "chroma": {
        "persist_dir": "./database/database",
    },
    "path": {
        "input_directory": "./documents",
    },
    "pkl_path": "./database/all_docs.pkl", 

4. init_vectorDB

다시 post1.ipynb으로 돌아와서, 앞으로 필요할 라이브러리를 불러온다.

# langchain_openai에서 ChatOpenAI(LLM)과 OpenAIEmbeddings(임베딩모델: text를 vector화하는 모델)을 load
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

# PDF파일등 데이터를 Chroma형식의 vectorDB에 저장하고 리트리버가 수집한 데이터에 접근하기 위해 Chroma를 load
from langchain_community.vectorstores import Chroma

# 우리가 만든 config.py에서 모델등 옵션들을 수정
from utils.config import config

# 추가 데이터를 업로드하기 위해 file을 다큐먼트로 만들고 (convert_file_to_documents) 다큐먼트를 자르는 (split_document) 함수를 load합니다.
from utils.update import convert_file_to_documents

그럼 그 밑에 datapipeline 클래스를 만들어보자.

class Datapipeline:
    def __init__(self):

    def init_vectorDB(self):
        """vectorDB 설정"""
        embeddings = OpenAIEmbeddings(model=config["embed_model"]["model_name"]) 
        vector_store = Chroma(
            persist_directory=config["chroma"]["persist_dir"],  
            embedding_function=embeddings,                     
            collection_name = 'india',                           
            collection_metadata = {'hnsw:space': 'cosine'},   
        )
        return vector_store

init_vectorDB 함수를 살펴보면

 

1. embeddings 변수로 OpenAIEmbeddings 모델을 가져온다. config에서 모델명을 정한다.

임베딩 모델을 정해야 빈 Chroma VectorStore에 저장된 내용들이 임베딩 모델로 임베딩된 벡터를 넣을 수 있기 때문이다.

 

2. persist_directory는 해당 폴더 이름의 폴더가 있으면 이미 존재하는 Chroma 벡터 스토어를 불러들이고, 없으면 해당 폴더명으로 새롭게 만든다.

 

3. embedding_function으로 임베딩 방식을 정한다.

 

4. collection_name을 통해 같은 벡터 스토어 폴더 위치라도 분리해서 관리할 수 있다. 우리는 인도라는 국가 타겟이고 앞으로 나라별로 구분할 가능성이 높으므로 국가명 별로 collection_name을 정한다.

 

5. collection_metadata를 통해 유사도 검색에 사용될 공간('hnsw:space')을 'cosine'으로 지정하여, 코사인 유사도를 사용하였다. default값은 L2이다.

 

다음 셀에서 아래와 같은 코드를 실행한다.

pipeline = Datapipeline()
vectorDB = pipeline.init_vectorDB()    

 

그러면 새로운 폴더가 생기고 거기에 chroma.sqlite3이 생긴것이 보일 것이다.

 

만약 새로운 위치에 만들고 싶다면, 또는 기존에 있는 벡터데이터를 불러오고 싶다면 아래와 같이 하면 된다.

 

# persist_dir = "./database"                            # 특정 vectorDB를 가져오고 싶다면 특정 폴더 위치를 지정해 줍니다.
# vectorDB = pipeline.init_vectorDB(persist_dir)

 

그럼 VectorDB에 뭐가 들었는지 확인해본다.

database = vectorDB.get()

database
# 비어 있는것을 확인할 수 있습니다.

 

해당 셀을 실행해보면 vectorDB가 비어있음을 알 수 있다.

5. PDF 파일 내용 넣기

update_vector_db

이제 빈 vectorDB를 채워보려고 한다.

클래스 안에 init_vectorDB 밑에 아래 함수를 추가해보자.

def update_vector_db(self, file, force=True, chunk_size=200):
        upload_documents = convert_file_to_documents(self.vector_store, file, force, chunk_size)

        if upload_documents:                                   
            self.vector_store.add_documents(upload_documents)  
            print(f"Added {len(upload_documents)} new documents to the vector store")
            return True
        else:
            print("모두 유사한 청크로 판단되어 해당 문서가 저장되지 않음")
            return False

 

convert_file_to_documents 함수를 통해 유사도 검사를 하고 유사도 검사를 통과하면

upload_documents에 어떤 값이 있을 것이다.

그럼 우리가 초기화한 vectorDB인 self.vector_store에 upload_documents 내용이 추가된다.

그럼 이제 convert_file_to_documents 함수를 만들어보자.

utils 폴더안에 update.py를 만든다.

def convert_file_to_documents(vector_store, file, force=True, chunk_size=500):
    """파일을 읽어 Langchain의 Document 객체로 변환"""
    SIMILARITY_THRESHOLD=0.3


    pattern = re.compile(r'\[(.*?)\]')
    matches = pattern.findall(file.name.split('\\')[-1])


    documents = []

    if file.name.endswith(".txt"):
        content = file.read().decode("utf-8")
        results = vector_store.similarity_search_with_score(content, k=1)
        # print(f'유사도 검사 중...results : {results}')
        if results and results[0][1] <= SIMILARITY_THRESHOLD:
            print(f"기존 DB에 유사한 청크가 있음으로 판단되어 추가되지 않음 - {results[0][1]}")
        else:
            documents = [Document(metadata={"source": file.path, "page": 0}, page_content=content)]
    else:
        # 파일에 맞는 loader
        if file.name.endswith(".pdf"):
            loader = PyPDFLoader(file.name)         
        elif file.name.endswith(".csv"):
            loader = CSVLoader(file_path=file.name)

        temp_documents = loader.load()  # loader를 통해
        for i, row in enumerate(tqdm(temp_documents, total=len(temp_documents))):
            content = row.page_content
            if not force:
                results = vector_store.similarity_search_with_score(content, k=1)
                # print(f'유사도 검사 중...results : {results}')
                if results and (results[0][1] <= SIMILARITY_THRESHOLD):
                    print(f"기존 DB에 유사한 청크가 있음으로 판단되어 추가되지 않음  - {results[0][1]}")
                    continue

            metadata = {"source": row.metadata["source"].split('\\')[-1], "page": row.metadata["page"], 'year':int(matches[-1].split('.')[0]), 'category':matches[0]}

            chunks = [content[i : i + chunk_size] for i in range(0, len(content), chunk_size)]
            doc = [Document(metadata=metadata, page_content=chunk) for chunk in chunks]
            documents.extend(doc)

    return documents

 

file이 txt파일이면 그냥 그 내용 그대로 가져와서 vector_store의 similarity_search_with_score를 통해 유사도를 측정한다.
results 값이 낮으면 낮을 수록 해당 내용이 있다는 뜻이다. 만약 새로운 내용이면 Document를 통해 메타데이터와 컨텐츠를 함께 넣어준다.

 

txt파일이 아닐경우, pdf와 csv 각각의 loader를 선언하고 loader를 통해 잘라 놓은 documents를 확인해 본다.


만약 새로운 내용이면 Document를 통해 메타데이터와 컨텐츠를 함께 넣어준다.

이때 컨텐츠는 인자로 받는 chunk_size로 정해진다. 청크사이즈를 변화해보는 실험도 필요하다.

100정도로 너무 짧아서도 안되고 2000정도로 너무 긿어서도 안된다.

 

이제 다시 post1.ipynb으로 돌아와서 아래와 같은 코드를 실행한다.

upload_file = 'documents\[정책][제약산업정보포털][2019.04.08]인도 통관 및 운송.pdf'

with open(upload_file, 'rb') as file:                               # 비슷하거나 같은 내용이더라도, 새롭게 업데이트된 내용이라 넣어주고 싶다면 force=True로 
    print(file.name)
    pipeline.update_vector_db(file, force=True, chunk_size=200)     # 이렇게 정확히 명시해줘도 되고 
    # pipeline.update_vector_db(file)                               # default값으로 넣어줘도 됩니다. 

 

force를 True로 해주면, 유사도 검사 없이 강제로 DB에 넣는것이고 False로 하면 유사도 검사를 수행하면서 DB에 넣는다.
똑같은 PDF파일을 force=False로 하고 넣어보자.

 

with open(upload_file, 'rb') as file:
    pipeline.update_vector_db(file, force=False, chunk_size=200)     # False로 하는 경우, 중복된 내용의 파일이 들어가는것을 방지해줍니다.

 

그럼 이제 다시 vectorDB에 데이터가 들어 있는지 확인해 보자.

 

# vectorDB에 들어갔는지 확인하기
database = vectorDB.get()   

database
# 비어 있는것을 확인할 수 있습니다.

 

pdf파일의 내용을 chuck_size별로 넣으면서 metadata 역시 우리가 설계한데로 들어간것을 확인할 수 있다.

 

6. PDF 파일의 내용들 제거하기

이번에는 pdf파일 소스를 기준으로 내용을 제거하자

Datapipeline 클래스에 update_vector_db 함수 밑에 아래 코드를 추가하자

def delete_vector_db_by_doc_id(self, filename):
        """
        주어진 문서 ID에 해당하는 벡터 임베딩을 삭제
        """
        # 벡터 데이터베이스에서 모든 문서 가져오기
        db_data = self.vector_store._collection.get(include=["metadatas"])
        ids = db_data['ids']
        metadatas = db_data['metadatas']
        ids_to_delete = [id for id, metadata in zip(ids, metadatas) if metadata.get('source') == filename]

        if ids_to_delete:
            self.vector_store._collection.delete(ids=ids_to_delete)
            print(f"[벡터 DB 삭제] 문서 ID [{filename}]의 임베딩을 벡터 DB에서 삭제했습니다.")
        else:
            print(f"[벡터 DB 삭제 실패] 문서 ID [{filename}]에 대한 임베딩을 찾을 수 없습니다.")

 

metadata를 기준으로 source에 원하는 filename을 갖는 경우 해당 filename을 갖는 source 메타데이터의 ids를 뽑아내고
해당 ids 리스트들을 vectorstore에서 제거한다.

그럼 아래 코드를 실행해 본다.

pdf_file = '[정책][제약산업정보포털][2019.04.08]인도 통관 및 운송.pdf'
pipeline.delete_vector_db_by_doc_id(pdf_file)

 

해당 파일의 내용이 제거되었다고 뜬다. 진짜로 제거되었는지 확인해보자.

 

all_documents = vectorDB._collection.get(include=["metadatas"])

all_documents
# 삭제 된거 확인 

 

지금까지 코드를 기반으로, 파일말고 특정 용어를 포함하거나, 특정 연도를 포함한 내용을 제거하는 함수도 추가하면 좋을 거 같다.

7. VectorDB에서 유사도가 높은 내용을 가져와 본다.

 

Retriever가 없어도 VectorDB에서 관련 내용을 검색할 수 있다.

 

1. similarity_search

# 문서 조회1
query = '인도 통관 및 운송'   # 질문할 문장
k = 3                      # 유사도 상위 k 개 문서 가져오기.

result = vectorDB.similarity_search(query, k = k) #← 데이터베이스에서 유사도가 높은 문서를 가져옴
print(len(result))
print(result[0].page_content)

print('-'*50)
print()
for i, doc in enumerate(result):
    print(i)
    print(f"문서 내용: {doc.page_content}") # 문서 내용 표시
    print('---'*10)

 

3. mmr search

 

mmr_docs = vectorDB.max_marginal_relevance_search(query, k=4, fetch_k=10)
print(len(mmr_docs))
print(mmr_docs[0].page_content)

 

4. similarity_search_with_score

 

question = "인도 통관 및 운송에 대해서 알려줘."

results = vectorDB.similarity_search_with_score(question, k=1)

results[0][1]

 

 

다음은 RAG Pipeline을 만들어 본다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.