본문 바로가기
Project/Personal

Local sLLM Chatbot

Unsloth에서 말뭉치(Corpus) 학습

base 모델: llama-3-8b-KrBllossom

- 로드 설정은 기본 Unsloth FastLanguageModel.from_pretrained()의 예제코드 값 사용 (4bit 양자화 포함)

 

peft 설정: r값을 16이 아닌 8로 설정하여 LoRA 개입을 줄이고 dropout값을 0.1로 지정하여 과적합을 방지

 

데이터: /home/data/의 하위 디렉토리를 전부 포함한 모든 xlsx파일의 “sentence”컬럼

“sentecne”컬럼 외에 더 학습해야하는 컬럼이 있을 수도 있으나 편의상 해당 컬럼만 선택

모든 xlsx파일의 sentence컬럼을 한 파일의 한 컬럼으로 합쳐서 저장 (109955개 데이터)

파일: /home/data/data-00000-of-00001.arrow

 

텍스트 전처리: 단순하게 각 문장 뒷부분마다 EOS_TOKEN을 붙임 formatting_func(example) 함수

훈련 방법: 단순 텍스트 문장 말뭉치 훈련에 맞는 SFTTrainer 사용

 

- 여기서 packing = True로 설정하여 109955개 데이터를 4862개 데이터로
압축하여 훈련하면 훈련 시간은 약 2시간 23분이 걸림

- Packing = False로 설정할 경우 109955개 전체를 학습하여 약 8시간이 걸림

packing = False 설정시, 학습이 잘 안되는 문제 발생

규제를 높이는 방향으로 파라미터 조정

 

훈련 결과: (packing=True): 607steps에서 loss 2.869, grad_norm 0.345         

                     (packing=False): 13744steps에서 loss 3.121, grad_norm 5.48

둘 다 학습이 실패하지는 않았으나 packing 옵션을 준 것이 상대적으로 안정적인 학습을 진행

 

모델 저장: 일단 말뭉치 학습한 모델을 저장

 

 

 

 

Unsloth에서 ORPO QA 학습

base 모델: /home/Llama-3-8B_UnslothCorpus (no packing 말뭉치 학 모델 사용)

- 로드 설정은 기본 Unsloth FastLanguageModel.from_pretrained()의 예제코드 값을 사용 (4bit 양자화 포함)

 

peft 설정: 데이터 개수가 6297개로 적음을 고려하여 r값과 alpha16이 아닌 32로 설정 LoRA 영향력

 

dropout = 0.1 과적합 방지

 

데이터: /home/QA_forUnsloth.parquet

- ORPO in Unsloth용 맞춤 6297개 지시문, 질문, 답변, 오답 컬럼이 있는 데이터.

 

텍스트 전처리: format_prompt(sample) 함수 공식 예제에 나온 함수 그대로 사용

 

훈련 방법: Unsloth 공식 예제에 나온 ORPOTrainer값 기준에서 조정

- max_steps은 따로 지정하지 않음. 예제처럼 max_steps = 30 지정시, 매우 빠른 훈련 종료 (130)

- batch size = 4, gradient steps = 8 지나치게 많은 step으로 인한 과적합을 방지.

- unsloth 환경에서는 ORPO 공식 learning_rate값보다 default값이 더 효율적 loss값이 더 줄어듦

- lr_scheduler_type = "cosine"로 설정 196 steps에서 약 30분의 훈련시간 소요

 

훈련 결과: loss 2.491, grad_norm 0.558, rewards/accuracy 0.937

lossnorm이 안정적인 낮은 값을 기록하고, accuracy1로 과적합되는 경우를 피하면서 높은 값을 유지한 훈련이다.

 

모델 저장: UnslothORPO 파일에 저장

 

추론 성능: 드물게 정답과 가깝게 말하는 경우도 존재한다. 그러나 답변이 같은 질문에 일관되지 않다.

주제와 관련있게 답변하지만 정답이 아닌 경우가 대부분이며 환각도 잦다.

 

 

 

 

 

 

RAG 적용

LangchainRetrievalQA 형태

 

 

- 과제: OpenAIEmbeddings을 사용하여 외부 연결 토큰이 항상 필요하므로 로컬 기반이라고 보기 어려움

- 기존 OpenAIEmbeddings을 사용하여 저장한 DB를 로컬 임베딩 모델로 로드하면 차원 불일치 오류 발생

