2024년 2월 14일 수요일

로컬 호스트 LLM 오픈소스 OLLAMA 기반 PDF 지식 챗봇 서비스 간단히 만들어보기

요즘 LLM 모델을 사용하는 방법이 점차 간편해 지고 있어, 자체적으로 LLM을 구축해, 챗봇, 전문가 시스템 등을 본인 서버에서 제공하는 경우가 많아지고 있다. 이 글은 GPU있는 PC에서 직접 실행해 볼 수 있도록, 로컬 호스트 LLM(Large Language Model) 오픈소스 기반 PDF 지식 챗봇 서비스를 간단히 개발해 본다. 

이를 위해, 기존 BIM pdf 파일을 검색해 학습하고, LLM에 증강 학습한 후, 이를 간단한 UI로 웹서비스 하는 과정을 간략히 보여주고, 구현한다.

이 글은 로컬 LLM의 편한 개발을 지원하는 OLLAMA, LLM 프롬프트 엔지니어링 프레임웍인 LangChain, 텍스트 임베딩 벡터 데이터베이스 Chroma, 손쉬운 web app 개발 지원 도구인 Streamlit을 사용한다. 이를 이용해, 간단한 BIM 전문 지식을 PDF로 학습한 챗봇을 간단히 개발한다.  
로컬 호스트 LLM 챗봇 아키텍처

구현된 BIM 지식 챗봇 서비스

LLM 에 관련된 깊은 내용은 아래 링크를 참고한다. 이 글은 여러 참고 자료를 이용해 작성된 것이다. 상세 내용은 레퍼런스를 참고바란다.
설치
설치를 위해서는 NVIDIA driver, CUDA, tensorflow, pytorch 등 기본 딥러닝 개발 환경이 설치되어 있어야 한다(최소 구동 GPU RAM 6GB).
설치 순서는 다음과 같다. 
 
1. 기본 패키지를 설치한다. LLM 모델 기반 서비스 개발 지원 라이브러리 LangChain, 앱 App UI 개발 지원 streamlit.io, 텍스트 임베딩 벡터 데이터베이스 Chroma DB 등을 설치한다. 
pip install langchain streamlit streamlit_chat pypdf fastembed chardet
pip install chromadb==0.4.15
다양한 LLM 모델을 이용한 서비스 개발을 지원하는 랭체인(LangChain) 패키지
간단한 코드로 웹 App 개발 지원 UI 라이브러리 streamlit.io 패키지

혹은 pip와 유사한 패키지 설치 관리자인 poetry 설치 후, 다음 사용 패키지들을 pyproject.toml 이름으로 저장한 후, 설치한다.
[tool.poetry]
name = "Local LLM"
version = "0.1.0"
description = ""
readme = "README.md"

[tool.poetry.dependencies]
python = ">=3.9,<3.9.7 || >3.9.7,<3.12"
pypdf = "^3.17.1"
streamlit = "^1.29.0"
streamlit-chat = "^0.1.1"
langchain = "^0.0.343"
fastembed = "^0.1.1"
openai = "^1.3.6"
langchainhub = "^0.1.14"
chromadb = "^0.4.18"
watchdog = "^3.0.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

poetry 설치는 다음과 같다. 
poetry install
패키지 설치 모습

2. Ollama 웹사이트를 방문해, 로컬 LLM을 지원하는 OLLAMA를 설치한다. 
ollama run llama2
3. 다음 Ollama 명령을 터미널에서 실행하여, 많이 사용되는 몇몇 LLM 학습 모델을 다운로드 한다. 상세한 모델 종류는 여기를 참고한다.
ollama pull mistral 
ollama pull dolphin-phi
ollama pull llama2

다음 명령으로 다운로드된 모델 이미지를 확인한다.
ollama list

PDF 기반 전문가 서비스 구축을 위한 RAG 파이프라인 구축
RAG(Retrieval-augmented generation)는 학습된 LLM 모델의 외부 데이터 소스를 활용하여, 검색 모델을 증강하는 기법이다. 이를 통해, 질문이 입력되면, QAChain을 통해, 벡터 스토리지에 해당 PDF 내용 임베딩이 있는 지 확인하고, 이 임베딩 컨텐츠를 LLAMA 서버에 전달하여, 적절한 답변을 얻는다. 
Chroma DB 기반 RAG 처리 개념

