"""콘텐츠 요소(메타데이터) 추출 F1 평가 (계획서 성능지표 No.3, p.24). 계획서 평가방식: KLUE 데이터셋의 NER(개체명 인식) 데이터를 활용하여 학습/테스트, KLUE Leaderboard 등재 baseline 대비 상대 평가. 목표 F1 83 이상(top5). 본 모듈은 정답 라벨(gold) 세트가 주어졌을 때 추출기 출력과의 집합 단위 precision/recall/F1 을 계산한다. 요소 유형(characters/motifs/keywords/genre) 별로 분리 측정 + 마이크로 평균. gold 라벨은 다음 두 경로 중 하나로 공급: ① 컴북스 1단계 '콘텐츠 구성요소 정의' 라벨 (article 단위) ② KLUE NER 공개 데이터의 PS(인물) 태그 → characters 평가 (scripts 에서 변환) """ from __future__ import annotations from dataclasses import dataclass def _norm(s: str) -> str: return s.strip().lower() @dataclass class PRF: precision: float recall: float f1: float tp: int fp: int fn: int def as_dict(self) -> dict: return { "precision": round(self.precision, 4), "recall": round(self.recall, 4), "f1": round(self.f1, 4), "tp": self.tp, "fp": self.fp, "fn": self.fn, } def _prf_from_counts(tp: int, fp: int, fn: int) -> PRF: precision = tp / (tp + fp) if (tp + fp) else 0.0 recall = tp / (tp + fn) if (tp + fn) else 0.0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0 return PRF(precision, recall, f1, tp, fp, fn) def set_prf(predicted: list[str], gold: list[str]) -> PRF: """단일 문서 한 요소 유형의 집합 단위 PRF.""" p = {_norm(x) for x in predicted if x and x.strip()} g = {_norm(x) for x in gold if x and x.strip()} tp = len(p & g) fp = len(p - g) fn = len(g - p) return _prf_from_counts(tp, fp, fn) # 평가 대상 요소 유형 (genre 는 단일값이라 별도 처리) _LIST_FIELDS = ("characters", "motifs", "keywords") def evaluate_extraction(predictions: list[dict], golds: list[dict]) -> dict[str, dict]: """예측/정답 메타데이터 리스트 → 요소 유형별 + 마이크로 평균 F1. predictions / golds 각 원소는 {"characters": [...], "motifs": [...], "keywords": [...], "genre": "..."} 형식. """ if len(predictions) != len(golds): raise ValueError("predictions/golds length mismatch") per_field_counts = {f: [0, 0, 0] for f in _LIST_FIELDS} # tp, fp, fn genre_tp = genre_total = 0 for pred, gold in zip(predictions, golds): for f in _LIST_FIELDS: r = set_prf(pred.get(f, []) or [], gold.get(f, []) or []) per_field_counts[f][0] += r.tp per_field_counts[f][1] += r.fp per_field_counts[f][2] += r.fn if gold.get("genre"): genre_total += 1 if _norm(str(pred.get("genre") or "")) == _norm(str(gold["genre"])): genre_tp += 1 result: dict[str, dict] = {} micro = [0, 0, 0] for f in _LIST_FIELDS: tp, fp, fn = per_field_counts[f] result[f] = _prf_from_counts(tp, fp, fn).as_dict() micro[0] += tp micro[1] += fp micro[2] += fn result["micro_avg"] = _prf_from_counts(*micro).as_dict() result["genre_accuracy"] = { "accuracy": round(genre_tp / genre_total, 4) if genre_total else None, "correct": genre_tp, "total": genre_total, } return result