2024년 5월 18일 토요일

LLM 기반 구조화된 JSON 데이터 RAG 및 생성하기

이 글은 최근 LLM 개발자 관심 중 하나인 LLM 기반 구조화된 형식의 데이터 생성하는 방법을 간략히 소개한다. 이를 위해, JSON 입출력이 가능하도록 RAG처리하는 방법을 개발한다. 

구조화된 LLM 출력

본 글은 OpenAI ChatGPT와 같이 API를 사용하려면 구독해야 하는 상용 모델 대신 라마, 미스랄과 같은 오픈소스 모델을 사용한다. 

JSON 파일을 RAG하기 위해서는 여러가지 부분을 고려해야 한다. 보통, RAG를 위해서는 랭체인, 라마 인덱스 등 라이브러리를 사용하는 데, 의존성 변화가 심한데다, 급격히 발전하고 있어 설치, 빌드에 여러 에러가 발생하고 있다. 벡터 데이터베이스를 이용한 RAG도 한계가 있어, 필요한 정보를 제대로 검색하지 못하는 이슈들이 있다. 이런 문제들을 고려하고, RAG처리해야 한다. 

개발 환경 준비
다음과 같이 개발환경을 설치한다. 그리고, ollama 도구를 설치하도록 한다.
pip install llama-cpp-python
pip install 'crewai[tools]'
pip install langchain

TEXT TO JSON 
라마 모델을 로딩하고, JSON 문법으로 출력하도록 GGUF 문법 정의를 이용해 JSON 출력을 생성한다. 다음 코드를 실행한다.

from llama_cpp.llama import Llama, LlamaGrammar
import httpx

grammar = LlamaGrammar.from_string(grammar_text)

llm = Llama("llama-2-13b.Q8_0.gguf")

response = llm(
    "JSON list of name strings of attractions in SF:",
    grammar=grammar, max_tokens=-1
)

import json
print(json.dumps(json.loads(response['choices'][0]['text']), indent=4))

출력 결과는 다음과 같이, 샌프란시스코에 있는 놀이 시설을 보여준다. 
[
    {
        "address": {
            "country": "US",
            "locality": "San Francisco",
            "postal_code": 94103,
            "region": "CA",
            "route": "Museum Way",
            "street_number": 151
        },
        "geocode": {
            "latitude": 37.782569,
            "longitude": -122.406605
        },
        "name": "SFMOMA",
        "phone": "(415) 357-4000",
        "website": "http://www.sfmoma.org/"
    }
]

이와 같이, LLM 출력을 컴퓨터 처리하기 용이한 구조로 생성할 수 있다.

참고로, 여기서 사용한 JSON 문법은 다음과 같이 정형 규칙 언어로 정의된 것을 사용한 것이다. 

TEXT TO XML 
다음은 XML에서 데이터를 검색하는 방법을 보여준다. 

from langchain.output_parsers import XMLOutputParser
from langchain_community.chat_models import ChatAnthropic
from langchain_core.prompts import PromptTemplate

model = ChatAnthropic(model="claude-2", max_tokens_to_sample=512, temperature=0.1)

actor_query = "Generate the shortened filmography for Tom Hanks."
output = model.invoke(
    f"""{actor_query}
Please enclose the movies in <movie></movie> tags"""
)
print(output.content)

JSON RAG 처리
이 예시는 특정 웹사이트 내용과 제품 정보가 포함된 JSON파일(예제 다운로드)를 RAG 한다. 예제파일은 input_json 폴더를 만들고, 그 아래 복사한다. 

이제 다음과 같이 랭체인를 이용해 JSON 파일을 RAG 처리한다. 
from langchain_community.vectorstores import Chroma
from langchain_community.chat_models import ChatOllama
from langchain_community.embeddings.fastembed import FastEmbedEmbeddings
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
from langchain_core.documents import Document
import sys, os, json
 
class ChatWebDoc:
vector_store = None
retriever = None
chain = None
 
def __init__(self):
self.model = ChatOllama(model="mistral:instruct")
#Loading embedding
self.embedding = FastEmbedEmbeddings()
 
self.text_splitter = CharacterTextSplitter(chunk_size=1024, chunk_overlap=100)
self.prompt = ChatPromptTemplate.from_messages(
[
("system", 
"""You are an assistant for question-answering tasks. Use only the following 
context to answer the question. If you don't know the answer, just say that you don't know.
 
CONTEXT:
 
{context}
"""),
("human", "{input}"),
]
)
 
