o2o-plagiarism-ai/scripts/rescore_recall.py
hbyang 8ae52b0a82 feat: add summary (No.7) consensus and grounded generation trials
요약 성능지표(No.7, ROUGE 65점) 실험 일습을 커밋한다.

- app/engine/summary_consensus.py: 후보 5개 생성 후 합의 선택(select_consensus).
  문자/어절 2-gram 가중 합의로 고르며, 유효 후보가 없으면 valid=False 를 낸다.
- app/engine/summary_grounded.py: 근거 추출 후 압축하는 2단계 생성.
- summarizer.py: 추출 전략 3종(textrank/coverage/lead). coverage 는 MMR 로
  중복 문장을 눌러 문서 전체를 넓게 담는다. 기본값은 textrank 로 유지한다.

스크립트는 생성·평가·검증을 분리했다. verify_* 는 API 호출 없이 저장된 산출물만
재계산하는 독립 검증기라 지표를 공용 함수로 합치지 않는다. 합치면 검증이 성립하지
않는다. tune_summarizer 는 사람 검수 참조(build_summary_annotation_packet ->
export_summary_annotations)를 입력으로 받아 선택과 최종 보고를 분리한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 10:02:04 +09:00

69 lines
2.6 KiB
Python

"""저장된 1,000건 재채점 — 문자 2-gram recall (계획서 p.24 수식).
API 재호출 없음. H절 재평가 산출물의 참조·출력 원문을 그대로 사용하고
집계 지표만 F1 에서 recall 로 바꾼다. 새 독립 시험이 아니다.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from scripts.build_summary_trial import metrics
TARGET = 0.65
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--source", type=Path,
default=Path("data/eval/summary_word_refined_20260917/scorecard.json"))
ap.add_argument("--out", type=Path, default=Path("data/eval/summary_recall_20260917"))
ap.add_argument("--excerpt", type=int, default=160, help="캡처본 원문 발췌 길이")
args = ap.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
rows = json.loads(args.source.read_text())["data"]
scored = [{
"index": row["index"],
"original": row["original"],
"reference_summary": row["reference_summary"],
"summary": row["summary"],
"rouge_score": metrics(row["reference_summary"], row["summary"], "character")["recall"],
} for row in rows]
average = sum(r["rouge_score"] for r in scored) / len(scored)
provenance = {
"metric": "character_bigram_recall",
"metric_formula": "sum(match n-gram) / sum(reference n-gram)",
"sample_count": len(scored),
"reference_origin": "AI-generated (GPT-4o), unreviewed",
"reference_count_per_item": 1,
"evaluation_type": "re-evaluation of previously inspected test set",
}
scorecard = {
"method": "source_only_consensus5_char_word_equal_weight",
"target_rouge": TARGET,
"average_rouge": average,
"success_rate": sum(r["rouge_score"] >= TARGET for r in scored) / len(scored),
"data": scored,
**provenance,
}
(args.out / "scorecard.json").write_text(
json.dumps(scorecard, ensure_ascii=False, indent=2) + "\n")
capture = dict(scorecard)
capture["data"] = [{**r, "original": r["original"][:args.excerpt] + " …"}
for r in scored[:3]]
(args.out / "capture.json").write_text(
json.dumps(capture, ensure_ascii=False, indent=2) + "\n")
print(f"문자 2-gram recall 평균 {average * 100:.2f}% "
f"(목표 {TARGET * 100:.0f}%, {len(scored)}건)")
print(f"success_rate {scorecard['success_rate'] * 100:.1f}%")
if __name__ == "__main__":
main()