AWS 기술 블로그

Amazon Bedrock으로 Multi Modal 문서에 대해 RAG 적용 하기

검색 증강 생성 (Retrieval-Augmented Generation, RAG)은 대규모 언어 모델 (Large Language Model, LLM)과 효율적인 데이터 검색 기능을 결합하여 정확하고 관련성 높은 응답을 생성하는 유망한 생성형 AI 기술입니다[1, 2]. RAG 방식은 최신 정보를 반영함으로써 답변의 부정확성이나 환각을 줄일 수 있어 많은 사용자들의 관심을 받고 있습니다. 그러나 RAG 시스템이 다양한 자연어 처리 작업에서 인상적인 성능을 보임에도 불구하고보다 복잡한 실제 애플리케이션에 적용되기 위해서는 여전히 해결해야 부분들이 남아 있습니다.

그 중 하나는 멀티모달 문서 (Complex Document)에 대한 처리 능력입니다. 대부분의 실제 문서에는 복잡한 아이디어와 인사이트를 전달하기위해 텍스트, 표, 이미지가 결합된 풍부한 정보가 포함되어 있습니다. 기존의 RAG 모델은 텍스트 처리에는 탁월하지만, 멀티모달 콘텐츠를 효과적으로 통합하고 이해하는 데는 어려움을 겪습니다. 이에 대응하기 위해서는 텍스트 뿐만 아니라 표, 이미지, 그래프에 대한 정보를 효과적으로 추출하고, 수집된 정보들을 RAG 파이프라인과 손쉽게 통합할 수 있는 체계가 필요합니다.

이 글에서는 텍스트, 표, 이미지와 같이 다중 포멧의 정보를 처리 할 수 있는 멀티모달 (Multi-modal) RAG 파이프라인을 소개합니다. 첫 번째로 문제 정의 에서는 멀티모달 RAG가 필요한 배경, 두 번째 해결전략 (Solution strategy) 섹션에서는 문제를 해결할 수 있는 멀티모달 LLM의 활용법 세번째 방법론에서는 멀티모달 RAG 파이프라인을 다룹니다. 마지막으로 평가 섹션에서는 평가용 데이터에 대한 성능체크를 통해 멀티모달 RAG 파이프라인의 효과를 확인 합니다.

이 글은 다중 포멧 문서(Complex Document)에 대한 RAG 시스템 구성에 필요한 배경 지식과 샘플 코드를 제공하여 독자들의 해당 시스템에 대한 이해도 향상을 목적으로 작성되었습니다.

문제 정의

대부분의 멀티모달 정보는 주로 PDF 형태로 제공되고 있기 때문에, PDF 파일로부터 정보를 추출하는 기술력은 RAG 시스템의 성능과 직결됩니다. 이러한 정보들은 PyPDF, PyMuPDF, LlamaParse, Unstructured.io와 같은 라이브러리를 통해 추출할 수 있습니다.

그러나 정보를 추출했다고 해서 끝나는 것은 아닙니다. RAG 시스템을 구축하기 위해서는 추출된 정보가 검색 가능한 형태로 색인 되어야 합니다.

잠시 검색 과정을 살펴보겠습니다. RAG 시스템에서는 사용자의 질문과 관련된 문서를 찾기 위해 임베딩 모델을 활용합니다. 하지만 이 임베딩 모델의 입력 형태는 텍스트이기 때문에 이미지 정보를 처리하기에 적합하지 않습니다. 또한, 테이블 정보의 경우 단순 텍스트만으로는 테이블의 레이아웃 정보를 제공하기 어렵습니다. 물론 테이블 정보를 파싱하는 과정에서 Markdown이나 HTML 형태로 변환하여 레이아웃 정보를 제공할 수 있지만, 현재까지는 오류가 많은 편입니다.

해결 전략

앞선 내용을 통해 멀티 모달 정보기반 RAG 시스템 구축을 위해서는 테이블 및 이미지 데이터에 대해 정보를 손실하지 않고, 검색 가능한 형태로 색인하는 과정이 필요하다는 것을 알 수 있었습니다. 이에 대한 해결책으로 멀티모달 LLM (Large Language Model)을 활용할 수 있습니다.

