← AI Document Processing
⚙️

05 / 11

모델 / 데이터 처리

모델 비교, Token 추출, INPUT 분류, 전처리/후처리 코드, S3 연결

DonutLayoutLMS3Preprocessing
개요환경 구성OCR 파이프라인모델 학습모델 / 데이터 처리LayoutLM 분석성능 관리배포 & 운영PaddleOCR 도입BIO 라벨 & 학습 개선문제 해결

모델 비교: Donut vs LayoutLM 계열

프로젝트 초기에 여러 문서 이해 모델을 비교 검토. Donut 모델은 간단한 파일(라벨 5~6개)에는 잘 적용되지만, 복잡한 Invoice 타입 문서에서는 성능이 부족했음.

결론: 복잡한 물류 문서 처리에는 LayoutLM 계열이 적합 → LayoutLM 채택

Donut 모델

채택하지 않음
간단한 파일(라벨 5~6개)에 대해서는 잘 적용됨
복잡한 Invoice 타입 문서에 적용 시 성능 부족
OCR 없이 이미지만으로 텍스트 추출 → 복잡한 레이아웃에서 정확도 저하

LayoutLM 버전별 비교

v1

LayoutLM (v1)

텍스트 + 위치(bbox)만 사용
이미지(픽셀) 정보는 없음
입력텍스트 + bbox
이미지없음
v2

LayoutLMv2

텍스트 + bbox + 이미지 feature (CNN backbone)
문서의 시각적 구조(표, 라인, 박스)를 더 잘 활용
일반적으로 v1보다 성능이 더 좋다고 알려져 있음
입력텍스트 + bbox + 이미지
이미지CNN backbone
v3

LayoutLM

채택
더 최신 pretraining (텍스트 + 이미지 함께 마스크)
v2보다 구조가 더 세련되고, 여러 태스크에서 성능 향상
MLM + WPA + MIM 세 가지 사전학습 Head 결합
입력텍스트 + bbox + 이미지 패치
이미지ViT 기반 패치 임베딩

비교 요약

모델텍스트위치(bbox)이미지복잡 문서
DonutOCR 불필요
LayoutLM v1
LayoutLMv2✓ (CNN)
LayoutLM✓ (ViT)✓✓

OCR Comparison

Token 추출 테스트

후보 1. EasyOCR

문제 1

추출 안 된 텍스트

EasyOCR 적용 시 추출하지 못한 텍스트가 존재. 예를 들어 'Sep 13 2025'에서 '13'을 추출하지 못하는 경우 발생.

Threshold 조정 이력

초기: Threshold 0.3 → 낮은 score의 데이터를 skipping 처리
현재: Threshold 0.0으로 조정 → 모든 결과를 포함하도록 변경
문제 2

추출된 문자 부정확

텍스트가 추출되더라도 문자 인식 정확도가 떨어지는 경우 발생. 특히 작은 글씨, 겹치는 영역, 특수 문자 등에서 오인식 빈번.

EasyOCR Skipping 로그

Threshold 0.3 적용 시 대량의 토큰이 low score로 skipping 처리됨. score가 0.0~0.3 범위에 분포하는 토큰들이 다수 존재.

[INFO] model::predict_single::222  Skipping result with low score: 0.111...
[INFO] model::predict_single::222  Skipping result with low score: 0.244...
[INFO] model::predict_single::222  Skipping result with low score: 0.161...
...
→ 수십 개의 토큰이 low score로 skipping 처리됨

→ Threshold를 0.0으로 낮춰 모든 결과를 포함시키되, 후처리 단계에서 필터링하는 방식으로 전환

후보 2. Tesseract

문제 1

추출 안 된 텍스트

Tesseract의 경우 EasyOCR보다 텍스트 미추출 문제가 더 심각. 특히 복잡한 레이아웃의 문서에서 누락되는 영역이 더 많음.

문제 2

추출된 문자 부정확

Tesseract도 문자 인식 정확도 문제가 존재. Label Studio에서 확인 시 잘못 인식된 텍스트가 다수 발견됨.

EasyOCR vs Tesseract 비교

항목EasyOCRTesseract
텍스트 미추출일부 누락더 심각
문자 정확도부정확 발생부정확 발생
Threshold 조정0.3 → 0.0 조정

→ 두 OCR 엔진 모두 한계가 있으며, 후처리 및 LayoutLM과의 결합으로 보완

Data Classification

INPUT 데이터 분류

문서 타입별 분류

