feat: emit precision result in scorecard JSON format
성적서 '4. 모델 결과' 는 plagiarism_detection_performance 아래에 test_dataset / confusion_matrix / performance_metrics / threshold / interpretation 을 담은 JSON 을 캡처해 싣는다. 그 형식으로 출력한다. threshold 는 전 차수처럼 단일 수치가 아니라 세 조건(결합유사도 0.65 / 연속일치 35자 / 커버리지 0.30)의 조합이므로 객체로 둔다. 유사도만으로 판정하지 않는다는 점이 판정 기준의 핵심이라 수치 하나로 줄이면 오해를 준다. interpretation 문구는 실제 confusion matrix 에서 계산한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a758423b26
commit
5669c73f10
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user