본문 바로가기
  • 머킹이의 머신로그
AI/코드 실습하기

[RAG 실습] GPT-4 + RAG 와 Fine-tuning 모델 + RAG 비교

by 머킹 2024. 8. 16.
728x90

[RAG 실습] GPT-4 + RAG 와 Fine-tuning 모델 + RAG 비교

 

안녕하세요 머킹입니다.

요즘 RAG를 정말 열심히 하고 있는데요.

 

RAG를 하면서 느끼는 점이 복잡한 query를 잘 이해하는게 정말 중요하겠더라구요.

그리고 마냥 RAG 를 하는 것 보단 그래도 파인튜닝한 모델에 RAG를 하는게 좋다.. 

라는 생각이 들어서 해보고 있는데

 

갑자기 진짜 더 좋을까? 라는 고민이 들었습니다.

 

GPT는 너무 뛰어난 성능을 가지고 있기 때문에

굳이 파인튜닝을 시켜서 붙여야할까... 그런 고민들이 드는 요즘입니다.

 

그래서 두 개의 코드를 직접 해보면서 

비교해보고자 합니다.

 


답변 비교 

GPT 4.0 + RAG 답변입니다.

 

굉장히 깔끔하게 잘 뽑아주었습니다.

 

 

그럼 Fine-tuning model + RAG 결과를 볼까요?

참고로 Fine-tuning 한 모델은 KULLM3 를 학습시켰습니다.

 

 

ㅎㅎ.. 제대로 나오지 않았네요 


 

GPT + RAG 프로세스

  • 쿼리 -> GPT -> RAG
    • 쿼리: 사용자가 질문을 입력합니다.
    • GPT: GPT 모델이 질문에 대한 초안을 생성하거나, 질문을 이해하고 관련 키워드 및 컨텍스트를 도출합니다.
    • RAG: 도출된 정보나 키워드를 바탕으로 RAG 기법을 사용하여 관련 문서를 검색하고, 최종 답변을 생성합니다.

 

Fine-tuning model + RAG 프로세스

  • 쿼리 -> 질문 이해 -> RAG -> LLM 답변
    • 쿼리: 사용자가 질문을 입력합니다.
    • 질문 이해: Fine-tuning된 모델이 사용자의 질문을 이해하고, 그에 따른 의도를 파악합니다.
    • RAG: Retrieval-Augmented Generation(RAG) 기법을 통해 관련 문서를 검색하고, 검색된 문서를 기반으로 답변을 생성합니다.
    • LLM 답변: Fine-tuning된 LLM(Large Language Model)이 검색된 정보와 질문을 바탕으로 최종 답변을 생성합니다.

 

차이점:

  • Fine-tuning model + RAG에서는 Fine-tuning된 모델이 질문을 이해하고 의도를 파악하는 단계가 추가되어 있으며, 이는 사용자 질문에 대한 깊이 있는 이해와 맞춤형 답변 생성에 유리할 수 있습니다.
  • GPT + RAG는 질문을 GPT 모델로 바로 전달하여 질문의 핵심을 도출하거나 바로 답변을 생성하고, RAG 기법을 통해 추가적인 검색과 답변을 생성합니다.

코드 비교 

 

GPT + RAG 예제 코드

import os
import pandas as pd
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain_community.vectorstores import FAISS
from langchain.docstore.document import Document as LangChainDocument
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.prompts import PromptTemplate
from rank_bm25 import BM25Okapi  # BM25를 사용하기 위해 필요한 import
import numpy as np

# 환경 변수 설정
os.environ["HUGGINGFACEHUB_API_TOKEN"] = "your_huggingfacehub_api_token"
os.environ["OPENAI_API_KEY"] = "your_openai_api_key"

# CSV 파일 로드 및 문서로 변환
csv_filepath = "path/to/your/data.csv"
df = pd.read_csv(csv_filepath)
documents = [LangChainDocument(page_content=row['칼럼'], metadata={"source": i}) for i, row in df.iterrows() if pd.notnull(row['칼럼'])]