처리 대상 문서를 크게 두 가지 타입으로 분류. 각 타입별로 목적, 필드 성격, 라벨 구조가 다름.

인보이스류

비용 청구 문서

금액 / 기간 / 컨테이너 중심

Sea Waybill (B/L)

화물 운송 정보 문서

Shipper / Consignee / Cargo 중심

인보이스류

대상 선사선사 B, 선사 A, 선사 D
문서명DET, Invoice 등
목적비용 청구
필드 성격금액 / 기간 / 컨테이너 중심

Labels (35개)

Invoice Header

INVOICE_NUMBERINVOICE_DATEDUE_DATE

Customer Info

CUSTOMER_NAMECUSTOMER_ADDRESSCUSTOMER_TAX_IDCUSTOMER_CODE

Supplier Info

SUPPLIER_NAMESUPPLIER_ADDRESS

Shipment Info

VESSEL_NAMEVOYAGEBL_NUMBERPOLPOD

Invoice Totals

TOTAL_NET_AMOUNTTOTAL_TAX_AMOUNTTOTAL_AMOUNTCURRENCY

Container Table

CONTAINER_NUMBERCONTAINER_SIZE_TYPESERVICE_TYPEPCD_DATESERVICE_CONTRACT_NUMBERERD_DATE

Line Item (Charges)

LINEITEM_DESCRIPTIONLINEITEM_CONTAINER_NUMBERLINEITEM_START_DATELINEITEM_END_DATELINEITEM_QUANTITYLINEITEM_UNITLINEITEM_RATELINEITEM_CHARGE_CURRENCYLINEITEM_EXTENDED_AMOUNTLINEITEM_TAX_AMOUNTLINEITEM_NET_AMOUNT

Sea Waybill (B/L)

대상 선사선사 E (COSCO 등)
문서명Sea Waybill
목적화물 운송 정보
필드 성격Shipper / Consignee / Cargo 중심

Labels (30개)

BL Header

BL_NUMBERBOOKING_NUMBEREXPORT_REFERENCE

Parties

SHIPPER_NAMESHIPPER_ADDRESSCONSIGNEE_NAMECONSIGNEE_ADDRESSNOTIFY_NAMENOTIFY_ADDRESS

Shipment Info

VESSEL_NAMEVOYAGEPOLPODPLACE_OF_RECEIPTPLACE_OF_DELIVERYSERVICE_CONTRACT_NUMBER

Cargo Info

CONTAINER_NUMBERSEAL_NUMBERCONTAINER_SIZE_TYPEPACKAGE_COUNTDESCRIPTIONGROSS_WEIGHTMEASUREMENT

Charges

CHARGE_NAMECHARGE_RATECHARGE_UNITCHARGE_AMOUNTCURRENCY

문서 타입 비교

항목인보이스류Sea Waybill (B/L)
목적비용 청구화물 운송 정보
핵심 필드금액 / 기간 / 컨테이너Shipper / Consignee / Cargo
라벨 수35개30개
Line Item✓ (Charges table)△ (있을 경우)

Preprocessing

전처리 코드 (PDF → PNG 변환)

전처리 파이프라인 목표

1

S3 버킷에 PDF가 업로드되면

2

Lambda가 자동 실행

3

PDF를 고해상도 단일 PNG로 변환 + 가벼운 전처리 적용

4

전처리된 PNG를 특정 prefix에 저장

5

Label Studio는 이 PNG만 바라봄

high_quality_pdf_process.py

PDF → 고품질 PNG 변환 (5배 해상도)
다중 페이지 세로 병합
가벼운 전처리 (컬러 유지)
원본 PDF 자동 삭제

Lambda 구성

Lambda 이름high_quality_pdf_process_lambda.py
IAM RoleAIOCRPreProcessingRole
IAM Policy
AIOCRPreProcessingPolicyAWSLambdaBasicExecutionRole
LayerPyMuPDF + Pillow, OpenCV + NumPy

Lambda Layer 구성

용량 제한: 콘솔 직접 업로드 50MB / Layer 언집 크기 250MB (총합) → S3 업로드 후 Layer 생성이 정석

Layer 1: PyMuPDF + Pillow

Lambda Python 3.12 이미지 + entrypoint override로 빌드

docker run --rm \
  --entrypoint /bin/bash \
  -v "$PWD":/var/task \
  public.ecr.aws/lambda/python:3.12 \
  -lc '
    pip install -t python/lib/python3.12/site-packages \
      PyMuPDF Pillow
  '