RAG는 LLM의 다음 문제를 해결한다.
  • 편향성: 훈련 데이터에만 편향된 답변 생성
  • 잘못된 정보 생성: 잘못되거나 유해한 정보 생성
  • 할루시네이션(Hallucination) 현상: 사실이 아닌 정보를 생성하는 문제
  • 비전문성 정보 생성: 학습되지 않은 질문에 대한 답변을 전문가처럼 생성하는 문제
이를 위해, 전문지식이 포함된 PDF파일들을 입력하면, 이를 문서 청크로 분할해, 벡터 저장소에 청크 파일을 임베딩하여 데이터베이스에 저장한다. 

다음은 그 구조를 보여준다. 
PDF 텍스트 임베딩 벡터 데이터베이스 구축 후 LLM 기반 질의 답변 생성 구조

다음 코드를 rag.py란 파일로 저장한다.
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOllama
from langchain.embeddings import FastEmbedEmbeddings
from langchain.schema.output_parser import StrOutputParser
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain.vectorstores.utils import filter_complex_metadata

class ChatPDF:
    vector_store = None
    retriever = None
    chain = None

    def __init__(self):
        self.model = ChatOllama(model="mistral")  # OLLAMA의 mistral 모델 이용
        self.text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100) # PDF 텍스트 분할
        self.prompt = PromptTemplate.from_template(
            """
            <s> [INST] 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. [/INST] </s> 
            [INST] Question: {question} 
            Context: {context} 
            Answer: [/INST]
            """
        )

    def ingest(self, pdf_file_path: str):
        docs = PyPDFLoader(file_path=pdf_file_path).load()  # 랭체인의 PDF 모듈 이용해 문서 로딩
        chunks = self.text_splitter.split_documents(docs) # 문서를 청크로 분할
        chunks = filter_complex_metadata(chunks)  

        vector_store = Chroma.from_documents(documents=chunks, embedding=FastEmbedEmbeddings())  # 임메딩 벡터 저장소 생성 및 청크 설정
        self.retriever = vector_store.as_retriever(search_type="similarity_score_threshold",
            search_kwargs={
                "k": 3,
                "score_threshold": 0.5,
            },
        )  # 유사도 스코어 기반 벡터 검색 설정

        self.chain = ({"context": self.retriever, "question": RunnablePassthrough()} | self.prompt | self.model | StrOutputParser()) # 프롬프트 입력에 대한 모델 실행, 출력 파서 방법 설정

    def ask(self, query: str):  # 질문 프롬프트 입력 시 호출
        if not self.chain:
            return "Please, add a PDF document first."
        return self.chain.invoke(query) 

    def clear(self):  # 초기화
        self.vector_store = None
        self.retriever = None
        self.chain = None 

웹 기반 App 개발
다음과 같이 UI를 가진 앱을 app.py 파일명으로 코딩한다.
import os, tempfile
import streamlit as st
from streamlit_chat import message
from rag import ChatPDF

st.set_page_config(page_title="ChatPDF") # 앞서 정의한 PDF 임베딩 벡터 데이터베이스 RAG 모듈 임포트

def display_messages():   # 메시지 출력
    st.subheader("Chat")  
    for i, (msg, is_user) in enumerate(st.session_state["messages"]):
        message(msg, is_user=is_user, key=str(i))
    st.session_state["thinking_spinner"] = st.empty()

def process_input():   # 챗 메시지 입력 
    if st.session_state["user_input"] and len(st.session_state["user_input"].strip()) > 0:
        user_text = st.session_state["user_input"].strip()
        with st.session_state["thinking_spinner"], st.spinner(f"Thinking"):
            agent_text = st.session_state["assistant"].ask(user_text)   # 사용자 입력에서 답변 획득

        st.session_state["messages"].append((user_text, True))
        st.session_state["messages"].append((agent_text, False))

def read_and_save_file():  # file_upader UI에서 PDF 선택 시 호출
    st.session_state["assistant"].clear()  # LLM 어시스턴스 초기화
    st.session_state["messages"] = []
    st.session_state["user_input"] = ""

    for file in st.session_state["file_uploader"]:
        with tempfile.NamedTemporaryFile(delete=False) as tf:
            tf.write(file.getbuffer())
            file_path = tf.name

        with st.session_state["ingestion_spinner"], st.spinner(f"Ingesting {file.name}"):
            st.session_state["assistant"].ingest(file_path)  # PDF 파일을 어시스턴스에 전달
        os.remove(file_path)

