2024년 7월 17일 수요일

sLLM과 vLLM에 대한 이야기

이 글은 sLLM과 vLLM에 대한 이야기를 간략히 나눔한다.


레퍼런스

LLM 기반 센서 데이터 해석 방법

이 글은 LLM 기반 센서 데이터 해석 방법을 정리한 것이다. 

사실, 센서 데이터를 다루는 목적은 주로 몇 개로 한정된다.
  • 예측
  • 분류
  • 이상패턴 검출
LLM은 이 과정을 모두 수행할 수 있으나, 사실, 잘 알려진 기존 딥러닝 모델에 비해 비효율적이다. 트랜스포머 계열 LLM은 구조와 사용 리소스를 고려했을 때, 좀 더 유용한 목적이 아니면, 그리 추천하기 어렵다. 

LLM의 강점은 연역, 추론(reasoning), 생성인데, 이와 관련된 연구들을 살펴본다. 

LLM sense

Semantic Space Informed Prompt Learning with LLM for Time Series Forecasting
Ragal LLM RAG
레퍼런스

2024년 7월 12일 금요일

pandas AI 이용한 표 형식 데이터 생성AI로 처리해 보기

이 글은 pandas AI 이용한 표 형식 데이터 생성AI로 처리해 보는 방법을 간략히 정리한다.

설치는 다음과 같다. 
pip install pandasai

표에서 정보를 질의하고 생성하는 코드는 다음과 같다. 
import os
import pandas as pd
from pandasai import Agent

sales_by_country = pd.DataFrame({
    "country": ["United States", "United Kingdom", "France", "Germany", "Italy", "Spain", "Canada", "Australia", "Japan", "China"],
    "revenue": [5000, 3200, 2900, 4100, 2300, 2100, 2500, 2600, 4500, 7000]
})

in your .env file)
os.environ["PANDASAI_API_KEY"] = "<input your key>"

agent = Agent(sales_by_country)
answer = agent.chat('Which are the top 5 countries by sales?')
print(answer)

결과는 다음과 같다.

레퍼런스

2024년 7월 4일 목요일

AutoRAG 활용 LLM RAG 최적화하기

이 글은 AutoRAG를 활용해 LLM RAG를 최적화하는 방법을 보여준다.
LLM RAG 전략 조합

LLM RAG는 질문에 대한 유사 문서를 얼마나 잘 검색할 수 있는 지가 중요하다. 문서 청크 집합에서 질문 관련 자료를 검색하는 방법은 다양한 알고리즘이 존재한다. 이를 자동화할 수 있는 라이브러리가 AutoRAG이다. 

AutoRAG는 다양한 전략을 조합해 튜닝하는 AutoTuning과 유사한 구조를 가지고 있다. 다음은 AutoRAG의 설정이다. 전략 파라메터를 먼저 설정해 놓고, 이를 조합해 정확도가 높은 전략을 얻는 식으로 동작한다. 

node_lines:
  - node_line_name: retrieve_node_line
    nodes:
      - node_type: retrieval
        strategy:
          metrics: [ retrieval_f1, retrieval_recall, retrieval_precision ]
        top_k: 3
        modules:
          - module_type: bm25
          - module_type: vectordb
            embedding_model: huggingface_all_mpnet_base_v2
          - module_type: hybrid_rrf
            target_modules: ('bm25', 'vectordb')
            rrf_k: [ 3, 5, 10 ]
          - module_type: hybrid_cc
            target_modules: ('bm25', 'vectordb')
            weights:
              - (0.5, 0.5)
              - (0.3, 0.7)
              - (0.7, 0.3)
          - module_type: hybrid_rsf
            target_modules: ('bm25', 'vectordb')
            weights:
              - (0.5, 0.5)
              - (0.3, 0.7)
              - (0.7, 0.3)
          - module_type: hybrid_dbsf
            target_modules: ('bm25', 'vectordb')
            weights:
              - (0.5, 0.5)
              - (0.3, 0.7)
              - (0.7, 0.3)
  - node_line_name: post_retrieve_node_line
    nodes:
      - node_type: prompt_maker
        strategy:
          metrics: [ meteor, rouge, bert_score ]
        modules:
          - module_type: fstring
            prompt: "Read the passages and answer the given question. \n Question: {query} \n Passage: {retrieved_contents} \n Answer : "
      - node_type: generator
        strategy:
          metrics: [ meteor, rouge, bert_score ]
        modules:
          - module_type: llama_index_llm
            llm: ollama
            model: llama3
            temperature: [ 0.1, 0.5, 1.0 ]
            batch: 1

