o2o-plagiarism-ai/app/engine/metadata_eval.py

102 lines
3.4 KiB
Python

"""콘텐츠 요소(메타데이터) 추출 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