def page():
    if len(st.session_state) == 0:
        st.session_state["messages"] = []
        st.session_state["assistant"] = ChatPDF()  # PDF, 벡터 데이터베이스, LLM 모델 호출 역할하는 객체 설정

    # UI 정의
    st.header("Chat BIM")  # 타이틀  
    st.subheader("Upload a document")  # 서브헤더 
    st.file_uploader("Upload document", type=["pdf"],
        key="file_uploader", on_change=read_and_save_file,
        label_visibility="collapsed", accept_multiple_files=True,
    )  # 업로더 
    st.session_state["ingestion_spinner"] = st.empty()

    display_messages()  # 메시지 출력
    st.text_input("Message", key="user_input", on_change=process_input)  # 채팅 입력 버튼 생성

if __name__ == "__main__":
    page()

LLM 기반 챗봇 실행하기
다음 명령을 실행한다. 
streamlit run app.py

실행되면, BIM 관련 PDF 문서를 업로드한다. 여기서는 Development of a Conceptual Mapping Standard to Link Building and Geospatial Information 논문 PDF파일을 사용하였다.

관련 질문을 하면, 증강 학습된 BIM 내용에 근거해 잘 답변하는 것을 확인할 수 있다. 이로써, BIM 전문가가 대답하는 것 같은 챗봇을 개발해 보았다. 

참고로, OLLAMA는 로컬에서 실행될 수 있도록, 경량화되어 다음과 같이 적은 GPU 메모리를 사용한다. 

마무리
이 글을 통해, 기존 BIM pdf 파일을 검색해 학습하고, LLM에 증강 학습한 후, 이를 간단한 UI로 웹서비스 하는 과정을 간략히 보여주고, 구현해 보았다. 이를 통해, 어느 정도 수준?의 BIM전문가 챗봇 시스템을 구현하였다. 

앞서 언급된 라이브러리를 이용하면, 수많은 PDF파일을 미리 임베딩 벡터 데이터베이스로 만들어 놓고, 좀 더 전문적인 질의 답변을 도출할 수 있다. 이외, LangChain등의 다양한 기능을 사용하면, 좀 더 특화된 서비스 개발이 가능하다. 

이와 같은 과정을 통해, 전문영역에 특화된 자체 서버에서 실행되는 로컬 LLM 서비스 개발이 가능하다.

참고: OLLAMA기반 Image to Text LMM(Large Multi-modal Model) 사용
OLLAMA가 설치되어 있다면, 다양한 멀티모달 LLM 모델을 다운로드하고 실행해 볼 수 있다.

예를 들어, 고양이 사진을 주어, 이를 설명하게 하는 Image to Text를 간단히 실행해 볼 수 있다. 이는 LMM 모델인 LLaVA를 다운로드해 프롬프트를 입력하면 된다. 

앞의 사진을 cat.jpg로 저장한 후, 다음 명령을 터미널에서 실행해 본다. 
ollama run llava "describe this iamge: ./cat.jpg"

그럼, 주어진 이미지를 설명하는 Text를 쉽게 얻을 수 있을 것이다.

글 내용은 다음과 같다.

"테이블 다리로 보이는 것 옆에 똑바로 앉아 있는 얼룩무늬 고양이의 이미지입니다. 고양이는 뚜렷한 어두운 줄무늬가 있는 밝은 주황색 털을 가지고 있는데, 이는 흔히 볼 수 있는 패턴입니다. 얼룩 고양이는 눈을 크게 뜨고 카메라를 정면으로 차분한 태도로 바라보고 있는 모습입니다. 
배경에는 바닥에 다음과 같은 패턴의 러그가 깔려 있습니다. 베이지색과 기타 중성색이 포함됩니다. 전경에 테이블 다리가 있기 때문에 다이닝 룸과 같은 생활 공간처럼 보입니다."

거의 일치한다.

참고: Error: Your app is having trouble loading the streamlit_chat.streamlit_chat component
Error: Your app is having trouble loading the streamlit_chat.streamlit_chat component 에러가 발생할 경우가 있다. 이 경우는 다음과 같이 패키지 설치 파일을 수정하고 재설치하면 해결 될 수 있다(참고-링크). 
streamlit-chat = "0.0.2.2"

참고: 커스텀 데이터 학습 파인튜닝 방법
다음은 사용자 데이터 학습, 파인튜닝 방법이다. 
참고: OLLAMA 메뉴얼
다음을 참고한다.

참고: LLAMA2 패키지 메뉴얼
레퍼런스

댓글 없음:

댓글 쓰기