설정을 확인하면 알 수 있지만, 학습 데이터셋, LLM 모델, RAG 벡터데이터베이스, RAG 정확도를 측정할 메트릭스, RAG 검색 알고리즘, 실행 프롬프트가 파라메터로 정의되어 있다. 

이를 읽어, langchain과 같은 라이브러리를 내부적으로 호출하며, 각 전략 조합 별 정확도 높은 옵션을 리스트한다.

레퍼런스

2024년 7월 2일 화요일

LLM 학습 데이터 개발 및 준비 방법

이 글은 LLM 학습 데이터 작성 방법을 간략히 정리한다.

LLM 모델 개발을 하면서, 가장 힘든 일 중 하나는 학습 데이터를 어떻게 확보하느냐는 것이다. 본인의 경우, 여러 시행착오를 거쳐, 다음과 같은 데이터 개발 전략을 사용했다.
  • 허깅페이스 등 기존에 있는 데이터가 있다면, 이를 사용해 본다. 
  • 부족하다면, ChatGPT API등을 이용해, 재료가 되는 텍스트, PDF을 업로드하고, 훈련할 질문 답변 등 데이터를 생성한다.
  • 비용이 비싸다면, LLAMA와 같은 오픈 LLM 모델을 사용해 훈련 데이터를 생성한다.
  • 생성된 학습 데이터 중 이상 데이터를 체크해, 가능하다면, 문장 및 토큰 해석기로 자동 필터링하도록 한다.
  • 사람이 샘플링해 이상 데이터를 체크, 제거한다.
  • 데이터 종류 별 분포가 균형되도록 조정한다.

레퍼런스

2024년 6월 30일 일요일

Langgraph 기반 다중 LLM 에이전트 개발하기

이 글은 Langgraph를 이용해, 간단히 다중 LLM 에이전트를 개발하는 방법을 정리한 것이다.
개발 환경 설정
다음과 같이 개발 환경을 설치한다.
pip install langchain langgraph milvus

RAG 처리 및 에이전트 프롬프트 정의하기
웹 사이트에서 얻은 BIM(Building Information Modeling), GIS, 기술 표준 전문 지식들을 RAG 처리한다. 이후, 에이전트에서 사용할 프롬프트들을 정의한다. 각 프롬프트가 포함된 LangChain Expression Language (LCEL) 체인은 RAG 벡터 데이터베이스에 문서에 대한 QA 질문, 질문 결과에 대한 정확도 및 확각 유무 평가, 만약, 질문에 대한 적절한 대답이 없다면 웹페이지 검색하도록 유도하도록 구성된다.

다음 코드를 입력 후 실행한다. 
import os
from dotenv import load_dotenv
from typing import List
from typing_extensions import TypedDict
from langchain import hub
from langchain.globals import set_verbose, set_debug
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import Chroma
from langchain_community.chat_models import ChatOllama
from langchain.prompts import PromptTemplate
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.output_parsers import StrOutputParser
from langchain_core.output_parsers import JsonOutputParser
from langchain.schema import Document

# setup environment for LLM RAG
load_dotenv()
set_debug(True)
set_verbose(True)

# Load documents
urls = [
"https://www.mdpi.com/2220-9964/7/5/162/",
"https://www.tandfonline.com/doi/full/10.1080/19475683.2020.1743355/",
"https://knowledge.bsigroup.com/articles/harmonize-digitize-and-rationalize-exchange-information-confidently-with-bs/",
]

docs = [WebBaseLoader(url).load() for url in urls]
docs_list = [item for sublist in docs for item in sublist]
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=250, chunk_overlap=0
)
doc_splits = text_splitter.split_documents(docs_list)

# Create vectorstore and retriever
vectorstore = Chroma.from_documents(
documents=doc_splits,
collection_name="BIM_GIS",
embedding=HuggingFaceEmbeddings(),
persist_directory="./Chroma_rag.db",

)
retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 3}) # type={similarity_score_threshold, mmr, bm25}, https://wikidocs.net/234016

