03 / 11
OCR 파이프라인
PDF → Label Studio → EasyOCR Backend → 텍스트/박스 추출, API 토큰 적용
Label Studio 모델 연결
무료 버전
모델 1개만 연결 가능
프로젝트당 ML Backend 하나만 연결 가능
Enterprise 버전
여러 모델 연결 가능
다중 ML Backend 동시 연결 지원
Label Studio API 토큰 적용
업로드 파일 경로가 http://labelstudio:8080/data/upload/... 처럼 LS 서버 내부 URL로 제공됨. ML 백엔드는 그 URL을 호출해 파일을 받아야 하고, 이때 API KEY로 인증해야 함. 같은 docker 네트워크라 호스트네임 labelstudio로 접근하며, 이를 LABEL_STUDIO_URL로 알려줘야 함.
설정 과정
Personal Access Token 발급
ls-stack/.env 파일에 값 설정
docker-compose.yml에서 환경변수 확인
컨테이너 재시작
.env 파일 설정
# Label Studio 내부 네트워크 주소 # (docker-compose의 labelstudio 서비스명 사용) LABEL_STUDIO_URL=http://labelstudio:8080 LABEL_STUDIO_API_KEY=ls_여기에_방금_발급한_토큰_붙여넣기
docker-compose.yml 환경변수
easyocr:
environment:
- LABEL_STUDIO_URL=${LABEL_STUDIO_URL}
- LABEL_STUDIO_API_KEY=${LABEL_STUDIO_API_KEY}
ner:
environment:
- LABEL_STUDIO_URL=${LABEL_STUDIO_URL}
- LABEL_STUDIO_API_KEY=${LABEL_STUDIO_API_KEY}컨테이너 재시작
cd ~/ls-stack docker compose --env-file .env up -d # 로그 확인 docker compose logs -f easyocr
관련 명령어 및 파일
자주 사용하는 명령어
# 도커 이미지 빌드 docker build -t ls-ml-backend:local . # 컨테이너 상태 확인 docker compose -f ~/ls-stack/docker-compose.yml ps # 로그 확인 docker compose -f ~/ls-stack/docker-compose.yml logs -f easyocr docker compose -f ~/ls-stack/docker-compose.yml logs -f ner docker compose -f ~/ls-stack/docker-compose.yml logs -f labelstudio # 컨테이너 재시작 docker compose -f ~/ls-stack/docker-compose.yml down docker compose -f ~/ls-stack/docker-compose.yml up -d # 컨테이너 내부 확인 docker exec -it easyocr bash # model.py 수정 vi ~/label-studio-ml-backend/label_studio_ml/\ examples/easyocr/model.py
관련 파일 경로
~/ls-stack/.env— 환경변수 설정 파일~/ls-stack/docker-compose.yml— Docker Compose 구성~/label-studio-ml-backend/Dockerfile— ML Backend 도커 이미지~/label-studio-ml-backend/.../easyocr/model.py— EasyOCR 모델 코드Troubleshooting
Label Studio + ML Backend(EasyOCR) 충돌 해결
1. 초기 문제: 401 Unauthorized
증상
Label Studio에서 Retrieve Predictions 실행 시 EasyOCR 컨테이너에서 에러 발생
401 Unauthorized Failed to download via get_local_path
원인: Label Studio 1.21.0에서 API 인증 방식이 변경됨 → 기존 Token 인증 무효화. PAT → refresh → bearer access token 형식으로 변경 필요.
2. PAT → Access Token 교환 테스트
수동 curl로 refresh 테스트 → 성공. EasyOCR 컨테이너 내에서도 수동 실행 시 정상 동작. 네트워크 문제는 아님을 확인.
curl -X POST http://localhost:7070/api/token/refresh \
-H "Content-Type: application/json" \
-d '{"refresh": "<PAT_TOKEN>"}'
# → 200 OK + access 토큰 반환3. 실제 문제: model.py 인증 로직
4. 최종 해결: 인증/다운로드 로직 개선
PAT 감지 → Bearer Access Token 자동 Refresh
def _is_pat(self, token):
return token.count(".") == 2PAT → Access Token 자동 교환
POST /api/token/refresh → Bearer access token 캐싱 후 사용
이미지 다운로드 시 Bearer 토큰 우선 적용
Authorization: Bearer <access>
Label Studio 내부 도메인(hostname) 제거
/data/upload/... 같은 상대 URL은 prefix 추가하지 않고, 완전한 URL만 prefix 처리
get_local_path는 fallback으로만 사용
PAT 환경에서는 반드시 Bearer 모드로 다운로드
토큰 검증 코드
docker exec -it easyocr bash -lc "python - <<'PY'
import os, requests
base = os.environ['LABEL_STUDIO_URL'].rstrip('/')
pat = os.environ.get('LABEL_STUDIO_API_KEY','').strip()
# 1) PAT -> access 토큰
r = requests.post(
base+'/api/token/refresh',
json={'refresh': pat}, timeout=10
)
print('refresh STATUS:', r.status_code, r.text[:200])
acc = r.json().get('access')
# 2) access 토큰으로 API 호출
if acc:
r2 = requests.get(
base+'/api/projects',
headers={'Authorization': f'Bearer {acc}'},
timeout=10
)
print('projects STATUS:', r2.status_code, r2.text[:200])
PY"해결 결과
참고: GPU 없는 CPU-only 환경에서 뜨는 PyTorch pin_memory Warning은 무시 가능
전체 해결 흐름
| 단계 | 문제/행동 | 결과 |
|---|---|---|
| 1 | Label Studio → EasyOCR 호출 | 이미지 다운로드에서 401 계속 발생 |
| 2 | PAT을 Token 헤더에 넣어 요청 | LS 1.21.0에서 폐기된 방식 |
| 3 | curl로 /api/token/refresh 테스트 | PAT 정상 → access 발급됨 |
| 4 | EasyOCR 컨테이너에서도 테스트 | refresh + /api/projects = 200 OK |
| 5 | 실제 OCR 호출에서는 계속 401 | 이미지 다운로드 경로/토큰 방식 문제 |
| 6 | model.py 전체 인증 로직 재작성 | Bearer 방식 + URL 정상화 |
| 7 | 최종 테스트 | OCR prediction 정상 표시 |
LayoutLM Integration
LayoutLM / Label Studio 연동
모델 선정 조건
AWS Textract
탈락학습 불가
Donut
탈락fixed된 필드 필요 (CORD notebook 기준, 사전 정의된 key 목록 필수)
LayoutLM
채택SER + RE 기반 유연한 추출
모델 선정 고민 과정
2023년에 같은 고민을 한 사람이 있었음 — Transformers-Tutorials GitHub Issue #308에서 'LayoutXLM SER + RE'를 언급.
"I have briefly looked through the notebook, seems the CORD notebook for key-value requires a list of pre-defined keys (which means the keys are fixed), is there a way to extract random key-values like what LayoutXLM SER + RE does?"
— NielsRogge/Transformers-Tutorials #308
핵심 문제: Donut 등 기존 모델은 key를 미리 정의해야 하지만, 실제 인보이스는 회사마다 필드가 다르고 계속 바뀜. LayoutXLM SER + RE 방식이라면 fixed key 없이 유연하게 key-value를 추출할 수 있음. → 이 방향으로 LayoutLM 채택을 결정.
라이센스 이슈
LayoutLM, LayoutXLM은 상업적 이용 불가 라이센스. 상업적 활용을 위해서는 Microsoft Research Licensing Team에 연락하여 별도 license grant 또는 commercial agreement 체결이 필요.
LayoutLMv2 / v3 / LayoutXLM
CC BY-NC-SA 4.0 (비상업적)
RE 모델 포함 — 상업적 사용 불가
LayoutLM v1
MIT License (상업적 사용 가능)
'LayoutLM v1 + 커스텀 Fine-Tuning' 방향으로 진행
단, v2/v3의 weight는 못 쓰지만 로직(코드)은 사용 가능. RE 모델이 "어떻게 linking을 예측하는지" 알고리즘은 저작권 위반이 아님: bounding-box normalization, linking 방식, loss 함수, entity merge 알고리즘, distance heuristic, post-processing pipeline 등.
LayoutLM 핵심 원리
문서 구조는 다양하고 계속 바뀜 → 구조에 대한 학습 필요. Position Embedding + Text Embedding + Image Embedding + Faster R-CNN 기반.
LayoutLM 적용해서 결과가 나오면 Label Studio에서 보여주고, 빠진 데이터나 수정해야 할 데이터가 있다면 사용자 입력을 통해 수정. 수정한 데이터를 기반으로 재학습 진행. (Human-in-the-Loop)
⚠ LayoutLM은 GPU 기반 모델이라 GPU 포함된 상태로 돌아가야 함
layoutlm_preprocess.py 역할
한 장의 이미지에 대해 전처리부터 추론까지 수행하는 핵심 모듈
pytesseract로 OCR 수행
OCR 결과를 LayoutLM 형식에 맞게 단어·bbox 추출
LayoutLM 입력 포맷으로 변환 (convert_example_to_features)
fine-tuned LayoutLM 모델로 inference (convert_to_features)
Output
- •
word_level_predictions: 각 단어의 label id 리스트 (BIOES, ANSWER/QUESTION 등) - •
final_boxes: 각 단어의 실제 픽셀 좌표 [x0, y0, x1, y1]
→ 이 함수들만 있으면 "이미지 1장 → 단어별 예측 라벨 + 실제 박스 좌표"까지 완성
Label Studio → ML Backend 호출 흐름
사용자가 Label Studio에서 문서 이미지를 열면
Label Studio가 해당 task JSON을 백엔드에 HTTP로 전송 (/predict)
백엔드에서 layoutlm_preprocess.py 기반으로 추론 수행
word별 라벨 + bbox를 Label Studio JSON 포맷으로 변환
Label Studio 화면에 자동으로 그려진 박스 + 라벨이 표시
사용자는 자동 생성된 박스를 보고: 잘못된 라벨 수정, 박스 위치 조정/삭제, 누락된 토큰 추가 등의 검수 작업 수행.
Label Studio 설정 (RectangleLabels)
문서 + LayoutLM은 이미지 + 사각형 박스(RectangleLabels) 구조로 구성
<View>
<Image name="image" value="$image" />
<RectangleLabels name="label" toName="image">
<Label value="QUESTION" background="#1f77b4" />
<Label value="ANSWER" background="#2ca02c" />
<Label value="HEADER" background="#ff7f0e" />
<Label value="OTHER" background="#9467bd" />
</RectangleLabels>
</View>ML Backend 코드 구조 (model.py)
layoutlm_preprocess.py를 Label Studio ML Backend로 포장한 핵심 클래스
class LayoutLMv1Backend(LabelStudioMLBase):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
self.labels = load_labels(LABELS_PATH)
self.id2label = {i: lab for i, lab in enumerate(self.labels)}
self.model = model_load(MODEL_PATH, num_labels)
self.model.to(self.device)
self.model.eval()
def predict(self, tasks, **kwargs):
predictions = []
for task in tasks:
image_url = task["data"]["image"]
image, words, boxes, actual_boxes = preprocess(image_url)
word_level_predictions, final_boxes = convert_to_features(
image, words, boxes, actual_boxes, self.model
)
img_w, img_h = image.size
ls_results = []
for pred_id, box in zip(
word_level_predictions, final_boxes
):
raw_label = self.id2label[pred_id]
entity = iob_to_label(raw_label).upper()
if entity == "":
continue
x0, y0, x1, y1 = box
# Label Studio는 퍼센트 좌표(0~100) 사용
result = {
"from_name": "label",
"to_name": "image",
"type": "rectanglelabels",
"value": {
"x": x0 / img_w * 100,
"y": y0 / img_h * 100,
"width": (x1 - x0) / img_w * 100,
"height": (y1 - y0) / img_h * 100,
"rotation": 0,
"labels": [entity]
}
}
ls_results.append(result)
predictions.append({"result": ls_results})
return predictions전체 진행 과정
PDF 300장 업로드 → S3 또는 Label Studio에 직접 업로드
PDF → PNG 변환 (pdf2image 등으로 일괄 처리)
LayoutLM v1 적용 — ML Backend에서 preprocess() + convert_to_features() 수행
LayoutLM 결과를 Label Studio로 전달 (Model Predictions)
Label Studio에서 검수/수정 (RectangleLabels UI)
검수된 데이터를 기반으로 재학습 (Export JSON → FUNSD 형식 변환 → Fine-tuning)
전체 구조
[Label Studio 컨테이너] 포트 7070
|
| HTTP /predict
v
[LayoutLM ML 백엔드 컨테이너] 포트 7073 → 9090
|
| 내부에서:
| - PNG 이미지 로드
| - layoutlm_preprocess.py
| (pytesseract + bbox + normalize)
| - LayoutLM v1 fine-tuned 모델 로드
| - 단어별 BIOES 라벨 예측
| → RECTANGLE 박스로 변환
v
Label Studio UI에 자동 박스 + 라벨 노출Automation
Retrieve Prediction 자동화
문제: UI에서 대량 처리 시 멈춤
UI에서 "Retrieve Predictions"로 대량(15~50개) 처리하다가 멈추는 문제 발생. API 호출로 전환하면 상당 부분 완화 가능.
UI에서 대량이 깨지는 원인
API(서버 스크립트)로 처리하면
해결 방향
핵심은 "Label Studio API로 Retrieve Predictions 버튼을 그대로 호출"이 아니라, Tesseract ML Backend(=BBOXOCR)가 제공하는 /predict 엔드포인트를 서버에서 직접 호출하고, 그 결과를 Label Studio의 /api/predictions로 저장하는 방식.
Tesseract ML Backend /predict 엔드포인트 직접 호출
결과를 Label Studio /api/predictions로 저장
layoutlm_batch_predict.py가 LayoutLM 결과를 올리는 것과 동일한 방식
구현에 필요한 값
1. Tesseract ML Backend URL
TESSERACT_ML_URL = "http://localhost:9090"
Docker로 띄웠으면 컨테이너 포트/서비스 주소 확인. Label Studio UI의 ML Backend 설정 화면에서도 URL 확인 가능.
2. /predict 요청/응답 포맷
// 요청
{ "tasks": [{ "id": 198, "data": {...} }] }
// 응답
{ "results": [{
"result": [...],
"score": 0.95,
"model_version": "BBOXOCR"
}]}핵심 함수: run_tesseract_and_upload_prediction
Tesseract ML Backend를 직접 호출하고 결과를 Label Studio에 업로드하는 함수
TESSERACT_ML_URL = "http://localhost:9090"
TESSERACT_MODEL_VERSION = "BBOXOCR-auto"
def run_tesseract_and_upload_prediction(task: dict) -> bool:
"""
1) Tesseract ML backend /predict 호출
2) Label Studio /api/predictions 로 업로드
"""
task_id = task.get("id")
if not task_id:
return False
payload = {"tasks": [task]}
try:
r = requests.post(
f"{TESSERACT_ML_URL}/predict",
headers={"Content-Type": "application/json"},
data=json.dumps(payload),
timeout=300,
)
r.raise_for_status()
pred = r.json()
except Exception as e:
print(f"[WARN] task {task_id}: "
f"tesseract /predict 실패 - {e}")
return False
# 결과 파싱 (환경마다 키가 다를 수 있음)
result_item = None
if isinstance(pred, dict):
if isinstance(pred.get("results"), list) \
and pred["results"]:
result_item = pred["results"][0]
elif isinstance(pred.get("predictions"), list) \
and pred["predictions"]:
result_item = pred["predictions"][0]
if not result_item:
return False
ls_result = result_item.get("result")
if not isinstance(ls_result, list) or not ls_result:
return False
# Label Studio에 prediction 생성
post_payload = {
"task": task_id,
"result": ls_result,
"model_version": result_item.get(
"model_version") or TESSERACT_MODEL_VERSION,
"score": float(result_item.get("score") or 0.0),
}
try:
resp = requests.post(
f"{LS_URL}/api/predictions",
headers=HEADERS,
data=json.dumps(post_payload),
timeout=60,
)
resp.raise_for_status()
print(f"[INFO] task {task_id}: "
f"Tesseract prediction 생성 완료(API)")
return True
except Exception as e:
print(f"[WARN] task {task_id}: "
f"prediction 업로드 실패 - {e}")
return False기존 배치 루프 수정
Before (SKIP)
preds = fetch_predictions_for_task(task_id)
if not preds:
print(f"[SKIP] task {task_id}: "
f"prediction 없음")
continueAfter (자동 Tesseract)
preds = fetch_predictions_for_task(task_id)
if not preds:
ok = run_tesseract_and_upload_prediction(task)
if not ok:
print(f"[SKIP] task {task_id}: "
f"tesseract 자동 수행 실패")
continue
preds = fetch_predictions_for_task(task_id)
if not preds:
print(f"[SKIP] task {task_id}: "
f"tesseract 후에도 prediction 없음")
continue→ 이렇게 하면 UI에서 "Retrieve Predictions"를 누를 필요가 거의 없어짐. prediction이 없는 task를 만나면 자동으로 Tesseract를 돌려서 prediction을 생성한 뒤 LayoutLM이 정상 진행.
운영 팁 (대량 처리 안정화)
배치 사이즈 조절
한 번에 50개를 던지지 말고, task를 5~10개 단위로 /predict 호출
딜레이 추가
task별 호출이면 sleep(0.2~0.5s) 같은 소량 딜레이도 도움
재시도 제한
실패 task는 최대 2~3회 재시도만 하고 로그로 남겨서 따로 처리
로그 관리
성공/실패/스킵을 구분하여 로그 기록, 실패 task 목록 별도 저장
SER → RE (다음 단계)
지금까지 했던 것은 SER(Sequence labeling for Entity Recognition)이고, 다음 단계가 RE(Relation Extraction).
OCR → LayoutLMv1 (SER: QUESTION/ANSWER 분류) ↓ RE 모델 (Q–A 관계 매칭) ↓ Key–Value Pair 생성
LayoutLMv2 / v3 / LayoutXLM 기반의 'SER+RE' 통합 모델 사용이 베스트이나, 라이센스 제약으로 인해 v1 기반 + 커스텀 RE 로직으로 진행.
Tesseract + LayoutLM Integration
Tesseract / LayoutLM 통합 로직
개요: 왜 통합이 필요한가
"Tesseract가 토큰 뿌려주고, 그 토큰을 LayoutLM이 라벨까지 달아서 한 번에 보여주는" 로직이 필요. Label Studio 입장에서는 bbox+text(OCR 결과)와 label(LayoutLM 예측)이 다 포함된 prediction JSON을 주면 됨.
1. 사용자가 PDF 업로드 2. 전처리 (png변환, 해상도 변경) 3. Tesseract OCR → 토큰 + bbox 추출 4. LayoutLM으로 각 토큰에 라벨 예측 (INVOICE_NUMBER, CUSTOMER_NAME 등) 5. Label Studio 화면에서 - 토큰 박스(polygon+text) 보이고 - LayoutLM이 예측한 라벨이 미리 붙어있는 상태 - 사람이 잘못된 라벨만 수정/추가 6. 저장된 결과 → S3 → 재학습
→ 즉, "OCR 토큰 + LayoutLM 라벨"을 한 화면에서 다루고, 어디선가 두 결과를 합치는 로직이 필요.
통합 패턴 선택
패턴 1) 콤바인 ML 백엔드
한 개의 ML backend로 통합
패턴 2) 순차 실행 ✓
Tesseract와 LayoutLM을 "순차로" 사용
현재 상태 & 목표
지금 상태
• Label Studio + Tesseract ML backend(도커) 이미 동작 중
• Labeling Interface: Image + Polygon + TextArea + Labels 구조
• LayoutLM 학습/추론 코드는 주피터/파이썬에서 이미 있음
하고 싶은 것
• Tesseract가 토큰/박스를 만들어주고
• LayoutLM이 거기에 라벨만 입혀서
• Label Studio에서 한 토큰마다 바운딩 박스 + 라벨이 붙어있게
Tesseract → LayoutLM 순차 흐름
PDF 업로드
사용자가 PDF 업로드 → Label Studio 프로젝트에 Task 생성
Tesseract ML backend 호출
이미지에 대해 Polygon + TextArea(토큰 + bbox)를 prediction으로 생성
파이썬 스크립트 (서버에서 직접 실행)
Label Studio API로 Task 읽기 → Tesseract prediction에서 words+bboxes 추출 → LayoutLM 모델에 넣고 라벨 예측 → prediction 형식으로 만들어서 Label Studio에 Push
Label Studio UI에서 확인
Tesseract가 만든 토큰 박스 그대로 + LayoutLM이 예측한 라벨 자동 입힘. 사용자는 잘못된 라벨만 수정 → 저장
재학습
수정된 결과를 다시 학습 데이터로 만들어 S3 → LayoutLM 재학습
준비 단계
Label 세트 맞추기
LayoutLM이 사용하는 라벨 목록과 파이썬에서 쓰는 LABELS 리스트가 일치해야 함.
Label Studio 세팅 (REST 버전)
일시적으로 사용하는 것이 아님으로 Legacy token으로 설정
# /home/user/layoutlm_batch_predict.py 세팅 부분
LS_URL = "http://localhost:7070"
TOKEN = "<legacy-token>"
r = requests.get(
f"{LS_URL}/api/projects/6",
headers={"Authorization": f"Token {TOKEN}"}
)LayoutLM 추론용 모델 로드
추론용으로 로드할 때 필요한 파일은 두 개뿐:
config.jsonmodel.safetensors가상 환경 설정
torch 라이브러리 때문에 Python 3.8 이상 필요
python3.8 -m venv lm_env source lm_env/bin/activate pip install "torch==1.13.1" "torchvision==0.14.1" pip install "transformers>=4.38.0" pillow requests label-studio-sdk
배치 실행 설정 (30초 주기)
수동 실행
source lm_env/bin/activate python layoutlm_batch_predict.py
루프용 쉘 스크립트 (run_layoutlm_loop.sh)
#!/bin/bash
cd /home/user
source lm_env/bin/activate
while true; do
echo "===== $(date) : layoutlm_batch_predict.py 실행 ====="
python /home/user/layoutlm_batch_predict.py
echo "===== $(date) : 실행 끝 ====="
sleep 30
done백그라운드 실행 / 관리
# 백그라운드 실행 nohup /home/user/run_layoutlm_loop.sh \ >> /home/user/layoutlm_batch.log 2>&1 & # 로그 확인 tail -n 50 /home/user/layoutlm_batch.log # 멈추기 pkill -f "layoutlm_batch_predict.py" pkill -f "run_layoutlm_loop.sh"
리팩토링 포인트
run_layoutlm_loop.sh
이전 작업이 안 끝났다면 다시 layoutlm_batch_predict.py를 수행하지 않고 skip
layoutlm_batch_predict.py
100개의 task를 한번에 수행하지 않고 10개씩 끊어서 수행. 다음 10개 수행 시 이전꺼 제외하고 진행.
PDF Uploader 흐름
사용자 업로드 ↓ 서버 로컬 볼륨(/data/raw/) 저장 ↓ high_quality_pdf_process 실행 ↓ PNG 생성 ↓ S3 preprocessed/ 업로드 ↓ (선택) 로컬 원본 삭제
| 파일 | 컨테이너 내부 | 호스트 경로 |
|---|---|---|
| PDF (원본) | /data/raw/<파일명>.pdf | /home/user/uploader-data/raw/ |
| PNG (전처리) | /data/processed/<base_name>.png | /home/user/uploader-data/processed/ |
주의: 업로드 처리 끝나면 코드에서 os.remove(raw_path)로 원본 PDF는 삭제됨. PNG는 Label Studio가 가져가야 해서 삭제하지 않고 유지.
Label Studio가 보는 URL: http://<서버IP>:8000/files/<base_name>.png— FastAPI가 PROCESSED_DIR을 정적 서빙하는 경로
CloudStorage Sync 배치
Label Studio에서 Cloud Storage sync를 30초마다 배치로 돌리되, 이미 sync가 진행 중이면 skip.
• storage를 title로 찾아서 id를 얻고
• "sync 진행 중"이면 skip
• 아니면 sync API를 여러 후보 엔드포인트로 시도
# 스크립트 파일 /home/user/cloudstorage_sync.py /home/user/run_cloudstorage_sync_loop.sh # 30초마다, 이미 돌고있으면 skip # 실행 chmod +x /home/user/run_cloudstorage_sync_loop.sh nohup /home/user/run_cloudstorage_sync_loop.sh \ >> /home/user/cloudstorage_sync_loop.log 2>&1 & # 로그 확인 tail -f /home/user/cloudstorage_sync.log # 파이썬 상세 로그 tail -f /home/user/cloudstorage_sync_loop.log # 루프 실행 로그
Batch Performance Tuning
Tesseract / LayoutLM 배치 성능 튜닝
현재 설정
| MAX_TESSERACT_TASKS_PER_RUN | 10 |
| RETRIEVE_API_CHUNK | 5 |
| HTTP_TIMEOUT | 30초 |
| VERIFY_TRIES | 5 |
| 루프 간격 | 60초 |
문제 정의: 50개에 20분
50개 정도 돌리는데 20분 정도 걸림. 병목은 LayoutLM 추론이 아니라 Retrieve Predictions(Tesseract) 쪽. 특히 timeout/재시도 때문에 더 늘어남.
Retrieve Predictions는 서버 내부에서 task N개를 모아서 ML backend에 전달하고 결과를 저장하는 무거운 작업. 한 번에 크게 보내면 응답이 늦어지고 timeout이 발생하며 retry가 중첩되어 서버에 더 큰 부하가 걸림.
이슈: Read timed out 발생 후 retry
"완전한 장애"라기보단 정상적으로도 자주 나올 수 있는 상황. 다만 과부하/지연이 꽤 있는 상태고, 배치가 불안정해질 가능성이 높음.
[TESS] retrieve ... 대상 10개 뽑음 RETRIEVE_API_CHUNK=5 → 5개씩 POST를 2번 첫 POST에서 Read timed out (read timeout=30) 발생 → retry 하다가 어떤 attempt에서 status=200 성공 → "Retrieved 5 predictions" → 다음 chunk/다음 run에서 또 timeout 반복
문제 없는 경우
• retry 끝에 결국 status=200이 자주 뜬다
• processed_items가 꾸준히 늘어난다
• 시간이 지나면 prediction이 실제로 생성된다
→ "느리지만 계속 전진"하는 상황
문제가 되는 경우
• status=200이 거의 안 나오고 timeout만 반복
• processed_items가 늘지 않음
• 7070이 계속 바빠져서 UI까지 느려짐
• flock 때문에 다음 run들이 계속 스킵
해결 방법 (3갈래)
1) 가장 효과 큰 방법: Retrieve 우회 — Tesseract를 밖에서 돌려서 prediction 직접 POST
현재: Label Studio가 dm/actions → ML backend 호출 → 결과 저장을 "동기/긴 요청"으로 처리 → timeout/대기/재시도 발생
개선: 배치가 직접 이미지 경로를 읽고 → tesseract로 words+bbox 만들고 → /api/predictions로 바로 넣으면 dm/actions 왕복이 없어져서 체감 속도 차이가 큼
단점: Tesseract 결과를 만들어주는 코드를 배치에 포함해야 함 (ML backend가 하던 일을 가져오는 것)
2) Retrieve 유지하되 속도 올리는 튜닝
(A) chunk 줄이고 동시성 올리기 (가장 안전)
RETRIEVE_API_CHUNK=3으로 줄이고, ThreadPoolExecutor(max_workers=2~4)로 3개짜리 POST를 병렬로 날리면 throughput이 올라감
(B) HTTP timeout 늘리기
timeout 늘리면 실패가 줄어서 "재시도 시간 낭비"는 줄 수 있지만, 요청 자체가 오래 걸리는 건 그대로라서 순수 처리량이 크게 늘진 않음
(C) selection 로직 최적화 (API 호출 수 줄이기)
task마다 fetch_predictions(task_id)를 호출하는 대신, LS task 응답의 total_predictions 값으로 1차 필터 → GET 호출이 확 줄고 전체 run 시간이 줄어듦
3) 인프라/구성 측면
• Label Studio gunicorn worker 수 늘리기 (worker 1개면 dm/actions가 잘 막힘)
• ML backend(tesseract docker)의 CPU/메모리 제한 확인
• 이미지 해상도가 너무 크면 OCR이 느림 → 전처리에서 DPI/리사이즈 더 aggressive하게
추천 튜닝 값
"가장 현실적 + 효과 좋은" 추천 순서:
| 설정 | 기존 | 변경 |
|---|---|---|
| RETRIEVE_API_CHUNK | 5 | 3 |
| HTTP_TIMEOUT | 30 | 60 |
| MAX_TESSERACT_TASKS_PER_RUN | 10 | 9 (chunk 3 × 3회) |
| 루프 간격 | 60초 | 60초 유지 |
task selection 최적화: total_predictions로 먼저 걸러서 GET 호출 최소화
Retrieve POST를 3개 단위 + 병렬 2~3개 workers
timeout은 30→60 정도만 (과하게 늘리면 flock 때문에 loop가 길어짐)
이 3개만 해도 "50개 20분"이 보통 반 이하로 내려가는 케이스가 많음.
Tesseract + LayoutLM 통합 요약