o2o-plagiarism-ai/scripts/eval_rouge.py

108 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""요약 성능(ROUGE) 평가 하니스 — 계획서 성능지표 No.7.
데이터(요약 정답셋)가 들어오면 즉시 측정할 수 있도록 평가환경을 선구축한다.
입력 형식 (JSONL, 한 줄당 한 건):
{"text": "원문 ...", "reference": "사람 작성 요약 정답 ..."}
또는 이미 시스템 요약이 있는 경우:
{"system": "엔진 요약 ...", "reference": "정답 요약 ..."}
동작:
- "text" 만 있으면 자체 Summarizer 로 system 요약을 생성한 뒤 reference 와 비교
- "system" 이 있으면 그대로 사용
- 정답셋 파일이 없으면 내장 dry-run 샘플로 파이프라인 동작만 검증
사용:
python -m scripts.eval_rouge # dry-run (내장 샘플)
python -m scripts.eval_rouge data/eval/summary.jsonl # 정답셋 평가
python -m scripts.eval_rouge data/eval/summary.jsonl --mode char --ratio 0.3
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.engine.rouge import evaluate_pairs # noqa: E402
from app.engine.summarizer import get_summarizer # noqa: E402
# 정답셋 도착 전 파이프라인 검증용 내장 샘플 (원문/정답 요약)
_DRY_RUN = [
{
"text": (
"홍길동은 조선시대 의적이었다. 그는 활빈당을 만들어 탐관오리의 재물을 빼앗았다. "
"빼앗은 재물은 가난한 백성들에게 나누어 주었다. 조정에서는 그를 잡으려 했으나 실패했다. "
"결국 홍길동은 율도국으로 떠나 새 나라를 세웠다."
),
"reference": "홍길동은 활빈당을 만들어 탐관오리의 재물을 빼앗아 백성에게 나눠주고 율도국을 세웠다.",
},
{
"text": (
"어린 왕자는 자신의 작은 별을 떠나 여러 행성을 여행했다. 여행 중 다양한 어른들을 만났다. "
"지구에서 여우를 만나 관계의 의미를 배웠다. 그는 자신의 장미가 소중함을 깨달았다."
),
"reference": "어린 왕자는 여러 행성을 여행하며 여우를 통해 자신의 장미가 소중함을 깨닫는다.",
},
]
def _load(path: str | None) -> tuple[list[dict], bool]:
if path is None:
return _DRY_RUN, True
p = Path(path)
if not p.exists():
print(f"[warn] 파일 없음: {path} → 내장 dry-run 샘플로 진행", file=sys.stderr)
return _DRY_RUN, True
rows = [json.loads(line) for line in p.read_text(encoding="utf-8").splitlines() if line.strip()]
return rows, False
def main() -> None:
ap = argparse.ArgumentParser(description="요약 ROUGE 평가 (성능지표 No.7)")
ap.add_argument("dataset", nargs="?", default=None, help="JSONL 정답셋 (없으면 dry-run)")
ap.add_argument("--mode", choices=["lemma", "char"], default="lemma", help="토큰화 방식")
ap.add_argument("--ratio", type=float, default=0.3, help="자체 요약 길이 비율")
args = ap.parse_args()
rows, is_dry = _load(args.dataset)
summarizer = get_summarizer()
pairs: list[tuple[str, str]] = []
for row in rows:
reference = row.get("reference", "")
if not reference:
continue
if "system" in row and row["system"]:
system = row["system"]
else:
system = summarizer.summarize(row.get("text", ""), ratio=args.ratio).final
pairs.append((system, reference))
if not pairs:
print("평가할 (system, reference) 페어가 없습니다.", file=sys.stderr)
sys.exit(1)
scores = evaluate_pairs(pairs, mode=args.mode)
banner = "DRY-RUN (내장 샘플)" if is_dry else f"{args.dataset} ({len(pairs)}건)"
print(f"\n=== 요약 ROUGE 평가 — {banner} / 토큰={args.mode} ===")
print(f"{'metric':<10}{'precision':>12}{'recall':>12}{'f1':>12}")
for metric in ("rouge1", "rouge2", "rougeL"):
s = scores[metric]
print(f"{metric:<10}{s['precision']:>12.4f}{s['recall']:>12.4f}{s['f1']:>12.4f}")
target = 0.65
r1f1 = scores["rouge1"]["f1"]
status = "달성" if r1f1 >= target else "미달"
print(f"\n목표(No.7) ROUGE 0.65 대비 ROUGE-1 F1 = {r1f1:.4f}{status}")
if is_dry:
print("\n※ 이는 파이프라인 검증용 dry-run 수치입니다. 컴북스 요약 정답셋 수령 후 본 평가로 정식 측정.")
if __name__ == "__main__":
main()