# Load LLM model using Ollama
local_llm = 'llama3'
llm = ChatOllama(model=local_llm, format="json", temperature=0)

# Test grader prompt with JSON output
prompt = PromptTemplate(
template="""You are a grader assessing relevance 
of a retrieved document to a user question. If the document contains keywords related to the user question, grade it as relevant. It does not need to be a stringent test. The goal is to filter out erroneous retrievals. 
Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.
Provide the binary score as a JSON with a single key 'score' and no premable or explaination.
 
Here is the retrieved document: 
{document}
Here is the user question: 
{question}
""",
input_variables=["question", "document"],
)

retrieval_grader = prompt | llm | JsonOutputParser()
question = "ISO 19166's BIM to GIS Element Mapping"
docs = retriever.invoke(question, ) # https://www.kaggle.com/code/marcinrutecki/rag-mmr-search-in-langchain
doc_txt = docs[0].metadata if 'description' in docs[0].metadata else docs[0].page_content
answer = retrieval_grader.invoke({"question": question, "document": doc_txt})
print(answer)

# Test QA prompt
prompt = PromptTemplate(
template="""You are an assistant for question-answering tasks. 
Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. 
Use three sentences maximum and keep the answer concise:
Question: {question} 
Context: {context} 
Answer: 
""",
input_variables=["question", "document"],
)

llm = ChatOllama(model=local_llm, temperature=0)

rag_chain = prompt | llm | StrOutputParser() # Chain

question = "ISO 19166's BIM to GIS Element Mapping"
docs = retriever.invoke(question)
answer = rag_chain.invoke({"context": docs, "question": question})
print(answer)

# Test hallucination grader prompt with JSON output
llm = ChatOllama(model=local_llm, format="json", temperature=0)

prompt = PromptTemplate(
template="""You are a grader assessing whether 
an answer is grounded in / supported by a set of facts. Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.
Here are the facts:
{documents} 

Here is the answer: 
{generation}
""",
input_variables=["generation", "documents"],
)

hallucination_grader = prompt | llm | JsonOutputParser()
answer = hallucination_grader.invoke({"documents": docs, "generation": answer})
print(answer)

출력이 다음과 같다면, 성공한 것이다.

LLM 기반 다중 에이전트 구현하기
이제 앞에서 정의된 체인들을 이용해, 에이전트를 정의하고, 그래프를 만든 후 빌드한다.
# Web search tool setup
from langchain_community.tools.tavily_search import TavilySearchResults
tavily_api_key = os.environ['TAVILY_API_KEY'] = <Travily API>
web_search_tool = TavilySearchResults(k=3,  tavily_api_key=tavily_api_key)

# Define the graph state using langgraph
from langgraph.graph import END, StateGraph

class GraphState(TypedDict):
"""
Represents the state of our graph.

Attributes:
question: question
generation: LLM generation
web_search: whether to add search
documents: list of documents 
"""
question : str
generation : str
web_search : str
documents : List[str]

def retrieve(state): # node. Retrieve documents from vectorstore
# Args. state (dict): The current graph state
# Returns. state (dict): New key added to state, documents, that contains retrieved documents
print("---RETRIEVE---")
question = state["question"]
documents = retriever.invoke(question)
return {"documents": documents, "question": question}

def generate(state): # node. Generate answer using RAG on retrieved documents
# Args: state (dict): The current graph state
# Returns: state (dict): New key added to state, generation, that contains LLM generation
print("---GENERATE---")
question = state["question"]
documents = state["documents"]
generation = rag_chain.invoke({"context": documents, "question": question})
return {"documents": documents, "question": question, "generation": generation}

