Initial commit

This commit is contained in:
2026-06-15 09:54:01 +09:00
commit ce22ba962d
21 changed files with 3465 additions and 0 deletions

183
train/classifier_dataset.py Normal file
View File

@@ -0,0 +1,183 @@
"""
files/json/{LABEL}/{파일명}/page_xxx.json 구조를 읽어
페이지 단위로 샘플을 생성하여 캐시 저장
문서 1개 = 샘플 N개 (페이지 수만큼)
"""
import json
import os
import pickle
from pathlib import Path
from loguru import logger
import config
# JSON_DIR = os.getenv("CLASSIFIER_JSON_DIR", "./files/json")
# CACHE_PATH = os.getenv("CLASSIFIER_CACHE_PATH", "./files/classifier_dataset.pkl")
# MIN_WORDS = int(os.getenv("CLASSIFIER_MIN_WORDS", "20")) # 페이지당 최소 단어 수
JSON_DIR = config.CLASSIFIER_JSON_DIR
CACHE_PATH = config.CLASSIFIER_CACHE_PATH
MIN_WORDS = config.CLASSIFIER_MIN_WORDS
_status: dict = {"status": "idle", "message": "", "total": 0}
def get_status() -> dict:
return _status
# def build() -> None:
# global _status
# try:
# _status = {"status": "building", "message": "JSON 파일 스캔 중...", "total": 0}
# json_path = Path(JSON_DIR)
#
# if not json_path.exists():
# raise FileNotFoundError(f"JSON 디렉토리 없음: {JSON_DIR}")
#
# data = []
# skip_count = 0
#
# for label_dir in sorted(json_path.iterdir()):
# if not label_dir.is_dir():
# continue
# label = label_dir.name
# label_count = 0
#
# for doc_dir in sorted(label_dir.iterdir()):
# if not doc_dir.is_dir():
# continue
#
# for page_file in sorted(doc_dir.glob("page_*.json")):
# try:
# with open(page_file, encoding="utf-8") as f:
# d = json.load(f)
# words = d.get("words", [])
# except Exception as e:
# logger.warning(f"파일 읽기 실패 {page_file}: {e}")
# continue
#
# # 단어 수 미달 페이지 제외
# if len(words) < MIN_WORDS:
# print(f"skip file : {len(words)} : {page_file}")
# skip_count += 1
# continue
#
# data.append({
# "text": " ".join(words),
# "label": label,
# "src": str(page_file), # 디버깅용
# })
# label_count += 1
#
# _status["message"] = f"{label}: {label_count}페이지 샘플 추가 (누적 {len(data)}개)"
# logger.info(_status["message"])
#
# if not data:
# raise ValueError("수집된 데이터가 없습니다.")
#
# os.makedirs(os.path.dirname(CACHE_PATH), exist_ok=True)
# with open(CACHE_PATH, "wb") as f:
# pickle.dump(data, f)
#
# _status = {
# "status": "done",
# "message": f"데이터셋 생성 완료 (단어 {MIN_WORDS}개 미만 {skip_count}페이지 제외)",
# "total": len(data),
# }
# logger.info(f"데이터셋 저장: {CACHE_PATH} ({len(data)}개, 제외 {skip_count}개)")
#
# except Exception as e:
# _status = {"status": "error", "message": str(e), "total": 0}
# logger.exception(f"데이터셋 생성 실패: {e}")
def _process_page_file(page_file: Path, label: str, data: list, skip_count: int) -> tuple[int, int]:
"""단일 page_*.json 파일 처리. (label_count 증가분, skip_count) 반환"""
try:
with open(page_file, encoding="utf-8") as f:
d = json.load(f)
words = d.get("words", [])
except Exception as e:
logger.warning(f"파일 읽기 실패 {page_file}: {e}")
return 0, skip_count
if len(words) < MIN_WORDS:
print(f"skip file : {len(words)} : {page_file}")
return 0, skip_count + 1
data.append({"text": " ".join(words), "label": label, "src": str(page_file)})
return 1, skip_count
def _process_label_dir(label_dir: Path, data: list, skip_count: int) -> tuple[int, int]:
"""label 폴더 처리. label_count, skip_count 반환"""
label = label_dir.name
label_count = 0
# label 폴더 직속 page_*.json
for page_file in sorted(label_dir.glob("*.json")):
added, skip_count = _process_page_file(page_file, label, data, skip_count)
label_count += added
# 하위 doc_dir 폴더
for doc_dir in sorted(label_dir.iterdir()):
if not doc_dir.is_dir():
continue
for page_file in sorted(doc_dir.glob("*.json")):
added, skip_count = _process_page_file(page_file, label, data, skip_count)
label_count += added
return label_count, skip_count
def build() -> None:
global _status
try:
_status = {"status": "building", "message": "JSON 파일 스캔 중...", "total": 0}
json_path = Path(JSON_DIR)
if not json_path.exists():
raise FileNotFoundError(f"JSON 디렉토리 없음: {JSON_DIR}")
data = []
skip_count = 0
for label_dir in sorted(json_path.iterdir()):
if not label_dir.is_dir():
continue
label_count, skip_count = _process_label_dir(label_dir, data, skip_count)
_status["message"] = f"{label_dir.name}: {label_count}페이지 샘플 추가 (누적 {len(data)}개)"
logger.info(_status["message"])
if not data:
raise ValueError("수집된 데이터가 없습니다.")
os.makedirs(os.path.dirname(CACHE_PATH), exist_ok=True)
with open(CACHE_PATH, "wb") as f:
pickle.dump(data, f)
_status = {
"status": "done",
"message": f"데이터셋 생성 완료 (단어 {MIN_WORDS}개 미만 {skip_count}페이지 제외)",
"total": len(data),
}
logger.info(f"데이터셋 저장: {CACHE_PATH} ({len(data)}개, 제외 {skip_count}개)")
except Exception as e:
_status = {"status": "error", "message": str(e), "total": 0}
logger.exception(f"데이터셋 생성 실패: {e}")
def load() -> list[dict]:
if not os.path.exists(CACHE_PATH):
return []
try:
with open(CACHE_PATH, "rb") as f:
return pickle.load(f)
except Exception as e:
logger.error(f"데이터셋 로드 실패: {e}")
return []

