0. Document Loader (PDF)
RAG를 하려면 수 많은 Raw Data들을 파싱해야 합니다.
Raw Data들 중에서도 가장 범용적으로 사용되는 파일은 PDF이기 때문에, 이번 글에서는 PDF 파일에서 한글을 추출해내는 것이 중요합니다.
아래는 Aurtorag 팀에서 한글 여러 도메인의 pdf를 가지고 한글 텍스트 추출 실험을 진행한 순위표 입니다.
아래 표기된 숫자는 등수를 나타냅니다. (The lower, the better)
PDFMiner | PDFPlumber | PyPDFium2 | PyMuPDF | PyPDF2 | |
Medical | 1 | 2 | 3 | 4 | 5 |
Law | 3 | 1 | 1 | 3 | 5 |
Finance | 1 | 2 | 2 | 4 | 5 |
Public | 1 | 1 | 1 | 4 | 5 |
Sum | 5 | 5 | 7 | 15 | 20 |
1. 표가 있는 PDF 문서
PDF에서 텍스트를 추출하는 것은 위에서 비교한 라이브러리 모두 어느정도 비슷한 성능을 보입니다.
하지만, 텍스트 추출 시 표의 구조를 잘 유지하면서 텍스트를 추출하는 것은 라이브러리마다 성능 차이가 천차만별입니다.
PDF 문서마다 이중 표, 테두리 선이 존재하지 않는 표 등 형식이 다르기 때문입니다. 이로 인해 표에서 추출된 텍스트를 자세히 살펴보면 헤더와 값이 잘못 매핑되면서 왜곡된 정보를 가진 텍스트 Document가 생성될 수 있습니다.
그래서 이 글에서는 표의 구조를 가장 잘 유지하면서 텍스트를 추출할 수 있는 PDF 라이브러리를 비교하고자 합니다.
2. PDF 라이브러리 비교
우선은 AutoRAG 팀에서 테스트한 순서대로 위의 표 예시를 텍스트로 추출해보겠습니다.
(모든 PDF 예시는 위의 표를 기준으로 진행하겠습니다.)
- PyPDFium2
from langchain_community.document_loaders import PyPDFium2Loader
pdf_path ="data/풍수해(1).pdf"
loader = PyPDFium2Loader(pdf_path)
load = loader.load()
PyPDFium2 결과를 보시면 깔끔하게 텍스트를 추출한 것 같습니다.
하지만, 실제 PDF 표의 Level와 비교하면 차이를 확인하실 수 있습니다.
# 실제 PDF 표의 Level 구조
Level 1 - [별표 1] 손해구분표
Level 2 --- 1. 주택
Level 3 ------ 파손
Level 4 ---------전파, 유실 / 전반파 / 반파 / 소파 / 지붕재
Level 1 - [별표 1] 손해구분표
Level 2 --- 1. 주택
Level 3 ------ 침수
# PyPDFium2 표의 Level 구조
Level 1 - [별표 1] 손해구분표
Level 2 --- 1. 주택
Level 3 ------ 파손 / 전파, 유실 / 전반파 / 반파 / 소파 / 지붕재 / 침수
파손 하위 Level에 전파,유실 / 전반파 와 같은 것이 위치해 있어야하는데, PyPDFium2에서 추출한 텍스트 형태는 사람이봐도 쉽게 Level을 구분할 수 없습니다.
이러한 Text Document를 RAG에 활용하여 "손해구분표에서 파손과 침수에 해당하는 항목을 각각 알려줘."라고 질문을 하게되면 과연 LLM이 답변을 잘 할 수 있을까요?
분명 제대로 구분하지 못하고 틀린 답변을 할 것입니다. 이에 최대한 표의 구조를 유지하면서 추출하는 것이 RAG 성능에 직결됩니다. 그럼 다른 라이브러리도 비교해보도록 하겠습니다.
- Fitz (pymupdf)
import fitz
doc = fitz.open(pdf_path)
for page in doc:
text = page.get_text()
print(text)
PyPDFium2와 비교해보면 침수를 동일 선상에 두지 않고 구분한 것은 잘 추출했다고 할 수 있습니다.
하지만, 3~5번쨰 줄을 보면 파손 전파, 유실 기둥, 벽체가 구분이 안되는 것을 확인할 수 있습니다.
- PyPDF2
from PyPDF2 import PdfReader
reader = PdfReader(pdf_path)
pages = reader.pages
text = ""
for page in pages:
sub = page.extract_text()
text += sub
print(text)
PyPDF2 역시 표의 구조를 유지하지 못하는 것을 확인하실 수 있습니다..
- PDFMiner
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer
def extract_text_by_page(pdf_path):
for page_layout in extract_pages(pdf_path):
page_text = ""
for element in page_layout:
if isinstance(element, LTTextContainer):
page_text += element.get_text()
yield page_text
for page_number, page_text in enumerate(extract_text_by_page(pdf_path), start=1):
print(f"Page {page_number}:\n{page_text}\n{'-'*40}\n")
PDFMiner는 상위 Level에 해당하는 파손이 오히려 중간으로 내려가서 위의 라이브러리들보다 완전히 표의 구조를 벗어난 것을 확인하실 수 있습니다.
- pdfplumber
import pdfplumber
pdf = pdfplumber.open(pdf_path)
pages = pdf.pages
text = []
for page in pages:
sub = page.extract_text()
text.append(sub)
print(text)
PDFMiner와 동일하게 상위 Level인 파손이 하위로 내려가면서 표의 구조를 반영하지 못한 것을 확인하실 수 있습니다.
결과적으로, PyPDFium2, PyMuPDF, PyPDF2, PDFMiner, PDFPlumber 모두 표의 구조를 유지한 상태로 텍스트를 추출하지 못했습니다.
그 중, PyPDFium2, PyMuPDF, PyPDF2는 어느정도 구조를 유지했으나,
PDFMiner, PDFPlumber는 표의 구조를 반영하지 못함을 확인했습니다.
PyPDFium2 | PyMuPDF | PyPDF2 | PDFMiner | PDFPlumber |
5/10 | 4/10 | 3/10 | 1/10 | 1/10 |
텍스트 추출이 뛰어나다고 자주 언급되는 5개의 라이브러리 모두 표의 구조를 반영하지 못하고 있습니다. 그렇다면 표의 구조를 반영하면서 텍스트를 추출할 수 있는 방법은 무엇이 있을까요?
바로 "마크다운" 입니다.
3. Why Markdown?
2024년 5월1일 Dhaval Nagar가 작성한 글입니다.
여기서 가장 중요한 부분은
LLM과 PDF에서 구조화된 데이터를 추출할 때, Markdown 텍스트가 일반 텍스트보다 우월하다는 것입니다. PDF 파일을 Markdown으로 변환하는 것은 데이터의 구조와 맥락을 유지하는 데 중요하며, 특히 Retrieval-Augmented Generation(RAG) 응용 프로그램에서 중요하다는 것입니다.
https://www.appgambit.com/blog/llms-love-structure-using-markdown-for-pdf-analysis
Markdown이 유리한 이유는 다음과 같습니다.
- 마크다운 학습 : 최신 LLM은 마크다운으로 학습한 데이터가 많기 때문에 LLM은 마크다운을 매우 잘 이해할 수 있습니다.
- 구조 유지 : 마크다운은 PDF에의 테이블, 헤딩, 목록 등 본질적인 구조를 보존하고 표현하는데 용이합니다. LLM은 질문 답변이나 요약과 같은 작업 중에 이 구조를 이해하고 활용할 수 있습니다. 일반 텍스트는 정보를 평면화하여 모델이 관계를 식별하기 어렵게 만듭니다.
- 의미 강조 : 헤더(#, ##), 굵은 글씨(* ), 기울임꼴( ), 코드 블록(```)과 같은 마크다운 요소는 텍스트의 중요성과 유형에 대한 의미적 단서를 제공합니다. 마크다운으로 훈련된 LLM은 이러한 단서를 포착하고 이해할 수 있습니다.
- 깔끔한 표현 : 마크다운은 표 형식의 데이터를 시각적으로 정리하고 읽기 쉽게 표현할 수 있게 해줍니다. 표와 텍스트 데이터가 있는 공존하는 페이지를 변환하면, 서로 쉽게 섞여서 복잡한 결과가 생성될 수 있습니다.
- 문맥 보존 : 더 깔끔한 표현은 문맥을 보존하는 데 도움이 됩니다. 또한 PDF에 그림이 있는 경우 텍스트의 문맥 내에 마크다운으로 이미지 링크나 대체 텍스트를 포함할 수 있습니다. 이렇게 하면 일반 텍스트에선 누락될 수 있는 정보를 제공할 수 있습니다.
- 청킹 : 검색 증강 생성(RAG) 시스템에 필수적인 청킹(일명 "분할")은 더 쉬운 처리를 위해 더 큰 문서를 더 작은 부분으로 나눕니다. 마크다운은 벡터 데이터베이스 내에서 벡터 표현 자체에 직접적인 영향을 미치지 않지만 벡터 스토리지를 사용하는 RAG 시스템 내에서 검색 프로세스 중에 컨텍스트를 보존하고 활용하는 데 중요한 역할을 합니다.
4. Markdown based PDF conversion (LlamaParse, pymupdf4llm)
PDF파일을 Markdown으로 텍스트 추출이 가능한 라이브러리는 대표적으로 LlamaParse, pymupdf4llm가 있습니다.
- LlamaParse
LlamaParse는 LlamaIndex에서 개발한 최첨단 문서 파싱 플랫폼입니다. 다음과 같은 주요 특징을 가지고 있습니다:
주요 기능
- PDF, Word, PowerPoint, Excel 등 10개 이상의 파일 형식 지원
- 최첨단 표 추출 기능
- 자연어 지시를 통한 맞춤형 출력 형식 지정
- JSON 모드 제공
- 이미지 추출 기능
주요 장점
- LLM 사용 사례에 최적화된 고품질 데이터 파싱 및 정제
- 복잡한 문서에서 표와 텍스트를 정확하게 추출
- LlamaIndex와의 원활한 통합으로 고급 검색 및 컨텍스트 증강 가능
가격 정책
- 하루 1,000페이지 무료 제공
- 유료 플랜 가입 시 주 7,000페이지 무료, 이후 페이지당 $0.003
LlamaParse는 복잡한 문서에서 정확한 정보를 추출하고 구조화하여 LLM 기반 애플리케이션의 성능을 크게 향상시킬 수 있는 강력한 도구입니다.
- 변환 결과
from llama_parse import LlamaParse
from llama_index.core import SimpleDirectoryReader
import os
import nest_asyncio
from dotenv import load_dotenv
load_dotenv('./RAG.env')
nest_asyncio.apply()
# 파서 설정
parser = LlamaParse(
result_type="markdown", # "markdown"과 "text" 사용 가능
num_workers=8, # worker 수 (기본값: 4)
verbose=True,
language="ko",
)
# SimpleDirectoryReader를 사용하여 파일 파싱
file_extractor = {".pdf": parser}
# LlamaParse로 파일 파싱
documents = SimpleDirectoryReader(
input_files=[pdf_path],
file_extractor=file_extractor,
).load_data()
여기서 보시면 [별표1] 손해구분표, 1. 주택 앞에 #으로 헤더 표시를 통해 강조가 된 것을 확인할 수 있습니다.
하지만, 파손 / 지붕재 / 전파, 유실 등 일부 텍스트가 사라진 것을 확인할 수 있으며, Markdown에서 표의 형식으로 반환하지도 못한 것을 확인할 수 있습니다. 이는 예시로 사용한 표는 컬럼의 헤더가 존재하지 않아서 그런 것 같습니다.
그래서 LlamaParse가 헤더가 존재하는 표는 어떻게 Markdown으로 변환하는지 보여드리고자 하나의 예시를 더 가져왔습니다.
매우 변환을 잘 한 것을 확인할 수 있습니다. Markdown 표 구조를 설명드리자면
표의 헤더(컬럼명) : |피해면적|보상수준|
표의 컬럼 갯수 (2개): | --- | --- |
표 컬럼에 해당하는 내용 : |균열길이 2m 이상 ~ 연면적의 미만 5%|보험가입금액의 5%|
이를 통해 컬럼이 2개가 있고, 첫 ||에 위치한 내용은 컬럼명 피해면적에 대한 정보이며, 두번째 ||에 위치한 내용은 컬럼명 보상수준에 해당하는 정보임을 손쉽게 파악할 수 있습니다.
사람이 보면 쉽게 이해할 수 있는 것처럼, LLM 역시 마크다운 구조로 학습을 했기 때문에 해당 표의 구조를 이해하고 정확히 맵핑하여 질문에 답변할 수 있습니다.
예를들어, "소파 피해면적이 20%이상인데 보상은 얼마 받을 수 있어?" 라는 질문에 대해
"소파 피해면적이 20%이상인 경우 보험 가입금액의 25% 받을 수 있습니다."라고 답할 수 있습니다.
반면 PDFMiner로 동일한 페이지에서 텍스트를 추출해보면 다음과 같은 결과를 반환합니다.
대충 봐도 LlamaParse의 Markdown 변환보다 이해하기 힘들고 구조 역시 제대로 반영되지 않은 것을 확인하실 수 있습니다.
- PyMuPDF4LLM
PyMuPDF4LLM은 PyMuPDF를 기반으로 한 강력한 문서 파싱 도구로, 특히 대규모 언어 모델(LLM)과 검색 증강 생성(RAG) 환경에서 사용하기 위해 설계되었습니다. 이 도구의 주요 특징과 기능은 다음과 같습니다:
주요 기능
- PDF 및 기타 문서 형식을 Markdown으로 변환
- 다중 열 페이지 지원
- 이미지 및 벡터 그래픽 추출 기능
- 페이지 청킹(chunking) 출력 지원
- LlamaIndex 문서 형식으로 직접 변환 가능
고급 기능
- 이미지 추출이 가능합니다.
md_text = pymupdf4llm.to_markdown("input.pdf", write_images=True)
이 옵션을 사용하면 페이지의 각 이미지나 벡터 그래픽이 추출되어 별도의 파일로 저장됩니다.
LlamaIndex 통합
PyMuPDF4LLM은 LlamaIndex와 직접 통합할 수 있는 기능을 제공합니다:
import pymupdf4llm
llama_reader = pymupdf4llm.LlamaMarkdownReader()
llama_docs = llama_reader.load_data("input.pdf")
이 방식으로 PDF를 LlamaIndex 문서 형식으로 직접 변환할 수 있습니다[2].
특징 및 장점
- 헤더 라인 식별 및 적절한 Markdown 형식 적용
- 굵은 글씨, 기울임꼴, 고정폭 텍스트, 코드 블록 등 다양한 텍스트 스타일 감지 및 포맷팅
- 순서 있는 목록과 순서 없는 목록 지원
- 표 감지 및 Markdown 형식으로 변환
- 페이지 하위 집합 처리 가능
PyMuPDF4LLM은 복잡한 문서 구조를 정확하게 파싱하고 LLM 및 RAG 시스템에 적합한 형식으로 변환하는 데 특화되어 있어, 문서 기반 AI 애플리케이션 개발에 매우 유용한 도구입니다
- 변환 결과
외부 헤더가 있는 테이블의 출력을 마크다운 출력 예시입니다. 이를 참고해 아래 변환 결과를 비교햊쉐요.
|열1|열2|
|---|---|
|셀(0, 0)|셀(0, 1)|
|셀(1, 0)|셀(1, 1)|
|셀(2, 0)|셀(2, 1)|
이는 가능한 최소한의 토큰 크기를 갖춘 GitHub 호환 포맷으로, RAG 시스템으로의 피드를 작게 유지하는 데 중요한 측면입니다.
열 테두리는 “|” 문자로 표시됩니다.
텍스트 줄은 “ |---|---| … “ 형태의 줄이 뒤에 오면 테이블 헤더 로 간주됩니다 .
전체 테이블 정의는 앞뒤에 적어도 하나의 빈 줄이 있어야 합니다.
기술적 이유로 마크다운 테이블에는 헤더가 있어야 하므로 외부 헤더를 사용할 수 없는 경우 첫 번째 테이블 행이 선택됩니다.
LlamaParse가 표의 헤더가 존재하지 않아 실패했던 예시를 변환한 결과입니다.
|1. 주행|Col2|Col3| : 1. 주택이라는 상위 Level이 표의 헤더로 잡히고, 표에 3개의 컬럼이 존재하기 때문에 Col2, Col3 빈컬럼을 포함해 3개로 나뉩니다.
| --- | --- | --- | : 컬럼이 3개임을 의미합니다.
|파손|전파,유실|기둥, 벽체 지붕 등이 완전히 파손되어...| : 파손 - 전파,유실이 매핑되고 해당하는 텍스트도 정상적으로 매핑 됐습니다.
||전반파|파손된 부분의 수리비가 재축비용의 | : 전반파의 상위 Level인 파손이 이전에 나왔기 때문에 || 공백으로 표시함.
|침수|주택의 주거생활공간...|| : 파손과 동일한 Level인 침수가 맨 처음 컬럼에 위치함.
이중 표가 아닌경우 LlamaParse와 동일하게 Markdown 변환을 잘하는 것을 확인할 수 있습니다.
Markdown 변환을 하는 LlamaParse에 비해 PyMuPDF4LLM의 장점은 다음과 같습니다.
1. 무료
2. 완벽하지는 않지만 이중 표의 구조를 더 잘 커버함.
3. 생략하지 않고 모든 텍스트를 추출함.
4. 헤더가 존재하지 않아도 표를 탐지하고, Markdown 형식으로 변환함.
5. 결론
표가 존재하는 PDF 변환 개인 평가 순위
PyPDFium2 | PyMuPDF | PyPDF2 | PDFMiner | PDFPlumber | LlamaParse | PyMuPDF4LLM |
5/10 | 4/10 | 3/10 | 1/10 | 1/10 | 7/10 | 9/10 |
이러한 실험 결과로 표가 존재하는 PDF에 한해서는 개인적으로 PyMuPDF4LLM이 가장 좋다고 평가하고 있습니다.
하지만, LlamaParse의 경우 LLM과 통합해 변환 성능을 끌어올릴 수도 있고, 다양한 파라미터가 존재하며 LlamaIndex와 기능 통합 역시 용이하여 잘 사용한다면 PyMuPDF4LLM의 성능을 어느정도 따라잡을 가능성도 존재하긴 합니다.
그러나, PyMuPDF4LLM역시 다양한 기능을 지원하여 커스터마이징을 진행하지 않는 이상 PyMuPDF4LLM을 사용하는 것이 좋아보입니다.
실습 코드도 함께 제공해드리도록 하겠습니다.
https://github.com/moonjoo98/RAG_R-D/tree/main/PDF_Conversion
'RAG' 카테고리의 다른 글
[RAG] Llama 3.1 프롬프트 형식 (0) | 2024.09.09 |
---|---|
[RAG] Perplexity - AI 검색 엔진 리뷰 (feat. ChatGPT 차이점) (5) | 2024.09.09 |