굴러가는 분석가의 일상

딥러닝 모델을 통한 PDF Parsing 기법 본문

LLM/RAG

딥러닝 모델을 통한 PDF Parsing 기법

G3LU 2025. 1. 22. 01:28
본 게시물은 Florian June님의 게시물을 참고하였습니다. 

 

PDF 파일과 스캔된 이미지를 인공지능에 활용하기 위해 구조화(Structured)되거나 반구조화(Semi-Structured)된 형식으로 변환하는 것은 매우 중요한 작업 중 하나이다. 하지만 PDF 파일은 텍스트나 이미지를 문서 내의 정확한 위치에 배치하기 위해 좌표 기반 렌더링(Coordinate-Based Rendering)을 사용하기 때문에 좌표 정보와 이에 따른 정보를 추출하는 정교한 기술이 필요하다. 

최근에는 Upstage와 Llamaindex와 같은 기업에서 제공하는 고성능 Document Parser들이 등장하면서 문서 분석 및 파싱 작업이 매우 정교하고 효율적으로 이루어지고 있는 추세이다.  이러한 상용 솔루션들은 뛰어난 성능을 제공하며, 가격도 비교적 저렴한 편이다. 하지만 만약 수백 개에서 수천 개에 이르는 대규모 문서를 파싱해야 할 경우, 누적되는 비용이 상당할 수 있기 때문에, 무료로 사용 가능한 Deep Learning 기반 모델을 활용하는 것이 효율적일때도 있을 것이다. 이에 본 게시물에서는 무료로 이용할 수 있는 딥러닝 기반 문서 파싱 모델에 대해 소개하고자 한다... 

 

시간이 없으신 분들께서는 아래의 개인적인 의견을 참고하시는 데 많은 도움이 될거라고 생각합니다.


Deep Learning Based Models 

딥러닝 모델을 기반으로 하는 방법은 표(Table)와 단락(Paragraphs)을 포함하여 문서 전체의 레이아웃을 정확하게 식별할 수 있다. 심지어 표 내부의 구조까지 파악할 수 있으며, 문서를 분할하면서도 원래 의미와 구조의 유지가 가능해진다.

 

하지만 한계점이 존재한다. 객체 탐지와 OCR 같은 경우, 문서의 양 혹은 H/W 환경에 따라 시간이 오래 걸릴 수 있다. 따라서 GPU 또는 기타 가속 장치를 사용하는 것이 권장되며, 다중 프로세스 및 쓰레드를 활용하여 처리 속도를 높이는 것이 상대적으로 좋을 것이다.

 

딥러닝 모델 접근법은 객체 탐지 및 OCR 모델을 포함하며, 대표적인 오픈 소스 프레임워크는 다음과 같다: 

  • Unstructured: 이는 LangChain에 통합이 되어 있어, 쉽게 사용할 수 있는 모델 중에 하나이다. 객체 탐지 모델을 사용하여 문서의 표나 이미지를 정확하게 구분하고, 텍스트를 정교하게 추출할 수 있다. 이는 파라미터를  infer_table_structure=True  으로 설정하면 되지만, 정확도가 높은 대신 처리 시간이 대체적으로 길다는 것이 문제이다. 더불어, 객체 탐지 모델을 사용하지 않고, 보다 단순한 방법으로 문서를 분석함으로써 처리 속도를 높일 수 있는 방법은  strategy = 'fast'로 설정하면 된다. 

  • Layout-parser: 복잡한 구조의 PDF를 분석해야 할 경우에는, 약간 느릴 수 있지만 정확도를 높이기 위해 가장 큰 모델을 사용하는 것을 추천한다. 하지만 Layout-Parser는 2021년 ICDAR에 등재된 논문이며, 깃허브가 업데이트 안된지 거의 3년이 훌쩍 넘어간다. 그럼에도 불구하고 추천하는 이유는 기본적인 문서 레이아웃을 분석하는 딥러닝 모델 중 하나이기 때문이다. 

  • PP-StructureV2: 댜앙한 모델들을 조합하여 문서를 분석하며, 이는 전반적으로 평균 이상의 성능을 보인다.

추가적으로 다양한 딥러닝 모델에 대해 궁금하시다면 링크 방문 부탁드립니다. 

 
 

Challenges 

이번에는 본격적으로 딥러닝 모델(Unstructred) 프레임워크를 사용하여 PDF를 어떻게 파싱하는지와 문제점에 대해서 알아보도록 하겠습니다. 딥러닝 모델을 써도... 문제점이 있다... 

 

문제점 1: 이미지와 테이블을 어떻게 추출할 것인가?

 

Unstructed 프레임워크를 예로 들어 설명하겠습니다. 감지된 테이블 데이터는 HTML 형식으로 직접 내보낼 수 있다. 해당 코드는 다음과 같다: 

 

