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() 함수 내에서 응답이 너무 짧을 경우 추가 정보를 검색하고 이를 결합하여 최종 응답을 생성하는 로직이 포함되어 있습니다.
'AI > 코드 실습하기' 카테고리의 다른 글
vLLM 이란 뭘까? (1) | 2024.08.29 |
---|---|
파인튜닝된 모델을 활용한 RAG (Retrieval-Augmented Generation) 예제 (0) | 2024.08.26 |
Transformer 모델 학습 중 발생하는 --load_best_model_at_end 에러: 원인과 해결 방법 (0) | 2024.08.20 |
KULLM3 학습 코드 예제 (0) | 2024.08.19 |
[코드 실습]Transformer 직접 코딩해보기 (2) | 2023.09.25 |