hpce-dev / scripts /sim_worker_health.py
μ΄λ™ν˜„
[FEAT] worker W2(μ†Œμ§„ν˜•) μ„€λ¬Έ 뢄리 + 건전성/off-persona 평가 μΆ”κ°€
65ace42
Raw
History Blame Contribute Delete
9.44 kB
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 # noqa: E402
from core.extractor import get_extractor # noqa: E402
from core.inference import infer_batch, infer_with_behavior, to_probability_dict # noqa: E402
from scripts.build_worker_dataset import PERSONAS # noqa: E402
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))
# ── [A] 뢄포 건전성 ─────────────────────────────────────────────
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}%")
# ── [B] μ„€λ¬Έ 민감도 ─────────────────────────────────────────────
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
# ── [C] 페λ₯΄μ†Œλ‚˜ μœ μ‚¬λ„: 검증 + off-persona λ§€ν•‘ ────────────────
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))
# ── [D] 행동 λ³€ν™” λ°©ν–₯μ„± ────────────────────────────────────────
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(" (행동이 κΈ°λŒ€ν•œ μ˜λ„ λ³€ν™”λ₯Ό λ§Œλ“ λ‹€λŠ” 페λ₯΄μ†Œλ‚˜-레퍼런슀 기반 검증)")
# ── [E] 행동 응닡성 (λ¬΄μž‘μœ„ ν”„λ‘œν•„ Γ— λ¬΄μž‘μœ„ μ•±) ─────────────────
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()