from unstructured.partition.pdf import partition_pdf

filename = "/Users/Florian/Downloads/Attention_Is_All_You_Need.pdf"

# infer_table_structure=True automatically selects hi_res strategy
elements = partition_pdf(filename=filename, infer_table_structure=True)
tables = [el for el in elements if el.category == "Table"]

print(tables[0].text)
print('--------------------------------------------------')
print(tables[0].metadata.text_as_html)

 

partition_pdf 함수의 내부 프로세스는 아래의 그림과 같다. 

 

parition_pdf 함수의 내부 프로세스 (Source: Florian June)

 

결과는 다음과 같다.  

### 결과
Layer Type Self-Attention Recurrent Convolutional Self-Attention (restricted) Complexity per Layer O(n2 · d) O(n · d2) O(k · n · d2) O(r · n · d) Sequential Maximum Path Length Operations O(1) O(n) O(1) O(1) O(1) O(n) O(logk(n)) O(n/r)
--------------------------------------------------
<table><thead><th>Layer Type</th><th>Complexity per Layer</th><th>Sequential Operations</th><th>Maximum Path Length</th></thead><tr><td>Self-Attention</td><td>O(n? - d)</td><td>O(1)</td><td>O(1)</td></tr><tr><td>Recurrent</td><td>O(n- d?)</td><td>O(n)</td><td>O(n)</td></tr><tr><td>Convolutional</td><td>O(k-n-d?)</td><td>O(1)</td><td>O(logy(n))</td></tr><tr><td>Self-Attention (restricted)</td><td>O(r-n-d)</td><td>ol)</td><td>O(n/r)</td></tr></table>
HTML 파일로 저장해서 인터넷 브라우저로 확인하는 방법은 아래와 같다.  
html_content = """
<html>
<head>
    <title>Layer Type Comparison</title>
</head>
<body>
    <h1>Layer Type Comparison Table</h1>
    <table border="1">
        <thead>
            <tr>
                <th>Layer Type</th>
                <th>Complexity per Layer</th>
                <th>Sequential Operations</th>
                <th>Maximum Path Length</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>Self-Attention</td>
                <td>O(n² · d)</td>
                <td>O(1)</td>
                <td>O(1)</td>
            </tr>
            <tr>
                <td>Recurrent</td>
                <td>O(n · d²)</td>
                <td>O(n)</td>
                <td>O(n)</td>
            </tr>
            <tr>
                <td>Convolutional</td>
                <td>O(k · n · d²)</td>
                <td>O(1)</td>
                <td>O(log<sub>k</sub>(n))</td>
            </tr>
            <tr>
                <td>Self-Attention (restricted)</td>
                <td>O(r · n · d)</td>
                <td>O(1)</td>
                <td>O(n/r)</td>
            </tr>
        </tbody>
    </table>
</body>
</html>
"""

# 파일 저장
file_path = "경로 지정"
with open(file_path, "w", encoding="utf-8") as file:
    file.write(html_content)

file_path

생성된 html 파일을 브라우저로 열어서 확인해보면 아래와 같다. 

문제점 2: 사람이 논문을 읽는 순서대로 각각의 문단을 나눌 수 있을까? 

대부분의 논문은 Double-Column 형식으로 구성되어 있다. 그렇다면, 현재 사용 중인 Unstructured 프레임워크가 과연 사람이 논문을 읽는 순서대로 각각의 문단을 정확히 나눌 수 있을까 하는 의문이 든다. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding” 논문을 가지고 한번 예시를 들어보도록 하겠습니다. 

 

Doulbe-Column 형태의 PDF 읽는 순서

 

위 그림과 같이 Double-Column 형식을 읽을 때, 빨간색 화살표 방향대로 논문을 읽는다. 파싱하는 관점에서 이는 굉장히 중요하다. Document Parse는 텍스트를 문서의 좌측 상단에서 우측 하단까지 단순하게 위에서 아래로 읽는 방식으로 처리하지만, 좌우 열을 고려하지 않고 직렬화한다는 단점이 존재한다. 

 

쉽게 말하자면, 왼쪽 열의 첫 번째 줄 → 오른쪽 열의 첫 번째 줄 → 왼쪽 열의 두 번째 줄... 이런 식으로 처리할 가능성이 매우 높다. 이에 결과적으로 문단이나 문맥의 흐름이 깨지는 상황이 생기기 때문에 이는 매우 중요하다! 

 

다시 본론으로 들어가서 Unstructured 프레임워크는 해당 페이지를 각각의 사각형 블록으로 문단을 나눌 수 있다. Unstructed에서 제공하는 파일을 보면, partition_pdf 함수에 coordinates 파라미터를 True로 설정하면 된다고 한다. 

각 문단을 나눈 결과

 

