diff --git a/scripts/run_precision_eval.py b/scripts/run_precision_eval.py index 9041413..9629a7d 100644 --- a/scripts/run_precision_eval.py +++ b/scripts/run_precision_eval.py @@ -58,6 +58,58 @@ def build_corpus(rows: list[dict], db_path: Path) -> None: % (len(documents), added, skipped)) +def build_scorecard(settings, tp: int, fp: int, tn: int, fn: int, + precision: float, recall: float, f1: float) -> dict: + """성적서 "4. 모델 결과" 게재 형식으로 정리한다. + + 전 차수 성적서(GERI `GERIR.CE.2511-02.009`) 13페이지와 같은 키 구성이라 + 이 출력을 그대로 캡처해 붙일 수 있다. + + threshold 는 단일 수치가 아니라 세 조건의 조합이므로 객체로 둔다. + 유사도만으로 판정하지 않는다는 점이 판정 기준의 핵심이다. + """ + predicted_positive = tp + fp + actual_positive = tp + fn + accuracy = (tp + tn) / (tp + fp + tn + fn) if (tp + fp + tn + fn) else 0.0 + fpr = fp / (fp + tn) if (fp + tn) else 0.0 + return { + "plagiarism_detection_performance": { + "model": "O2O Triple-Similarity Detector", + "engine_version": settings.engine_version, + "test_dataset": { + "total_samples": tp + fp + tn + fn, + "plagiarism_cases": actual_positive, + "non_plagiarism_cases": fp + tn, + }, + "confusion_matrix": { + "true_positive": tp, + "false_positive": fp, + "true_negative": tn, + "false_negative": fn, + }, + "performance_metrics": { + "precision": round(precision, 4), + "recall": round(recall, 4), + "f1_score": round(f1, 4), + "accuracy": round(accuracy, 4), + }, + "threshold": { + "combined_similarity": settings.persistent_similarity_threshold, + "min_exact_span_chars": settings.persistent_min_exact_span, + "min_coverage": settings.persistent_min_coverage, + "require_exact_span_evidence": settings.require_exact_span_evidence, + }, + "interpretation": { + "precision": "모델이 표절로 판단한 %d건 중 %d건(%.1f%%)이 실제 표절" + % (predicted_positive, tp, precision * 100), + "recall": "실제 표절 %d건 중 %d건(%.1f%%)을 정확히 탐지" + % (actual_positive, tp, recall * 100), + "false_positive_rate": "%.1f%%" % (fpr * 100), + }, + } + } + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--testset", type=Path, required=True) @@ -138,6 +190,15 @@ def main() -> int: r = b["tp"] / (b["tp"] + b["fn"]) if (b["tp"] + b["fn"]) else 0.0 print(" %-18s n=%4d P=%.3f R=%.3f TP=%d FP=%d TN=%d FN=%d" % (name, total, p, r, b["tp"], b["fp"], b["tn"], b["fn"])) + scorecard = build_scorecard(settings, tp, fp, tn, fn, precision, recall, f1) + rendered = json.dumps(scorecard, ensure_ascii=False, indent=2) + print("\n" + "=" * 62) + print("4. 모델 결과 :") + print() + print(rendered) + print("\n최종 결과 : precision %.2f%% 달성" % (precision * 100)) + (args.testset / "scorecard.json").write_text(rendered + "\n", encoding="utf-8") + result_path = args.testset / "result.json" result_path.write_text(json.dumps({ "precision": precision, "recall": recall, "f1": f1,