hpce-dev / scripts /analyze_rule_validation.py
μ΄λ™ν˜„
[REFACTOR] Step F: GenericEngine 수렴 (Phase 4 μ™„μ£Ό)
97cf40f
Raw
History Blame Contribute Delete
5.58 kB
#!/usr/bin/env python3
"""
가상 μ‹œλ“œ 데이터셋(seed_dataset.json)으둜 Rule 좜λ ₯의 합리성 검증.
μ‚°μΆœ:
1. Rule 좜λ ₯ vs μ–‘μ„± 라벨: ROC-like 톡계 (μ–‘μ„± 평균 score vs λΉ„μ–‘μ„± 평균 score)
2. 페λ₯΄μ†Œλ‚˜λ³„ Top 5 Rule Intent 뢄포 β€” μ˜λ„λœ Intentκ°€ μƒμœ„μ— μ˜€λŠ”μ§€
3. Rule이 μ •μ˜λœ Intent와 baseline(0.05)만 λ°˜ν™˜ν•˜λŠ” Intent λΉ„μœ¨
μ‹€ν–‰:
cd roadshow-server-v3
python scripts/analyze_rule_validation.py
"""
from __future__ import annotations
import json
import sys
from collections import Counter, defaultdict
from pathlib import Path
from statistics import mean
sys.path.insert(0, str(Path(__file__).parent.parent))
from data.executor import init_db, get_executor # noqa: E402
from data.seed import load_intents_catalog # noqa: E402
from core.engines import config, formula # noqa: E402
_CS_RULE_SPEC = config.get_rule_spec("cs-myk-v3")
def _load_dataset(path: Path) -> list[dict]:
with open(path, encoding="utf-8") as f:
return json.load(f)["samples"]
def _score_with_rules(features: dict, intents: list[dict]) -> dict[str, float]:
f = dict(features)
if isinstance(f.get("κ²°ν•© μ—¬λΆ€"), bool):
f["κ²°ν•© μ—¬λΆ€"] = 1 if f["κ²°ν•© μ—¬λΆ€"] else 0
out = {}
for intent in intents:
if intent["inference_type"] != "Rule":
continue
out[intent["id"]] = formula.rule_predict(_CS_RULE_SPEC, intent["id"], f)
return out
def main() -> None:
init_db()
intents = load_intents_catalog()
intent_meta = {i["id"]: i for i in intents}
rule_intent_ids = [i["id"] for i in intents if i["inference_type"] == "Rule"]
dataset_path = Path(__file__).parent.parent / "scenarios" / "cs-myk-v3" / "seed_dataset.json"
samples = _load_dataset(dataset_path)
# ── 1. μ–‘μ„±/λΉ„μ–‘μ„± 평균 score 비ꡐ ────────────────────────
print("=" * 72)
print(" Rule 검증: μ–‘μ„± 라벨 vs λΉ„μ–‘μ„± 라벨의 평균 Rule score")
print("=" * 72)
pos_scores: dict[str, list[float]] = defaultdict(list)
neg_scores: dict[str, list[float]] = defaultdict(list)
for s in samples:
scores = _score_with_rules(s["batch_features"], intents)
positives = set(s["intent_labels"].keys())
for iid, sc in scores.items():
if iid in positives:
pos_scores[iid].append(sc)
else:
neg_scores[iid].append(sc)
# μ–‘μ„± λΉˆλ„κ°€ μΆ©λΆ„ν•œ Intent만
rows = []
for iid in rule_intent_ids:
pos = pos_scores.get(iid, [])
neg = neg_scores.get(iid, [])
if len(pos) < 5:
continue
pos_mean = mean(pos)
neg_mean = mean(neg) if neg else 0.0
rows.append((iid, len(pos), pos_mean, neg_mean, pos_mean - neg_mean))
rows.sort(key=lambda r: r[4], reverse=True)
print(f"\n {'Intent':10s} {'n(+)':>5s} {'pos_avg':>8s} {'neg_avg':>8s} {'gap':>7s} L3")
print(f" {'-'*10} {'-'*5} {'-'*8} {'-'*8} {'-'*7} {'-'*30}")
good, bad = 0, 0
for iid, n_pos, p, n, gap in rows[:30]:
name = intent_meta[iid]["name"]
flag = "βœ“" if gap > 0.05 else ("Β·" if gap > 0 else "βœ—")
if gap > 0.05: good += 1
elif gap <= 0: bad += 1
print(f" {iid:10s} {n_pos:>5d} {p:>8.3f} {n:>8.3f} {gap:>+7.3f} {flag} {name}")
print(f"\n (전체 {len(rows)}개 쀑) Rule이 양성을 잘 ꡬ뢄: {good}개, 양성↔비양성 격차 λΆ€μ‘± λ˜λŠ” μ—­μ „: {bad}개")
# ── 2. 페λ₯΄μ†Œλ‚˜λ³„ Rule Top 5 μΌμΉ˜λ„ ───────────────────────
print("\n" + "=" * 72)
print(" 페λ₯΄μ†Œλ‚˜λ³„ Rule Top 5 Intent β€” μ˜λ„λœ Intentκ°€ μƒμœ„μ— μ˜€λŠ”κ°€?")
print("=" * 72)
persona_top: dict[str, Counter] = defaultdict(Counter)
persona_labels: dict[str, Counter] = defaultdict(Counter)
persona_hits: dict[str, list[int]] = defaultdict(list)
for s in samples:
scores = _score_with_rules(s["batch_features"], intents)
top5 = [iid for iid, _ in sorted(scores.items(), key=lambda kv: kv[1], reverse=True)[:5]]
positives = set(s["intent_labels"].keys())
hit = sum(1 for iid in top5 if iid in positives)
pid = s["persona_id"]
persona_hits[pid].append(hit)
for iid in top5:
persona_top[pid][iid] += 1
for iid in positives:
persona_labels[pid][iid] += 1
for pid in sorted(persona_top.keys()):
top_intents = persona_top[pid].most_common(5)
avg_hit = mean(persona_hits[pid]) if persona_hits[pid] else 0
top_labels = persona_labels[pid].most_common(5)
print(f"\n [{pid}] 평균 Top5 hit (μ–‘μ„±κ³Ό 일치): {avg_hit:.2f}/5")
print(f" Rule Top5 : {', '.join(f'{iid}({c})' for iid, c in top_intents)}")
print(f" μ–‘μ„± Top5 : {', '.join(f'{iid}({c})' for iid, c in top_labels)}")
# ── 3. Rule μ •μ˜ vs baseline-only 뢄포 ────────────────────
print("\n" + "=" * 72)
print(" Rule μ •μ˜ 톡계")
print("=" * 72)
defined = len(_CS_RULE_SPEC)
rule_n = len(rule_intent_ids)
print(f"\n Rule Intent 총: {rule_n} (Model μ œμ™Έ)")
print(f" λͺ…μ‹œμ  Rule ν•¨μˆ˜ μ •μ˜: {defined}개")
print(f" baseline(0.05)만 λ°˜ν™˜: {rule_n - defined}개")
print(f" β†’ ν–₯ν›„ ~{rule_n - defined}개 Intent에 λͺ…μ‹œμ  Rule μΆ”κ°€ λ˜λŠ” Model둜 μ „ν™˜ κ²€ν† ")
if __name__ == "__main__":
main()