멀티모달 LLM은 텍스트뿐만 아니라 이미지도 입력으로 활용할 수 있는 인공지능 모델입니다. 이는 모델에게 이미지를 제공하면 해당 이미지에 대한 분석 결과를 텍스트 형태로 제공할 수 있다는 의미입니다. 이를 활용하여 다음과 같은 방법으로 문제를 해결하고자 합니다.

  1. 테이블 (이미지 변환) 및 이미지 정보는 멀티모달 LLM을 이용하여 텍스트 형태로 요약
  2. 텍스트 기반 요약 정보는 기존의 임베딩 모델를 이용하여 검색 가능한 형태로 재표현 (Representation)후 색인

방법론

아래 그림은 멀티모달 RAG 구성을 위한 솔루션 다이어그램입니다. 솔루션에 대한 전체 코드는 GitHub에서 확인 할 수 있습니다. 링크로 이동 후 “Getting Started” 섹션의 안내대로 리소스를 다운(git clone) 및 Hand on Lab을 수행하시면 됩니다. 이 과정은 환경설정 (패키지 설치 및 Amazon OpenSearch 셋업) 이 포함되어 있습니다.

그림 1: 멀티모달 RAG 솔루션 다이이그램

1. 문서로 부터 멀티모달 정보 (텍스트, 테이블, 이미지) 추출하기

현재 다양한 라이브러리(PyPDF, PyMuPDF, LlamaParse, Unstructured.io)들이 멀티 모달 정보 추출 기능을 제공하고 있습니다. 라이브러리별로 각각의 장단점이 존재하기 때문에 어떤 것이 가장 뛰어나다고 단언하기는 어렵습니다. 다만, 제안하는 솔루션에서는 텍스트, 테이블, 이미지를 모두 처리할 수 있는 Unstructured.io를 기본 파서(Parser)로 제공하고, 다중 라이브러리(LlamaParse, PyMuPDF) 활용에 대한 옵션도 제공하고 있으니 사용자 상황에 맞게 활용하시는 것을 권장드립니다.

이 글에서는 기본 파서인 Unstructured.io에 대해서만 다루도록 하겠습니다. Unstructured.io는 문서로부터 텍스트, 테이블 그리고 이미지 정보를 추출합니다. 뿐만 아니라 테이블 및 이미지에 대해서는 Coordinate 정보를 함께 제공함으로써 테이블에 대한 이미지 변환을 용이하게 합니다.

Unstructured.io의 가장 큰 장점은 API/Local hosting 모두를 제공한다는 점입니다. API 기반 라이브러리의 경우 문서가 외부로 반출되는 것이기 때문에 보안상 문제가 될 수 있지만, Local hosting의 경우 이런 문제에 해당되지 않기 때문에 안전하게 사용할 수 있습니다.

Unstructured.io를 통해 문서에서 멀티모달 정보를 파싱하는 방법은 다음과 같습니다.

from langchain_community.document_loaders import UnstructuredFileLoader

image_path = <your directory>
file_path = <your filepath>

if os.path.isdir(image_path): shutil.rmtree(image_path)
os.mkdir(image_path)

loader = UnstructuredFileLoader(
    file_path=file_path,

    chunking_strategy = "by_title",
    mode="elements",

    strategy="hi_res",
    hi_res_model_name="yolox", #"detectron2_onnx", "yolox", "yolox_quantized"

    extract_images_in_pdf=True,
    #skip_infer_table_types='[]', # ['pdf', 'jpg', 'png', 'xls', 'xlsx', 'heic']
    pdf_infer_table_structure=True, ## enable to get table as html using tabletrasformer

    extract_image_block_output_dir=image_path,
    extract_image_block_to_payload=False, ## False: to save image

    max_characters=4096,
    new_after_n_chars=4000,
    combine_text_under_n_chars=2000,

    languages= ["kor+eng"],

    post_processors=[clean_bullets, clean_extra_whitespace]
)

docs = loader.load()