215
train/classifier_trainer.py Normal file
View File

@@ -0,0 +1,215 @@
"""
classifier_dataset.load() 로 데이터 로드 후 TF-IDF + SVM 학습
"""
import random
from collections import Counter
from loguru import logger
from sklearn.calibration import CalibratedClassifierCV
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.svm import LinearSVC
from config import SERVICE
from train import classifier_dataset
from utils import classifier_model_store
_status: dict = {"status": "idle", "message": ""}
def get_status() -> dict:
return _status
def augment_text(text: str, n: int = 25) -> list:
"""텍스트 증강 - 단어 순서 변경, 일부 단어 제거"""
words = text.split()
augmented = []
for _ in range(n):
aug = words.copy()
# 1. 랜덤 단어 10% 제거
drop_count = max(1, int(len(aug) * 0.1))
for _ in range(drop_count):
if aug:
aug.pop(random.randint(0, len(aug) - 1))
# 2. 랜덤 단어 일부 순서 섞기
if len(aug) > 5:
idx = random.randint(0, len(aug) - 3)
aug[idx], aug[idx + 1] = aug[idx + 1], aug[idx]
augmented.append(" ".join(aug))
return augmented
def run_train() -> None:
global _status
try:
_status = {"status": "training", "message": "데이터셋 로딩 중..."}
data = classifier_dataset.load()
if not data:
raise ValueError("데이터셋이 없습니다. /classifier/dataset/build 먼저 실행하세요.")
texts = [d["text"] for d in data]
labels = [d["label"] for d in data]
# ── 특정 클래스 데이터 증강 ──────────────────────
if SERVICE == "amko":
AUGMENT_LABELS = ["AMKOR TECHNOLOGY TAIWAN_B"] # 증강할 클래스
AUGMENT_TARGET = 30 # 목표 샘플 수
augmented_texts, augmented_labels = [], []
label_groups = {}
for t, l in zip(texts, labels):
label_groups.setdefault(l, []).append(t)
for label in AUGMENT_LABELS:
samples = label_groups.get(label, [])
current_count = len(samples)
if current_count < AUGMENT_TARGET:
need = AUGMENT_TARGET - current_count
logger.info(f"{label}: {current_count}개 → {AUGMENT_TARGET}개 증강 ({need}개 추가)")
for i in range(need):
base_text = samples[i % current_count] # 기존 샘플 순환
aug = augment_text(base_text, n=1)[0]
augmented_texts.append(aug)
augmented_labels.append(label)
texts = texts + augmented_texts
labels = labels + augmented_labels
logger.info(f"증강 후 클래스별 샘플: {Counter(labels)}")
# ────────────────────────────────────────────────
# # ── 클래스 언더샘플링 ─────────────────────────
# MAX_SAMPLES_PER_CLASS = 20 # B타입 8개의 2~3배 수준으로 제한
#
# balanced_data = []
# class_counts = Counter(labels)
#
# for label in set(labels):
# class_texts = [t for t, l in zip(texts, labels) if l == label]
# # MAX_SAMPLES_PER_CLASS 초과 시 랜덤 샘플링
# if len(class_texts) > MAX_SAMPLES_PER_CLASS:
# class_texts = random.sample(class_texts, MAX_SAMPLES_PER_CLASS)
# balanced_data.extend([(t, label) for t in class_texts])
#
# texts, labels = zip(*balanced_data)
# texts, labels = list(texts), list(labels)
# logger.info(f"언더샘플링 후: {Counter(labels)}")
# # ── 핵심 키워드 가중치 부여 (특정 클래스만) ──────────────────────
# KEYWORD_BOOST_BY_LABEL = {
# "AMKOR TECHNOLOGY TAIWAN_A": { # A타입에만
# "Process Fee": 5,
# "Total Process Fee": 5,
# "Consigned Value": 5,
# "Total Process Value": 5,
# },
# "AMKOR TECHNOLOGY TAIWAN_B": { # B타입에만
# "PACKING LIST SUMMARY": 5,
# "FOC Y": 3,
# },
# }
#
# def boost_keywords(text: str, label: str) -> str:
# boost_map = KEYWORD_BOOST_BY_LABEL.get(label, {}) # 해당 label 없으면 빈 dict
# boosted = text
# for keyword, repeat in boost_map.items():
# if keyword.lower() in text.lower():
# boosted += f" {keyword}" * repeat
# return boosted
#
# texts = [boost_keywords(t, l) for t, l in zip(texts, labels)]
# logger.info("핵심 키워드 가중치 부여 완료")
# # ────────────────────────────────────────────────
# # ── 샘플 부족 클래스 필터링 ──────────────────────
# counts = Counter(labels)
# MIN_SAMPLES = 5
# before = len(texts)
# filtered = [(t, l) for t, l in zip(texts, labels) if counts[l] >= MIN_SAMPLES]
# if not filtered:
# raise ValueError("모든 클래스가 샘플 부족으로 제외되었습니다.")
# texts, labels = map(list, zip(*filtered))
# excluded = {k: v for k, v in counts.items() if v < MIN_SAMPLES}
# if excluded:
# logger.warning(f"샘플 부족 제외: {excluded}")
# logger.info(f"필터링: {before}개 → {len(texts)}개")
# # ────────────────────────────────────────────────
#
# counts = Counter(labels)
# min_count = min(counts.values())
# cv = 5
# stratify = labels if min_count >= cv else None
MIN_SAMPLES = 5
# ── 필터링 ───────────────────────────────────────
counts = Counter(labels)
before = len(texts)
filtered = [(t, l) for t, l in zip(texts, labels) if counts[l] >= MIN_SAMPLES]
if not filtered:
raise ValueError("모든 클래스가 샘플 부족으로 제외되었습니다.")
texts, labels = map(list, zip(*filtered))
excluded = {k: v for k, v in counts.items() if v < MIN_SAMPLES}
if excluded:
logger.warning(f"샘플 부족 제외: {excluded}")
logger.info(f"필터링: {before}개 → {len(texts)}")
# ── MIN_SAMPLES 기준으로 cv/stratify 자동 결정 ──
counts = Counter(labels)
min_count = min(counts.values())
cv = max(2, min(5, int(MIN_SAMPLES * 0.8))) # MIN_SAMPLES의 80% (train 비율)
stratify = labels if min_count >= cv else None
logger.info(f"MIN_SAMPLES={MIN_SAMPLES} → cv={cv}, stratify={'적용' if stratify else '비적용'}")
_status["message"] = f"{len(texts)}개 샘플 / {len(counts)}개 클래스 학습 중..."
_status["samples"] = len(texts)
_status["classes"] = len(counts)
logger.info(_status["message"])
X_train, X_test, y_train, y_test = train_test_split(
texts, labels,
test_size=0.2,
stratify=stratify,
random_state=42,
)
# train_counts = Counter(y_train)
# min_train_count = min(train_counts.values())
# cv = max(2, min(5, min_train_count))
pipeline = Pipeline([
("tfidf", TfidfVectorizer(max_features=50000, ngram_range=(1, 2))),
("clf", CalibratedClassifierCV(LinearSVC(max_iter=2000, class_weight="balanced"), cv=cv)),
])
pipeline.fit(X_train, y_train)
report = classification_report(y_test, pipeline.predict(X_test), zero_division=0)
logger.info(f"\n{report}")
classifier_model_store.set_model(pipeline)
_status = {
"status": "done",
"message": f"학습 완료 | 샘플={len(texts)}, 클래스={len(counts)}",
"samples": len(texts),
"classes": len(counts),
"report": report, # 필요 시 별도 API로 노출
}
logger.info("classifier 학습 완료")
except Exception as e:
_status = {"status": "error", "message": str(e)}
logger.exception(f"classifier 학습 실패: {e}")

