| from __future__ import annotations |
| """ |
| μ§μ₯μΈ(worker-v3) λΆν¬ 건μ μ± + νλ₯΄μλ μ μ¬λ κΈ°λ° off-persona νκ°. |
| |
| νλ₯΄μλ κΈ°λμΉ(expected_intents)μ μμ‘΄νμ§ μλ 보쑰 μ§ν (sim_healthμ workerν). |
| worker νλμ λ¨μΌμ ν μ±(app_open + entity, app_seqs)μ΄λΌ bundleμ _behavior_mapμ΄ λΆνμ. |
| |
| [A] λΆν¬ 건μ μ± : top-1 λ€μμ±(HHI)Β·μ κ·ν μνΈλ‘νΌΒ·saturation(raw>0.9) |
| [B] μ€λ¬Έ λ―Όκ°λ : λ΅λ³ 1κ° λ³κ²½ μ top-k λ³ν(turnover) |
| [C] νλ₯΄μλ μ μ¬λ: νλ₯΄μλ νλ³Έ self-match(λΆλ¦¬λ) + off-persona μ΅κ·Όμ μ μ¬λ |
| [D] νλ λ°©ν₯μ± : off-personaκ° 'νλ₯΄μλ Pμ μ±'μ μ°λ©΄ λΆν¬κ° P μͺ½μΌλ‘ μ΄λνλ |
| [E] νλ μλ΅μ± : 무μμ νλ‘ν à 무μμ μ± β κ·Έ μ±μ μ νΈ intent μμ μμΉ/Top-5 μ§μ
|
| |
| μ€ν: python scripts/sim_worker_health.py |
| """ |
| import math |
| import random |
| import sys |
| from collections import Counter |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent)) |
|
|
| from core.engines import config |
| from core.extractor import get_extractor |
| from core.inference import infer_batch, infer_with_behavior, to_probability_dict |
| from scripts.build_worker_dataset import PERSONAS |
|
|
| SID = "worker-v3" |
| _INTENTS = [i["id"] for i in config.get_taxonomy(SID)["intents"]] |
| _IDX = {iid: i for i, iid in enumerate(_INTENTS)} |
| _NAMES = {i["id"]: i.get("name", i["id"]) for i in config.get_taxonomy(SID)["intents"]} |
| _TOPK = min(5, len(_INTENTS)) |
| _EXT = get_extractor() |
|
|
|
|
| def _rand_answers(rng: random.Random) -> dict[str, str]: |
| """λͺ¨λ λ¬Ένμ μ΅μ
μ€ κ· λ± λ¬΄μμλ‘ (νλ₯΄μλ 무κ΄).""" |
| return {q["id"]: rng.choice([o["code"] for o in q["options"]]) |
| for q in config.get_survey(SID)["questions"]} |
|
|
|
|
| def _sample_answers(dist: dict, rng: random.Random) -> dict[str, str]: |
| return {qid: rng.choices(list(d), weights=list(d.values()), k=1)[0] for qid, d in dist.items()} |
|
|
|
|
| def _apply_apps(sess: str, seq: list[str]) -> None: |
| _EXT.reset(sess) |
| for ent in seq: |
| _EXT.add_event(sess, "app_open", ent) |
|
|
|
|
| def _prob_vec(answers: dict, seq: list[str] | None = None, tag: str = "") -> np.ndarray: |
| """μ΄κΈ°(seq=None) λλ νλ λ°μ λΆν¬μ N-μ°¨μ νλ₯ 벑ν°.""" |
| if seq: |
| sess = f"__wh_{tag}" |
| _apply_apps(sess, seq) |
| _, scores = infer_with_behavior(answers, sess, SID) |
| _EXT.reset(sess) |
| else: |
| _, scores = infer_batch(answers, SID) |
| v = np.zeros(len(_INTENTS)) |
| for iid, d in to_probability_dict(scores, scenario_id=SID).items(): |
| v[_IDX[iid]] = d["p"] |
| return v |
|
|
|
|
| def _cos(a: np.ndarray, b: np.ndarray) -> float: |
| return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-12)) |
|
|
|
|
| |
| def health(m: int = 150, seed: int = 3) -> None: |
| rng = random.Random(seed) |
| top1 = Counter() |
| ents, sats = [], [] |
| for _ in range(m): |
| ans = _rand_answers(rng) |
| _, scores = infer_batch(ans, SID) |
| v = np.zeros(len(_INTENTS)) |
| for iid, d in to_probability_dict(scores, scenario_id=SID).items(): |
| v[_IDX[iid]] = d["p"] |
| top1[_INTENTS[int(np.argmax(v))]] += 1 |
| p = v[v > 0] |
| ents.append(-(p * np.log(p)).sum() / math.log(len(_INTENTS))) |
| sats.append(sum(1 for s in scores if s.final_score > 0.9) / len(scores)) |
| hhi = sum((c / m) ** 2 for c in top1.values()) |
| mc = top1.most_common(1)[0] |
| print("ββ [A] λΆν¬ 건μ μ± (무μμ %d) ββ" % m) |
| print(f" top-1 λ€μμ±: μλ‘ λ€λ₯Έ top1 intent {len(top1)}/{len(_INTENTS)}μ’
, HHI={hhi:.3f} " |
| f"(μ΅λΉ top1 {mc[0]} {mc[1]/m*100:.0f}%)") |
| print(f" μ κ·ν μνΈλ‘νΌ νκ· : {np.mean(ents):.3f} (0=μ€νμ΄ν¬ 1=νν)") |
| print(f" saturation(raw>0.9) νκ· : {np.mean(sats)*100:.1f}%") |
|
|
|
|
| |
| def sensitivity(m: int = 60, seed: int = 5) -> None: |
| rng = random.Random(seed) |
| survey = config.get_survey(SID)["questions"] |
| turn = [] |
| for _ in range(m): |
| ans = _rand_answers(rng) |
| _, base = infer_batch(ans, SID) |
| base_top = {s.intent_id for s in sorted(base, key=lambda s: s.final_score, reverse=True)[:_TOPK]} |
| q = rng.choice(survey) |
| alts = [o["code"] for o in q["options"] if o["code"] != ans[q["id"]]] |
| if not alts: |
| continue |
| a2 = dict(ans); a2[q["id"]] = rng.choice(alts) |
| _, sc2 = infer_batch(a2, SID) |
| top2 = {s.intent_id for s in sorted(sc2, key=lambda s: s.final_score, reverse=True)[:_TOPK]} |
| jac = len(base_top & top2) / len(base_top | top2) |
| turn.append(1 - jac) |
| print("\nββ [B] μ€λ¬Έ λ―Όκ°λ (λ΅λ³ 1κ° λ³κ²½ μ top-%d λ³ν) ββ" % _TOPK) |
| print(f" νκ· turnover: {np.mean(turn)*100:.1f}% (0=λκ° / 100=κ³Όλ―Ό, μ λΉν λ°μμ±μ΄ 건μ )") |
|
|
|
|
| |
| def _persona_refs(k: int = 25, seed: int = 9): |
| rng = random.Random(seed) |
| init, beh = {}, {} |
| for p in PERSONAS: |
| vs_i, vs_b = [], [] |
| for j in range(k): |
| a = _sample_answers(p["answer_dist"], rng) |
| vs_i.append(_prob_vec(a)) |
| vs_b.append(_prob_vec(a, rng.choice(p["app_seqs"]), tag=f"{p['id']}{j}")) |
| init[p["id"]] = np.mean(vs_i, axis=0) |
| beh[p["id"]] = np.mean(vs_b, axis=0) |
| return init, beh |
|
|
|
|
| |
| def persona_similarity(init_ref: dict, k: int = 25, m: int = 120, seed: int = 13) -> None: |
| rng = random.Random(seed) |
| pids = [p["id"] for p in PERSONAS] |
| hit = 0; tot = 0 |
| for p in PERSONAS: |
| for _ in range(k): |
| v = _prob_vec(_sample_answers(p["answer_dist"], rng)) |
| nearest = max(pids, key=lambda q: _cos(v, init_ref[q])) |
| hit += (nearest == p["id"]); tot += 1 |
| print("\nββ [C] νλ₯΄μλ μ μ¬λ ββ") |
| print(f" κ²μ¦(λΆλ¦¬λ): νλ₯΄μλ νλ³Έμ μ΅κ·Όμ =μκΈ°μμ {hit/tot*100:.1f}%") |
| sims = [] |
| near_cnt = Counter() |
| for _ in range(m): |
| v = _prob_vec(_rand_answers(rng)) |
| scored = sorted(((q, _cos(v, init_ref[q])) for q in pids), key=lambda x: -x[1]) |
| sims.append(scored[0][1]); near_cnt[scored[0][0]] += 1 |
| print(f" off-persona μ΄κΈ°λΆν¬μ μ΅κ·Όμ νλ₯΄μλ μ μ¬λ: νκ· {np.mean(sims):.2f}, " |
| f"μ€μ {np.median(sims):.2f} (β₯0.7 λΉμ¨ {np.mean([s>=0.7 for s in sims])*100:.0f}%)") |
| print(f" β μμ μλ΅μ΄ νλ₯΄μλ κ³΅κ° μμ μ¬μλ¨. μ΅κ·Όμ λΆν¬: " |
| + ", ".join(f"{q}:{near_cnt[q]}" for q in pids)) |
|
|
|
|
| |
| def behavior_direction(beh_ref: dict, m: int = 80, seed: int = 21) -> None: |
| rng = random.Random(seed) |
| moved = 0; tot = 0 |
| for _ in range(m): |
| ans = _rand_answers(rng) |
| p = rng.choice(PERSONAS) |
| v0 = _prob_vec(ans) |
| v1 = _prob_vec(ans, rng.choice(p["app_seqs"]), tag=f"d{tot}") |
| before = _cos(v0, beh_ref[p["id"]]) |
| after = _cos(v1, beh_ref[p["id"]]) |
| moved += (after > before); tot += 1 |
| print("\nββ [D] νλ λ³ν λ°©ν₯μ± ββ") |
| print(f" off-persona μλ΅μ΄ 'νλ₯΄μλ Pμ μ±'μ μ°λ©΄ λΆν¬κ° P μͺ½μΌλ‘ μ΄λ: {moved/tot*100:.1f}%") |
| print(" (νλμ΄ κΈ°λν μλ λ³νλ₯Ό λ§λ λ€λ νλ₯΄μλ-λ νΌλ°μ€ κΈ°λ° κ²μ¦)") |
|
|
|
|
| |
| def behavior_responsiveness(k: int = 300, seed: int = 11) -> None: |
| rng = random.Random(seed) |
| sig = config.get_behavior_signals(SID) |
| apps = [en for en in sig if sig.get(en)] |
| rose, in_top5, total = 0, 0, 0 |
| for i in range(k): |
| ans = _rand_answers(rng) |
| en = rng.choice(apps) |
| targets = sig[en] |
| sess = f"__woff_{i}" |
| _apply_apps(sess, [en]) |
| _, scores = infer_with_behavior(ans, sess, SID) |
| _EXT.reset(sess) |
| smap = {s.intent_id: s for s in scores} |
| for t in targets: |
| s = smap.get(t) |
| if s is None: |
| continue |
| total += 1 |
| rose += (s.rank_change > 0) |
| in_top5 += (s.rank <= 5) |
| print("\nββ [E] νλ μλ΅μ± (무μμ νλ‘ν à 무μμ μ±, k=%d) ββ" % k) |
| print(f" μ° μ±μ μ νΈ intentκ° baseline λλΉ μμ μμΉ: {rose/total*100:.1f}%") |
| print(f" μ° μ±μ μ νΈ intentκ° Top-5 μ§μ
: {in_top5/total*100:.1f}%") |
|
|
|
|
| if __name__ == "__main__": |
| health() |
| sensitivity() |
| init_ref, beh_ref = _persona_refs() |
| persona_similarity(init_ref) |
| behavior_direction(beh_ref) |
| behavior_responsiveness() |
|
|