102 lines
3.4 KiB
Python
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
|