hpce-dev / scripts /build_intent_positions_embedding.py
이동현
엔진 구조 통일 + 레이어 Config 아키텍처(Model 2)
13f78b2
Raw
History Blame Contribute Delete
4.86 kB
#!/usr/bin/env python3
"""
intent_positions.json (임베딩 + UMAP 기반) 산출 스크립트.
L1 grid 백업과 같은 형식으로 산출하므로 산출 후 덮어쓰면
서버·클라이언트 코드 변경 없이 시연 화면이 새 좌표로 동작.
사전 설치 필요:
pip install sentence-transformers umap-learn
실행:
cd roadshow-server-v3
python scripts/build_intent_positions_embedding.py
옵션:
--model sentence-transformer 모델 이름 (기본: jhgan/ko-sroberta-multitask)
--neighbors UMAP n_neighbors (기본: 15)
--min-dist UMAP min_dist (기본: 0.15)
--seed UMAP random_state (기본: 42)
"""
import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from core.engines import config
SCENARIO_ID = "cs-myk-v3"
SCENARIO_DIR = Path(__file__).parent.parent / "scenarios" / SCENARIO_ID
L1_COLORS = {
"INT-1000": "#3b82f6",
"INT-2000": "#10b981",
"INT-3000": "#eab308",
"INT-4000": "#a855f7",
"INT-5000": "#ef4444",
"INT-6000": "#f97316",
"INT-7000": "#1f2937",
}
def _normalize_to_range(values, target_min=-1.0, target_max=1.0):
import numpy as np
v = np.asarray(values, dtype=float)
vmin, vmax = v.min(axis=0), v.max(axis=0)
span = (vmax - vmin)
span[span == 0] = 1.0
return (v - vmin) / span * (target_max - target_min) + target_min
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="jhgan/ko-sroberta-multitask")
parser.add_argument("--neighbors", type=int, default=15)
parser.add_argument("--min-dist", type=float, default=0.15)
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args()
try:
import numpy as np
from sentence_transformers import SentenceTransformer
import umap
except ImportError as e:
raise SystemExit(
"Missing dependencies. Install with:\n"
" pip install sentence-transformers umap-learn\n"
f"({e})"
)
intents_data = config.get_taxonomy(SCENARIO_ID)
intents = intents_data["intents"] if isinstance(intents_data, dict) else intents_data
print(f"Loaded {len(intents)} intents")
# 1. 임베딩
print(f"Loading model: {args.model}")
model = SentenceTransformer(args.model)
texts = [f"{it['name']} ({it['L1_name']} > {it['L2_name']})" for it in intents]
print(f"Encoding {len(texts)} texts ...")
emb = model.encode(texts, normalize_embeddings=True, show_progress_bar=True)
print(f"Embeddings shape: {emb.shape}")
# 2. UMAP 차원 축소
print(f"UMAP: n_neighbors={args.neighbors}, min_dist={args.min_dist}")
reducer = umap.UMAP(
n_components=2,
n_neighbors=args.neighbors,
min_dist=args.min_dist,
metric="cosine",
random_state=args.seed,
)
coords = reducer.fit_transform(emb)
coords_norm = _normalize_to_range(coords, -1.0, 1.0)
# 3. payload 구성
intent_positions = [
{
"intent_id": it["id"],
"L1_id": it["L1_id"],
"x": round(float(coords_norm[i, 0]), 4),
"y": round(float(coords_norm[i, 1]), 4),
}
for i, it in enumerate(intents)
]
# L1 zone centroid (= 같은 L1 점들의 평균)
from collections import defaultdict
by_l1: dict[str, list[tuple[float, float]]] = defaultdict(list)
for p in intent_positions:
by_l1[p["L1_id"]].append((p["x"], p["y"]))
l1_zones = []
seen_l1_names = {it["L1_id"]: it["L1_name"] for it in intents}
for l1_id, pts in by_l1.items():
cx = round(sum(x for x, _ in pts) / len(pts), 4)
cy = round(sum(y for _, y in pts) / len(pts), 4)
l1_zones.append({
"L1_id": l1_id,
"L1_name": seen_l1_names.get(l1_id, l1_id),
"centroid": {"x": cx, "y": cy},
"color": L1_COLORS.get(l1_id, "#94a3b8"),
})
payload = {
"scenario_id": "cs-myk-v3",
"embedding_model": args.model,
"reducer": "umap",
"reducer_params": {
"n_neighbors": args.neighbors,
"min_dist": args.min_dist,
"metric": "cosine",
"random_state": args.seed,
},
"coord_range": [-1, 1],
"generated_at": datetime.utcnow().isoformat() + "Z",
"intents": intent_positions,
"l1_zones": l1_zones,
}
out_path = SCENARIO_DIR / "intent_positions.json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print(f"Wrote {out_path}")
if __name__ == "__main__":
main()