hpce-dev / core /inference.py
์ด๋™ํ˜„
[DOCS] ๋ฐฑ์—”๋“œ ์ „๋ฐ˜ docstring ๋ณด๊ฐ• (Google ์Šคํƒ€์ผ Args/Returns/Raises)
58debbc
Raw
History Blame Contribute Delete
18.6 kB
from __future__ import annotations
"""Intent Inference ํ†ตํ•ฉ.
ํ๋ฆ„:
- infer_batch(survey) : Batch Feature๋งŒ์œผ๋กœ baseline Intent Score ์‚ฐ์ถœ
- infer_with_behavior : Batch + Behavioral Pattern Feature๋ฅผ ํ•ฉ์ณ ์žฌ์ถ”๋ก 
(boost ๋ˆ„์  ๋ฐฉ์‹์ด ์•„๋‹ˆ๋ผ, ๋ชจ๋“  ํ”ผ์ฒ˜๋ฅผ ์ž…๋ ฅ์œผ๋กœ ๋‹ค์‹œ ๋ชจ๋ธ/๋ฃฐ์„ ํ†ต๊ณผ์‹œํ‚ด)
์‚ฐ์ถœ๋˜๋Š” IntentScore๋Š” baseline ๋Œ€๋น„ ๋ณ€ํ™”๋Ÿ‰(delta_score, rank_change)์„ ํ•จ๊ป˜ ๋ณด๊ด€ํ•œ๋‹ค.
"""
import logging
import math
from dataclasses import dataclass
from typing import Any
# Softmax ๋ถ„ํฌ sharpness ์กฐ์ ˆ. ์ž‘์„์ˆ˜๋ก Top ์ ์ˆ˜์— ๋ถ„ํฌ๊ฐ€ ์ง‘์ค‘.
# T=0.15 โ†’ ์‹œ์—ฐ ์ž„ํŒฉํŠธ(์ƒ์œ„ Intent ๊ฐ•์กฐ) ์šฐ์„ . Top 5๊ฐ€ ๋ถ„ํฌ์˜ ~60% ์ ์œ , ํ–‰๋™ 1๋ฒˆ์— ฮ”p ์ˆ˜%p
PROBABILITY_TEMPERATURE = 0.15
from config import settings
from core.engines import get_engine, config
from core.engines.base import ScenarioEngine
from core.extractor import get_extractor
logger = logging.getLogger(__name__)
# ํ–‰๋™ โ†’ ์ง์ ‘ ์‹ ํ˜ธ Intent ๋ถ€์ŠคํŠธ. (์‹œ๋‚˜๋ฆฌ์˜ค config L2_inference.ranker.action_signal๋กœ override)
# Rule pattern_boost๊ฐ€ ์ผ๋ถ€ ํ–‰๋™๋งŒ ์ปค๋ฒ„ํ•˜๋Š” ํ•œ๊ณ„๋ฅผ ๋ณด์™„ํ•ด, behaviors.json์˜ ๋ชจ๋“  ํ–‰๋™์ด
# ์˜๋ฏธ์ƒ ์—ฐ๊ฒฐ๋œ Intent๋ฅผ ๋Œ์–ด์˜ฌ๋ฆฌ๋„๋ก ํ•œ๋‹ค. (final ์žฌ์ถ”๋ก ์—๋งŒ ์ ์šฉ; baseline์€ batch๋งŒ)
# ์•„๋ž˜ ๊ฐ’์€ scenario_id ๋ฏธ์ง€์ •/๋ˆ„๋ฝ ์‹œ fallback ๊ธฐ๋ณธ๊ฐ’.
ACTION_SIGNAL_SCALE = 0.28 # ์ตœ์‹  ํ–‰๋™ 1ํšŒ๋‹น ๊ฐ€์‚ฐ (weight=1.0 ๊ธฐ์ค€)
ACTION_SIGNAL_CAP = 0.55 # ํ–‰๋™ ๋ฐ˜๋ณต ์‹œ ์ƒํ•œ
ACTION_SIGNAL_DECAY = 0.6 # ์œ„์น˜ ๊ธฐ๋ฐ˜ recency ๊ฐ์‡  (์ตœ์‹  age=0 โ†’ 1.0, ์ง์ „ 0.6, ๊ทธ์ „ 0.36 โ€ฆ)
def _resolve_temperature(scenario_id: str | None) -> float:
"""์‹œ๋‚˜๋ฆฌ์˜ค config์˜ softmax ์˜จ๋„๋ฅผ ์กฐํšŒํ•œ๋‹ค.
Args:
scenario_id: ์กฐํšŒํ•  ์‹œ๋‚˜๋ฆฌ์˜ค ID. None์ด๋ฉด ๋ชจ๋“ˆ ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.
Returns:
softmax ์˜จ๋„. config ๋ˆ„๋ฝ ์‹œ ๋ชจ๋“ˆ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ fallbackํ•œ๋‹ค.
"""
if scenario_id is None:
return PROBABILITY_TEMPERATURE
try:
return config.get_probability_temperature(scenario_id)
except (KeyError, FileNotFoundError):
return PROBABILITY_TEMPERATURE
def _resolve_action_signal(scenario_id: str | None) -> tuple[float, float, float]:
"""์‹œ๋‚˜๋ฆฌ์˜ค config์˜ ํ–‰๋™ ๋ถ€์ŠคํŠธ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์กฐํšŒํ•œ๋‹ค.
Args:
scenario_id: ์กฐํšŒํ•  ์‹œ๋‚˜๋ฆฌ์˜ค ID. None์ด๋ฉด ๋ชจ๋“ˆ ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.
Returns:
(scale, cap, decay) ํŠœํ”Œ. config ๋ˆ„๋ฝ ์‹œ ๋ชจ๋“ˆ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ fallbackํ•œ๋‹ค.
"""
if scenario_id is not None:
try:
sig = config.get_action_signal(scenario_id)
return sig["scale"], sig["cap"], sig["decay"]
except (KeyError, FileNotFoundError):
pass
return ACTION_SIGNAL_SCALE, ACTION_SIGNAL_CAP, ACTION_SIGNAL_DECAY
def _resolve_boost_mode(scenario_id: str | None) -> str:
"""ํ–‰๋™ boost ํ•ฉ์„ฑ ๋ฐฉ์‹์„ ์กฐํšŒํ•œ๋‹ค.
config L2.ranker.action_signal.boost_mode ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.
Args:
scenario_id: ์กฐํšŒํ•  ์‹œ๋‚˜๋ฆฌ์˜ค ID. None์ด๋ฉด ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.
Returns:
ํ•ฉ์„ฑ ๋ฐฉ์‹ ๋ฌธ์ž์—ด. ๋ˆ„๋ฝ ์‹œ 'additive'๋กœ fallbackํ•œ๋‹ค.
"""
if scenario_id is not None:
try:
return config.get_action_signal(scenario_id).get("boost_mode", "additive")
except (KeyError, FileNotFoundError):
pass
return "additive"
def _resolve_action_suppress(scenario_id: str | None) -> dict | None:
"""ํ–‰๋™ ๊ธฐ๋ฐ˜ ์˜๋„ ๊ฐ์‡  ์„ค์ •์„ ์กฐํšŒํ•œ๋‹ค.
config L2.ranker.action_signal.suppress ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.
ํ˜•์‹: {"by_entity": {entity: [๊ฐ์‡  ๋Œ€์ƒ intent_id, ...]}, "scale": float, "cap": float}.
์˜ˆ) ํšŒ๋ณต ํ–‰๋™(mental_recovery/exercise) ๋ˆ„์  ์‹œ ๋ฒˆ์•„์›ƒ ์‹ฌํ™” intent๋ฅผ ์ ์ง„ ๊ฐ์‡ .
Args:
scenario_id: ์กฐํšŒํ•  ์‹œ๋‚˜๋ฆฌ์˜ค ID. None์ด๋ฉด None์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
Returns:
๊ฐ์‡  ์„ค์ • dict. ๋ฏธ์„ค์ • ์‹œ None.
"""
if scenario_id is not None:
try:
return config.get_action_signal(scenario_id).get("suppress")
except (KeyError, FileNotFoundError):
pass
return None
def _action_intent_signals(
events: list[dict],
behavior_map: dict[str, list[str]],
decay: float = ACTION_SIGNAL_DECAY,
) -> dict[str, float]:
"""์„ธ์…˜ ๋ˆ„์  ํ–‰๋™์„ entityโ†’intent ๋งคํ•‘์œผ๋กœ ์˜๋„๋ณ„ ๊ฐ€์ค‘ ์‹ ํ˜ธ๋กœ ํ™˜์‚ฐํ•œ๋‹ค.
- recency decay: ์ตœ์‹  ํ–‰๋™์ผ์ˆ˜๋ก ํฐ weight (DECAY^age). ๋ฐฉ๊ธˆ ํ•œ ํ–‰๋™์ด ํ˜„์žฌ ์˜๋„๋ฅผ ์ฃผ๋„ํ•˜๋˜,
๊ฐ™์€ ํ–‰๋™ ๋ฐ˜๋ณต์€ ๋ˆ„์ ๋˜์–ด ๊ฐ•ํ•ด์ง„๋‹ค(๊ณผ๊ฑฐ๋„ 0์œผ๋กœ ์ฃฝ์ด์ง„ ์•Š์Œ).
- BACK(navigate_back)์€ ๋ฉ”๋‰ด ๋ณต๊ท€์šฉ ์ˆœ์ˆ˜ ๋‚ด๋น„๊ฒŒ์ด์…˜ โ†’ ์‹ ํ˜ธยทaging ๋ชจ๋‘์—์„œ ์ œ์™ธ(๋ฌดํšจ๊ณผ).
์„น์…˜์„ ๋– ๋‚œ ํ–‰๋™์€ ์ดํ›„ ๋‹ค๋ฅธ ํ–‰๋™์ด ์Œ“์ด๋ฉฐ decay๋กœ ์ž์—ฐ ์†Œ๋ฉธํ•œ๋‹ค.
Args:
events: ์„ธ์…˜์— ๋ˆ„์ ๋œ ํ–‰๋™ ์ด๋ฒคํŠธ ๋ฆฌ์ŠคํŠธ.
behavior_map: entity โ†’ intent_id ๋ฆฌ์ŠคํŠธ ๋งคํ•‘.
decay: ์œ„์น˜ ๊ธฐ๋ฐ˜ recency ๊ฐ์‡  ๊ณ„์ˆ˜.
Returns:
intent_id โ†’ ๊ฐ€์ค‘ ์‹ ํ˜ธ ํ•ฉ ๋งคํ•‘.
"""
real = [ev for ev in events if ev.get("event_type") != "navigate_back"]
n = len(real)
weights: dict[str, float] = {}
for i, ev in enumerate(real):
age = (n - 1) - i
w = decay ** age
for iid in behavior_map.get(ev.get("entity", ""), []):
weights[iid] = weights.get(iid, 0.0) + w
return weights
@dataclass
class IntentScore:
"""๋‹จ์ผ intent์˜ baselineยทfinal ์ ์ˆ˜์™€ ์ˆœ์œ„ ๋ณ€ํ™”(ํ–‰๋™ ๋ฐ˜์˜ ์ „ํ›„)."""
intent_id: str
intent_name: str
L1_id: str
L1_name: str
L2_id: str
L2_name: str
inference_type: str
baseline_score: float # Batch Feature๋งŒ์œผ๋กœ ์ถ”๋ก ํ•œ ์ ์ˆ˜
final_score: float # Batch + Behavioral Pattern Feature ํ•ฉ์ณ ์žฌ์ถ”๋ก ํ•œ ์ ์ˆ˜
delta_score: float # final - baseline
baseline_rank: int # baseline ๊ธฐ์ค€ rank
rank: int # final_score ๊ธฐ์ค€ rank
rank_change: int # baseline_rank - rank (์–‘์ˆ˜๋ฉด ์ƒ์Šน)
def infer_batch(
survey_answers: dict[str, str],
scenario_id: str = settings.SCENARIO_ID,
) -> tuple[dict[str, Any], list[IntentScore]]:
"""์„ค๋ฌธ ๋‹ต๋ณ€๋งŒ์œผ๋กœ baseline Intent Score๋ฅผ ์‚ฐ์ถœํ•œ๋‹ค (ํ–‰๋™ ๋ฐ˜์˜ ์ „).
Args:
survey_answers: ์งˆ๋ฌธ ID โ†’ ์„ ํƒ ์‘๋‹ต ์ฝ”๋“œ ๋งคํ•‘.
scenario_id: ์ถ”๋ก ์— ์‚ฌ์šฉํ•  ์‹œ๋‚˜๋ฆฌ์˜ค ID.
Returns:
(batch_features, scores) ํŠœํ”Œ. batch_features๋Š” ์‚ฐ์ถœ๋œ Batch Feature,
scores๋Š” final ์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ IntentScore ๋ฆฌ์ŠคํŠธ.
"""
engine = get_engine(scenario_id)
batch_features = engine.build_batch_features(survey_answers)
all_features = {
**batch_features,
**engine.empty_pattern_features(),
**engine.empty_event_features(),
}
raw = _score_all(all_features, engine)
scores = _to_intent_scores(raw, raw, engine)
return batch_features, scores
def infer_with_behavior(
survey_answers: dict[str, str],
session_id: str,
scenario_id: str = settings.SCENARIO_ID,
) -> tuple[dict[str, Any], list[IntentScore]]:
"""Batch + ๋ˆ„์  Pattern + ์ตœ์‹  Event Feature๋ฅผ ํ•ฉ์ณ ์žฌ์ถ”๋ก ํ•œ๋‹ค.
baseline(ํ–‰๋™ ์—†๋Š” ์ƒํƒœ) ์ ์ˆ˜๋ฅผ ํ•จ๊ป˜ ์‚ฐ์ถœํ•ด delta_score / rank_change๋ฅผ ์ฑ„์šด๋‹ค.
Args:
survey_answers: ์งˆ๋ฌธ ID โ†’ ์„ ํƒ ์‘๋‹ต ์ฝ”๋“œ ๋งคํ•‘.
session_id: ๋ˆ„์  ํ–‰๋™ ์ด๋ฒคํŠธ๋ฅผ ์กฐํšŒํ•  ์„ธ์…˜ ID.
scenario_id: ์ถ”๋ก ์— ์‚ฌ์šฉํ•  ์‹œ๋‚˜๋ฆฌ์˜ค ID.
Returns:
(batch_features, scores) ํŠœํ”Œ. scores๋Š” baseline ๋Œ€๋น„ ๋ธํƒ€ยท์ˆœ์œ„๋ณ€ํ™”๊ฐ€ ์ฑ„์›Œ์ง„
IntentScore ๋ฆฌ์ŠคํŠธ๋กœ, final ์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ๋‹ค.
"""
engine = get_engine(scenario_id)
batch_features = engine.build_batch_features(survey_answers)
# baseline: Pattern/Event Feature๋ฅผ 0์œผ๋กœ ๋‘” ์ƒํƒœ
baseline_features = {
**batch_features,
**engine.empty_pattern_features(),
**engine.empty_event_features(),
}
baseline_raw = _score_all(baseline_features, engine)
# final: ์‹ค์ œ ๋ˆ„์  Pattern + ์ตœ์‹  Event Feature ๋ฐ˜์˜ (์—”์ง„ ์ „์šฉ ๊ณ„์‚ฐ)
pattern_features = engine.pattern_features(session_id)
event_features = engine.event_features(session_id)
events = get_extractor()._events_by_session.get(session_id, [])
combined_features = {**batch_features, **pattern_features, **event_features}
final_raw = _score_all(combined_features, engine)
# ํ–‰๋™์ด ์ง์ ‘ ๊ฐ€๋ฆฌํ‚ค๋Š” Intent๋ฅผ ๋Œ์–ด์˜ฌ๋ฆผ (final ์—๋งŒ ์ ์šฉ โ†’ ฮ”ยทrank_change๊ฐ€ ํ–‰๋™์— ๊ท€์†)
scale, cap, decay = _resolve_action_signal(scenario_id)
boost_mode = _resolve_boost_mode(scenario_id) # "additive"(๊ธฐ๋ณธ) | "headroom"
behavior_map = engine.behavior_intent_map()
for iid, cnt in _action_intent_signals(events, behavior_map, decay=decay).items():
if iid in final_raw:
boost = min(cnt * scale, cap)
if boost_mode == "headroom":
# ํ—ค๋“œ๋ฃธ ๋น„๋ก€ ๊ฐ€์‚ฐ: ์ฒœ์žฅ(0.97) ๋‹ค์ค‘ ๋™์  ๋ฐฉ์ง€ + base ์ˆœ์„œ ๋ณด์กด
final_raw[iid] = final_raw[iid] + boost * (1.0 - final_raw[iid])
else:
final_raw[iid] = min(final_raw[iid] + boost, 0.97)
# ํ–‰๋™ ๊ธฐ๋ฐ˜ ๊ฐ์‡ : ํŠน์ • ํ–‰๋™(์˜ˆ: ํšŒ๋ณต) ๋ˆ„์  ์‹œ ๋Œ€์ƒ intent(์˜ˆ: ๋ฒˆ์•„์›ƒ ์‹ฌํ™”)๋ฅผ ์ ์ง„ ๊ฐ์‡ .
# ๊ฐ€์‚ฐ boost์™€ ๋Œ€์นญ โ€” ๊ฐ™์€ recency decay ๊ฐ€์ค‘์„ penalty๋กœ ํ™˜์‚ฐํ•ด ์ ์ˆ˜๋ฅผ ๋น„์œจ ์ถ•์†Œ.
suppress = _resolve_action_suppress(scenario_id)
if suppress:
sup_scale = suppress.get("scale", 0.0)
sup_cap = suppress.get("cap", 1.0)
sup_map = suppress.get("by_entity", {})
for iid, cnt in _action_intent_signals(events, sup_map, decay=decay).items():
if iid in final_raw:
penalty = min(cnt * sup_scale, sup_cap)
final_raw[iid] = final_raw[iid] * (1.0 - penalty)
scores = _to_intent_scores(baseline_raw, final_raw, engine)
return batch_features, scores
def _score_all(features: dict[str, Any], engine: ScenarioEngine) -> dict[str, float]:
"""๋ชจ๋“  Intent์— ๋Œ€ํ•ด ์ ์ˆ˜๋งŒ ์‚ฐ์ถœํ•œ๋‹ค.
inference_type์— ๋”ฐ๋ผ rule/model๋กœ ๋ถ„๊ธฐํ•œ๋‹ค.
Args:
features: ์ถ”๋ก  ์ž…๋ ฅ feature ๋งคํ•‘.
engine: ์‹œ๋‚˜๋ฆฌ์˜ค ์—”์ง„.
Returns:
intent_id โ†’ score ๋งคํ•‘.
"""
f = dict(features)
if isinstance(f.get("๊ฒฐํ•ฉ ์—ฌ๋ถ€"), bool):
f["๊ฒฐํ•ฉ ์—ฌ๋ถ€"] = 1 if f["๊ฒฐํ•ฉ ์—ฌ๋ถ€"] else 0
out: dict[str, float] = {}
for intent in engine.intents():
iid = intent["id"]
if intent["inference_type"] == "Model":
score = engine.model_predict(iid, f)
else:
score = engine.rule_predict(iid, f)
out[iid] = float(score)
return out
def _rank_map(raw: dict[str, float]) -> dict[str, int]:
"""raw ์ ์ˆ˜๋ฅผ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ˆœ์œ„๋กœ ํ™˜์‚ฐํ•œ๋‹ค.
Args:
raw: intent_id โ†’ score ๋งคํ•‘.
Returns:
intent_id โ†’ ์ˆœ์œ„(1-๊ธฐ๋ฐ˜) ๋งคํ•‘.
"""
ordered = sorted(raw.items(), key=lambda kv: kv[1], reverse=True)
return {iid: i for i, (iid, _) in enumerate(ordered, start=1)}
def _to_intent_scores(
baseline_raw: dict[str, float],
final_raw: dict[str, float],
engine: ScenarioEngine,
) -> list[IntentScore]:
"""baselineยทfinal raw ์ ์ˆ˜๋ฅผ IntentScore ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
๋ธํƒ€ยท์ˆœ์œ„๋ณ€ํ™”๋ฅผ ์ฑ„์šฐ๊ณ  final ์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌํ•œ๋‹ค.
Args:
baseline_raw: baseline intent_id โ†’ score ๋งคํ•‘.
final_raw: final intent_id โ†’ score ๋งคํ•‘.
engine: ์‹œ๋‚˜๋ฆฌ์˜ค ์—”์ง„.
Returns:
final ์ ์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ IntentScore ๋ฆฌ์ŠคํŠธ.
"""
baseline_ranks = _rank_map(baseline_raw)
final_ranks = _rank_map(final_raw)
results: list[IntentScore] = []
for intent in engine.intents():
iid = intent["id"]
b = baseline_raw.get(iid, 0.0)
f = final_raw.get(iid, 0.0)
br = baseline_ranks.get(iid, 0)
fr = final_ranks.get(iid, 0)
results.append(IntentScore(
intent_id=iid,
intent_name=intent["name"],
L1_id=intent["L1_id"],
L1_name=intent["L1_name"],
L2_id=intent["L2_id"],
L2_name=intent["L2_name"],
inference_type=intent["inference_type"],
baseline_score=round(b, 4),
final_score=round(f, 4),
delta_score=round(f - b, 4),
baseline_rank=br,
rank=fr,
rank_change=br - fr,
))
results.sort(key=lambda s: s.final_score, reverse=True)
return results
# โ”€โ”€ WebSocket ํŽ˜์ด๋กœ๋“œ ํ—ฌํผ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _softmax(values: list[float], temperature: float) -> list[float]:
"""์ˆ˜์น˜์ ์œผ๋กœ ์•ˆ์ •์ ์ธ softmax๋กœ raw score๋ฅผ ์ •๊ทœํ™” ํ™•๋ฅ  ๋ถ„ํฌ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
Args:
values: ์ •๊ทœํ™”ํ•  raw score ๋ฆฌ์ŠคํŠธ.
temperature: softmax ์˜จ๋„. ์ž‘์„์ˆ˜๋ก ์ƒ์œ„ ๊ฐ’์— ๋ถ„ํฌ๊ฐ€ ์ง‘์ค‘๋œ๋‹ค.
Returns:
ํ•ฉ์ด 1์ด ๋˜๋Š” ์ •๊ทœํ™” ํ™•๋ฅ  ๋ฆฌ์ŠคํŠธ. ์ž…๋ ฅ์ด ๋น„๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ.
"""
if not values:
return []
scaled = [v / temperature for v in values]
m = max(scaled)
exps = [math.exp(s - m) for s in scaled]
total = sum(exps) or 1.0
return [e / total for e in exps]
def to_probability_dict(
scores: list[IntentScore],
scenario_id: str | None = None,
temperature: float | None = None,
) -> dict[str, dict[str, float]]:
"""Intent raw score๋ฅผ softmax ์ •๊ทœํ™” ํ™•๋ฅ (p)๊ณผ baseline ํ™•๋ฅ (p0)๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
raw score ํ•ฉ ๋ถ„๋ชจ ์ •๊ทœํ™”๋Š” ๋ถ„ํฌ๊ฐ€ ๋„ˆ๋ฌด ํ‰ํƒ„ํ•˜๋ฏ€๋กœ softmax(score / T) ๋ถ„ํฌ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
T๊ฐ€ ์ž‘์„์ˆ˜๋ก ์ƒ์œ„ Intent์— ๋ถ„ํฌ๊ฐ€ ์ง‘์ค‘๋œ๋‹ค.
T๋Š” scenario_id์˜ config(L2_inference.calibrator)์—์„œ ์กฐํšŒํ•œ๋‹ค(๋ฏธ์ง€์ • ์‹œ ๋ชจ๋“ˆ ๊ธฐ๋ณธ๊ฐ’).
INTENT_UPDATE ํŽ˜์ด๋กœ๋“œ์˜ `all_probabilities` ํ•„๋“œ์— ์‚ฌ์šฉ๋œ๋‹ค.
Args:
scores: ํ™•๋ฅ ๋กœ ๋ณ€ํ™˜ํ•  IntentScore ๋ฆฌ์ŠคํŠธ.
scenario_id: ์˜จ๋„ ์กฐํšŒ์— ์‚ฌ์šฉํ•  ์‹œ๋‚˜๋ฆฌ์˜ค ID. None์ด๋ฉด ๋ชจ๋“ˆ ๊ธฐ๋ณธ๊ฐ’.
temperature: softmax ์˜จ๋„ ์ง์ ‘ ์ง€์ •. None์ด๋ฉด scenario_id๋กœ ์กฐํšŒํ•œ๋‹ค.
Returns:
intent_id โ†’ {"p": final ํ™•๋ฅ , "p0": baseline ํ™•๋ฅ } ๋งคํ•‘.
"""
if temperature is None:
temperature = _resolve_temperature(scenario_id)
p_vals = _softmax([s.final_score for s in scores], temperature)
p0_vals = _softmax([s.baseline_score for s in scores], temperature)
return {
s.intent_id: {
"p": round(p_vals[i], 6),
"p0": round(p0_vals[i], 6),
}
for i, s in enumerate(scores)
}
def to_topn_with_others(
scores: list[IntentScore],
top_n: int = 5,
scenario_id: str | None = None,
) -> tuple[list[dict], dict]:
"""Top-N + ๊ธฐํƒ€(others) ํŽ˜์ด๋กœ๋“œ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค.
Args:
scores: ํ™•๋ฅ ๋กœ ๋ณ€ํ™˜ํ•  IntentScore ๋ฆฌ์ŠคํŠธ.
top_n: ์ƒ์œ„๋กœ ๋…ธ์ถœํ•  intent ๊ฐœ์ˆ˜.
scenario_id: ์˜จ๋„ ์กฐํšŒ์— ์‚ฌ์šฉํ•  ์‹œ๋‚˜๋ฆฌ์˜ค ID. None์ด๋ฉด ๋ชจ๋“ˆ ๊ธฐ๋ณธ๊ฐ’.
Returns:
(top_list, others) ํŠœํ”Œ.
top_list: [ {intent_id, ..., probability, baseline_probability, delta_probability} ]
others: { count, probability, baseline_probability, delta_probability }
"""
probs = to_probability_dict(scores, scenario_id=scenario_id)
sorted_scores = sorted(scores, key=lambda s: s.final_score, reverse=True)
top_items: list[dict] = []
for s in sorted_scores[:top_n]:
pr = probs[s.intent_id]
top_items.append({
"intent_id": s.intent_id,
"intent_nm_ko": s.intent_name,
"L1_id": s.L1_id,
"L1_name": s.L1_name,
"L2_id": s.L2_id,
"L2_name": s.L2_name,
"inference_type": s.inference_type,
"rank": s.rank,
"baseline_rank": s.baseline_rank,
"rank_change": s.rank_change,
"probability": pr["p"],
"baseline_probability": pr["p0"],
"delta_probability": round(pr["p"] - pr["p0"], 6),
})
rest = sorted_scores[top_n:]
others_p = sum(probs[s.intent_id]["p"] for s in rest)
others_p0 = sum(probs[s.intent_id]["p0"] for s in rest)
others = {
"count": len(rest),
"probability": round(others_p, 6),
"baseline_probability": round(others_p0, 6),
"delta_probability": round(others_p - others_p0, 6),
}
return top_items, others
# โ”€โ”€ Customer Context JSON ์ƒ์„ฑ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def to_customer_context_json(
session_id: str,
stage: str,
scenario_id: str,
scores: list[IntentScore],
batch_features: dict[str, Any],
) -> dict[str, Any]:
"""์„ธ์…˜ยท๋‹จ๊ณ„ยท์ „์ฒด intent ์ ์ˆ˜๋ฅผ Customer Context JSON์œผ๋กœ ์ง๋ ฌํ™”ํ•œ๋‹ค.
์ €์žฅ/์กฐํšŒ์šฉ ํŽ˜์ด๋กœ๋“œ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค.
Args:
session_id: ์„ธ์…˜ ID.
stage: ์ถ”๋ก  ๋‹จ๊ณ„.
scenario_id: ์‹œ๋‚˜๋ฆฌ์˜ค ID.
scores: ์ง๋ ฌํ™”ํ•  IntentScore ๋ฆฌ์ŠคํŠธ.
batch_features: ์‚ฐ์ถœ๋œ Batch Feature ๋งคํ•‘.
Returns:
session/scenario/stage/intents ํ‚ค๋ฅผ ๊ฐ€์ง„ Customer Context JSON dict.
"""
intents = []
for s in scores:
intents.append({
"intent_id": s.intent_id,
"intent_nm_ko": s.intent_name,
"L1": {"id": s.L1_id, "name": s.L1_name},
"L2": {"id": s.L2_id, "name": s.L2_name},
"baseline_score": s.baseline_score,
"final_score": s.final_score,
"delta_score": s.delta_score,
"baseline_rank": s.baseline_rank,
"rank": s.rank,
"rank_change": s.rank_change,
"inference_type": s.inference_type,
})
return {
"session_id": session_id,
"scenario_id": scenario_id,
"stage": stage,
"intents": intents,
}