InvalidDimensionException: Dimensionality of ( ) does not match index dimensionality ( )

 

로컬화 방법: 로컬 임베딩 모델로 DB를 저장하고 로드 (bge-m3)

- DB 저장 코드에서 로컬 모델로 변경 후 HuggingFaceBgeEmbeddings을 통해 bge 모델 연결

/home/rag_data/vector_DB에 저장

 

실행 방법: [3]chatbot_RAG.py을 터미널에서 실행 후 노란색 지시문에 따라서 원하는 내용을 수행한다.

 

unsloth으로 모델을 로드하고 작동한 코드이다.

sloth: 나무늘보

따라서 unsloth을 정상적으로 사용한다면 그림과 같이 나무늘보 그림이 나온다.

 

 

 

질문하고 답변받고 답변 출처를 챗봇이 무엇을 참고했는지 확인한다.

 

 

 

잘못 입력한 경우 다시 입력하면 된다.

 

 

q를 통해 py파일을 종료할 수 있다.

 

 

 

 

 

 

 

 

 

이런 py 파일 코드는?

import transformers, torch
import time, re, os
from langchain_huggingface import HuggingFacePipeline
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain.chains import RetrievalQA, ConversationalRetrievalChain
from unsloth import FastLanguageModel

# 모델 로드
max_seq_length = 4096
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "UnslothORPO",
    max_seq_length = max_seq_length,
    # dtype = None,
    # load_in_4bit = True ,
)


# 파이프라인 설정
text_generation_pipeline = transformers.pipeline(
    model=model,
    tokenizer=tokenizer,
    task="text-generation",
    repetition_penalty=1.1,
    return_full_text=True,
    max_new_tokens=2048,
    output_scores=True,
)

# llm 설정
llm = HuggingFacePipeline(pipeline=text_generation_pipeline)

# 벡터DB 설정
vectorstore = Chroma(persist_directory = '/rag_data/vector_DB', 
                    embedding_function = HuggingFaceBgeEmbeddings(model_name="/model/bge-m3", 
                                                                  model_kwargs={'device': 'cuda'})
                    )

# Retriever 설정
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 4}) 

# RetrievalQA 설정
qa_chain = RetrievalQA.from_chain_type(
                                    llm=llm, 
                                    chain_type="stuff", 
                                    retriever=retriever, 
                                    verbose=True,
                                    return_source_documents=True,
                                   )   

# 챗봇
def chatbot():
    while True:
        print("\033[93m안녕하세요. 궁금한 점을 물어보세요.\033[0m")
        user_input = input()
        
        if user_input.lower() == 'q':
            print("\033[93m채팅을 종료합니다.\033[0m")
            break

        start_time = time.time()
        test_qa = qa_chain.invoke(user_input) # 호출시 invoke를 사용해야 불필요한 경고문을 줄일 수 있다.
        end_time = time.time()

        # 질문 텍스트
        print("\033[1;91mQuestion:\033[0m\n", test_qa['query'])

        # 답변 텍스트
        helpful_answer = test_qa['result'].split('Helpful Answer:')[1].strip()

        # 정규 표현식으로 '(출처:', '출처:', '$$' 등 불필요한 부분 제거
        helpful_answer_clean = re.split(r'\(출처:|출처:|\$\$|If you have|OR', helpful_answer)[0].strip()
        print("\n\n\033[1;96mAnswer:\033[0m\n", helpful_answer_clean)

        # 시간 텍스트
        print(f"\n\n\033[1;95mTotal time:\033[0m {round(end_time-start_time, 2)} sec.")

        while True:
            print("\n\033[93m답변의 출처가 궁금하면 s를, 계속해서 질문하려면 enter를, 채팅을 종료하려면 q를 입력하세요.\033[0m\n")
            next_action = input().strip().lower()

            if next_action == 's':
                for i in test_qa['source_documents']:
                    print(i, '\n')
            elif next_action == 'q':
                print("\033[93m채팅을 종료합니다.\033[0m")
                return
            elif next_action == '':
                break
            else:
                print("\n\033[41m잘못된 입력입니다. 다시 시도해주세요.\033[0m\n")




if __name__ == "__main__":
    chatbot()

 

 

 

물론 추가로 RAG 작업, 파인튜닝 작업이 필요할 수 있다. 이것은 어디까지나 예시일 뿐.

 

 

 

 

 

728x90
반응형