37
train/schemas.py Normal file
View File

@@ -0,0 +1,37 @@
from pydantic import BaseModel, Field
class TrainRequest(BaseModel):
epochs: int = Field(10, description="학습 에폭 수")
lr: float = Field(1e-5, description="학습률")
batch_size: int = Field(4, description="배치 사이즈")
ewc_lambda: float = Field(1000., description="EWC 패널티 강도 (클수록 기존 지식 보호)")
strategy: str = Field("full",description="auto | full | ewc | replay")
class TrainStatus(BaseModel):
running: bool
epoch: int
total: int
loss: float
val_acc: float = 0.0
message: str
batch: int = 0 # 현재 배치
total_batches: int = 0 # 전체 배치 수
class PredictResult(BaseModel):
filename: str
label: str | None = None
confidence: float | None = None
all_probs: dict | None = None
key_tokens: list[str] | None = None
ocr_word_count: int | None = None
total_pages: int | None = None
per_page: list | None = None
error: str | None = None
class DatasetStatus(BaseModel):
total_samples: int
by_label: dict
label2id: dict

348
train/trainer.py Normal file
View File

@@ -0,0 +1,348 @@
import random
import traceback
from pathlib import Path
import torch
import torch.nn as nn
from PIL import Image
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from torch.utils.data import Dataset, DataLoader
from torch.optim import AdamW
from transformers import (
LayoutLMv3Processor,
LayoutLMv3ForSequenceClassification,
get_scheduler,
)
from tqdm import tqdm
from loguru import logger
import config
from train.schemas import TrainRequest, TrainStatus
from utils.cache import (
load_cache, save_label2id,
load_replay_buffer, update_replay_buffer, load_label2id
)
# 학습 상태 전역
train_status = TrainStatus(
running=False, epoch=0, total=0, loss=0.0, message="대기 중"
)
stop_requested = False
# ── Dataset ───────────────────────────────────────
class InvoiceDataset(Dataset):
def __init__(self, samples: list, processor):
self.samples = samples
self.processor = processor
def __len__(self):
return len(self.samples)
def __getitem__(self, idx):
s = self.samples[idx]
image = Image.open(s["image_path"]).convert("RGB")
encoding = self.processor(
image, text=s["words"], boxes=s["boxes"],
return_tensors="pt", truncation=True,
padding="max_length", max_length=config.MAX_LEN,
)
return {
"input_ids": encoding["input_ids"].squeeze(),
"attention_mask": encoding["attention_mask"].squeeze(),
"bbox": encoding["bbox"].squeeze(),
"pixel_values": encoding["pixel_values"].squeeze(),
"labels": torch.tensor(s["label"], dtype=torch.long),
}
# ── Fisher (EWC) ──────────────────────────────────
def compute_fisher(model, loader) -> dict:
fisher = {n: torch.zeros_like(p)
for n, p in model.named_parameters() if p.requires_grad}
model.eval()
for batch in loader:
model.zero_grad()
out = model(
input_ids = batch["input_ids"].to(config.DEVICE),
attention_mask = batch["attention_mask"].to(config.DEVICE),
bbox = batch["bbox"].to(config.DEVICE),
pixel_values = batch["pixel_values"].to(config.DEVICE),
labels = batch["labels"].to(config.DEVICE),
)
out.loss.backward()
for n, p in model.named_parameters():
if p.requires_grad and p.grad is not None:
fisher[n] += p.grad.pow(2)
return {n: f / max(len(loader), 1) for n, f in fisher.items()}
# ── 검증 ──────────────────────────────────────────
def evaluate_loader(model, loader) -> float:
model.eval()
correct, total = 0, 0
with torch.no_grad():
for batch in loader:
out = model(
input_ids = batch["input_ids"].to(config.DEVICE),
attention_mask = batch["attention_mask"].to(config.DEVICE),
bbox = batch["bbox"].to(config.DEVICE),
pixel_values = batch["pixel_values"].to(config.DEVICE),
)
preds = out.logits.argmax(dim=-1).cpu()
correct += (preds == batch["labels"]).sum().item()
total += len(batch["labels"])
return correct / total if total else 0.0
def _get_latest_model_path():
"""
날짜/차수 폴더에서 가장 최신 모델 경로 반환
files/models/20260305/0002/ ← 최신
"""
base = config.MODEL_BASE_PATH
all_models = sorted([
d for d in base.rglob("config.json")
])
if not all_models:
return None
print(all_models[-1].parent)
return all_models[-1].parent
# ── 학습 루프 ─────────────────────────────────────
def run_train_loop(model, processor, train_s, val_s, req: TrainRequest,
label2id: dict, strategy: str, save_path: Path):
global train_status
train_loader = DataLoader(
InvoiceDataset(train_s, processor),
batch_size=req.batch_size, shuffle=True
)
val_loader = DataLoader(
InvoiceDataset(val_s, processor),
batch_size=req.batch_size
)
optimizer = AdamW(model.parameters(), lr=req.lr)
scheduler = get_scheduler(
"cosine", optimizer=optimizer,
num_warmup_steps=len(train_loader),
num_training_steps=len(train_loader) * req.epochs,
)
# EWC 사전 계산
ewc_params, ewc_fisher = {}, {}
if strategy == "ewc":
ewc_params = {n: p.clone().detach()
for n, p in model.named_parameters() if p.requires_grad}
ewc_fisher = compute_fisher(model, val_loader)
logger.info("EWC Fisher 계산 완료")
best_acc = 0.0
total_batches = len(train_loader) * req.epochs
for epoch in range(req.epochs):
model.train()
total_loss = 0
pbar = tqdm(
train_loader,
desc=f"Epoch {epoch+1}/{req.epochs}",
ncols=100,
leave=True,
)
for batch_idx, batch in enumerate(pbar):
if stop_requested:
logger.info("학습 중단 요청 감지")
return
out = model(
input_ids = batch["input_ids"].to(config.DEVICE),
attention_mask = batch["attention_mask"].to(config.DEVICE),
bbox = batch["bbox"].to(config.DEVICE),
pixel_values = batch["pixel_values"].to(config.DEVICE),
labels = batch["labels"].to(config.DEVICE),
)
loss = out.loss
if strategy == "ewc" and ewc_fisher:
for n, p in model.named_parameters():
if n in ewc_fisher:
loss += req.ewc_lambda * (
ewc_fisher[n] * (p - ewc_params[n]).pow(2)
).sum()
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
scheduler.step()
optimizer.zero_grad()
total_loss += loss.item()
pbar.set_postfix(loss=f"{loss.item():.4f}")
# 배치 단위 상태 업데이트
done_batches = epoch * len(train_loader) + (batch_idx + 1)
train_status.epoch = epoch + 1
train_status.total = req.epochs
train_status.loss = round(loss.item(), 4)
train_status.batch = done_batches # 완료된 전체 배치
train_status.total_batches = total_batches # 전체 배치 수
train_status.message = (
f"Epoch {epoch+1}/{req.epochs} | "
f"batch {batch_idx+1}/{len(train_loader)} | "
f"loss={loss.item():.4f}"
)
avg_loss = total_loss / len(train_loader)
val_acc = evaluate_loader(model, val_loader)
msg = (
f"Epoch {epoch+1}/{req.epochs} | "
f"loss={avg_loss:.4f} | val_acc={val_acc:.4f}"
)
tqdm.write(f"[결과] {msg}")
logger.info(msg)
train_status.epoch = epoch + 1
train_status.loss = round(avg_loss, 4)
train_status.val_acc = round(val_acc, 4)
train_status.message = msg
if val_acc > best_acc:
best_acc = val_acc
model.save_pretrained(str(save_path))
processor.save_pretrained(str(save_path))
save_label2id(label2id)
logger.info(f"모델 저장 (val_acc={val_acc:.4f}) → {save_path}")
# ── 학습 진입점 ───────────────────────────────────
def run_train(req: TrainRequest):
global train_status
stop_requested = False
train_status = TrainStatus(
running=True, epoch=0, total=req.epochs,
loss=0.0, message="학습 준비 중"
)
try:
from utils.cache import load_label2id
# 날짜/차수 폴더 생성
save_path = config.get_model_save_path()
logger.info(f"모델 저장 경로: {save_path}")
train_status.message = f"저장 경로: {save_path}"
samples = load_cache()
label2id = load_label2id()
id2label = {v: k for k, v in label2id.items()}
processor_path = (
str(config.MODEL_BASE_PATH / "preprocessor_config.json")
if (config.MODEL_BASE_PATH / "preprocessor_config.json").exists()
else config.MODEL_NAME
)
# 최신 모델 폴더 찾기 (이어 학습)
latest_model = _get_latest_model_path()
model_path = str(latest_model) if latest_model else config.MODEL_NAME
logger.info(f"기반 모델: {model_path}")
processor = LayoutLMv3Processor.from_pretrained(
latest_model and str(latest_model) or config.MODEL_NAME,
apply_ocr=False
)
model = LayoutLMv3ForSequenceClassification.from_pretrained(
model_path,
num_labels=len(label2id),
id2label=id2label,
label2id=label2id,
ignore_mismatched_sizes=True,
).to(config.DEVICE)
# 전략 자동 선택
strategy = req.strategy
if strategy == "auto":
buf = load_replay_buffer()
existing_types = set(buf.keys())
new_types = {id2label[s["label"]] for s in samples}
strategy = "replay" if (new_types - existing_types) else "ewc"
train_status.message = f"전략: {strategy}"
logger.info(f"학습 전략: {strategy}")
train_s, val_s = train_test_split(
samples, test_size=0.2, random_state=42,
stratify=[s["label"] for s in samples],
)
if strategy == "replay":
replay_s = [s for v in load_replay_buffer().values() for s in v]
combined = train_s + replay_s
random.shuffle(combined)
run_train_loop(model, processor, combined, val_s, req, label2id, strategy, save_path)
update_replay_buffer(train_s, id2label)
else:
run_train_loop(model, processor, train_s, val_s, req, label2id, strategy, save_path)
update_replay_buffer(train_s, id2label)
train_status.message = f"학습 완료 → {save_path}"
logger.info(f"학습 완료: {save_path}")
except Exception:
train_status.message = f"오류: {traceback.format_exc()}"
logger.exception("학습 중 예외 발생")
finally:
stop_requested = False
train_status.running = False
# ── 평가 진입점 ───────────────────────────────────
def run_evaluate(test_size: float = 0.2) -> dict:
samples = load_cache()
label2id = load_label2id()
id2label = {v: k for k, v in label2id.items()}
_, test_s = train_test_split(
samples, test_size=test_size, random_state=42,
stratify=[s["label"] for s in samples],
)
model_path = _get_latest_model_path()
if not model_path:
raise RuntimeError("저장된 모델이 없습니다.")
processor = LayoutLMv3Processor.from_pretrained(
str(model_path), apply_ocr=False
)
model = LayoutLMv3ForSequenceClassification.from_pretrained(
str(model_path)
).to(config.DEVICE)
model.eval()
loader = DataLoader(InvoiceDataset(test_s, processor), batch_size=config.BATCH_SIZE)
all_preds, all_labels = [], []
with torch.no_grad():
for batch in loader:
out = model(
input_ids = batch["input_ids"].to(config.DEVICE),
attention_mask = batch["attention_mask"].to(config.DEVICE),
bbox = batch["bbox"].to(config.DEVICE),
pixel_values = batch["pixel_values"].to(config.DEVICE),
)
all_preds.extend(out.logits.argmax(dim=-1).cpu().tolist())
all_labels.extend(batch["labels"].tolist())
target_names = [id2label[i] for i in range(len(id2label))]
report_dict = classification_report(
all_labels, all_preds, target_names=target_names, output_dict=True
)
report_str = classification_report(
all_labels, all_preds, target_names=target_names
)
logger.info(f"평가 완료 | accuracy={report_dict['accuracy']:.4f}")
return {
"test_samples": len(test_s),
"accuracy": round(report_dict["accuracy"], 4),
"report": report_dict,
"report_text": report_str,
}