# 문서 벡터화 및 FAISS 벡터 저장소 생성
embedding_function = HuggingFaceEmbeddings(model_name="jhgan/ko-sroberta-multitask")
vector_store = FAISS.from_documents(documents, embedding_function)

# BM25 초기화
corpus = [doc.page_content for doc in documents]
tokenized_corpus = [doc.split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)

# LLM 초기화
llm = ChatOpenAI(model="gpt-4", temperature=0)

# 하이브리드 검색 함수
def hybrid_search(query, k=15):
    # 의미론적 검색
    semantic_docs = vector_store.similarity_search(query, k=k)
    
    # 키워드 기반 검색
    tokenized_query = query.split()
    bm25_scores = bm25.get_scores(tokenized_query)
    top_n = np.argsort(bm25_scores)[-k:]
    keyword_docs = [documents[i] for i in reversed(top_n)]
    
    # 결과 결합 (중복 제거)
    combined_docs = []
    seen = set()
    for doc in semantic_docs + keyword_docs:
        if doc.page_content not in seen:
            seen.add(doc.page_content)
            combined_docs.append(doc)
    
    return combined_docs[:k]

# RAG 파이프라인 초기화
prompt_template = """주어진 컨텍스트를 바탕으로 질문에 답변해주세요. 컨텍스트에 관련 정보가 없다면, "제공된 정보로는 답변할 수 없습니다."라고 말씀해 주세요.

컨텍스트:
{context}

질문: {question}
답변:"""

PROMPT = PromptTemplate(
    template=prompt_template, input_variables=["context", "question"]
)

rag_pipeline = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=vector_store.as_retriever(search_kwargs={"k": 10}),
    chain_type_kwargs={"prompt": PROMPT}
)

# LLM을 사용한 질문 분리 및 처리
def get_combined_answer(query):
    # LLM을 사용해 질문을 분리하고 각각의 질문으로 처리
    prompt_for_splitting = f"질문: '{query}'\n위의 질문을 개별적인 질문으로 나눠주세요."
    split_response = llm.invoke(prompt_for_splitting)
    
    sub_queries = split_response.content.split('\n')
    
    answers = []
    
    for sub_query in sub_queries:
        if sub_query.strip():
            # 각 질문에 대해 독립적으로 검색 및 요약 수행
            docs = hybrid_search(sub_query, k=15)
            summarized_content = " ".join([doc.page_content for doc in docs])
            
            result = rag_pipeline.invoke({"query": sub_query, "context": summarized_content})
            answers.append(result['result'])
    
    final_answer = "\n".join(answers)
    
    return final_answer

# 사용 예시
query = "question"
answer = get_combined_answer(query)
print("RAG 응답 생성 결과:")
print(answer)

 

 

Fine-tuning model + RAG 예제 코드

import os
import torch
import pandas as pd
from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoModelForCausalLM, pipeline, BitsAndBytesConfig
from sentence_transformers import SentenceTransformer
from langchain.chains import RetrievalQA
from langchain.llms import HuggingFacePipeline
from langchain.prompts import PromptTemplate
from langchain_community.vectorstores import FAISS
from langchain.docstore.document import Document as LangChainDocument
from langchain_community.embeddings import HuggingFaceEmbeddings
from rank_bm25 import BM25Okapi
import numpy as np

# 설정
os.environ["HUGGINGFACEHUB_API_TOKEN"] = "your_huggingfacehub_api_token"

# CSV 파일 로드 및 문서로 변환
csv_filepath = "path/to/your/data.csv"
df = pd.read_csv(csv_filepath)
documents = [LangChainDocument(page_content=row['칼럼'], metadata={"source": i}) for i, row in df.iterrows() if pd.notnull(row['칼럼'])]

# BM25 초기화
corpus = [doc.page_content for doc in documents]
tokenized_corpus = [doc.split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)

# FAISS 벡터 저장소 초기화
embedding_function = HuggingFaceEmbeddings(model_name="snunlp/KR-SBERT-V40K-klueNLI-augSTS")
vector_store = FAISS.from_documents(documents, embedding_function)