# 용량 정리 (strip 금지!)
find python -type d -name "tests" -exec rm -rf {} +
find python -type d -name "__pycache__" -exec rm -rf {} +

# zip 생성
zip -r9 layer-pymupdf-py312.zip python

Layer 2: OpenCV + NumPy

OpenCV는 용량이 크므로 불필요 데이터 제거 + 바이너리 strip 필수

docker run --rm \
  --entrypoint /bin/bash \
  -v "$PWD":/var/task \
  public.ecr.aws/sam/build-python3.12 \
  -lc "
    pip install --no-cache-dir \
      -t python/lib/python3.12/site-packages \
      opencv-python-headless numpy

    # 불필요 폴더 정리
    rm -rf python/.../cv2/data cv2/qt
    rm -rf python/.../numpy/*/tests

    # 바이너리 strip (용량 크게 줄어듦)
    find python -name '*.so' -exec strip --strip-unneeded {} +
  "

zip -r9 layer-opencv-numpy-py312.zip python

→ zip을 S3에 업로드 → Lambda Layer 생성 → Lambda 함수에 Customer Layers 2개 연결

S3 Prefix 설계

labelstudio-aiocr-poc-source/
├── raw-pdf/              ← 사용자가 업로드하는 원본 PDF
│   └── *.pdf
│
├── preprocessed/         ← Lambda가 만든 고품질 PNG
│   └── *.png               (Label Studio 연결 대상)
│
└── _failed/              ← 전처리 실패 PDF (선택)
재처리 / 재학습 / 롤백이 쉬워짐
PDF 원본은 항상 보존됨
S3 Trigger: raw-pdf/ prefix에 연결
Label Studio: preprocessed/ prefix에 연결

전처리 파이프라인 흐름

PDF 업로드S3 raw-pdf/S3 TriggerLambda고해상도 PNG 변환S3 preprocessed/Label Studio

Model Comparison

OCR / KV 추출 모델 비교

OCR 모델 비교

역할

이미지 → 텍스트 + bbox 생성

선택 기준

학습 가능 여부언어 지원속도 vs 정확도

PaddleOCR

korean_PP-OCRv3_mobile_rec

한글 문서

한국어 특화 OCR

학습: ✅ 가능
방식: crop 이미지 + text
장점: 빠름, 한글 지원
단점: 다국어 약함

PaddleOCR

custom fine-tuned

Document AI 핵심

Label Studio 기반 커스텀 OCR

학습: ✅⭐⭐⭐
방식: bbox + corrected_text
장점: 성능 지속 개선
단점: 초기 데이터 필요

Tesseract

기본

baseline 비교

전통 OCR (rule 기반)

학습: ❌ 불가
방식: 없음 (비현실적)
장점: 초기 안정성
단점: 개선 불가

EasyOCR

기본

PoC

PyTorch OCR

학습: ⚠️ 제한적
방식: 일부 fine-tune
장점: 간단 사용
단점: 성능 한계

AWS Textract

DetectText

빠른 구축

OCR SaaS

학습: ❌ 불가
방식: 없음
장점: 안정적
단점: 커스터마이징 불가

Google Document AI

OCR Processor

SaaS

OCR SaaS

학습: ✅ 가능
방식: annotation 기반
장점: 매우 높은 성능
단점: 비용

Key-Value 추출 (Document Understanding) 모델 비교

역할

OCR 결과 → Key-Value 구조 추출

선택 기준

bbox 활용 여부fine-tune 가능 여부도메인 적응력

LayoutLM

microsoft/LayoutLM-base

Document AI 핵심

텍스트 + bbox + 이미지 기반 모델

학습: ✅ 가능
입력: OCR 결과 (text + bbox)
장점: 정확도 높음
단점: 학습 필요

LayoutLM

custom checkpoint

현재 운영 구조

도메인 특화 모델

학습: ✅⭐⭐⭐
입력: Label Studio label
장점: 성능 최상
단점: 데이터 필요

LayoutXLM

multilingual

글로벌 문서

다국어 LayoutLM

학습: ✅ 가능
입력: 동일
장점: 다국어 강점
단점: 성능 약간 낮음

Donut

naver-clova-ix/donut

연구/PoC

OCR-free (이미지→JSON)

학습: ✅ 가능
입력: 이미지
장점: end-to-end
단점: 불안정

AWS Textract

AnalyzeExpense

SaaS

OCR + KV 통합

학습: ❌ 불가
입력: 이미지
장점: 바로 KV 추출
단점: 커스터마이징 불가

Google Document AI

Custom Extractor

SaaS

KV 추출 SaaS

학습: ✅ 가능
입력: annotation
장점: 매우 높은 성능
단점: 비용

Post-processing

후처리 코드 & 학습 데이터 전달

Lambda 구성

로컬 코드single_task_label_classifier.py
Lambda 이름aiocr-post-processing-function
IAM RoleAIOCRPostProcessingRole
IAM Policy
AIOCRPostProcessingPolicyAWSLambdaBasicExecutionRole

Label Studio annotation 결과 구조

Label Studio의 annotation 결과는 "한 개의 텍스트 조각 = 여러 컴포넌트(polygon, textarea, labels)"로 쪼개져 저장됨. 이들을 다시 합치기 위해 id 기준 그룹핑이 반드시 필요.

사람 눈에 보이는 것

[ 선사 A ]

박스 + 텍스트 + 라벨이 한 덩어리

라벨: SUPPLIER_NAME

내부 JSON 구조

polygon어디에 있나? (좌표)
textarea무슨 글자인가? (텍스트)
labels이게 무슨 의미인가? (라벨)
// 같은 id로 3개 컴포넌트가 쪼개져 있음
[
  { "id": "d18baf28", "type": "polygon",  ... },
  { "id": "d18baf28", "type": "textarea", ... },
  { "id": "d18baf28", "type": "labels",   ... }
]

Label Studio의 설계 철학: UI 편집을 자유롭게 하기 위해 분리 (박스만 다시 그리기, 텍스트만 수정, 라벨만 변경 등)

후처리 입장에서 원하는 데이터

// id가 같은 것끼리 묶어서 복원한 결과
{
  "text": "선사 A",
  "bbox": [...],
  "label": "SUPPLIER_NAME"
}

→ id가 같은 것끼리 묶어서 사람/모델이 쓰기 좋은 단위로 복원

전체 후처리 메인 흐름 (lambda_handler)

1

S3 이벤트로 들어온 파일(bucket/key) 확인

2

results/ 폴더면 재처리 방지로 스킵

3

S3에서 원본 JSON 읽고 파싱

4

normalize_payload()로 "task + results 리스트" 형태로 통일

5

3종류 각각 추출/가공: single_values, container_table, lineitem_table

6

결과를 results/{원본파일명}.json으로 S3에 저장

핵심 엔진: id 기준 그룹핑 → "의미 단위" 복원

_group_results_by_id(results)

result 전체를 id로 묶어 "한 박스 단위"로 복원하는 작업. 이게 없으면 "ACME가 어떤 라벨인지", "LOGISTICS가 어떤 라벨인지"를 연결할 방법이 없음.

Before (Raw)

id=AAA: polygon
id=AAA: textarea("ACME")
id=AAA: labels(["CUSTOMER_NAME"])
id=BBB: polygon
id=BBB: textarea("LOGISTICS")
id=BBB: labels(["CUSTOMER_NAME"])

After (Grouped)

{
  "AAA": [polygon, textarea, labels],
  "BBB": [polygon, textarea, labels]
}

extract_labeled_items() 동작

특정 라벨 리스트(target_labels)에 해당하는 박스들만 뽑아내면서, 각 박스를 (라벨, 텍스트, 좌표, y_position) 형태로 표준화.

A

라벨 찾기

labels / polygonlabels / rectanglelabels 모두 대응

B

타겟 라벨과 교집합

matching = ["CUSTOMER_NAME"]

C

텍스트 찾기

textarea에서 text 추출 → "ACME"

D

좌표 찾기

polygon에서 points 추출

E

y_position 계산

행 구분용 y 좌표

// 최종 표준 item 1개
{
  "id": rid,
  "labels": ["CUSTOMER_NAME"],
  "text": "ACME",
  "points": [...],
  "y_position": 12.5
}

단일 값 라벨 처리: 같은 라벨끼리 이어 붙이기

extract_single_values()

CUSTOMER_NAME 라벨을 가진 박스들을 전부 가져와서 각 박스의 text를 확보한 뒤, 같은 라벨끼리 공백으로 이어 붙여 1개 값으로 만듦.

추출된 items

[
  { "labels": ["CUSTOMER_NAME"], "text": "ACME" },
  { "labels": ["CUSTOMER_NAME"], "text": "LOGISTICS" },
  { "labels": ["CUSTOMER_NAME"], "text": "MEXICO" }
]
out = {}
for it in items:
    for lb in it["labels"]:
        if lb not in out:
            out[lb] = it["text"]
        else:
            out[lb] = (out[lb] + " " + it["text"]).strip()
1"ACME" → CUSTOMER_NAME = "ACME"
2"LOGISTICS" → CUSTOMER_NAME = "ACME LOGISTICS"
3"MEXICO" → CUSTOMER_NAME = "ACME LOGISTICS MEXICO"

→ 모델/라벨링이 토큰 단위로 박스를 쪼개도, 후처리에서 같은 라벨끼리 합쳐서 1개 값으로 복원

테이블 라벨 처리 (Container / Line Item)

테이블은 단일 값과 다르게 "여러 행"이 필요해서 추가 단계가 있음.

1

라벨 item 추출

extract_labeled_items(results, CONTAINER_LABELS / LINEITEM_LABELS)

2

행 단위 묶기 — group_by_rows()

y_position 기준 정렬 → y 차이가 threshold(0.5) 넘으면 새 행

3

행 내 컬럼 합치기 — process_row()

같은 컬럼 라벨은 " " + text로 이어붙임 (단일값과 동일)

후처리 동작 요약

Label Studio 결과(result)를 id로 그룹핑 → "한 박스 단위" 복원

단일 값 라벨: 같은 라벨끼리 텍스트를 이어 붙여 1개 값으로

테이블 라벨: y좌표로 행을 묶어 행/컬럼 구조로 변환

결과 JSON을 S3 results/에 저장

Training Data Pipeline

Label Studio → SageMaker 학습 데이터 전달 방식

현재 방식: 전체 task에 대한 JSON 전달

1

Completed(=라벨링 완료) task만 모아서 단일 JSON 파일로 Export

2

S3 train-data/ 아래 저장

3

GPU 서버(SageMaker)에서 해당 JSON을 읽어 학습 진행

S3 저장 경로

labelstudio-aiocr-poc-destination/train-data/

Export 전략: API 우선 + Fallback

UI에서 "Export data → JSON"으로 받는 포맷을 최대한 그대로 받기 위해 두 가지 방식을 준비.

1

Project Export API (우선)

Label Studio의 Project Export API를 호출하여 Completed task를 JSON으로 Export

2

Tasks API Fallback

버전/설정 차이로 Export API 실패 시, tasks를 API로 직접 모아서 JSON으로 저장

코드 관리

스크립트layoutlm_train_data_export.py
실행 방법source lm_env/bin/activate && python layoutlm_train_data_export.py
배치 설정run_layoutlm_export_loop.sh와 동일한 방식으로 cron/배치 구성

학습 데이터 전달 흐름

Label StudioExport APICompleted JSONS3 train-data/GPU 서버 학습

Incremental Export

이전 Export 데이터 제외 후 GPU 서버 전달

기존 방식의 문제점 (Before)

사용자가 Label Studio UI에서 Export data → JSON 클릭
생성된 JSON을 로컬로 다운로드
다시 S3 또는 Jupyter로 업로드
이미 전에 export한 task가 또 들어갈 수 있음
어떤 데이터로 학습했는지 추적이 어려움
사람이 개입해야 해서 자동화 불가

→ 반복 · 수작업 · 중복 위험

새 구조: "Completed + New"만 자동 수집

Completed된 task만 — 사람이 실제로 라벨링 완료한 데이터
이전에 export하지 않은 task만 — 이미 학습에 쓴 데이터는 다시 안 가져옴
단일 JSON 파일로 — LayoutLM 학습에 바로 쓰기 좋은 형태
S3에 자동 적재 — Jupyter / 학습 파이프라인에서 바로 사용 가능

상태 관리: S3 _state 파일

이전에 export한 task ID 목록을 S3에 상태 파일로 관리. 다음 실행 시 이 목록과 비교하여 새로운 task만 export.

s3://[labelstudio-bucket]/
└── train-data/
    ├── project-6-completed-NEW-{timestamp}.json  ← 새 데이터
    └── _state/
        └── project-6-exported-ids.json           ← export 이력

최초 실행

• Completed 전체 task export

• exported-ids.json 생성

이후 실행

• exported-ids.json 로드

• 새로 completed된 task만 export

• exported-ids.json 갱신

실행 방법

source lm_env/bin/activate
python /home/user/layoutlm_train_data_export.py

결과 확인

train-data/project-6-completed-NEW-...json 생성

train-data/_state/project-6-exported-ids.json 생성/갱신

• 이후부터는 새로 completed된 task만 export됨

모델 학습LayoutLM 분석