Spaces:
Running
Running
| """ | |
| 검색 품질 정량 평가 스크립트 | |
| 실행: python scripts/eval_search.py | |
| python scripts/eval_search.py --top_n 5 (상위 N개 안에 정답 있으면 pass) | |
| python scripts/eval_search.py --verbose (틀린 케이스 상세 출력) | |
| 평가 지표: | |
| - Acc@1 : 1위가 정답인 비율 (가장 중요) | |
| - Acc@3 : 상위 3개 안에 정답 포함 비율 | |
| - Acc@5 : 상위 5개 안에 정답 포함 비율 | |
| - MRR : Mean Reciprocal Rank (정답이 몇 위인지 평균) | |
| - 카테고리별 성적표 | |
| """ | |
| import sys, json, argparse | |
| from pathlib import Path | |
| from collections import defaultdict | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| sys.stdout.reconfigure(encoding='utf-8') | |
| from core.search_engine import MenuSearchEngine | |
| # ── 설정 ──────────────────────────────────────────────────────────────────── | |
| EVAL_PATH = Path(__file__).parent.parent / "data" / "eval_queries.json" | |
| TOP_N = 5 # 검색 시 몇 개 가져올지 | |
| def run_eval(verbose: bool = False, top_n_check: int = 1): | |
| queries = json.loads(EVAL_PATH.read_text(encoding="utf-8")) | |
| engine = MenuSearchEngine.get_instance() | |
| results = [] | |
| cat_stats = defaultdict(lambda: {"total": 0, "hit1": 0, "hit3": 0, "hit5": 0}) | |
| print(f"\n{'='*70}") | |
| print(f"{'쿼리':<35} {'정답':^12} {'1위':^12} {'순위':^4} {'결과'}") | |
| print(f"{'='*70}") | |
| for item in queries: | |
| qid = item["id"] | |
| query = item["query"] | |
| exp_id = item["expected_id"] | |
| exp_name = item["expected_name"] | |
| category = item["category"] | |
| res = engine.search(query, top_n=TOP_N, use_reranker=False, use_hyde=True) | |
| # 정답 순위 찾기 | |
| rank = None | |
| for i, r in enumerate(res, 1): | |
| if r["menu_id"] == exp_id: | |
| rank = i | |
| break | |
| hit1 = rank == 1 | |
| hit3 = rank is not None and rank <= 3 | |
| hit5 = rank is not None and rank <= 5 | |
| top1_name = res[0]["menu_name"] if res else "없음" | |
| rank_str = f"{rank}위" if rank else "miss" | |
| ok_mark = "✅" if hit1 else ("🔶" if hit3 else ("🔸" if hit5 else "❌")) | |
| # 전체 출력 또는 틀린 것만 출력 | |
| if verbose or not hit1: | |
| print( | |
| f"{qid:>3}. {query:<33} {exp_name:^12} {top1_name:^12} " | |
| f"{rank_str:^6} {ok_mark}" | |
| ) | |
| results.append({ | |
| "id": qid, "query": query, "exp_id": exp_id, | |
| "rank": rank, "hit1": hit1, "hit3": hit3, "hit5": hit5, | |
| "top1": top1_name, "category": category, | |
| }) | |
| cat = category.split("-")[0] | |
| cat_stats[cat]["total"] += 1 | |
| if hit1: cat_stats[cat]["hit1"] += 1 | |
| if hit3: cat_stats[cat]["hit3"] += 1 | |
| if hit5: cat_stats[cat]["hit5"] += 1 | |
| # ── 전체 지표 ── | |
| n = len(results) | |
| acc1 = sum(r["hit1"] for r in results) / n * 100 | |
| acc3 = sum(r["hit3"] for r in results) / n * 100 | |
| acc5 = sum(r["hit5"] for r in results) / n * 100 | |
| mrr_sum = sum(1 / r["rank"] for r in results if r["rank"]) | |
| mrr = mrr_sum / n * 100 | |
| print(f"\n{'='*70}") | |
| print(f" 총 {n}개 쿼리") | |
| print(f" Acc@1 = {acc1:5.1f}% ({sum(r['hit1'] for r in results)}/{n})") | |
| print(f" Acc@3 = {acc3:5.1f}% ({sum(r['hit3'] for r in results)}/{n})") | |
| print(f" Acc@5 = {acc5:5.1f}% ({sum(r['hit5'] for r in results)}/{n})") | |
| print(f" MRR = {mrr:5.1f}%") | |
| # ── 카테고리별 ── | |
| print(f"\n{'카테고리':<18} {'Acc@1':>7} {'Acc@3':>7} {'Acc@5':>7} {'건수':>5}") | |
| print("-" * 50) | |
| for cat, s in sorted(cat_stats.items()): | |
| t = s["total"] | |
| print( | |
| f" {cat:<16} " | |
| f"{s['hit1']/t*100:5.0f}% " | |
| f"{s['hit3']/t*100:5.0f}% " | |
| f"{s['hit5']/t*100:5.0f}% " | |
| f"{t:4}건" | |
| ) | |
| # ── 틀린 케이스 요약 (verbose 아닐 때) ── | |
| if not verbose: | |
| missed = [r for r in results if not r["hit1"]] | |
| if missed: | |
| print(f"\n❌ 1위 불일치 {len(missed)}건:") | |
| for r in missed: | |
| print(f" {r['id']:>3}. {r['query'][:30]:<30} → 1위:{r['top1']} (정답:{r['exp_id']} {r['rank']}위)") | |
| return acc1, acc3, acc5, mrr | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--verbose", action="store_true", help="전체 케이스 출력") | |
| args = parser.parse_args() | |
| run_eval(verbose=args.verbose) | |