각각의 사각형에 대한 좌표 위치를 아래처럼 구할 수 있다. 

 

[

LayoutElement(bbox=Rectangle(x1=851.1539916992188, y1=181.15073777777613, x2=1467.844970703125, y2=587.8204599999975), text='These approaches have been generalized to coarser granularities, such as sentence embed- dings (Kiros et al., 2015; Logeswaran and Lee, 2018) or paragraph embeddings (Le and Mikolov, 2014). To train sentence representations, prior work has used objectives to rank candidate next sentences (Jernite et al., 2017; Logeswaran and Lee, 2018), left-to-right generation of next sen- tence words given a representation of the previous sentence (Kiros et al., 2015), or denoising auto- encoder derived objectives (Hill et al., 2016). ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9519357085227966, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=196.5296173095703, y1=181.1507377777777, x2=815.468994140625, y2=512.548237777777), text='word based only on its context. Unlike left-to- right language model pre-training, the MLM ob- jective enables the representation to fuse the left and the right context, which allows us to pre- In addi- train a deep bidirectional Transformer. tion to the masked language model, we also use a “next sentence prediction” task that jointly pre- trains text-pair representations. The contributions of our paper are as follows: ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9517233967781067, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=200.22352600097656, y1=539.1451822222216, x2=825.0242919921875, y2=870.542682222221), text='• We demonstrate the importance of bidirectional pre-training for language representations. Un- like Radford et al. (2018), which uses unidirec- tional language models for pre-training, BERT uses masked language models to enable pre- trained deep bidirectional representations. This is also in contrast to Peters et al. (2018a), which uses a shallow concatenation of independently trained left-to-right and right-to-left LMs. ', source=<Source.YOLOX: 'yolox'>, type='List-item', prob=0.9414362907409668, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=851.8727416992188, y1=599.8257377777753, x2=1468.0499267578125, y2=1420.4982377777742), text='ELMo and its predecessor (Peters et al., 2017, 2018a) generalize traditional word embedding re- search along a different dimension. They extract context-sensitive features from a left-to-right and a right-to-left language model. The contextual rep- resentation of each token is the concatenation of the left-to-right and right-to-left representations. When integrating contextual word embeddings with existing task-specific architectures, ELMo advances the state of the art for several major NLP benchmarks (Peters et al., 2018a) including ques- tion answering (Rajpurkar et al., 2016), sentiment analysis (Socher et al., 2013), and named entity recognition (Tjong Kim Sang and De Meulder, 2003). Melamud et al. (2016) proposed learning contextual representations through a task to pre- dict a single word from both left and right context using LSTMs. Similar to ELMo, their model is feature-based and not deeply bidirectional. Fedus et al. (2018) shows that the cloze task can be used to improve the robustness of text generation mod- els. ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.938507616519928, image_path=None, parent=None), 


LayoutElement(bbox=Rectangle(x1=199.3734130859375, y1=900.5257377777765, x2=824.69873046875, y2=1156.648237777776), text='• We show that pre-trained representations reduce the need for many heavily-engineered task- specific architectures. BERT is the first fine- tuning based representation model that achieves state-of-the-art performance on a large suite of sentence-level and token-level tasks, outper- forming many task-specific architectures. ', source=<Source.YOLOX: 'yolox'>, type='List-item', prob=0.9461237788200378, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=195.5695343017578, y1=1185.526123046875, x2=815.9393920898438, y2=1330.3272705078125), text='• BERT advances the state of the art for eleven NLP tasks. The code and pre-trained mod- els are available at https://github.com/ google-research/bert. ', source=<Source.YOLOX: 'yolox'>, type='List-item', prob=0.9213815927505493, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=195.33956909179688, y1=1360.7886962890625, x2=447.47264000000007, y2=1397.038330078125), text='2 Related Work ', source=<Source.YOLOX: 'yolox'>, type='Section-header', prob=0.8663332462310791, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=197.7477264404297, y1=1419.3353271484375, x2=817.3308715820312, y2=1527.54443359375), text='There is a long history of pre-training general lan- guage representations, and we briefly review the most widely-used approaches in this section. ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.928022563457489, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=851.0028686523438, y1=1468.341394166663, x2=1420.4693603515625, y2=1498.6444497222187), text='2.2 Unsupervised Fine-tuning Approaches ', source=<Source.YOLOX: 'yolox'>, type='Section-header', prob=0.8346447348594666, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=853.5444444444446, y1=1526.3701822222185, x2=1470.989990234375, y2=1669.5843488888852), text='As with the feature-based approaches, the first works in this direction only pre-trained word em- (Col- bedding parameters from unlabeled text lobert and Weston, 2008). ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9344717860221863, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=200.00000000000009, y1=1556.2037353515625, x2=799.1743774414062, y2=1588.031982421875), text='2.1 Unsupervised Feature-based Approaches ', source=<Source.YOLOX: 'yolox'>, type='Section-header', prob=0.8317819237709045, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=198.64227294921875, y1=1606.3146266666645, x2=815.2886352539062, y2=2125.895459999998), text='Learning widely applicable representations of words has been an active area of research for decades, including non-neural (Brown et al., 1992; Ando and Zhang, 2005; Blitzer et al., 2006) and neural (Mikolov et al., 2013; Pennington et al., 2014) methods. Pre-trained word embeddings are an integral part of modern NLP systems, of- fering significant improvements over embeddings learned from scratch (Turian et al., 2010). To pre- train word embedding vectors, left-to-right lan- guage modeling objectives have been used (Mnih and Hinton, 2009), as well as objectives to dis- criminate correct from incorrect words in left and right context (Mikolov et al., 2013). ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9450697302818298, image_path=None, parent=None), 

