데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      OpenLab - OpenLLM, Ensemble Retriever(pre), Rerank(post)가 적용된 RAG 챗봇 구현하기

      leeeeeegilho 24.06.18
      204 0 0
      DEVOTEE 요약
      OpenLLM 스터디 모임에서 RAG 챗봇을 구현했으며, CPU 환경에서 BM25와 FAISS 리트리버를 사용한 챗봇을 테스트했습니다. Post 방식(Rerank)을 적용한 챗봇이 더 자연스럽고 정확한 답변을 제공하며, 성능 향상을 위해 GPU 환경에서 추가 실험이 필요하다는 결론을 도출했습니다. 또한 MultiQuery, LongContextReorder 기법을 추가하여 성능 비교를 진행할 계획입니다.
      DEVOTEE 추천 블로그

      OpenLLM 스터디 모임 경과를 정리하면서 현재까지 구현된 RAG 챗봇 내용을 함께 공유드립니다.


      스터디 모임 경과

      - 주제 : OpenLLM 스터디 4차 오프라인 모임

      - 일시 : 6/19 20:00 ~ 21:00

      - 장소 : 판교사옥

      - 참석 : 멤버 7명 중 4명

      • 10명에서 3명 탈퇴(1명:이직, 2명:업무변경)

      - 스터디 내용

      1) RAG 에서 Pre/Post Retriever 코드 구현 공유 및 의견 교환

      2) RAG 평가 ragas 공유

      3) 다음 스터디까지 제출 코드(파이썬 가상환경 requirements 포함)

      • openllm 별도 로딩 및 서비스 시작

      • 파일 업로드: 질문에 답변할 문서 데이터셋(pdf)을 업로드

      • Splitter: 업로드된 파일을 분할

      • 임베딩 저장: 각 문서에 대한 벡터 임베딩 저장

      • Pre-Retriever: 질문 벡터와 문서 임베딩을 비교하여 초기 문서 후보군을 선택

      • Retriever: Pre-Retriever 결과를 재평가하여 상위 문서들을 선별

      • Reranking: Retriever 결과 문서들에 대해 순위를 재조정

      • Ensemble (옵션): 여러 Retriever 모델의 결과 중 가장 우수한 결과 선택

      • Post-Retriever: 최종 선별된 문서들과 질문을 생성 모델에 전달하여 openllm이 답변 생성


      RAG 챗봇 구현하기

      Streamlit 을 베이스로 하는 챗봇을 구현 했습니다.

      챗봇에 적용된 리트리버는

      • Pre : Ensemble Retriever (BM25 + FAISS)

      • Post : Rerank

      로 구성 되어 있습니다.


      실습은 2가지 형태로 이루어져 있습니다.

      1. Pre 방식만 적용된 챗봇

      2. Pre + Post 방식이 적용된 챗봇

      환경

      • CPU : Apple M1 Max

      • RAM : 32GB


      Pre 방식만 적용된 챗봇

      리트리버 : Ensemble (BM25 + FAISS)

      
      
      import streamlit as st
      from pathlib import Path
      from langchain_community.chat_models import ChatOllama
      from langchain_community.document_loaders import PyMuPDFLoader
      from langchain.text_splitter import RecursiveCharacterTextSplitter
      from langchain_community.embeddings import HuggingFaceEmbeddings
      from langchain_community.vectorstores import Chroma, FAISS
      from langchain.prompts import ChatPromptTemplate
      from langchain.schema.output_parser import StrOutputParser
      from langchain.schema.runnable import RunnablePassthrough
      from langchain.retrievers import BM25Retriever, EnsembleRetriever
      
      class RagEnsemble:
          def __init__(self):
              """GemmaRAG 클래스의 초기화 메서드로 필요한 객체들을 초기화합니다."""
              self.model = ChatOllama(model="gemma:7b-instruct", temperature=0)
              self.text_splitter = RecursiveCharacterTextSplitter(
                  chunk_size=1024,
                  chunk_overlap=100
              )
              self.retriever = None
              self.rag_chain = None
      
          def format_docs(self, docs):
              """문서 리스트를 하나의 문자열로 포맷팅합니다."""
              return '\n\n'.join([d.page_content for d in docs])
      
          def setup_pdf_retriever(self, pdf_file_path: str):
              """PDF 파일을 읽어들여 리트리버를 설정합니다."""
              # PDF 파일 로드
              pages = PyMuPDFLoader(file_path=pdf_file_path).load()
              # 문서 분할
              docs = self.text_splitter.split_documents(pages)
      
              # 문장을 임베딩으로 변환하고 벡터 저장소에 저장
              embeddings = HuggingFaceEmbeddings(
                  model_name='jhgan/ko-sroberta-nli',
                  model_kwargs={'device':'cpu'},
                  encode_kwargs={'normalize_embeddings':True}
              )
      
              # FAISS 벡터 스토어 생성
              vectorstore_faiss = FAISS.from_documents(docs, embeddings)
              # BM25 리트리버 생성
              bm25_retriever = BM25Retriever.from_documents(docs)
              # FAISS 리트리버 생성
              faiss_retriever = vectorstore_faiss.as_retriever(search_kwargs={'k': 5})
      
              # 앙상블 리트리버 생성
              self.retriever = EnsembleRetriever(
                  retrievers=[bm25_retriever, faiss_retriever],
                  weights=[0.5, 0.5]  # 가중치 설정
              )
      
              # 답변 생성을 위한 프롬프트 템플릿 설정
              template = '''Answer the question based only on the following context:
              {context}
      
              Question: {question}
              '''
              self.prompt = ChatPromptTemplate.from_template(template)
              self.rag_chain = (
                  {"context": self.retriever | self.format_docs, "question": RunnablePassthrough()}
                  | self.prompt
                  | self.model
                  | StrOutputParser()
              )
      
          def ask(self, query: str):
              """질문을 받아 답변을 생성합니다."""
              if not self.rag_chain:
                  return "PDF 파일을 먼저 설정해주세요."
              return self.rag_chain.invoke(query)
      
          def clear(self):
              """설정을 초기화합니다."""
              self.retriever = None
              self.rag_chain = None
      
      # Streamlit 앱 구성
      def main():
          """Streamlit 앱의 메인 함수로, PDF Q&A 챗봇을 구성합니다."""
          st.title("PDF Q&A 챗봇")
          st.write("PDF 파일에서 정보를 추출하여 질문에 답변하는 챗봇입니다.")
      
          # GemmaRAG 클래스 인스턴스 생성
          gemma_rag = RagEnsemble()
          # PDF 파일 경로 설정
          pdf_file_path = '경남교육청_수목관리_업무매뉴얼.pdf'
          # PDF 리트리버 설정
          gemma_rag.setup_pdf_retriever(pdf_file_path)
      
          # 사용자 입력 받기
          user_input = st.text_input("질문을 입력하세요:")
      
          if st.button("질문하기"):
              if user_input:
                  # 질문에 대한 답변 생성
                  answer = gemma_rag.ask(user_input)
                  st.write("답변: ", answer)
              else:
                  st.write("질문을 입력하세요.")
      
      if __name__ == "__main__":
          main()

      실행 및 답변 결과

      스크린샷 2024-06-18 오전 1.26.14.png


      깔끔하게 답변하는 것을 볼 수 있습니다.


      여기에 Post방식(Rerank)를 추가 해보겠습니다


      Pre + Post 방식 적용된 챗봇

      리트리버 : Ensemble (BM25 + FAISS) + Rerank

      
      import streamlit as st
      
      from pathlib import Path
      
      from langchain_community.chat_models import ChatOllama
      
      from langchain_community.document_loaders import PyMuPDFLoader
      
      from langchain.text_splitter import RecursiveCharacterTextSplitter
      
      from langchain_community.embeddings import HuggingFaceEmbeddings
      
      from langchain_community.vectorstores import FAISS
      
      from langchain.prompts import ChatPromptTemplate
      
      from langchain.schema.output_parser import StrOutputParser
      
      from langchain.schema.runnable import RunnablePassthrough
      
      from langchain.retrievers import BM25Retriever, EnsembleRetriever
      
      import torch
      
        
      
      class RagEnsembleRerank:
      
      def __init__(self):
      
      """초기화 메서드로 필요한 객체들을 초기화합니다."""
      
      self.model = ChatOllama(model="gemma:7b-instruct", temperature=0)
      
      self.text_splitter = RecursiveCharacterTextSplitter(
      
      chunk_size=1024,
      
      chunk_overlap=100
      
      )
      
      self.retriever = None
      
      self.rag_chain = None
      
      self.embeddings = HuggingFaceEmbeddings(
      
      model_name='jhgan/ko-sroberta-nli',
      
      model_kwargs={'device': 'cpu'},
      
      encode_kwargs={'normalize_embeddings': True}
      
      )
      
        
      
      def format_docs(self, docs):
      
      """문서 리스트를 하나의 문자열로 포맷팅합니다."""
      
      return '\n\n'.join([d.page_content for d in docs])
      
        
      
      def rerank(self, query, docs):
      
      """질문과 문서 리스트를 받아 유사도 기반으로 문서를 재정렬합니다."""
      
      query_embedding = self.embeddings.embed_query(query) # 질문 임베딩 생성
      
      doc_texts = [doc.page_content for doc in docs] # 문서 텍스트 추출
      
      doc_embeddings = [self.embeddings.embed_query(doc) for doc in doc_texts] # 문서 임베딩 생성
      
      scores = [torch.cosine_similarity(torch.tensor(query_embedding), torch.tensor(doc_embedding), dim=0).item()
      
      for doc_embedding in doc_embeddings] # 코사인 유사도 계산
      
      sorted_docs = [doc for _, doc in sorted(zip(scores, docs), key=lambda x: x[0], reverse=True)] # 유사도 기준 정렬
      
      return sorted_docs
      
        
      
      def setup_pdf_retriever(self, pdf_file_path: str, bm25_weight: float, faiss_weight: float):
      
      """PDF 파일을 읽어들여 리트리버를 설정합니다."""
      
      pages = PyMuPDFLoader(file_path=pdf_file_path).load() # PDF 파일 로드
      
      docs = self.text_splitter.split_documents(pages) # 문서 분할
      
        
      
      vectorstore_faiss = FAISS.from_documents(docs, self.embeddings) # FAISS 벡터 스토어 생성
      
      bm25_retriever = BM25Retriever.from_documents(docs) # BM25 리트리버 생성
      
      faiss_retriever = vectorstore_faiss.as_retriever(search_kwargs={'k': 5}) # FAISS 리트리버 생성
      
        
      
      self.retriever = EnsembleRetriever(
      
      retrievers=[bm25_retriever, faiss_retriever],
      
      weights=[bm25_weight, faiss_weight] # 가중치 설정
      
      )
      
        
      
      # 답변 생성을 위한 프롬프트 템플릿 설정
      
      template = '''Answer the question based only on the following context:
      
      {context}
      
        
      
      Question: {question}
      
      '''
      
      self.prompt = ChatPromptTemplate.from_template(template)
      
      self.rag_chain = (
      
      self.prompt | self.model | StrOutputParser()
      
      )
      
        
      
      def ask(self, query: str):
      
      """질문을 받아 답변을 생성합니다."""
      
      if not self.rag_chain:
      
      return "PDF 파일을 먼저 설정해주세요."
      
      result = self.retriever.get_relevant_documents(query) # 관련 문서 검색
      
      reranked_docs = self.rerank(query, result) # 문서 재정렬
      
      formatted_docs = self.format_docs(reranked_docs) # 문서 포맷팅
      
      response = self.rag_chain.invoke({"context": formatted_docs, "question": query}) # 답변 생성
      
      return response
      
        
      
      def clear(self):
      
      """설정을 초기화합니다."""
      
      self.retriever = None
      
      self.rag_chain = None
      
        
      
      # Streamlit 앱 구성
      
      def main():
      
      # 페이지 설정
      
      st.set_page_config(page_title="PDF Q&A Chatbot", page_icon=":books:")
      
        
      
      # CSS 스타일링
      
      st.markdown(
      
      """
      
      <style>
      
      body {
      
      background-color: #f0f2f6;
      
      color: #333;
      
      }
      
      .chat-container {
      
      display: flex;
      
      flex-direction: column;
      
      align-items: flex-start;
      
      margin-bottom: 100px;
      
      }
      
      .user-message, .bot-message {
      
      padding: 10px 15px;
      
      border-radius: 10px;
      
      margin: 5px 0;
      
      max-width: 80%;
      
      }
      
      .user-message {
      
      background-color: #DCF8C6;
      
      align-self: flex-end;
      
      }
      
      .bot-message {
      
      background-color: #E8E8E8;
      
      }
      
      .input-container {
      
      position: fixed;
      
      bottom: 0;
      
      width: 100%;
      
      background-color: white;
      
      padding: 10px;
      
      box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
      
      }
      
      .slider-container {
      
      display: flex;
      
      justify-content: space-between;
      
      margin-bottom: 20px;
      
      }
      
      </style>
      
      """, unsafe_allow_html=True
      
      )
      
        
      
      # 타이틀과 설명
      
      st.title("PDF Q&A 챗봇 :books:")
      
      st.write("PDF 파일에서 정보를 추출하여 질문에 답변하는 챗봇입니다.")
      
        
      
      # 가중치 슬라이더 추가
      
      st.markdown('<div class="slider-container">', unsafe_allow_html=True)
      
      bm25_weight = st.slider("BM25 Weight", 0.0, 1.0, 0.5)
      
      faiss_weight = st.slider("FAISS Weight", 0.0, 1.0, 0.5)
      
      st.markdown('</div>', unsafe_allow_html=True)
      
        
      
      if bm25_weight + faiss_weight != 1.0:
      
      st.error("BM25 Weight와 FAISS Weight의 합은 1.0이어야 합니다.")
      
      return
      
        
      
      gemma_rag = RagEnsembleRerank()
      
      pdf_file_path = '경남교육청_수목관리_업무매뉴얼.pdf'
      
      gemma_rag.setup_pdf_retriever(pdf_file_path, bm25_weight, faiss_weight)
      
        
      
      # 대화 기록 초기화
      
      if 'chat_history' not in st.session_state:
      
      st.session_state.chat_history = []
      
        
      
      # 대화 상자
      
      chat_placeholder = st.empty()
      
      for message in st.session_state.chat_history:
      
      if message['role'] == 'user':
      
      chat_placeholder.markdown(
      
      f"<div class='chat-container'><div class='user-message'>{message['content']}</div></div>",
      
      unsafe_allow_html=True)
      
      else:
      
      chat_placeholder.markdown(
      
      f"<div class='chat-container'><div class='bot-message'>{message['content']}</div></div>",
      
      unsafe_allow_html=True)
      
        
      
      # 사용자 입력
      
      user_input = st.text_input("질문을 입력하세요:", key="input", label_visibility="hidden")
      
        
      
      # 입력 컨테이너
      
      st.markdown('<div class="input-container">', unsafe_allow_html=True)
      
      if st.button("질문하기", key="button"):
      
      if user_input:
      
      # 사용자 질문 기록
      
      st.session_state.chat_history.append({"role": "user", "content": user_input})
      
      answer = gemma_rag.ask(user_input)
      
      if isinstance(answer, str):
      
      # 챗봇 응답 기록
      
      st.session_state.chat_history.append({"role": "bot", "content": answer})
      
      chat_placeholder.markdown(
      
      f"<div class='chat-container'><div class='user-message'>{user_input}</div><div class='bot-message'>{answer}</div></div>",
      
      unsafe_allow_html=True)
      
      else:
      
      st.write("오류가 발생했습니다. 다시 시도해주세요.")
      
      else:
      
      st.write("질문을 입력하세요.")
      
      st.markdown('</div>', unsafe_allow_html=True)
      
        
      
      if __name__ == "__main__":
      
      main()

      Class 구성 요소 설명

      • init: 초기화 메서드로 필요한 객체들을 초기화합니다.

      • format_docs: 문서 리스트를 하나의 문자열로 포맷팅합니다.

      • rerank: 질문과 문서 리스트를 받아 유사도 기반으로 문서를 재정렬합니다.

      • setup_pdf_retriever: PDF 파일을 읽어들여 리트리버를 설정합니다.

      • ask: 질문을 받아 답변을 생성합니다.

      • clear: 설정을 초기화합니다.

      Ensemble 리트리버의 경우 BM25의 가중치와 Faiss의 가중치의 합이 1이 되어야 하고 각 가중치 설정에 따라 값이 다르게 나올 '가능성'이 있기 때문에 UI에 입력받는 것을 추가했습니다.

      또한 채팅의 기록을 저장하려고 했습니다.(제대로 작동이 되질 않는 것 같습니다 ㅜ )

      실행 및 답변 결과

      스크린샷 2024-06-18 오전 1.14.18.png

      스크린샷 2024-06-18 오전 1.18.58.png

      Rerank 적용 전과 후의 답변차이

      질문 : 겨울철에 내한성이 강한 나무에는 어떤 것이 있을까요?


      적용 전 : 겨울철에 내한성이 강한 나무는 낙엽수입니다. 낙엽수는 동기 전정 시기인 12~3월 때는 눈을 잎을 잘 절때 순다.

      -> 시도 마다 약간의 차이는 있지만, 낙엽수라는 정답을 얘기해줬습니다. 하지만 뒤에 따라오는 문장이 질문에 비해 부자연스럽다는 것을 알 수 있습니다.


      적용 후 : 동기 전정은 12~3월에 실시하는 전정으로 주로 내한성이 강한 낙엽수를 대상으로 한다.

      -> 완벽하게 자연스러운 답변이라고 볼 수는 없습니다. 하지만 위의 답변보다는 더 자연스럽게 문장이 나오고 답변이 조금 더 정확하다고 볼 수 있습니다.


      결론

      Post 방식을 적용하면 조금 더 낫다! 하지만 이번에 CPU만을 가지고 했기 때문에 아무래도 성능에 문제가 있을거라고 생각됩니다.

      따라서 더 특정한 도메인 데이터를 가지고 GPU 환경에서 진행할 필요가 있습니다.


      또한 MultiQuery, LongContextReorder 방식을 추가하여 비교해 보는 것도 좋을것 같습니다.

      이유는 다양한 RAG 기법 중 자신의 의도와 맞는 것을 찾는게 중요하기 때문입니다.


      그리고 다양한 기법에 대한 RAG 점수를 측정하여 정확하게 접근을 할 필요가 있습니다.


      Future Work

      1. 다른 기법 추가

      2. UI변경 ->LangServe

      3. Agent 제작

      4. RAG 점수 측정

      5. 데이터 셋 선정

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      leeeeeegilho 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기