← AI Document Processing
🏷

10 / 11

BIO 라벨 & 학습 개선

B-I-O 태깅 도입, Classifier Head 확장, 행 경계 학습, 학습 결과 검증

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

학습 로직 개선: 라벨 추가 시 Classifier Head 확장

LayoutLMv3ForTokenClassification 모델 구조는 크게 두 부분입니다:

LayoutLMv3 본체 (Encoder)

텍스트 + 위치 정보를 이해하는 부분

Classifier Head (Linear Layer)

"이 토큰이 어떤 라벨인지" 분류하는 부분

현재 Checkpoint의 Classifier Head

Linear(in_features=768, out_features=33)

out_features=33인 이유: 기존 라벨이 33개(O 포함)이기 때문. weight 행렬 shape = (33, 768)

문제: 새 라벨 추가 시 shape 불일치

새 라벨 5개 추가 → 총 38개 → (38, 768) weight 필요 → 기존 checkpoint에는 (33, 768)만 저장 → 로드 불가

Classifier Head 확장 방법

1

기존 checkpoint에서 (33, 768) weight 로드

2

새로운 (38, 768) weight 생성

3

기존 33개 라벨에 해당하는 행은 학습된 값을 그대로 복사

4

새로 추가된 5개 라벨에 해당하는 행만 랜덤 초기화

✅ 기존에 학습한 지식은 보존하면서 새 라벨만 추가로 학습 가능. 처음부터 다시 학습하는 것보다 훨씬 효율적.

BIO Tagging

B-I-O 라벨 태그 도입

라벨링 규칙

O관심 없는 토큰 (기존과 동일)
B-라벨명해당 필드의 첫 번째 단어
I-라벨명해당 필드의 연속 단어 (같은 셀 안에서)

모든 라벨에 적용합니다. 헤더 필드든 표 필드든 동일.

비표 영역 (단일 값 필드) 예시

단어: 7535962046     라벨: B-INVOICE_NUMBER       ← 단어 1개면 B만

단어: LX             라벨: B-CUSTOMER_NAME        ← 여러 단어면 B로 시작
단어: PANTOS         라벨: I-CUSTOMER_NAME        ← 이어지는 단어는 I
단어: MEXICO         라벨: I-CUSTOMER_NAME

표 영역 (반복되는 행) 예시

[1행]
단어: CAAU8385778    라벨: B-CONTAINER_NUMBER
단어: 40/9'6/DRY     라벨: B-CONTAINER_SIZE_TYPE
단어: SD-CY          라벨: B-SERVICE_TYPE
단어: Sep            라벨: B-PCD_DATE
단어: 11,            라벨: I-PCD_DATE
단어: 2025           라벨: I-PCD_DATE

[2행]
단어: CAAU8459865    라벨: B-CONTAINER_NUMBER      ← B가 다시 나옴 = 새 행
단어: 40/9'6/DRY     라벨: B-CONTAINER_SIZE_TYPE
단어: SD-CY          라벨: B-SERVICE_TYPE
단어: Sep            라벨: B-PCD_DATE
단어: 11,            라벨: I-PCD_DATE
단어: 2025           라벨: I-PCD_DATE

라벨 수 변화

현재

33개

O + 32개 라벨

BIO 적용 후

65개

O + 32개 × 2 (B-, I-)

Key Benefit

핵심: 행 경계를 모델이 직접 알려준다

현재 방식의 문제

모델 출력:
  54.00   → LINEITEM_RATE
  732.00  → LINEITEM_RATE
  50.00   → LINEITEM_RATE

LINEITEM_RATE가 3개 나왔지만, 어떤 RATE가 어떤 DESCRIPTION과 같은 행인지 알 수 없음. 현재는 "y좌표가 비슷하면 같은 행"이라는 후처리 규칙에 의존.

B-I 적용 후

