Llama3–8b와 LlamaIndex로 RAG 구현하기

Gandalf_Song
17 min readMay 21, 2024

--

이번에 저희 2차 LLM모임에서는 각 주제를 선정하여 RAG를 구현하기로 했습니다.

저는 이번에도 로컬을 먼저 진행하고, 그 다음에 코랩으로 옮겼습니다.

먼저 로컬진행을 할때는 3.11.9 파이썬 버전으로 진행을 했으며,

벡터디비의 경우 도커를 통해서 구현시켰으나,

코랩으로 옮긴 후에는 메모리로 지정하여 구현하였습니다.

RAG를 구현하는 많은 예제가 랭체인으로 되어있는데, 레딧에 의하여 랭체인은 많은 추상화를 했기 때문에

특별히 조작해야하거나 커스텀마이징이 필요할 경우 결국 안으로 파고들어야 하기 때문에 입문용 말고는

추천하지 않고 있었습니다.

그나마 랭체인에 비해 라마인덱스가 덜 추상화 되었다는 평이 있었기에 저는 라마인덱스 프레임워크를 사용했습니다.

또한 라마인덱스는 랭체인에 비해 데이터 검색 및 관리 작업에서 우수한 성능을 보인다고 합니다.

때문에 보다 RAG에 더 적합하다는 결론을 내렸습니다.

qdrant 는 하이퍼포머스라는 수식어를 달고 있고, 언어는 러스트로 작성되어있습니다. 또한 빠른 검색에 용이하며, 라마인덱스와 연계가 쉽게 된다는 점에서 이것을 선택했습니다.

먼저 의존성을 설치합니다.

llama-index
llama-index-llms-huggingface
llama-index-embeddings-fastembed
fastembed
Unstructured[md]
qdrant-client
llama-index-vector-stores-qdrant
einops
accelerate
sentence-transformers
transformers
llama-index-agent-openai
llama-index-cli
llama-index-core
llama-index-legacy

그 다음은 파일의 구조입니다.

저는 대체적으로 파일을 책임과 관심사별로 나눠 그 부분만 고치는 것을 좋아합니다. 주로 백엔드 개발을 할때 이렇게 나누게 되는데, 습관이 들어 LLM을 만들때도 되도록 나누고 있습니다.

config.py

HUGGING_TOKKEN = ""
BASE_MODEL = "MLP-KTLim/llama-3-Korean-Bllossom-8B"
DATASET = "content/Data"
FAST_EMBED_MODEL_NAME = "BAAI/bge-small-en-v1.5"
SYSTEM_PROMPT = "You are a Q&A assistant. Summarize the questions and answer accurately in Korean based on the given instructions and context."
QUERY_WRAPPER_PROMPT = "<|USER|>{query_str}<|ASSISTANT|>"
ENCODER_MODEL = "cross-encoder/ms-marco-MiniLM-L-2-v2"

보면 아시겠지만 이번에 라마3를 한국어 모델로 학습시킨 모델을 썼습니다.

dataset.py

from llama_index.core import SimpleDirectoryReader
from config import DATASETclass DataSetter:
def __init__(self):
self.dataset_dir = DATASET

def load_dataset(self):
print("Loading dataset...")
return SimpleDirectoryReader(self.dataset_dir).load_data()

여기서 PDF를 로드합니다.

db.py — local

from llama_index.core import VectorStoreIndex
from llama_index.core import StorageContext
from llama_index.vector_stores.qdrant import QdrantVectorStore
from qdrant_client import QdrantClient

class Db:
def __init__(self, documents):
self.client = QdrantClient(host="localhost", port=6333)
self.vector_store = QdrantVectorStore(client=self.client,collection_name="test")
self.storage_context = StorageContext.from_defaults(vector_store=self.vector_store)
self.documents = documents

def get_index(self):
return VectorStoreIndex.from_documents(self.documents,storage_context=self.storage_context)

로컬의 경우 아래의 도커 실행 명령어로 실행하면 됩니다.