def ingest(self, url_list):
#Load web pages
docs = WebBaseLoader(url_list).load()
chunks = self.text_splitter.split_documents(docs)
 
#Create vector store
vector_store = Chroma.from_documents(documents=chunks, 
embedding=self.embedding, persist_directory="./chroma_db")
def ingest_json(self, input_folder):
all_chunks = []
for filename in os.listdir(input_folder):
if filename.endswith('.json') == False:
continue
file_path = os.path.join(input_folder, filename)
with open(file_path, 'r', encoding='utf-8') as file:
dataset = json.load(file)
for data in dataset:
text = json.dumps(data) if isinstance(data, dict) else str(data)
document = Document(page_content=text, metadata={"source": "local"})
all_chunks.append(document)
vector_store = Chroma.from_documents(documents=all_chunks, 
embedding=self.embedding, 
persist_directory="./chroma_db")        
 
def load(self):
vector_store = Chroma(persist_directory="./chroma_db", 
embedding_function=self.embedding)
 
self.retriever = vector_store.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"k": 3,
"score_threshold": 0.5,
},
)
 
document_chain = create_stuff_documents_chain(self.model, self.prompt)
self.chain = create_retrieval_chain(self.retriever, document_chain)
 
def ask(self, query: str):
if not self.chain:
self.load()
 
result = self.chain.invoke({"input": query})
 
print(result["answer"])
for doc in result["context"]:
print("Source: ", doc.metadata["source"])
 
 
def build():
w = ChatWebDoc()
w.ingest([
"https://www.webagesolutions.com/courses/WA3446-comprehensive-angular-programming",
"https://www.webagesolutions.com/courses/AZ-1005-configuring-azure-virtual-desktop-for-the-enterprise",
"https://www.webagesolutions.com/courses/AZ-305T00-designing-microsoft-azure-infrastructure-solutions",
])
w.ingest_json("./input_json")
 
def chat():
w = ChatWebDoc()
 
w.load()
build()

while True:
query = input(">>> ")
 
if len(query) == 0:
continue
 
if query == "/exit":
break
 
w.ask(query)
 
if len(sys.argv) < 2:
chat()
elif sys.argv[1] == "--ingest":
build()

실행 결과는 다음과 같다. 

답변이 잘 생성되나, 매 질문에 따라 잘못된 답을 생성하기도 한다. 이런 이유로, 답변 정확도를 높이기 위한 좀 더 다양한 LLM RAG 처리 옵션과 기법이 적용될 필요가 있다.

결론
앞서 다양한 방식으로 목적 달성을 위해 솔류션 테크 트리를 탐색하고, 시도했으나, 아직 완벽하지 않고, 공개된 자료들도 에러가 많이 발생하였다. 앞에 예시된 내용은 그 테크트리 중 일부 성공한 것만 기술한 것이다.

사실, LLM 튜닝은 리소스 제약으로 인해 대부분 열악한 인프라 환경?인 국내에서는 RAG를 대안으로 선택하지만, 한계가 명확하다. 사실, LLM 튜닝 및 풀튜닝 할 수 있는 능력있는 국내 IT기업은 거의 없다고 봐야 한다. 참고로, 라마3 8B모델 훈련에는 H100 GPU(80GB) 하나를 사용했을 때 학습 기간은 1,388.9 개월이 걸린다(계산방법 참고 - Transformer Math 101). 

이보다 더 적은 파라메터수를 가진 라마2 7B 모델 훈련에는 114.5개월(약 10년)/A100 GPU 이 소요된다.

대안인 파인튜닝, 양자화, LoRA방식은 많은 학습 데이터(최소 수만건 이상 잘 정재된 데이터셋)이 필요하고, 그 결과 또한 한계가 있다(계산된 logit 확률로만 이야기하는 논문이나 리더보드 발표 결과를 믿을 수 없음. 실제 결과는 사용하기 어려운 경우가 많다). 이런 이유로 RAG를 하지만, 이 또한, 연구가 필요한 이슈들이 많다. 

좀 더 상세한 내용은 아래 링크를 참고한다.

레퍼런스
JSON에만 특화된 RAG는 다음과 같다. 

부록: RAG 기반 SQL 코딩 에이전트 개발

댓글 없음:

댓글 쓰기