LangChain 활용해서 내 글스타일에 맞춰 글쓰는 LLM 구축하기 Ver0.1

LangChain 활용해서 내 글스타일에 맞춰 글쓰는 LLM 구축하기 Ver0.1
Photo by Joshua Hoehne / Unsplash

목적 및 배경

  • 매일 글을 쓰는 입장에서 보다 많은 글을 작성하기 위해서 1차로 글의 구조만 잡아주고, 나머지 내용은 LLM으로 채운 후 퇴고를 하는 형태를 시도해보려고 한다.

Code

01. Obsidian에 있는 내 글을 모아서 전처리하기

import os  
import re  
import time  
import pickle  
  
def preProcessNote(file, path_dir):  
    try:  
        file_path = os.path.join(path_dir, file)  
        with open(file_path, 'r', encoding='utf-8') as f:  
            lines = f.readlines()  
  
        # 지워야 할 지점 체크  
        start_idx = None  
        end_idx = None  
        for idx, line in enumerate(lines):  
            if re.match('---', line):  
                if start_idx is None:  
                    start_idx = idx  
                else:  
                    end_idx = idx  
                    break  
  
        if start_idx is None or end_idx is None:  
            raise ValueError(f"Start or end delimiter not found in file: {file_path}")  
  
        # 삭제하기  
        lines = lines[:start_idx] + lines[end_idx+1:]  
        note = ''.join(lines)  
  
        # 데이터 정제  
        note = re.sub(r'[\n\t]+', '', note)  
  
        return note  
  
    except FileNotFoundError as e:  
        raise FileNotFoundError(f"File not found: {file_path}") from e  
    except PermissionError as e:  
        raise PermissionError(f"Permission denied for file: {file_path}") from e  
    except UnicodeDecodeError as e:  
        raise UnicodeDecodeError(f"Error decoding file: {file_path}. Ensure the file is UTF-8 encoded.") from e  
    except ValueError as e:  
        raise ValueError(f"Processing error in file {file_path}: {str(e)}") from e  
    except OSError as e:  
        raise OSError(f"OS error occurred while processing file: {file_path}") from e  
    except Exception as e:  
        raise Exception(f"Unexpected error while processing file: {file_path}") from e  
  
def preProcessNotes(path_dir="data/md", file_ext="md", output_dir="output", output_filename="notes.pkl"):  
    start_time = time.time()  # 시작 시간 기록  
  
    try:  
        # 파일 리스트 가지고 오기  
        file_list = os.listdir(path_dir)  
        file_list = [file for file in file_list if file.endswith(f'.{file_ext}')]  
    except FileNotFoundError as e:  
        raise FileNotFoundError(f"Directory not found: {path_dir}") from e  
    except PermissionError as e:  
        raise PermissionError(f"Permission denied for directory: {path_dir}") from e  
    except OSError as e:  
        raise OSError(f"OS error occurred while accessing directory: {path_dir}") from e  
    except Exception as e:  
        raise Exception(f"Unexpected error while listing files in directory: {path_dir}") from e  
  
    # 출력 디렉토리 생성  
    if not os.path.exists(output_dir):  
        try:  
            os.makedirs(output_dir)  
        except OSError as e:  
            raise OSError(f"Failed to create output directory: {output_dir}") from e  
  
    notes = []  
  
    for file in file_list:  
        try:  
            note = preProcessNote(file, path_dir)  
            notes.append(note)  
        except Exception as e:  
            print(f"Error occurred while processing {file}: {e}")  
  
    # notes 리스트를 pickle 파일로 저장  
    output_file_path = os.path.join(output_dir, output_filename)  
    try:  
        with open(output_file_path, 'wb') as pkl_file:  
            pickle.dump(notes, pkl_file)  
    except Exception as e:  
        raise Exception(f"Failed to save notes to pickle file: {output_file_path}") from e  
  
    end_time = time.time()  # 종료 시간 기록  
    elapsed_time = end_time - start_time  # 경과 시간 계산  
  
    print(f"Processing completed successfully in {elapsed_time:.2f} seconds. Notes saved to {output_file_path}")  
  
    return notes

02. LangChain 구축

import pickle  
import os  
import ollama  
from langchain.schema import Document  
from langchain_community.embeddings import OllamaEmbeddings  
from langchain_chroma import Chroma

notes = pickle.load(open('output/notes.pkl', 'rb'))  
notes_dict = {index: document for index, document in enumerate(notes)}

# notes_dict에서 각 문서를 Document 객체로 변환  
documents = [Document(page_content=content) for content in notes_dict.values()]  
embeddings = OllamaEmbeddings(model="llama3.1:latest")

output_dir = "db"  
# 출력 디렉토리 생성  
if not os.path.exists(output_dir):  
    try:  
        os.makedirs(output_dir)  
    except OSError as e:  
        raise OSError(f"Failed to create output directory: {output_dir}") from e  
  
collection_name = "obsidian"  
# ChromaDB에 저장  
try:  
    vectordb = Chroma.from_documents(  
        documents=documents,  
        embedding=embeddings,  
        collection_name=collection_name,  # 컬렉션 이름 추가  
        persist_directory=output_dir  
    )  
    print("데이터 저장 성공")  
