RAG 활용하기 (PyPDF, RecursiveCharacterTextSplitter, CacheBackedEmbeddings)

2star_·2024년 11월 14일
0

AI활용

목록 보기
12/13

인공지능최신동향.pdf파일을 로드해서 RAG 실습을 해 봤다.

코드

API 호출 및 파일 로드

import os
from dotenv import load_dotenv

# .env 파일 로드
load_dotenv()

# 환경 변수에서 API 키 불러오기
api_key = os.getenv("OPENAI_API_KEY")

#print(api_key) 확인

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

# 모델 초기화
model = ChatOpenAI(model="gpt-4o-mini")

from langchain_community.document_loaders import PyPDFLoader

file_path = "인공지능최신동향.pdf"
# PDF 파일 경로
loader = PyPDFLoader(file_path=file_path)

docs = loader.load()

PyPDFLoader?

LangchainPyPDFLoader를 통해 PDF파일을 불러왔다.

PyPDF는 평균적으로 한글 인코딩 처리와 속도, metadata 들이 우수한 Loader

Document란 ?

LangChain 의 기본 문서 객체다. 기본 속성은 : {'id': None, 'metadata': {}, 'page_content': ' ', 'type': 'Document'} 이런 형식으로 되어 있다.

# 메타데이터 추가
document.metadata["source"] = "파일이름 @"
document.metadata["page"] = 1
document.metadata["author"] = "저자 1"

이런 식으로 메타데이터 딕셔너리에 값을 추가하는 것이 가능하다

PyPDFLoader로 로드한 Document ?

Document(
    metadata={'source': '파일명', 'page': 0},
     page_content='')

이렇게 metadata에 딕셔너리로 source(파일명)와, page(페이지)를 가져오고, page_content에 텍스트 파일들(문자열)을 가져오는 형식이다.

matadata는 딕셔너리 형식으로 구성됐고, 이 페이지를 가져오려면,
page_number = docs[0].metadata['page'] 이렇게 로드된 docs에 메타데이터 딕셔너리 키인 page를 불러오는 형식으로 볼 수 있다.

source = docs[0].metadata['source'] source 부분도 마찬가지

page_content 텍스트 파일을 불러오려면, page_text = docs[0].page_content 이렇게 page_content 속성을 불러오면 된다.

형태 확인하기

page_number = docs[1].metadata['page']
print(page_number)
page_text = docs[0].page_content
print(page_text)
print(list(docs[0].metadata.keys())) # matadata의 key가 어떤것들이 있는지 확인 가능하다
1
2024년 11월호
['source', 'page']
# 각 페이지의 텍스트 출력
# for i, doc in enumerate(docs):
#     print(f"페이지 {i + 1} 내용:")
#     print(doc.page_content)
#     print("\n" + "="*50 + "\n")

CharacterTextSplitter vs RecursiveCharacterTextSplitter

1. CharacterTextSplitter (문자 텍스트 분할)의 매개변수

  • separator="\n\n": 두 개의 줄 바꿈 문자를 기준으로 텍스트를 분할합니다. 이는 문단 단위로 분할하려는 의도를 나타냅니다.
  • chunk_size=100: 각 청크의 최대 길이를 100자로 설정합니다.
  • chunk_overlap=10: 각 청크 간에 10자의 중첩을 허용하여 문맥을 유지합니다.
  • length_function=len: 텍스트의 길이를 측정하는 함수를 len으로 지정하여 문자 수를 기준으로 길이를 계산합니다.
  • is_separator_regex=False: separator를 정규식이 아닌 일반 문자열로 처리합니다.

2. split_documents 메서드의 기능

  • split_documents는 docs에 포함된 각 Document 객체의 page_content를 분할하여 문서의 길이에 맞는 여러 청크로 변환합니다. 반환된 splits는 각 청크가 하나의 텍스트 블록으로 되어 있으며, 이 블록은 원래 Document 객체의 일부 정보를 유지합니다. (Document 형식을 유지한다!)

