File size: 5,576 Bytes
a248495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97cf40f
 
 
a248495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97cf40f
a248495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97cf40f
a248495
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/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()