전체 코드는 여기의 “Extract Text, Table and Image from documents” 입니다.

2. 테이블을 이미지로 변환하기

Unstructured.io는 내부에 빌트인 된 Table transformer 모델을 통해 테이블을 HTML 형태로 변환하여 결과를 제공합니다. 하지만 저자들이 테스트 한 결과 성능이 좋지 못하다고 판단 하였고, 이를 해결하고자 테이블 또한 이미지로 변환하여 처리하였습니다.

또한 큰 이미지에 대해 토큰 사용량이 늘어나는 것을 방지하기 위해, 특정 크기 이상의 이미지에 대해서는 이미지 크기 재조정을 해 주었습니다. 이미지에 대한 Claude 모델의 인풋 토큰 산정방식은 아래와 같습니다.

  • Image tokens for claude 3 = width px * height px)/750)

수행 코드는 아래와 같습니다.

import cv2
import math
import base64
from pdf2image import convert_from_path

def image_to_base64(image_path):
    
    with open(image_path, "rb") as image_file:
        encoded_string = base64.b64encode(image_file.read())
        
    return encoded_string.decode('utf-8')

table_as_image = True

if table_as_image:
    image_tmp_path = os.path.join(image_path, "tmp")
    if os.path.isdir(image_tmp_path): shutil.rmtree(image_tmp_path)
    os.mkdir(image_tmp_path)
    
    # from pdf to image
    pages = convert_from_path(file_path)
    for i, page in enumerate(pages):
        print (f'pdf page {i}, size: {page.size}')    
        page.save(f'{image_tmp_path}/{str(i+1)}.jpg', "JPEG")

    print ("==")

    #table_images = []
    for idx, table in enumerate(tables):
        points = table.metadata["coordinates"]["points"]
        page_number = table.metadata["page_number"]
        layout_width, layout_height = table.metadata["coordinates"]["layout_width"], table.metadata["coordinates"]["layout_height"]

        img = cv2.imread(f'{image_tmp_path}/{page_number}.jpg')
        crop_img = img[math.ceil(points[0][1]):math.ceil(points[1][1]), \
                       math.ceil(points[0][0]):math.ceil(points[3][0])]
        table_image_path = f'{image_path}/table-{idx}.jpg'
        cv2.imwrite(table_image_path, crop_img)
        #table_images.append(table_image_path)

        print (f'unstructured width: {layout_width}, height: {layout_height}')
        print (f'page_number: {page_number}')
        print ("==")

        width, height, _ = crop_img.shape
        image_token = width*height/750
        print (f'image: {table_image_path}, shape: {img.shape}, image_token_for_claude3: {image_token}' )

        ## Resize image
        if image_token > 1500:
            resize_img = cv2.resize(img, (0, 0), fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
            print("   - resize_img.shape = {0}".format(resize_img.shape))
            table_image_resize_path = table_image_path.replace(".jpg", "-resize.jpg")
            cv2.imwrite(table_image_resize_path, resize_img)
            os.remove(table_image_path)
            table_image_path = table_image_resize_path

        img_base64 = image_to_base64(table_image_path)
        table.metadata["image_base64"] = img_base64

    if os.path.isdir(image_tmp_path): shutil.rmtree(image_tmp_path)
    #print (f'table_images: {table_images}')
    images = glob(os.path.join(image_path, "*"))
    print (f'images: {images}')

전체 코드는 여기의 “table as image” 입니다.

3. 테이블 및 이미지 정보 요약하기

파서 (Unstructured.io)를 통해 추출된 텍스트, 테이블 그리고 이미지 정보는 검색 가능한 형태로의 변환이 필요합니다. 문제정의 섹션에서 언급한 대로 우리는 이 과정에서 임베딩 모델을 사용을 사용합니다. 하지만 이미지 정보의 경우 임베딩 모델에 입력으로 활용할 수 없기 때문에 해결전략 섹션에서 언급된 대로 멀티모달 LLM을 통해 이미지 정보를 텍스트로 요약하는 과정이 필요합니다. 이 글에서는 테이블 또한 이미지로 변환하여 사용하기 때문에 테이블 및 이미지에 모두에 대해 요약하는 과정을 수행합니다.

멀티모달 LLM은 Amazon BerockClaude3 Sonnet을, LLM 어플리케이션 프레임워크로는 LangChain을 사용하였습니다.

from langchain_aws import ChatBedrock
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

llm_text = ChatBedrock(
    model_id="anthropic.claude-3-sonnet-20240229-v1:0",
    client=boto3_bedrock,
    streaming=True,
    callbacks=[StreamingStdOutCallbackHandler()],
    model_kwargs={
        "max_tokens": 2048,
        "stop_sequences": ["\n\nHuman"],
    }
)

이미지 요약에 대한 코드는 아래와 같습니다.

import time
from PIL import Image
from io import BytesIO
import matplotlib.pyplot as plt
import botocore
from utils.common_utils import retry

human_prompt = [
    {
        "type": "image_url",
        "image_url": {
            "url": "data:image/png;base64," + "{image_base64}",
        },
    },
    {
        "type": "text",
        "text": '''
                Given image, give a concise summary.
                Do not insert any XML tag such as <text> and </text> when answering.
                Write in Korean.
                '''
    },
]
human_message_template = HumanMessagePromptTemplate.from_template(human_prompt)

prompt = ChatPromptTemplate.from_messages(
    [system_message_template, human_message_template]
)
summarize_chain = prompt | llm_text | StrOutputParser()

img_info = [image_to_base64(img_path) for img_path in images if os.path.basename(img_path).startswith("figure")]

@retry(total_try_cnt=5, sleep_in_sec=10, retryable_exceptions=(botocore.exceptions.EventStreamError))
def summary_img(summarize_chain, img_base64):

    img = Image.open(BytesIO(base64.b64decode(img_base64)))
    plt.imshow(img)
    plt.show()

    summary = summarize_chain.invoke(
        {
            "image_base64": img_base64
        }
    )

    return summary
    
image_summaries = []
for idx, img_base64 in enumerate(img_info):
    summary = summary_img(summarize_chain, img_base64)
    image_summaries.append(summary)
    print ("\n==")
    print (idx)

결과에 대한 예시는 다음과 같습니다.

그림 2: [이미지 요약 예시] “이 이미지는 여러 가지 상황 시 대처방법을 안내하고 있습니다. 상황별로 구분되어 응급조치법을 설명하고 있으며, 의료진, 환자, 보호자 등 다양한 인물이 묘사되어 있습니다. 전반적으로 응급상황 발생 시 신속하고 적절한 대응의 중요성을 강조하고 있습니다. 이해를 돕기 위해 구체적인 지침과 예시 상황을 제시하고 있습니다.”

테이블 요약에 대한 코드는 아래와 같습니다.

human_prompt = [
    {
        "type": "text",
        "text": '''
                 Here is the table: <table>{table}</table>
                 Given table, give a concise summary.
                 Do not insert any XML tag such as <table> and </table> when answering.
                 Write in Korean.
        '''
    },
]
human_message_template = HumanMessagePromptTemplate.from_template(human_prompt)

prompt = ChatPromptTemplate.from_messages(
    [
        system_message_template,
        human_message_template
    ]
)

summarize_chain = {"table": lambda x:x} | prompt | llm_text | StrOutputParser()

table_info = [(t.page_content, t.metadata["text_as_html"]) for t in tables]
table_summaries = summarize_chain.batch(table_info, config={"max_concurrency": 1})

결과에 대한 예시는 다음과 같습니다.

그림 3: [테이블 요약 예시] “이 표는 어린이와 청소년을 대상으로 한 예방접종 일정을 보여줍니다. 좌측 열에는 대상 감염병이 나열되어 있고, 상단 행에는 연령대별로 4세, 6세, 11세, 12세로 구분되어 있습니다. 각 셀에는 해당 연령대에서 접종해야 하는 백신의 종류와 횟수가 명시되어 있습니다.”

전체 코드는 여기의 “Summarization of table and image” 입니다.

4. OpenSearch Indexing

텍스로 형태로 변환된 정보들은 임베딩 모델을 거쳐 벡터 DB에 저장하게 됩니다. 임베딩 모델은 Amazon Bedrock의 Amazon Titan Text Embeddings을, 벡터 DB는 Amazon OpenSearch를 활용 하였습니다.

Amazon Titan Text Embedding 모델은 아래와 같이 사용할 수 있습니다.

from langchain.embeddings import BedrockEmbeddings

llm_emb = BedrockEmbeddings(
                client=boto3_bedrock,
                model_id="amazon.titan-embed-text-v2:0"
)
dimension = 1024

오픈서치 인덱싱을 위한 코드는 아래와 같습니다. 세부적인 코드는 여기의 “5. LangChain OpenSearch VectorStore 생성”을 참고 하세요.

from langchain.vectorstores import OpenSearchVectorSearch

vector_db = OpenSearchVectorSearch(
    index_name=index_name,
    opensearch_url=opensearch_domain_endpoint,
    embedding_function=llm_emb,
    http_auth=http_auth, # http_auth
    is_aoss=False,
    engine="faiss",
    space_type="l2",
    bulk_size=100000,
    timeout=60
)

doc_ids = vector_db.add_documents(
    documents=docs,
    vector_field="vector_field",
    bulk_size=1000000
)

전체 코드는 여기의 “Index 생성” 입니다.

결과

1. 멀티모달 문서에 대한 RAG

멀티모달 문서로 부터 정보 추출 및 색인 작업이 완료가 되었다면 질의 (Query)를 통해 결과를 확인해 보도록 하겠습니다. RAG의 작업 흐름 관리 (Orchestration)은 LangChain을 활용하였고, RAG 시스템 성능을 높이기 위해 LangChain의 BaseRetriever를 상속받아 Advanced RAG Retriever를 구현하였습니다. Retriever는 “OpenSearchHybridSearchRetriever”로 정의되어 있으며 여기에서 “OpenSearchHybridSearchRetriever”로 검색 후 확인 할 수 있습니다. 이를 바탕으로 “Retriever” 및 QA 체인을 아래와 같이 정의할 수 있습니다.

from utils.rag import qa_chain
from utils.rag import prompt_repo, show_context_used
from langchain.callbacks.tracers import ConsoleCallbackHandler
from utils.rag import retriever_utils, OpenSearchHybridSearchRetriever

opensearch_hybrid_retriever = OpenSearchHybridSearchRetriever(
    # necessary
    os_client=os_client,
    index_name=index_name,
    llm_text=llm_text,
    llm_emb=llm_emb,
    
    # hybird-search debugger
    hybrid_search_debugger="None", #[semantic, lexical, None]
    
    # option for lexical
    minimum_should_match=0,
    filter=[],
    
    # option for rank fusion
    fusion_algorithm="RRF", # ["RRF", "simple_weighted"], rank fusion 방식 정의
    ensemble_weights=[.51, .49], # [for semantic, for lexical], Semantic, Lexical search 결과에 대한 최종 반영 비율 정의
    
    # reranker
    reranker=False, # enable reranker with reranker model
    reranker_endpoint_name=endpoint_name, # endpoint name for reranking model
    
    # option for complex documents consisting of text, table and image
    complex_doc=True,
    
    # option for async search
    async_mode=True,
    
    # option for output
    k=5, # 최종 Document 수 정의
    verbose=False
)

system_prompt = """
                 You are a master answer bot designed to answer user's questions.
                 I'm going to give you contexts which consist of texts, tables and images.
                 Read the contexts carefully, because I'm going to ask you a question about it.
                """

qa = qa_chain(
    llm_text=llm_text,
    retriever=opensearch_hybrid_retriever,
    system_prompt=system_prompt,
    return_context=True,
    verbose=False
)

QnA에 대한 전체 수행 코드는 여기서 확인 하실 수 있으며, 코드 내 Reranker에 대한 정보는 AWS 기술 블로그, 한국어 Reranker를 활용한 검색 증강 생성(RAG) 성능 올리기 에서 확인 할 수 있습니다.

멀티모달 문서에 대한 QnA 예시는 아래와 같습니다.

예시 1 – 질문에 대한 정답이 테이블에 있을 경우

  • 질문: 감염병의 종류와 각각의 백신 이름은?
  • 답변: 표에서 확인할 수 있는 감염병의 종류와 백신 이름은 다음과 같습니다.
    – 인플루엔자 – 인플루엔자 백신
    – 수두 – 수두 백신
    – 유행성 이하선염 – 머프스 백신
    – 풍진 – 풍진 백신
    – 유행성 각결막염 – 아데노바이러스 백신
    – 성홍열 – A군B군용혈성 연쇄상구균 백신
    – 홍역 – 홍역 백신
  • 컨텍스트(Retriever에 의해서 회수된 질문과 관련성 높은 문서):

예시 2 – 질문에 대한 정답이 이미지에 있을 경우

  • 질문: 여름철 폭염 검강관리 3대 수칙은?
  • 답변: 여름철 폭염 대비 3대 건강 수칙은 다음과 같습니다.
    물 자주 마시기
    2. 시원하게 지내기(그늘, 실내 등)
    3. 더운 시간대에 휴식하기
  • 컨텍스트 (Retriever에 의해서 회수된 질문과 관련성 높은 문서)

질문에 대한 정답이 각각 테이블, 이미지 내에 있는 각각의 질문에 대해서도 질문과 관련된 정보를 retrieval 하고 있으며, 이미지 및 테이블 정보를 바탕으로 질문에 대한 답변을 정확히 생성해 주고 있습니다.

2. 문서 파서 (Parser) 종류에 따른 결과 비교

RAG 아키텍처의 성능은 문서 내용을 정확하게 추출하여 OpenSearch로 색인하고 저장하는 것에 크게 의존합니다. RAG 아키텍처에서 쉽게 사용할 수 있는 PDF 파서는 다음과 같은 유형으로 분류할 수 있습니다:

  1. Rule 기반 파서 (PyMuPDF): 사전 정의된 규칙을 기반으로 PDF 파일을 구문 분석합니다. 이 방법은 빠르지만 유연성이 부족합니다.
  2. API 기반 파서 (Llama-Parse): PDF를 업로드하면 파싱된 결과를 반환하는 SaaS 형태의 서비스와 유사한 API 기반 파서가 있습니다. 이러한 솔루션은 공개된 API를 통해 파싱 기능을 제공합니다.
  3. Pipeline 기반 파서 (Unstructured.io): 이 유형의 파서는 일련의 모델 또는 알고리즘을 사용하여 PDF 구문 분석 전체 프로세스를 처리합니다. 각 단계는 자체 하위 작업을 처리하여 전체 작업을 체계적으로 해결합니다.

각 파서들은 아래와 같은 특징을 가지고 있습니다.

PyMuPDF는 대표적인 Rule 기반 파서로, PDF 문서 포맷을 룰 기반으로 해석하여 원본 컨텐츠에 가깝게 텍스트로 파싱합니다. PyMuPDF는 무료 오픈소스 라이브러리로, 경량화되어 있어 대용량 문서 처리에 효율적이며 룰 기반으로 테이블의 원본 레이아웃을 그대로 파싱할 수 있다는 장점이 있습니다. 하지만 PDF 파일만 처리 가능하고, 레이아웃 인식과 테이블 추출 등의 고급 기능이 제한적이며, 스캔된 PDF에 대해서는 별도의 외부 OCR 패키지와 연계해야 하는 단점이 있습니다. 또한, 테이블과 이미지가 복잡하게 구성된 문서의 경우 문서 구조 해석 시 성능이 떨어질 수 있습니다.

LlamaParse는 Gen-AI와 연계하여 PDF 파싱을 효율적으로 처리하고, LLM 편의 기능 및 LlamaIndex와 연계한 최적화 솔루션을 제공하는 API 기반 솔루션입니다. LLM이 파싱 처리를 보조하여 단위가 따로 표현된 테이블도 구조를 인식해 개별 레코드에 단위를 삽입해주며, “Parse Instruction” 기능으로 문서 구조에 대한 힌트를 제시하면 파싱 품질이 높아집니다. 그러나 API 호출이 필요하고, PDF 문서만 지원하며, 하루 1000 페이지까지만 무료이고 그 이상은 별도 비용이 발생합니다. 또한, 복잡한 구조의 테이블은 원본 레이아웃과 다르게 재해석될 수 있어 주의가 필요합니다.

Unstructured.io는 Pipeline 방식으로 PDF를 파싱하는 오픈소스 솔루션으로, 문서의 레이아웃을 분석하여 텍스트, 테이블, 이미지로 구분하여 파싱하는 능력이 뛰어납니다. LangChain과 통합되어 RAG 아키텍처 구성에 용이하게 사용할 수 있으며, 레이아웃 인식, OCR, 테이블 추출 등의 기능으로 복잡한 문서 파싱에 강점이 있습니다. 또한, PDF 외에도 다양한 문서 유형과 URL 파싱을 지원합니다. 그러나 파이프라인 형태로 다양한 솔루션을 조합하여 수행하기 때문에 대용량 문서 파싱 시 속도가 느릴 수 있고, OCR 기반 파싱으로 인해 일반 PDF에서는 Rule 기반 파싱보다 정확성이 떨어질 수 있습니다. 이를 보완하기 위해 Table Transformer를 사용하여 추출된 테이블 레이아웃을 별도 이미지로 저장한 후 LLM이 답변하도록 유도할 수 있습니다.

다음으로, 위에서 언급한 각 파서 유형별 파싱 결과에 따른 질의 응답 성능을 테스트를 통해 살펴보겠습니다. 테스트는 엔터프라이즈 환경에서 사용되는 문서를 기반으로 하였습니다. 여러 레코드가 병합된 컬럼이 존재하고, 단위가 각 컬럼에 존재하지 않고, 표 상단에 한 번만 표현되며 “L” 자 형태의 컬럼 구조로 인해 정확한 파싱이 어려울 수 있습니다.

각 파서별 결과는 아래와 같습니다. 빨간색으로 표시된 항목은 제대로 답변하지 못한 항목이며, 나머지 항목들은 성공적으로 답변한 항목 들입니다.

위 수행 결과에서 알 수 있듯이 각각의 파서들은 각 파서들의 장점과 단점들이 있어서 파서별 수행 결과가 상이하게 나타날 수 있습니다. 위에서 파서별로 수행한 결과들에 대해서 각 파서들의 결과들에 대해서 살펴보겠습니다.

  • (a) PyMuPDF: PyMuPDF는 규칙 기반으로 구조화된 문서를 효과적으로 파싱합니다. 그러나 원본 문서의 복잡한 레이아웃(예: 병합된 컬럼)을 그대로 유지하여 파싱하기 때문에, 특히 표 내부의 내용에 대해 정확하지 않은 답변을 생성할 수 있습니다. 이러한 한계로 인해 일부 질문에 대해 부정확한 답변이 발생할 수 있습니다.
  • (b) Llama-Parse: LlamaParse는 병합된 열을 독립적인 레코드로 변환하여 파싱하는 능력이 뛰어납니다. 그러나 계층 구조가 복잡한 열들도 과도하게 병합하려는 경향이 있어, 원래 열 형식이 전달하고자 하는 맥락이 변경될 수 있습니다. 앞서 언급된 예시에서 LlamaParse가 3개의 질문에 답변하지 못한 것도 이러한 특성 때문일 수 있습니다.
  • (c) Unstructured.io: io는 PyMuPDF에 비해 규칙 기반 파싱 능력이 다소 떨어집니다. 그러나 Unstructured.io는 테이블 인식이 가능한 테이블 트랜스포머를 내장하고 있어, 전체 테이블 이미지를 추출하여 저장할 수 있습니다. 이렇게 저장된 이미지를 벡터 인덱스에 저장해 두었다가, LLM을 통해 질의할 때 추출된 이미지를 활용하여 답변하도록 유도할 수 있습니다. 따라서 위 결과에서 다른 파서들보다 답변 성능이 뛰어난 이유는 LLM이 테이블 이미지를 통해 답변했기 때문입니다. 그러나 LLM의 이미지 해석 결과가 항상 정확하다고 보장할 수 없어, 위 결과에서 한 개 항목에 대해서는 답변하지 못했습니다.

위의 설명에서 볼 수 있듯이, 각 파서는 고유한 파싱 특성을 보유하고 있습니다. 이러한 독립적인 파싱 능력들을 조합하여 사용하면 각 파서의 부족한 부분을 상호 보완할 수 있습니다. 위의 ‘(d) PyMuPDF+LlamaParse+Unstructured.io’ 결과는 이러한 파서 조합의 우수한 성능을 보여주는 예시입니다. 이것이 가능한 이유는 이렇습니다. 우선, PyMuPDF로 파싱한 결과를 벡터 인덱스에 저장합니다. 동일한 벡터 인덱스에 LlamaParse로 파싱한 결과를 다시 저장합니다. 마찬가지로 Unstructured.io로 파싱한 결과도 동일한 인덱스에 저장합니다. 이런 식으로 각각의 파서가 파싱한 결과를 하나의 벡터 인덱스에 모두 저장하도록 합니다. 이렇게 벡터 인덱스에 저장한 다음에 LLM의 RAG 아키텍처를 통해서 질문하게 되면, 해당 질문이 벡터 인덱스 내에서 가장 파싱이 잘 된 결과를 통해서 답변이 찾아질 가능성이 높아집니다. 이런 방식으로 LLM이 답변을 찾아 오기 때문에, 각 파서들의 결과를 조합한 결과가 더 좋은 성능으로 결과를 리턴하게 되는 것입니다.

결론

이 글은 멀티모달 LLM을 활용하여 멀티모달 문서에 대한 RAG 파이프라인을 구성하고 활용하는 방법을 다루고 있습니다.멀티모달 정보 추출을 위해 Unstructured.io 파서를 사용하였으며, 텍스트뿐만 아니라 테이블 및 이미지 정보 또한 멀티모달 LLM을 통해 텍스트 형태로 변환하여 기존의 임베딩 방법으로 색인할 수 있게 하였습니다. 이를 통해 멀티모달 정보에 대한 질의에 대응할 수 있음을 확인하였습니다.

이러한 도구는 사용자의 비즈니스에서 더욱 정확한 데이터를 기반으로 인사이트를 얻고, 정보에 입각한 의사 결정을 하는 데 도움이 될 것으로 기대됩니다.

Reference

  • [1] Question answering using Retrieval Augmented Generation with foundation models in Amazon SageMaker JumpStart, Huang et al., 2023
  • [2] Amazon Bedrock을 이용하여 Stream 방식의 한국어 Chatbot 구현하기,Park et al., 2023

함께 읽으면 도움이 되는 참조 블로그

Dongjin Jang, Ph.D.

Dongjin Jang, Ph.D.

장동진 AIML 스페셜리스트 솔루션즈 아키텍트는 데이터 사이언티스트 경험을 바탕으로 고객의 머신러닝 기반 워크로드를 도와드리고 있습니다. 추천 시스템, 이상탐지 및 수요 예측과 같은 다양한 분야에 대한 고민을 고객과 함께 해결 하였고, 최근에는 생성형 AI을 통해 고객의 혁신을 지원하고 있습니다.

Byeong-eok Kang

Byeong-eok Kang

강병억 솔루션즈 아키텍트는 금융회사의 IT시스템에 대한 구축 및 컨설팅 경험을 가지고 있어서, 금융 IT 요구사항에 적합한 클라우드 시스템을 구성하고 사용하실 수 있도록 도움을 드리는 역할을 하고 있습니다. 금융IT이외에도 Database, AIML, Analytics, SaaS 등의 다양한 기술 영역에 대해서도 고객들에게 AWS를 잘 사용하실 수 있도록 가이드를 제공해 드리고 합니다.