LLM Day8 - project

Soyee Sung·2025년 2월 8일
0

LLM

목록 보기
7/34

이 코드는 PDF 문서를 기반으로 한 RAG(Retrieval-Augmented Generation) 챗봇입니다.
RAG는 사용자의 질문에 대해 정보 검색(Retrieval)과 생성(Generation)을 결합한 방식으로 동작합니다.
즉, PDF 문서에서 질문과 관련 있는 내용을 찾아(OpenAI API 사용), 가장 적절한 답변을 제공합니다.

1. 필요한 라이브러리 및 모듈 로드

📌 FAISS (Facebook AI Similarity Search)
➡️ 고차원 벡터 검색을 위한 라이브러리로, 빠른 최근접 이웃(Nearest Neighbor) 검색을 지원한다.
➡️ 코사인 유사도(Cosine Similarity) 또는 내적(Inner Product) 기반의 검색이 가능하다.

📌 LangChain
➡️ LLM 기반 애플리케이션을 구축하는 라이브러리로, 문서 처리, 검색, LLM과의 상호작용을 지원한다.
➡️ PyPDFLoader: PDF 문서를 불러와서 텍스트로 변환
➡️ RecursiveCharacterTextSplitter: 문서를 적절한 크기의 청크(chunk)로 분할
➡️ HuggingFaceEmbeddings: 문장을 벡터로 변환하여 검색에 활용

📌 Gradio
➡️ 웹 UI 프레임워크로, Python 코드에서 간단하게 인터페이스를 만들 수 있도록 지원한다.

import os
import gradio as gr
import faiss
import numpy as np
import pandas as pd
import pickle
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.chat_models import ChatOpenAI
from dotenv import load_dotenv

2. OpenAI API 연결 및 모델 설정

📌 GPT-4o-mini 모델
➡️ OpenAI에서 제공하는 경량화된 GPT-4 모델로, 빠른 응답 시간과 효율성을 제공
➡️ "temperature=0.3" 값은 생성된 응답의 일관성을 유지하면서도 어느 정도 창의성을 부여하는 역할
➡️ "predict(prompt)"를 사용하여 입력된 prompt에 대해 최적의 응답을 생성

# 환경 변수 로드
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
llm = ChatOpenAI(api_key=OPENAI_API_KEY, model_name="gpt-4o-mini", temperature=0.3)

3. 검색 데이터 저장소 설정

📌 FAISS 인덱스 저장
➡️ FAISS_INDEX_PATH: 벡터화된 문서 데이터를 저장하는 파일
➡️ DOCUMENTS_PATH: 문서의 메타데이터(출처, 내용)를 저장하는 파일

FAISS_INDEX_PATH = "faiss_index.bin"
DOCUMENTS_PATH = "documents.pkl"

4. 문서 임베딩(Embedding) 모델 설정

📌 Hugging Face Embedding
➡️ 문장을 벡터로 변환하는 임베딩 모델
➡️ "BAAI/bge-m3" 모델은 BioMedical 및 General Embeddings(BGE) 모델로, 의료 및 일반적인 문서 검색에 최적화
➡️ FAISS 검색을 위해 벡터화를 수행하여 의미론적 검색(Semantic Search) 적용

embedding_model = HuggingFaceEmbeddings(model_name="BAAI/bge-m3")

5. FAISS 벡터 데이터 구조 및 초기화

📌 IndexFlatIP(Inner Product)
➡️ FAISS의 벡터 검색 방식 중 하나로, 내적 기반(Inner Product) 유사도 검색을 수행
➡️ 코사인 유사도(Cosine Similarity) 기반의 벡터 정규화를 활용하여 최적의 검색 결과를 제공

embedding_dim = 1024  # 벡터 차원 수
index = faiss.IndexFlatIP(embedding_dim)  # FAISS 인덱스 (내적 기반)
documents = []  # 문서 메타데이터 저장용

6. PDF 문서에서 텍스트 추출

📌 PyPDFLoader
➡️ PDF 문서에서 페이지별 텍스트를 추출하여, 자연어 검색이 가능하도록 변환

def extract_text_from_pdf(pdf_path):
    """PDF에서 텍스트를 추출"""
    loader = PyPDFLoader(pdf_path)
    documents = loader.load()
    text = "\n".join([doc.page_content for doc in documents])
    return text

7. PDF 내용을 벡터로 변환 후 저장

📌 RecursiveCharacterTextSplitter
➡️ 문서를 일정 크기(500 토큰)로 분할하여, 검색 성능 최적화