모델 출력:
  Emission     → B-LINEITEM_DESCRIPTION   ← 1행 시작
  Surcharge    → I-LINEITEM_DESCRIPTION
  3            → B-LINEITEM_QUANTITY
  54.00        → B-LINEITEM_RATE
  USD          → B-LINEITEM_CHARGE_CURRENCY
  162.00       → B-LINEITEM_NET_AMOUNT
  Basic        → B-LINEITEM_DESCRIPTION   ← 2행 시작 (B가 다시 나옴)
  Ocean        → I-LINEITEM_DESCRIPTION
  Freight      → I-LINEITEM_DESCRIPTION
  3            → B-LINEITEM_QUANTITY
  732.00       → B-LINEITEM_RATE
  USD          → B-LINEITEM_CHARGE_CURRENCY
  2,196.00     → B-LINEITEM_NET_AMOUNT

파싱 결과:

행 1: {DESC: "Emission Surcharge", QTY: "3", RATE: "54.00", CURRENCY: "USD", AMOUNT: "162.00"}

행 2: {DESC: "Basic Ocean Freight", QTY: "3", RATE: "732.00", CURRENCY: "USD", AMOUNT: "2,196.00"}

y좌표 계산 없이, 모델 출력 순서만으로 행이 구분됩니다. B가 나오면 새 행 열기, I가 나오면 현재 행에 이어붙이기만 하면 됩니다.

Model Learning

LayoutLM 내부 학습 방법

입력 단계

모델에 들어가는 입력은 토큰마다 3가지 정보입니다:

토큰: "CAAU8385778"
├─ 텍스트 임베딩: "CAAU8385778"이라는 문자열의 의미 벡터
├─ 위치 임베딩: bbox [350, 1200, 580, 1230] → 0~1000 정규화 좌표
└─ 1D 위치 임베딩: 시퀀스에서 몇 번째 토큰인지

이 세 가지를 더해서 하나의 벡터(768차원)로 만듭니다. 이게 각 토큰의 초기 표현입니다.

Transformer Encoder

12개 레이어의 self-attention을 통과합니다:

"CAAU8385778"이 주변 토큰들을 봄:
  - 왼쪽에 "Container No"라는 헤더가 있네 → 컨테이너 번호일 가능성 높음
  - 위치가 y=1200 근처네 → 표의 특정 행
  - 바로 아래 y=1250에 "CAAU8459865"가 있네 → 다른 행의 같은 컬럼

self-attention이 텍스트 내용과 위치를 동시에 보면서, 각 토큰이 문서 전체 맥락에서 어떤 역할인지를 768차원 벡터에 압축합니다.

Classifier Head & 학습 (역전파)

[768차원 벡터] → Linear(768, 89) → [89개 라벨 확률]

예: "CAAU8385778"
  O: 0.01
  B-CONTAINER_NUMBER: 0.92  ← 가장 높음
  I-CONTAINER_NUMBER: 0.03
  B-INVOICE_NUMBER: 0.01
  ...

정답 라벨과 예측을 비교해서 CrossEntropyLoss를 계산하고, 역전파로 weight를 업데이트합니다.

B-I를 어떻게 구분하는가

핵심은 위치 임베딩입니다:

1행: "CAAU8385778" (y=1200) → B-CONTAINER_NUMBER
2행: "CAAU8459865" (y=1250) → B-CONTAINER_NUMBER  (y가 바뀜 → 새 행 → B)

같은 셀: "Sep"  (y=1200, x=600) → B-PCD_DATE
같은 셀: "11,"  (y=1200, x=630) → I-PCD_DATE  (y 같고 x만 약간 이동 → 연속 → I)

학습 데이터에서 이 패턴을 반복적으로 보면, 모델이 "y좌표가 비슷하고 같은 라벨이면 I-, y좌표가 크게 바뀌면 B-"를 자연스럽게 학습합니다. 우리가 규칙을 정의한 게 아니라 모델이 데이터에서 패턴을 찾아낸 것입니다.

Code Changes (0331)

학습 코드 변경 사항

주요 변경 3가지

8-4) 라벨 검증 → 자동 확장으로 변경

기존:새 라벨이 있으면 에러 발생 (raise ValueError)
변경:새 라벨이 있으면 LABELS/label2id/id2label에 자동 추가, HEAD_EXPANDED = True 플래그 설정

B-I 라벨(B-CONTAINER_NUMBER, I-CONTAINER_NUMBER 등)이 기존 checkpoint에 없으니까, 자동으로 스키마를 확장합니다.

8-6) 모델 로드 → Head 확장 로직 추가

