- feature_dqn_policy: ScoreNet(상태+카드특징 → 점수) 학습 정책 (replay+타깃넷) - feature_builder: 이산화 없는 연속 상태 벡터(9) + 테넌트 성향 벡터(5) - dqn_store: numpy 전용 서빙(컨테이너 PyTorch 불필요), DQN_SERVING 플래그, 미지원 테넌트는 Q-table 자동 폴백 - 파이프라인: build_card_embeddings -> train_feature_dqn -> export_dqn_serving(npz) - retrain_from_logs: 실로그 재학습 + OPE(SNIPS) 게이트, 통과 시에만 번들 교체(.prev 백업) - probe_serving_dqn / compare_qtable_vs_dqn: 배포 전 행동 점검 도구 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
45 lines
2.4 KiB
Python
45 lines
2.4 KiB
Python
"""build_state_features — snapshot(raw 연속값) → 실수 벡터 (DQN/action-as-feature 용).
|
|
|
|
state_calculator.build_state(이산화)와 대비되는 연속 표현. 이산화(등급/162칸)를 하지 않고
|
|
정규화된 raw 값을 그대로 벡터로 내보낸다. 협력사 특징(매출·경쟁사수·유통)이 벡터에 포함되므로
|
|
'협력사를 입력으로'(Phase 3)가 자연스럽게 달성된다.
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
from negotiation.qtable.domain.model.snapshot import NegotiationSnapshot
|
|
|
|
DIST_CLASSES = ("A", "B", "C")
|
|
STATE_FEATURE_DIM = 9 # build_state_features 벡터 길이. feature 추가 시 갱신.
|
|
TENANT_FEATURE_DIM = 5 # build_tenant_features 벡터 길이.
|
|
|
|
|
|
def build_tenant_features(reward_cfg) -> np.ndarray:
|
|
"""고객사 '성향'을 ID 가 아니라 보상 설정값(내용)으로 벡터화 (Phase 3 고객사 조건화).
|
|
|
|
새 고객사도 tenant.yaml 의 reward 설정만 있으면 즉시 조건화된다 (cold-start 없음).
|
|
"""
|
|
return np.array([
|
|
reward_cfg.max_weight, # 가격 중시 정도 (W↑ = 가격보상 비중↑)
|
|
reward_cfg.success_reward / 2.0, # 성사를 얼마나 크게 치는가
|
|
-reward_cfg.failure_penalty / 2.0, # 결렬을 얼마나 무서워하는가
|
|
reward_cfg.penalty_lambda * 20.0, # 속도 성향 (오래 끌수록 벌점)
|
|
reward_cfg.beta, # 앵커 초과달성 보너스 성향
|
|
], dtype=np.float32)
|
|
|
|
|
|
def build_state_features(s: NegotiationSnapshot) -> np.ndarray:
|
|
"""정규화된 연속 상태 벡터. 등급화 없음 — 990원과 850원이 구별된다."""
|
|
dist_onehot = [1.0 if s.distribution_code == c else 0.0 for c in DIST_CLASSES]
|
|
anchor = max(s.anchor_price, 1.0)
|
|
target = max(s.target_price, 1.0)
|
|
return np.array([
|
|
min(s.revenue_amount, 5e8) / 5e8, # 협력사 매출 (0~1)
|
|
*dist_onehot, # 유통 A/B/C
|
|
min(s.partner_count, 5) / 5.0, # 대안 협력사 수 (BATNA)
|
|
float(np.clip(s.acceptance_ratio, 0.0, 1.0)), # 수용률
|
|
float(np.clip((s.input_price - anchor) / anchor, -1.0, 2.0)), # 앵커 대비 격차 (연속!)
|
|
float(np.clip((target - s.input_price) / target, -2.0, 1.0)), # 목표 대비 여유
|
|
min(s.round_number, 10) / 10.0, # 라운드
|
|
], dtype=np.float32)
|