오류 해결 과정

RAG 제작시 목차 제거 자동화 하기

필만이 2024. 10. 7. 16:14

배경

  1. 목차가 있으면 Rag 검색기에 걸려서, 검색기의 성능을 떨어트렸다.

  2. 원본을 눈으로 보고 전처리(목차 제거) 해줬다.

  3. 대량의 문서를 업로드 할 때는, 목차 분류를 자동화 할 필요성 느낌.

해결과정

  1. 시행착오를 거치며 목차를 구별하는 아래 프롬트를 제작해서 LLM을 돌림.

    """You are tasked with identifying whether the provided text is a "목차" or part of the "본문" of a book. 
    Follow these instructions:
    
    1. If the text contains 4 or more numeric indicators like chapter numbers or page numbers, label it as "목차".
    2. If the text includes continuous, long sentences with descriptions, examples, or in-depth explanations, label it as "본문".
    4. If the text contains a mix of both, prioritize the second rule (numeric indicators).
    
    Here is the text:
    
    "{context}"
    
    Please return only "목차" or "본문" without any extra explanation.
    """
    
    원본 정보 : 출판정보(1~5), 프롤로그(6~11), 목차(12~18), 본문(21~)
    {1: '본문', -> 오답 : 원래 출판정보
    3: '본문', -> 오답 : 원래 출판정보
    5: '본문', -> 오답 : 원래 출판정보
    6: '본문', -> 오답 : 원래 프롤로그
    7: '본문', -> 오답 : 원래 프롤로그
    8: '본문', -> 오답 : 원래 프롤로그
    9: '본문', -> 오답 : 원래 프롤로그
    10: '본문', -> 오답 : 원래 프롤로그
    11: '본문', -> 오답 : 원래 프롤로그
    12: '목차',
    13: '목차',
    14: '목차',
    15: '목차',
    16: '목차',
    17: '목차',
    18: '목차',
    21: '본문',
    22: '본문',
    23: '본문',

    결과 : 목차는 잘 구분했지만 필요없는 정보를 본문으로 분류했다.

  2. 출판정보가 제목등으로 이루어져 글자수가 작은걸 보고 코드적으로 150자 이하는, 일괄 목차로 분류.
    결과 : 출판정보는 잘 구분했지만, 프롤로그는 글자수가 많아서 실패, 그리고 150자 제한은 일부 본문도 제거할 위험이 있음. 이 코드는 폐기하고 적용하지 않도록한다.

  3. 생각해보니 1번처럼 한번 분석하고, 마지막 목차 이전 페이지를 모두 목차로 한꺼번에 바꾸게 코드처리 하면 됨.

    {1: '목차:재설정',
    3: '목차:재설정',
    5: '목차:재설정',
    6: '목차:재설정',
    7: '목차:재설정',
    8: '목차:재설정',
    9: '목차:재설정',
    10: '목차:재설정',
    11: '목차:재설정',
    12: '목차:재설정',
    13: '목차:재설정',
    14: '목차:재설정',
    15: '목차:재설정',
    16: '목차:재설정',
    17: '목차:재설정',
    18: '목차:재설정',
    21: '본문',
    22: '본문',
    23: '본문',

    결과 : 목차와 본문이 잘 구분됐다. 나중에 rag에 임베딩할때는 이 목차 페이지를 제거하고 하면된다.

효율화

  1. 만약 GPT등 api를 사용한다면, 모든 페이지를 돌려서 목차를 구분하면 비효율적이다. 전 페이지의 1/15만 목차 구분 연산을 하게 설정
  2. 근래에는 rag 제작시 페이지 요약 연산을 따로 하는 경우도 많은데 -> 앞 목차를 제외하고 본문 페이지만 요약한다면, 사용량을 절약할 수 있다.

결론

  1. 문서의 구조상 거의 대부분이 목차가 본문전에 있다. 추가로 프롤로그 찾아내는 프롬트 제작하는 것보다,마지막 목차 이전을 일괄 제거하는 전처리는 합리적으로 보인다.
  2. 목차를 구분하는 과정은 추가 요금이 들수 있지만, 일부 페이지로 한정한다면, 추가 비용은 그리 크지 않다.

전체코드

from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
# create_stuff_documents_chain은 여러 문서를 결합하고 요약하여 하나의 결과로 만드는 
# 작업에 사용됩니다. 특히, 여러 개의 문서를 한꺼번에 처리하고 이를 하나의 통합된 정보로 제공해야 할 때 유용
from langchain.chains.combine_documents import (
    create_stuff_documents_chain,
)
from langchain_core.documents import Document
from langchain_community.chat_models import ChatOllama
from dotenv import load_dotenv
import os

# .env 파일 로드
load_dotenv()

# 요약을 위한 프롬프트 템플릿을 정의합니다.
prompt = PromptTemplate.from_template(
    """You are tasked with identifying whether the provided text is a "목차" or part of the "본문" of a book. 
Follow these instructions:

1. If the text contains 4 or more numeric indicators like chapter numbers or page numbers, label it as "목차".
2. If the text includes continuous, long sentences with descriptions, examples, or in-depth explanations, label it as "본문".
4. If the text contains a mix of both, prioritize the second rule (numeric indicators).

Here is the text:

"{context}"

Please return only "목차" or "본문" without any extra explanation.
"""
)

# ChatOpenAI 모델의 또 다른 인스턴스를 생성합니다. (이전 인스턴스와 동일한 설정)
llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0,)
# llm = ChatOllama(model="llama3.1:70b", temperature=0.3)


# 문서 요약을 위한 체인을 생성합니다.
# 이 체인은 여러 문서를 입력받아 하나의 요약된 텍스트로 결합합니다.
text_summary_chain = create_stuff_documents_chain(llm, prompt)

last_page = None  # 마지막 목차 페이지를 추적할 변수

def create_text_summary(state: GraphState):
    """
    주어진 상태에서 텍스트 데이터를 추출하고 요약을 생성하는 함수.
    마지막 목차 페이지 이전의 모든 항목을 '목차:재설정'으로 재분류.

    Args:
    - state (GraphState): 텍스트 데이터가 포함된 상태 객체.

    Returns:
    - GraphState: 요약된 텍스트가 포함된 새로운 상태 객체.
    """
    # state에서 텍스트 데이터를 가져옴
    texts = state["texts"]

    # 전체 텍스트 중 1/15 크기로 선택 (너무 많은 데이터를 한꺼번에 처리하지 않도록 설정)
    selection_size = round(len(texts) / 15)
    selected_texts = dict(list(texts.items())[:selection_size])

    # 요약된 텍스트를 저장할 딕셔너리 초기화
    text_summary = {}

    # 선택된 텍스트를 페이지 번호(키) 기준으로 정렬
    sorted_texts = sorted(selected_texts.items(), key=lambda x: x[0])

    # 각 페이지의 텍스트를 Document 객체로 변환하고 요약 요청을 만듦
    inputs = [
        {"context": [Document(page_content=text)]} for page_num, text in sorted_texts
    ]

    # text_summary_chain을 사용하여 일괄 처리로 요약을 생성 (batch 처리)
    summaries = text_summary_chain.batch(inputs)

    # 생성된 요약을 페이지 번호와 함께 딕셔너리에 저장
    for (page_num, _), summary in zip(sorted_texts, summaries):
        text_summary[page_num + 1] = summary

    # 마지막 목차 페이지를 찾아서 추적
    last_page = max((i for i, value in text_summary.items() if value == '목차'), default=None)

    # 마지막 목차 페이지 이전의 모든 항목을 '목차:재설정'으로 변경
    if last_page is not None:
        for i in text_summary:
            if i > last_page:
                break
            text_summary[i] = '목차:재설정'

    # 요약된 텍스트를 포함한 새로운 GraphState 객체 반환
    return GraphState(text_summary=text_summary)

# create_text_summary 함수를 호출하여 텍스트 요약을 생성합니다.
state_out = create_text_summary(loaded_state)

# 생성된 요약을 기존 state에 업데이트
state_out['text_summary']