요약 성능지표(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>
92 lines
4.2 KiB
Python
92 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""사람 참조 요약으로 추출 전략을 고르고, 잠금 테스트셋에서 한 번만 측정한다.
|
|
|
|
선택(검증)과 최종 보고(테스트)를 분리해, 65점에 맞춰 전 평가 요약문을 수정하는
|
|
전 차수식 접근을 방지한다. source_group 단위로 분리하므로 같은 작성자의 문장이
|
|
튜닝·시험 양쪽에 섞이지 않는다.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from app.engine.rouge import evaluate_pairs
|
|
from app.engine.summarizer import extractive_summary
|
|
|
|
|
|
def stable_bucket(group: str, seed: int) -> int:
|
|
return int(hashlib.sha256((str(seed) + ":" + group).encode()).hexdigest()[:8], 16) % 100
|
|
|
|
|
|
def split_rows(rows: list[dict], seed: int) -> tuple[list[dict], list[dict]]:
|
|
# 20% 그룹을 최종 시험에 잠근다. 나머지에서만 전략을 고른다.
|
|
test = [row for row in rows if stable_bucket(row.get("source_group", row["id"]), seed) < 20]
|
|
validation = [row for row in rows if row not in test]
|
|
if not test or not validation:
|
|
raise ValueError("그룹 분할 결과가 비었습니다. source_group을 확인하세요.")
|
|
return validation, test
|
|
|
|
|
|
def score(rows: list[dict], ratio: float, strategy: str, mode: str) -> dict[str, dict[str, float]]:
|
|
pairs = [
|
|
(extractive_summary(row["text"], ratio=ratio, strategy=strategy).final, row["references"])
|
|
for row in rows
|
|
]
|
|
return evaluate_pairs(pairs, mode=mode)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("dataset", type=Path, help="export_summary_annotations.py 출력 JSONL")
|
|
parser.add_argument("--out", type=Path, required=True, help="선택 근거와 잠금 시험 결과 JSON")
|
|
parser.add_argument("--seed", type=int, default=20260916)
|
|
parser.add_argument("--mode", choices=["lemma", "char"], default="lemma")
|
|
parser.add_argument("--ratios", type=float, nargs="+", default=[0.25, 0.30, 0.35],
|
|
help="사전 등록 가능한 요약 비율 후보(권장 25~35%%)")
|
|
args = parser.parse_args()
|
|
if any(not 0 < ratio <= 0.35 for ratio in args.ratios):
|
|
parser.error("요약 비율은 0보다 크고 0.35 이하여야 합니다")
|
|
rows = [json.loads(line) for line in args.dataset.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
if len(rows) < 20:
|
|
parser.error("최소 20건 이상의 완료된 사람 참조 요약이 필요합니다")
|
|
validation, test = split_rows(rows, args.seed)
|
|
candidates = []
|
|
for strategy in ("textrank", "coverage", "lead"):
|
|
for ratio in args.ratios:
|
|
metrics = score(validation, ratio, strategy, args.mode)
|
|
candidates.append({"strategy": strategy, "ratio": ratio, "validation": metrics})
|
|
# 같은 recall이면 ROUGE-L F1, 더 짧은 출력 순으로 결정한다.
|
|
winner = max(candidates, key=lambda item: (
|
|
item["validation"]["rouge1"]["recall"],
|
|
item["validation"]["rougeL"]["f1"],
|
|
-item["ratio"],
|
|
))
|
|
final = score(test, winner["ratio"], winner["strategy"], args.mode)
|
|
record = {
|
|
"purpose": "summary strategy selection with source-group-held-out final test",
|
|
"dataset": str(args.dataset), "seed": args.seed, "tokenization": args.mode,
|
|
"samples": {"total": len(rows), "validation": len(validation), "locked_test": len(test)},
|
|
"candidates": candidates, "selected": {"strategy": winner["strategy"], "ratio": winner["ratio"]},
|
|
"locked_test_scores": final,
|
|
"reported_metric": "rouge1_recall",
|
|
"target": 0.65,
|
|
"achieved": final["rouge1"]["recall"] >= 0.65,
|
|
}
|
|
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
args.out.write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print("선택: %s / ratio=%.2f" % (winner["strategy"], winner["ratio"]))
|
|
print("잠금 테스트 ROUGE-1 recall: %.4f (%s)" %
|
|
(final["rouge1"]["recall"], "달성" if record["achieved"] else "미달"))
|
|
print("근거 저장: %s" % args.out)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|