기존:checkpoint에서 그대로 로드
변경:HEAD_EXPANDED = True이면 기존 checkpoint를 원래 num_labels로 로드 → classifier Linear layer를 새 크기로 교체 → 기존 라벨 weight 복사, 새 라벨만 xavier 초기화

8-9) 학습 실행 → Head 확장 시 Resume 비활성화

기존:항상 resume_from_checkpoint=CKPT_PATH
변경:HEAD_EXPANDED = True이면 resume 없이 trainer.train() 실행

head 크기가 바뀌면 optimizer state shape이 안 맞아서 resume이 불가능. 대신 encoder weight는 이미 checkpoint에서 로드된 상태라 보존됩니다.

Training Result

BIO 학습 결과

학습 결과 요약

라벨 수

33 → 89

B-I 태깅 적용

Head 확장

33 → 89

기존 weight 보존 + 56개 새 라벨 랜덤 초기화

지표
학습10 epoch, 740 step
Loss1.623 → 0.026
eval F10.954
eval Precision0.951
eval Recall0.957
eval Accuracy0.987

✅ 이전 학습(B-I 없이) F1 0.956 → BIO 적용 후 F1 0.954로 거의 동일. 라벨 수가 33에서 89로 거의 3배 늘었는데도 성능이 유지된 건 좋은 신호.

Prediction 샘플 검증

B-I가 잘 동작하고 있는 예시:

CUSTOMER_NAME

B-CUSTOMER_NAME → I-CUSTOMER_NAME → I-CUSTOMER_NAME

→ "PANTOS POLAND SP ZOO"가 하나의 엔티티

SUPPLIER_NAME

B-SUPPLIER_NAME → I-SUPPLIER_NAME → ...

→ "CMA CGM POLSKA SP Z OO"가 하나의 엔티티

VESSEL_VOYAGE

B-VESSEL_VOYAGE → I-VESSEL_VOYAGE

→ "CMA CGM CENTAURUS"가 하나의 엔티티

학습 결과 확인 화면

BIO 학습 결과 확인 1

BIO 학습 결과 1

BIO 학습 결과 확인 2

BIO 학습 결과 2

BIO 학습 결과 확인 3

BIO 학습 결과 3

라벨링 가이드

헤더 필드B-/I- (INVOICE_NUMBER, CUSTOMER_NAME 등)
요금 표B-/I- LINEITEM_* (행마다 B- 다시 시작)
컨테이너 표B-/I- CNTR_* (행마다 B- 다시 시작)
2페이지 반복 값O
VESSEL과 VOYAGE분리

작업 순서

1

Label Studio에 B-/I- 라벨 등록

2

40건 데이터를 B-I-O 방식으로 라벨링

가장 시간이 걸리는 단계

3

export.json 추출

4

노트북에서 EXPORT_PATHS에 추가 후 학습 실행

head 자동 확장

5

학습 완료 후 추론 코드에 B-I 파싱 로직 추가

추론 코드 변경 (학습 후)

추론 결과에서 B-/I-를 파싱하는 로직이 필요합니다. layoutlm_batch_predict_paddle_latin.py에 추가:

# 모델 출력 예시:
["O", "B-CONTAINER_NUMBER", "B-CONTAINER_SIZE_TYPE",
 "B-PCD_DATE", "I-PCD_DATE", "I-PCD_DATE",
 "B-CONTAINER_NUMBER", ...]

# 파싱 결과:
# 엔티티 1: {label: "CONTAINER_NUMBER", text: "CAAU8385778"}
# 엔티티 2: {label: "CONTAINER_SIZE_TYPE", text: "40/9'6/DRY"}
# 엔티티 3: {label: "PCD_DATE", text: "Sep 11, 2025"}  ← B+I+I 합침
# 엔티티 4: {label: "CONTAINER_NUMBER", text: "CAAU8459865"}  ← 새 행

같은 라벨의 B가 다시 나오면 새 행으로 그룹핑 가능.

BIO 도입 요약

Classifier head 자동 확장 (33 → 89 라벨)
기존 weight 보존 + 새 라벨만 xavier 초기화
F1 0.954 유지 (라벨 3배 증가에도 성능 유지)
행 경계를 모델이 직접 학습 — y좌표 후처리 불필요
Loss 1.623 → 0.026 정상 수렴
PaddleOCR 도입문제 해결