except ValueError as e:  
    print(f"오류 발생: {e}")  
except Exception as e:  
    print(f"예상치 못한 오류 발생: {e}")  
  
  
[#Defines the retriever
retriever = vectordb.as_retriever(search_type='mmr', search_kwargs ={'k':1})

#Gets the document for the retriever
retriever.get_relevant_documents('사전분포란?')

from langchain_core.output_parsers import StrOutputParser  
from langchain_core.prompts import ChatPromptTemplate  
from langchain_core.runnables import RunnablePassthrough  
from langchain_openai import ChatOpenAI  
from langchain_experimental.llms.ollama_functions import OllamaFunctions  
from operator import itemgetter   
  
# This is the prompt I used  
  
# It takes in the documents as {context} and user provide {topic}  
template = """Mimic the writing style in the context:  
{context} and produce a blog on the topic in Korean.  
The number of character should be more than 2000 characters.  
Topic: {topic}  
  
  
"""  
  
prompt = ChatPromptTemplate.from_template(template)  
  
model = OllamaFunctions(model="llama3.1:latest", format="json")  
# model = ChatOpenAI(api_key = "key")  
  
# Using LangCHain LCEL to supply the prompt and generate output  
chain = (  
    {  
        "context":itemgetter("topic") | retriever,  
        "topic": itemgetter("topic"),  
  
    }  
    | prompt  
    | model  
    | StrOutputParser()  
)  
#running the Chain  
chain.invoke({"topic":  "Airflow "})

수정포인트

  • VectorDB에서 검색해본 겨로가, 우선 임베딩이 충분히 한글을 반영하는 것으로 보이지 않는다 → 모델 교체 필요
  • VectorDB는 sqlite 형태로 저장이 되는데 데이터를 다시 로딩하는 과정에서 에러 발생
  • 기타 Warning에 대한 수정 필요
ValueError: Expected collection name that (1) contains 3-63 characters, (2) starts and ends with an alphanumeric character, (3) otherwise contains only alphanumeric characters, underscores or hyphens (-), (4) contains no two consecutive periods (..) and (5) is not a valid IPv4 address, got ./db/chroma.sqlite3

Read more

다중공선성은 잘못된 인과추론 결과를 만들어낼 수 있습니다.

다중공선성은 잘못된 인과추론 결과를 만들어낼 수 있습니다.

다중공선성(Multi Collinearity) * **Multi-Collinearity(다중공선성)**는 독립 변수들 간의 강한 상관관계가 존재할 때 발생합니다. 즉, 한 독립 변수가 다른 독립 변수에 의해 설명될 수 있을 정도로 상관관계가 높은 상황을 의미합니다. * 이 문제는 주로 회귀 분석에서 나타나며, 변수들 간의 관계를 해석하는 데 있어 큰 장애물이 될 수 있습니다. * 일반적인 회귀식을 $Y=

Bayesian P-Value는 불확실성을 감안하여 모델의 적합도를 평가합니다.

Bayesian P-Value는 불확실성을 감안하여 모델의 적합도를 평가합니다.

Bayesian P- Value * Bayesian P-Value는 **모델의 적합도(goodness-of-fit)**를 평가하는 데 사용됩니다. * 사후 분포(posterior distribution)를 이용하여 실제 데이터와 모델이 생성한 예상 데이터를 비교함으로써, 관측된 데이터가 모델에 의해 얼마나 잘 설명되는지를 평가합니다. * 빈도주의 p-값은 "관찰된 데이터보다 극단적인 데이터가 나올 확률"을 계산하지만, Bayesian P-Value는 "모델이 실제

Non-Identifiability는 Model Parameter를 고유하게 식별할 수 없는 현상입니다.

Non-Identifiability는 Model Parameter를 고유하게 식별할 수 없는 현상입니다.

Non Identifiability * Non-Identifiability는 주어진 데이터와 모델에 대해 특정 파라미터를 고유하게 식별할 수 없는 상황을 의미합니다. 즉, 여러 파라미터 값들이 동일한 데이터를 생성할 수 있으며, 이로 인해 특정 파라미터 값을 확정적으로 추정하기 어렵게 됩니다. * 베이지안 추론에서 Non-Identifiability는 사후 분포가 특정 파라미터 값에 대해 명확하게 수렴하지 않고, 여러 값들에 대해 비슷한 확률을

Rootgram은 큰 분산을 갖거나 비정규 형태의 데이터를 위한 히스토그램입니다.

Rootgram은 큰 분산을 갖거나 비정규 형태의 데이터를 위한 히스토그램입니다.

Rootgram * 히스토그램의 변형으로 데이터가 비정규적이거나 큰 분산을 가지는 경우, 정확한 분포를 파악하기 위해 사용됩니다. * 일반적으로 히스토그램은 데이터의 빈도를 직접적으로 나타내기 때문에, 큰 값이 빈번하게 발생하는 경우 상대적으로 작은 값을 잘 드러내지 못하는 경향이 있습니다. 반면, Rootgram은 빈도를 제곱근 형태로 변환하여, 데이터 분포의 차이를 더 잘 시각화할 수 있도록 돕습니다 * 여기서