docker run -p 6333:6333 -p 6334:6334 -v $(pwd)/qdrant_storage:/qdrant/storage:z qdrant/qdrant

db.py — colab

from llama_index.core import VectorStoreIndex
from llama_index.core import StorageContext
from llama_index.vector_stores.qdrant import QdrantVectorStore
from qdrant_client import QdrantClient

class Db:
def __init__(self, documents):
self.client = QdrantClient(location=":memory:")
self.vector_store = QdrantVectorStore(client=self.client,collection_name="test")
self.storage_context = StorageContext.from_defaults(vector_store=self.vector_store)
self.documents = documents

def get_index(self):
return VectorStoreIndex.from_documents(self.documents,storage_context=self.storage_context)

embdding_setter.py

from llama_index.embeddings.fastembed import FastEmbedEmbedding
from llama_index.core import Settings
from config import FAST_EMBED_MODEL_NAME

class EmbedSettings:
def __init__(self):
self.embed_model = FastEmbedEmbedding(model_name=FAST_EMBED_MODEL_NAME)
self.chunk_size = 512

def set_and_get_liama_settings(self):
print("Setting Llama settings...")
Settings.embed_model = self.embed_model
Settings.chunk_size = self.chunk_size
return Settings

임베딩 모델을 설정합니다.

hugging.py

from huggingface_hub import login
from config import HUGGING_TOKKEN

class HuggingFaceNotebookLogin:
def __init__(self ):
self.token = HUGGING_TOKKEN
def login(self):
print("Logging in...")
login(self.token)

load_llm.py — cpu

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from llama_index.llms.huggingface import HuggingFaceLLM
from config import BASE_MODEL

class LLMLoader:
def __init__(self, system_prompt, query_wrapper_prompt):
self.tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
self.stopping_ids = [self.tokenizer.eos_token_id, self.tokenizer.convert_tokens_to_ids("<|eot_id|>")]
self.system_prompt = system_prompt
self.query_wrapper_prompt = query_wrapper_prompt
self.base_model = BASE_MODEL
def load_llm(self):
print("Loading LLM...")

return HuggingFaceLLM(
context_window=8192,
max_new_tokens=256,
generate_kwargs={"temperature": 0.7, "do_sample": False},
system_prompt=self.system_prompt,
query_wrapper_prompt=self.query_wrapper_prompt,
tokenizer_name=self.base_model,
model_name=self.base_model,
device_map="cpu", # CPU에서 실행되도록 설정 (필요시 'auto'로 변경)
stopping_ids=self.stopping_ids,
tokenizer_kwargs={"max_length": 4096},
#model_kwargs={"torch_dtype": torch.float32} # CPU에서 float32를 사용
)

cpu의 경우 model_kwargs를 쓰지말라고 라마인덱스 홈페이지에 기재되어있습니다.

load_llm.py — gpu

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from llama_index.llms.huggingface import HuggingFaceLLM
from config import BASE_MODEL


class LLMLoader:
def __init__(self, system_prompt, query_wrapper_prompt):
self.tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
self.stopping_ids = [self.tokenizer.eos_token_id, self.tokenizer.convert_tokens_to_ids("<|eot_id|>")]
self.system_prompt = system_prompt
self.query_wrapper_prompt = query_wrapper_prompt
self.base_model = BASE_MODEL
def load_llm(self):
print("Loading LLM...")

return HuggingFaceLLM(
context_window=8192,
max_new_tokens=256,
generate_kwargs={"temperature": 0.7, "do_sample": False},
system_prompt=self.system_prompt,
query_wrapper_prompt=self.query_wrapper_prompt,
tokenizer_name=self.base_model,
model_name=self.base_model,
device_map="auto", # CPU에서 실행되도록 설정 (필요시 'auto'로 변경)
stopping_ids=self.stopping_ids,
tokenizer_kwargs={"max_length": 4096},
model_kwargs={"torch_dtype": torch.float16} # CPU에서 float32를 사용
)