📌 L2 정규화(Normalization)
➡️ FAISS의 faiss.normalize_L2(embeddings)를 통해 코사인 유사도 기반 검색 최적화

def process_pdf(pdf_path):
  """PDF 문서를 벡터 저장소에 저장"""
  text = extract_text_from_pdf(pdf_path)
  text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
  texts = text_splitter.split_text(text)
  
  embeddings = embedding_model.embed_documents(texts)
  embeddings = np.array(embeddings).astype('float32')
  
  faiss.normalize_L2(embeddings)
  index.add(embeddings)
  
  for i, chunk in enumerate(texts):
      documents.append({"source": pdf_path, "chunk": chunk})
  
  print(f"{pdf_path} 처리 완료. 총 {len(texts)}개의 청크 저장 완료.")

8. OpenAI API를 활용한 요약 생성

📌 Prompt Engineering 기법
➡️ context 내 문서를 질문과 함께 주고, 가장 적절한 답변을 생성하도록 LLM을 활용
➡️ "strip()"을 사용하여 불필요한 공백을 제거하고 응답을 최적화

def extract_most_relevant_summary(question, context):
  """OpenAI API를 사용하여 질문과 관련 있는 답변을 요약"""
  prompt = f"""
  다음은 질문과 관련 있는 문서 내용입니다:
  {context}
  
  위 내용을 바탕으로 질문에 대한 가장 적절한 답변을 짧고 명확하게 작성하세요:
  질문: {question}
  
  답변:
  """
  response = llm.predict(prompt)
  return response.strip()

9. FAISS 검색 + OpenAI 요약 결합

📌 MMR (Maximal Marginal Relevance) 기반 검색
➡️ FAISS 검색 후 질문과 가장 관련 있는 문서 3개를 선택
➡️ "max_similarity < 0.4" → 유사도가 너무 낮으면 답변을 주지 않음

def query_chatbot(question):
  """MMR 검색을 적용한 주요 답변 생성 (유사도 0.4 이하이면 답변 없음)"""
  if len(documents) == 0:
      return "답변을 찾을 수 없습니다. (인덱스가 로드되지 않음)"
  
  question_embedding = np.array(embedding_model.embed_query(question)).astype('float32').reshape(1, -1)
  faiss.normalize_L2(question_embedding)
  D, I = index.search(question_embedding, 10)
  
  max_similarity = np.max(D[0])
  if max_similarity < 0.4:
      return "적절한 답변을 찾을 수 없습니다. PDF와 관련있는 질문을 해주세요."
  
  valid_indices = [(idx, score) for idx, score in zip(I[0], D[0]) if score > 0.4]
  
  if not valid_indices:
      return "적절한 답변을 찾을 수 없습니다. PDF와 관련있는 질문을 해주세요."
  
  selected_indices = valid_indices[:3]
  
  results = []
  sources = []
  scores = []
  context_texts = []
  
  for idx, score in selected_indices:
      chunk_text = documents[idx]["chunk"]
      results.append(chunk_text)
      sources.append(documents[idx]['source'])
      scores.append(f"{score:.4f}")
      context_texts.append(chunk_text)
  
  context = "\n\n".join(context_texts)
  main_answer = extract_most_relevant_summary(question, context)
  
  df = pd.DataFrame({
      "💡 주요 답변": [main_answer],
      "🔍 유사도 점수": ["\n".join(scores)],
      "📖 근거 발췌": ["\n\n".join(results)],
      "📁 출처": ["\n".join(sources)]
  })
  
  return df.to_html(index=False, escape=False)

10. Gradio를 이용한 챗봇 UI

gr.Blocks() → Gradio UI 생성
question 입력 후 submit 버튼 클릭 시 query_chatbot() 호출

def chatbot_interface():
    with gr.Blocks() as demo:
        gr.Markdown("# 📚 PDF 검색 Q&A 챗봇")
        with gr.Row():
            question = gr.Textbox(label="💬 질문을 입력하세요")
            submit = gr.Button("🔍 질문하기")
        answer = gr.HTML(label="📢 답변")
        submit.click(query_chatbot, inputs=question, outputs=answer)
    demo.launch(share=True)

11. 실행 코드

PDF 처리 후, 챗봇 UI 실행

if __name__ == "__main__":
    pdf_dir = "/Users/SY/LLM_project/llm29/data/paper"
    process_all_pdfs(pdf_dir)
    chatbot_interface()

이 코드는 PDF 논문을 벡터 저장소에 저장하고, 사용자의 질문에 대해 관련 정보를 검색하여 응답을 생성하는 챗봇을 구현합니다. 🚀

0개의 댓글