AI_Menu_Search / scripts /eval_search.py
Juhaha
HF Spaces 데모 배포 (Streamlit + Qdrant 임베디드, 색인 빌드타임 생성)
fbd1091
Raw
History Blame Contribute Delete
4.66 kB
"""
검색 품질 정량 평가 스크립트
실행: 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)