def grade_documents(state): # node. Determines whether the retrieved documents are relevant to the question If any document is not relevant, we will set a flag to run web search
# Args: state (dict): The current graph state
# Returns: state (dict): Filtered out irrelevant documents and updated web_search state
print("---CHECK DOCUMENT RELEVANCE TO QUESTION---")
question = state["question"]
documents = state["documents"]
filtered_docs = [] # Score each doc
web_search = "No"
for d in documents:
score = retrieval_grader.invoke({"question": question, "document": d.page_content})
grade = score['score']
if grade.lower() == "yes": # Document relevant
print("---GRADE: DOCUMENT RELEVANT---")
filtered_docs.append(d)
else: # Document not relevant
print("---GRADE: DOCUMENT NOT RELEVANT---")
# We do not include the document in filtered_docs. We set a flag to indicate that we want to run web search
web_search = "Yes"
continue
return {"documents": filtered_docs, "question": question, "web_search": web_search}
def web_search(state): # Web search based based on the question 
# Args: state (dict): The current graph state
# Returns: state (dict): Appended web results to documents
print("---WEB SEARCH---")
question = state["question"]
documents = state["documents"]

docs = web_search_tool.invoke({"query": question}) # Web search
web_results = "\n".join([d["content"] for d in docs])
web_results = Document(page_content=web_results)
if documents is not None:
documents.append(web_results)
else:
documents = [web_results]
return {"documents": documents, "question": question}

def route_question(state): # Conditional edge. Route question to web search or RAG.
# Args: state (dict): The current graph state
# Returns: str: Next node to call
print("---ROUTE QUESTION---")
question = state["question"]
print(question)

source = question_router.invoke({"question": question})  
print(source)
print(source['datasource'])
if source['datasource'] == 'web_search':
print("---ROUTE QUESTION TO WEB SEARCH---")
return "websearch"
elif source['datasource'] == 'vectorstore':
print("---ROUTE QUESTION TO RAG---")
return "vectorstore"

def decide_to_generate(state): # Determines whether to generate an answer, or add web search
# Args: state (dict): The current graph state
# Returns: str: Binary decision for next node to call
print("---ASSESS GRADED DOCUMENTS---")
question = state["question"]
web_search = state["web_search"]
filtered_documents = state["documents"]

if web_search == "Yes": # All documents have been filtered check_relevance. We will re-generate a new query
print("---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, INCLUDE WEB SEARCH---")
return "websearch"
else: # We have relevant documents, so generate answer
print("---DECISION: GENERATE---")
return "generate"

def grade_generation_v_documents_and_question(state): # Conditional edge. Determines whether the generation is grounded in the document and answers question.
# Args: state (dict): The current graph state
# Returns: str: Decision for next node to call
print("---CHECK HALLUCINATIONS---")
question = state["question"]
documents = state["documents"]
generation = state["generation"]

score = hallucination_grader.invoke({"documents": documents, "generation": generation})
grade = score['score']

if grade == "yes": # Check hallucination
print("---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---")
print("---GRADE GENERATION vs QUESTION---") # Check question-answering
score = answer_grader.invoke({"question": question,"generation": generation})
grade = score['score']
if grade == "yes":
print("---DECISION: GENERATION ADDRESSES QUESTION---")
return "useful"
else:
print("---DECISION: GENERATION DOES NOT ADDRESS QUESTION---")
return "not useful"
else:
pprint("---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---")
return "not supported"

workflow = StateGraph(GraphState)

# Define the nodes
workflow.add_node("websearch", web_search) # web search
workflow.add_node("retrieve", retrieve) # retrieve
workflow.add_node("grade_documents", grade_documents) # grade documents
workflow.add_node("generate", generate) # generatae

# Build graph and compile
workflow.set_conditional_entry_point(
route_question,
{
"websearch": "websearch",
"vectorstore": "retrieve",
},
)

workflow.add_edge("retrieve", "grade_documents")
workflow.add_conditional_edges(
"grade_documents",
decide_to_generate,
{
"websearch": "websearch",
"generate": "generate",
},
)
workflow.add_edge("websearch", "generate")
workflow.add_conditional_edges(
"generate",
grade_generation_v_documents_and_question,
{
"not supported": "generate",
"useful": END,
"not useful": "websearch",
},
)

app = workflow.compile()

# Test the graph
from pprint import pprint
inputs = {"question": "What are the types of agent memory?"}
for output in app.stream(inputs, {"recursion_limit": 3}):
for key, value in output.items():
pprint(f"Finished running: {key}:")
pprint(value["generation"])

다음과 같이 출력되면 성공한 것이다..

레퍼런스