LayoutElement(bbox=Rectangle(x1=853.4905395507812, y1=1681.5868488888855, x2=1467.8729248046875, y2=2125.8954599999965), text='More recently, sentence or document encoders which produce contextual token representations have been pre-trained from unlabeled text and fine-tuned for a supervised downstream task (Dai and Le, 2015; Howard and Ruder, 2018; Radford et al., 2018). The advantage of these approaches is that few parameters need to be learned from scratch. At least partly due to this advantage, OpenAI GPT (Radford et al., 2018) achieved pre- viously state-of-the-art results on many sentence- level tasks from the GLUE benchmark (Wang language model- Left-to-right et al., 2018a). ', source=<Source.YOLOX: 'yolox'>, type='Text', prob=0.9476840496063232, image_path=None, parent=None)

]

 

여기에서 (x1, y1)은 좌측 상단 꼭짓점의 좌표를 나타내며, (x2,y2)는 우측 하단 꼭짓점의 좌표를 나타낸다. 

 

위에서 언급한 것처럼 문맥의 흐름을 끊지않는 것이 중요하다고 하였다. 다행히 Unstrutured 프레임워크에서 페이지 읽기 순서를 재정렬하는 옵션이 존재한다. 하지만, 이는 double-column 측면에서의 결과가 좋진 않다...

 

이에 따라 알고리즘을 직접 설계할 필요가 있다. 가장 간단한 방법은 좌측 상단 꼭짓점의 가로 좌표를 기준으로 먼저 정렬한 다음, 가로 좌표가 동일한 경우 세로 좌표를 기준으로 정렬하는 것이다. 

 

하지만, 동일한 열에 있는 문단들 중에 위치가 살짝 변형이 된 것을 볼 수 있다... 여기에서 추가적으로 고려해야할 것은 bullet point로 살짝 Indent 되어 있는 문단들이다. 이는 앞서 정의한 알고리즘에 위반하는 경우가 된다. 

 

이를 해결하기 위해 다음과 같은 알고리즘을 설계하면 될거 같다. 

  • 모든 좌측 상단 x-좌표인 x₁을 정렬하여 x₁_min을 구한다.
  • 모든 우측 하단 x-좌표인 x₂를 정렬하여 x₂_max를 구한다.
  • 페이지의 중앙선 x-좌표를 다음과 같이 결정한다.
x1_min = min([el.bbox.x1 for el in layout])
x2_max = max([el.bbox.x2 for el in layout])
mid_line_x_coordinate = (x2_max + x1_min) /  2

 

bbox.x1 < 중앙선 x-좌표일 경우, 해당 블록은 왼쪽 열의 일부로 분류된다. 그렇지 않다면 오른쪽 열의 일부로 간주될 것이다. 분류가 완료되면, 각 열 내에서 블록들을 y-좌표를 기준으로 정렬하고 최종적으로  오른쪽 열을 왼쪽 열의 오른쪽에 연결하여 최종 순서를 완성한다. 아래의 코드처럼 말이다. 

left_column = []
right_column = []
for el in layout:
    if el.bbox.x1 < mid_line_x_coordinate:
        left_column.append(el)
    else:
        right_column.append(el)

left_column.sort(key = lambda z: z.bbox.y1)
right_column.sort(key = lambda z: z.bbox.y1)
sorted_layout = left_column + right_column

 


딥러닝 모델을 기반으로 문서를 파싱할 때 고려해야 할 사항이 생각보다 많은거 같습니다. 예상 외로 귀찮고 번거로운 작업들이 많아질 수 있습니다. RAG를 구축하기도 전에 파싱 단계에서 여러 문제점에 직면하게 되면, 생각보다 많은 시간이 소요될 가능성이 큽니다. 본 게시물을 작성하면서 이러한 점들을 더욱 실감하게 되었으며, 상용화된 파싱 솔루션을 사용하는 것을 강력하게 추천드립니다.