3. 단점

  • CharacterTextSplitter 로 청크를 하면, 중요한 정보인 소제목이 포함 안될 가능성이 높습니다.!

  • 여러 페이지가 있는 PDF 파일을 처리할 때, CharacterTextSplitter를 사용하여 separator="\n\n"로 설정하면, 텍스트 내에서 두 개의 줄바꿈 문자가 연속으로 나타나는 부분을 기준으로 분할합니다. 그러나 PDF 파일은 페이지 단위로 구분되어 있으며, 각 페이지의 내용이 반드시 두 개의 줄바꿈 문자를 포함하지 않을 수 있습니다. 이로 인해 CharacterTextSplitter는 페이지 전체를 하나의 청크로 처리하게 되어, 설정한 chunk_size=100가 제대로 적용되지 않을 수 있습니다.

예시 코드

from langchain.text_splitter import CharacterTextSplitter

text_splitter = CharacterTextSplitter(
    separator="\n\n",
    chunk_size=100,
    chunk_overlap=10,
    length_function=len,
    is_separator_regex=False,
)

splits = text_splitter.split_documents(docs)

1. RecursiveCharacterTextSplitter 의 매서드

  • chunk_size=100: 각 청크의 최대 길이를 100자로 설정합니다. 이는 분할된 텍스트 조각의 최대 크기를 정의하며, 모델의 입력 제한에 맞게 조정할 수 있습니다.

  • chunk_overlap=10: 인접한 청크 간에 10자의 중첩을 허용합니다. 이는 문맥을 유지하기 위해 청크 간에 겹치는 부분을 설정하는 것으로, 모델이 이전 청크의 내용을 참고할 수 있도록 돕습니다.

  • length_function=len: 텍스트의 길이를 측정하는 함수를 len으로 지정하여, 각 청크의 길이를 문자 수로 계산합니다. 이를 통해 청크의 크기를 정확하게 제어할 수 있습니다.

  • is_separator_regex=False: 구분자를 정규 표현식이 아닌 일반 문자열로 처리합니다. 기본 구분자 목록은 ["\n\n", "\n", " ", ""]로 설정되어 있으며, 이는 문단, 문장, 단어, 문자 단위로 순차적으로 분할을 시도합니다.

2. CharacterTextSplitter, RecursiveCharacterTextSplitter 차이점

문단 단위로 텍스트를 분할하고자 할 때 유용한 CharacterTextSplitter, RecursiveCharacterTextSplitter는 텍스트의 의미와 문맥을 최대한 유지하면서 분할하려는 목적을 가지고 있습니다. 큰 단위에서 작은 단위로 점진적으로 분할하므로, 의미 있는 단위로 텍스트를 나누는 데 효과적입니다.

3. 결론

이 실습과제에서는 RecursiveCharacterTextSplitter를 사용하는 것이 더 적합하다고 생각해서 RecursiveCharacterTextSplitter를 사용했습니다.

사용코드

from langchain.text_splitter import RecursiveCharacterTextSplitter

recursive_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=30,
    length_function=len,
    is_separator_regex=False,
)