prompt_template.py

from llama_index.core import PromptTemplate
from config import SYSTEM_PROMPT, QUERY_WRAPPER_PROMPT


class PromptTemplates:
def __init__(self):
self.system_prompt = SYSTEM_PROMPT
self.query_wrapper_prompt = PromptTemplate(QUERY_WRAPPER_PROMPT)
def get_system_prompt(self):
print("PromptTemplates.get_system_prompt()")
return self.system_prompt

def get_query_wrapper_prompt(self):
print("PromptTemplates.get_query_wrapper_prompt()")
return self.query_wrapper_prompt

템플릿을 설정해줍니다.

main.py

from llama_index.core.postprocessor import SentenceTransformerRerank


import time
from hugging import HuggingFaceNotebookLogin
from dataset import DataSetter
from embdding_setter import EmbedSettings
from load_llm import LLMLoader
from prompt_template import PromptTemplates
from db import Db
from config import ENCODER_MODEL


print("hugging Login Start...")
HuggingFaceNotebookLogin().login()

print("load PDF Data Start...")
documents = DataSetter().load_dataset()

promptTP = PromptTemplates()
system_prompt = promptTP.get_system_prompt()
query_wrapper_prompt = promptTP.get_query_wrapper_prompt()

print("Setting Embed Model settings...")
settting = EmbedSettings().set_and_get_liama_settings()

print("Setting LLM settings...")
settting.llm = LLMLoader(system_prompt, query_wrapper_prompt).load_llm()

print("Setting db Index settings...")
dbIndex = Db(documents).get_index()

print("Setting SentenceTransformerRerank settings...")
rerank = SentenceTransformerRerank(model=ENCODER_MODEL, top_n=3)

print("Setting Query Engine settings...")
query_engine = dbIndex.as_query_engine(similarity_top_k=10, node_postprocessors=[rerank])

print("Query Engine Start...")
now = time.time()
response = query_engine.query("What is instruction finetuning?")
print(f"Response Generated: {response}")
print(f"Elapsed: {round(time.time() - now, 2)}s")

CPU의 버전의 경우 맥 M2에서 돌렸는데 CPU추론이 되질 않았습니다.

이것을 알 수 있는 이유가 맥의 CPU점유율이 증가하지 않았기 때문이죠.

때문에 할 수 없이 저는 다시 코랩으로 가서 GPU버전으로 돌렸습니다.

1차 결과

Response Generated:

Instruction tuning is the act of teach a model to follow specific instructions, like “be creative”. It is a type of training data used in the field of natural language processing (NLP). The idea is to teach a model to follow specific “ Prompts” or “Instructables” in order to solve various NLP tasks. This is useful for models that are not always exposed to the same training data, and need to be able to follow different instructions across different tasks.

Elapsed: 9.86s

저는 분명 한국어로 변환하는 시스템을 넣었지만 영어로 물었기 때문에 영어로 답변이 돌아왔습니다.

그래서 다시 질문 사항 자체에 한국어로 변환할 것을 요청했습니다.

now = time.time()
response = query_engine.query("What is instruction finetuning? and this answer transfer korean")
print(f"Response Generated: {response}")
print(f"Elapsed: {round(time.time() - now, 2)}s")

Response Generated:

“인스트럭션 피닝 튜닝”은 BERT와 같은 사전 훈련된 언어 모델의 파라미터를 조정하여 특정 다운스트림 작업에 적합한 방식으로 조정하는 작업입니다. 예를 들어, 질문 및 답변 형식의 데이터셋을 사용하여 Q&A 모델을 구축합니다. 이러한 작업은 대규모 언어 모델의 대중적 사용에 큰 영향을 미쳤습니다.”

Elapsed: 6.01s

참조 문서

https://nayakpplaban.medium.com/build-an-advanced-reranking-rag-system-using-llama-index-llama-3-and-qdrant-a8b8654174bc

https://hypro2.github.io/langchain-llamaindex/

--

--