def identify_intent(query, device_map):
    model_path = "path/to/your/saved_model"
    quantization_config = BitsAndBytesConfig(load_in_8bit=True)
    intent_model = AutoModelForSequenceClassification.from_pretrained(model_path, quantization_config=quantization_config, device_map=device_map)
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    intent_pipeline = pipeline("text-classification", model=intent_model, tokenizer=tokenizer)
    
    intent_probs = intent_pipeline(query, return_all_scores=True)[0]
    intent = max(intent_probs, key=lambda x: x['score'])['label']
    
    return intent

def hybrid_search(query, k=20):
    # BM25로 키워드 기반 검색
    tokenized_query = query.split()
    bm25_scores = bm25.get_scores(tokenized_query)
    top_n = np.argsort(bm25_scores)[-k:]
    keyword_docs = [documents[i] for i in reversed(top_n)]

    # FAISS로 벡터 기반 검색
    semantic_docs = vector_store.similarity_search(query, k=k)
    
    # 두 검색 결과 결합 후 필터링
    combined_docs = []
    seen = set()
    for doc in semantic_docs + keyword_docs:
        if doc.page_content not in seen:
            seen.add(doc.page_content)
            bm25_score = bm25.get_scores(tokenized_query)[corpus.index(doc.page_content)]
            semantic_score = np.dot(embedding_function.embed_query(query), embedding_function.embed_query(doc.page_content))
            combined_score = (bm25_score * 0.3 + semantic_score * 0.7)
            combined_docs.append((doc, combined_score))
    
    # 점수에 따라 문서 정렬
    combined_docs.sort(key=lambda x: x[1], reverse=True)
    combined_docs = [doc for doc, _ in combined_docs]

    return combined_docs[:min(k, len(combined_docs))]

def search_information(query, intent):
    expanded_query = f"{query} {intent}"
    docs = hybrid_search(expanded_query, k=20)
    return docs

def generate_answer(docs, query, device_map):
    model_path = "path/to/your/saved_model"
    quantization_config = BitsAndBytesConfig(load_in_8bit=True)
    model = AutoModelForCausalLM.from_pretrained(model_path, quantization_config=quantization_config, device_map=device_map)
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    
    hf_pipeline = pipeline(
        "text-generation", 
        model=model, 
        tokenizer=tokenizer, 
        max_new_tokens=300,
        temperature=0.3,
        do_sample=True,
        top_p=0.95,
        repetition_penalty=1.1
    )
    llm = HuggingFacePipeline(pipeline=hf_pipeline)
    
    prompt_template = """다음의 컨텍스트를 바탕으로 주어진 질문에 대해 정확하고 상세한 답변을 제공해주세요. 
    질문이 애매하거나 불분명할 경우, 관련된 가장 중요하고 유용한 정보를 포함하여 포괄적인 답변을 제공해 주세요.

    컨텍스트:
    {context}

    질문: {question}
    답변:"""

    PROMPT = PromptTemplate(template=prompt_template, input_variables=["context", "question"])

    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=vector_store.as_retriever(search_kwargs={"k": 10}),
        return_source_documents=True,
        chain_type_kwargs={"prompt": PROMPT}
    )

Here is the rest of the gisted and sanitized code:

```python
    summarized_content = " ".join([doc.page_content for doc in docs[:3]])
    
    result = qa_chain({"query": query, "context": summarized_content})
    answer = result['result']

    return answer

def process_query(query):
    device_map = "auto"
    intent = identify_intent(query, device_map)
    docs = search_information(query, intent)
    answer = generate_answer(docs, query, device_map)
    
    if len(answer.split()) < 50:
        additional_query = f"{query}에 대한 추가 정보"
        additional_docs = search_information(additional_query, intent)
        additional_answer = generate_answer(additional_docs, additional_query, device_map)
        answer = f"{answer}\n\n추가 정보:\n{additional_answer}"
    
    return answer

# 사용 예시
query = "question"
final_answer = process_query(query)
print("최종 답변:")
print(final_answer)

 

1. 모델 사용:

  • 코드 1: OpenAI의 GPT-4 모델을 사용합니다. 이 모델은 ChatOpenAI 객체를 통해 사용되며, 질문을 처리하고 응답을 생성하는 데 사용됩니다.
  • 코드 2: Hugging Face의 GPT 모델을 기반으로 AutoModelForCausalLM을 사용합니다. 이 모델은 pipeline을 통해 자연어 생성 작업을 수행하며, 사용자가 직접 저장한 모델(saved_models/KULLM3)을 로드하여 사용합니다.

2. 질문 처리:

  • 코드 1: 질문을 분리하고 각각의 질문에 대해 독립적으로 검색 및 답변을 생성합니다. 질문을 분리하는 작업은 GPT-4 모델을 사용하여 수행됩니다.
  • 코드 2: 질문의 의도를 먼저 파악하고(예: AutoModelForSequenceClassification 모델 사용), 그 의도를 기반으로 검색 및 답변 생성을 수행합니다.

3. 하이브리드 검색:

  • 코드 1: 하이브리드 검색에서는 의미론적 검색과 키워드 기반 검색을 결합하여 중복을 제거한 후 최종 문서를 제공합니다. 여기서 두 검색 결과를 단순히 결합하고 중복을 제거하는 방식입니다.
  • 코드 2: 검색된 결과는 BM25 점수와 의미론적 점수를 결합하여(가중치를 적용) 최종 점수를 계산한 뒤, 이 점수에 따라 문서를 정렬합니다. 즉, 더 정교한 방식으로 검색 결과를 필터링하고 있습니다.

4. LLM 호출 방식:

  • 코드 1: ChatOpenAI를 직접 호출하여 질문을 분리하고 답변을 생성하는 데 사용합니다.
  • 코드 2: Hugging Face의 HuggingFacePipeline을 사용하여 모델을 초기화하고, AutoModelForCausalLM을 통해 답변을 생성합니다.

5. 추가 정보 검색:

  • 코드 1: 기본적으로 질문을 처리하고 응답을 생성하는 프로세스에는 추가적인 정보 검색이 포함되지 않음.
  • 코드 2: 생성된 답변이 너무 짧을 경우, 추가 정보를 검색하여 응답을 보완하는 기능이 포함되어 있습니다.

** 코드에서 사용한 함수 차이 **

 

1. LLM 호출 관련 함수

  • 코드 1: ChatOpenAI 클래스와 RetrievalQA를 사용하여 GPT-4 모델을 호출하고, 질문을 분리하거나 답변을 생성하는 데 사용합니다.
    • 주요 함수: llm.invoke(), rag_pipeline.invoke()
  • 코드 2: AutoModelForCausalLM을 사용하여 Hugging Face의 GPT 모델을 호출하며, 파이프라인을 통해 텍스트 생성을 수행합니다.
    • 주요 함수: pipeline(), HuggingFacePipeline(), qa_chain()

2. 의도 파악 관련 함수

  • 코드 1: 질문 의도를 파악하는 별도의 함수가 사용되지 않음.
  • 코드 2: AutoModelForSequenceClassification을 사용하여 질문의 의도를 파악하는 identify_intent() 함수가 사용됩니다.

3. 하이브리드 검색 함수

  • 코드 1: hybrid_search() 함수는 의미론적 검색과 키워드 기반 검색 결과를 단순히 결합하여 중복을 제거하는 방식으로 동작합니다.
  • 코드 2: hybrid_search() 함수는 BM25 점수와 의미론적 점수를 가중치를 적용하여 결합한 후, 최종 점수에 따라 문서를 정렬하고 필터링하는 방식으로 동작합니다.

4. 추가 정보 검색 및 처리

  • 코드 1: 추가적인 정보 검색이나 처리 기능이 없음.
  • 코드 2: process_query() 함수 내에서 응답이 너무 짧을 경우 추가 정보를 검색하고 이를 결합하여 최종 응답을 생성하는 로직이 포함되어 있습니다.