splits = recursive_text_splitter.split_documents(docs)
splits[0:50] 
결과 예시
[Document(metadata={'source': '인공지능최신동향.pdf', 'page': 0}, page_content='2024년 11월호'), Document(metadata={'source': '인공지능최신동향.pdf', 'page': 1}, page_content='2024년 11월호'), Document(metadata={'source': '인공지능최신동향.pdf', 'page': 1}, page_content='Ⅰ. 인공지능 산업 동향 브리프 1. 정책/법제    ▹ 미국 민권위원회, 연방정부의 얼굴인식 기술 사용에 따른 민권 영향 분석························1   ▹ 미국 백악관 예산관리국, 정부의 책임 있는 AI 조달을 위한 지침 발표·····························2   ▹ 유로폴, 법 집행에서 AI의 이점과 과제를 다룬 보고서 발간··············································3   ▹ OECD, 공공 부문의 AI 도입을 위한 G7 툴킷 발표··························································4   ▹ 세계경제포럼, 생성AI 시대의 거버넌스 프레임워크 제시····················································5  2. 기업/산업    ▹ CB인사이츠 분석 결과, 2024년 3분기 벤처 투자 31%가 AI 스타트업에'), Document(metadata={'source': '인공지능최신동향.pdf', 'page': 1},
.
.
.
.
.
.
, Document(metadata={'source': '인공지능최신동향.pdf', 'page': 12}, page_content='<미스트랄 3B/7B와 경쟁 모델의 벤치마크 평가 비교>\n☞ 출처: Mistral AI, Un Ministral, des Ministraux-Introducing the world’s best edge models, 2024.10.16.')]

OpenAIEmbeddings, CacheBackedEmbeddings

  • OpenAIEmbeddings의 텍스트 임베딩 모델 text-embedding-3-small이 가격과 성능면에서 text-embedding-ada-002보다 우수하기에 변경
  • 임베딩시 계속 api 호출을 방지하기 위해 로컬에 임베딩된 파일을 저장하는 형식인 CacheBackedEmbeddings를 적용

코드


from langchain_openai import OpenAIEmbeddings
from langchain.storage import LocalFileStore
from langchain.embeddings import CacheBackedEmbeddings

# OpenAI 임베딩 모델 초기화
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")


# 로컬 파일 저장소 설정
store = LocalFileStore("C:\\Users\\1\\Desktop\\emb")

# 캐시를 지원하는 임베딩 생성 - 임베딩시 계속 api 호출을 방지하기 위해 로컬에 임베팅 파일을 저장하는 형식
cached_embedder = CacheBackedEmbeddings.from_bytes_store(
    underlying_embeddings=embeddings,
    document_embedding_cache=store,
    namespace=embeddings.model,  # 기본 임베딩과 저장소를 사용하여 캐시 지원 임베딩을 생성
)

import faiss
from langchain_community.vectorstores import FAISS
    
    
vectorstore = FAISS.from_documents(documents=splits, embedding=cached_embedder)

retriever 정의 및 프롬프트 템플릿 정의

retriever정의하기

  • search_type : 일반적 검색에 사용되는 Default값 similarity을 사용
  • search_kwargs : 가져올 청크 수를 결정하는 "k" : 3 으로 유사도가 높은 3개의 청크를 가져오도록 지정

코드

retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 3}) # 가져올 청크 수를 3으로 늘림

간단한 프롬프트 템플릿 정의

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough

# 프롬프트 템플릿 정의
contextual_prompt = ChatPromptTemplate.from_messages([
    ("system", "Answer the question using only the following context."),
    ("user", "Context: {context}\\n\\nQuestion: {question}")
])
class DebugPassThrough(RunnablePassthrough):
    def invoke(self, *args, **kwargs):
        output = super().invoke(*args, **kwargs)
        print("Debug Output:", output)
        return output
# 문서 리스트를 텍스트로 변환하는 단계 추가
class ContextToText(RunnablePassthrough):
    def invoke(self, inputs, config=None, **kwargs):  # config 인수 추가
        # context의 각 문서를 문자열로 결합
        context_text = "\n".join([doc.page_content for doc in inputs["context"]])
        return {"context": context_text, "question": inputs["question"]}

# RAG 체인에서 각 단계마다 DebugPassThrough 추가
rag_chain_debug = {
    "context": retriever,                    # 컨텍스트를 가져오는 retriever
    "question": DebugPassThrough()        # 사용자 질문이 그대로 전달되는지 확인하는 passthrough
}  | DebugPassThrough() | ContextToText()|   contextual_prompt | model

동작 확인

while True: 
	print("========================")
	query = input("질문을 입력하세요: ")
	response = rag_chain_debug.invoke(query)
	print("Final Response:")
	print(response.content)

결과예시

========================
질문을 입력하세요:  인디드 조사결과를 요약해줘
Debug Output: 인디드 조사결과를 요약해줘
Debug Output: {'context': [Document(metadata={'source': '인공지능최신동향.pdf', 'page': 22}, page_content='SPRi AI Brief |  2024-11월호\n20\n인디드 조사 결과, 생성AI가 인간 근로자 대체할 가능성은 희박n인디드가 2,800개 이상의 직무 기술에 대한 생성AI의 수행 능력을 분석해 인간을 대체할 가능성을 평가한 결과, 생성AI로 대체될 가능성이 “매우 높은” 것으로 평가된 기술은 전무n생성AI의 최대 강점은 직무 기술과 관련된 이론적 지식을 제공하는 능력이며, 물리적 작업 수행이 필요한 직무 기술에서는 인간 근로자를 대체할 가능성이 희박 \nKEY Contents'), Document(metadata={'source': '인공지능최신동향.pdf', 'page': 1}, page_content='▹ 다이스 조사, AI 전문가의 73%는 2025년 중 이직 고려················································18   ▹ 가트너 예측, AI로 인해 엔지니어링 인력의 80%가 역량 향상 필요 ·····························19   ▹ 인디드 조사 결과, 생성AI가 인간 근로자 대체할 가능성은 희박·····································20'), Document(metadata={'source': '인공지능최신동향.pdf', 'page': 5}, page_content='조사에서 원활한 국제협력을 위해서도 필수적n그러나 법 집행에서 AI 도구의 효과적이고 책임 있는 활용을 위해 해결되어야 할 기술적 과제 및 다양한 윤리적·사회적 우려도 존재∙일례로 관할권 간 데이터 수집과 보관 관행의 차이에 따른 데이터셋의 편향으로 인해 AI 산출물의 무결성(無缺性)이 손상될 수 있어 표준화된 데이터 수집 규약 필요∙데이터 규모나 활용 사례의 복잡성과 관계없이 AI 도구를 효과적으로 사용하려면 다양한 데이터 규모와 운영 요구사항에 적응할 수 있는 확장성과 성능을 갖춘 AI 모델도 개발 필요∙편향, 개인정보 침해와 인권 침해와 같은 다양한 윤리적·사회적 우려도 존재하며, 이를 해소하기 위해 데이터 편향을 제거하고 공공 안전과 개인정보 간 균형을 유지하며 AI 의사 결정 과정에 대한 투명성과 책임성을 보장 필요n보고서는 2024년 8월 발효된 EU AI 법이 법 집행기관에 미칠 영향도 분석∙EU AI 법은 공공장소에서 실시간 생체인식 식별과 같은 특정 애플리케이션의')], 'question': '인디드 조사결과를 요약해줘'}
Final Response:
인디드 조사 결과, 생성AI가 인간 근로자를 대체할 가능성은 매우 낮은 것으로 평가되었다. 2,800개 이상의 직무 기술에 대한 분석에서, 생성AI가 대체할 가능성이 "매우 높은" 기술은 전무하며, 생성AI는 주로 직무 기술과 관련된 이론적 지식을 제공하는 데 강점을 가진다. 그러나 물리적 작업을 수행해야 하는 직무는 인간 근로자를 대체하기 어려운 것으로 나타났다.
========================

이렇게 과제에 1번 문서를 적용시켜 RAG 실습을 해 봤다. 각 과정에서 필요한 매개변수들을 확인하고 적용되는 것들이 신기했다. 공식문서를 처음으로 꼼꼼하게 본 것 같고 제대로 된거 같아 행복하다.


CacheBackedEmbeddings 을 발견해 사용도 한번 해 봤다. API 호출을 여러번 피할 수 있는 아주 필수적인 ...그렇다.

profile
안녕하세요.

0개의 댓글