- 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>
62 lines
2.9 KiB
Python
62 lines
2.9 KiB
Python
"""probe_serving_dqn — 서빙 번들(dqn_serving.npz)의 상황별 카드 선택 프로브.
|
||
|
||
배포된 모델이 '상황에 맞게' 고르는지 눈으로 확인하는 진단 도구:
|
||
협력사 세그먼트 × 고객사 성향 × 협상 국면(가격대)별 선택 카드를 표로 출력한다.
|
||
전부 다르길 기대하는 게 아니라, 축을 바꿨을 때 선택이 '움직이는지'를 본다.
|
||
|
||
실행: APP_ENV=local python -m tools.probe_serving_dqn (numpy 만 필요, DB 불필요)
|
||
"""
|
||
|
||
import numpy as np
|
||
|
||
from negotiation.qtable.domain.model.snapshot import NegotiationSnapshot
|
||
from negotiation.qtable.domain.service.feature_builder import build_state_features, build_tenant_features
|
||
from tenancy.config_loader import TenantConfigLoader
|
||
from tools.export_dqn_serving import OUT_PATH
|
||
from tools.retrain_from_logs import np_scorer_from_bundle
|
||
from tools.train_feature_dqn import pref_config
|
||
|
||
ANCHOR, TARGET = 495_000.0, 500_000.0 # BUGCHECK 견적과 동일 스케일
|
||
|
||
SUPPLIERS = {
|
||
"소형·경쟁多": dict(revenue_amount=5_000_000, partner_count=3, distribution_code="A"),
|
||
"소형·단독": dict(revenue_amount=5_000_000, partner_count=1, distribution_code="A"),
|
||
"대형·경쟁多": dict(revenue_amount=200_000_000, partner_count=3, distribution_code="A"),
|
||
"대형·단독": dict(revenue_amount=200_000_000, partner_count=1, distribution_code="A"),
|
||
}
|
||
PHASES = { # (라운드, 제시가): 첫턴 높은 가격 / 중반 목표가 근접 / 막판 앵커존 직전
|
||
"첫턴(575k)": (1, 575_000.0),
|
||
"중반(510k)": (2, 510_000.0),
|
||
"막판(501k)": (3, 501_000.0),
|
||
}
|
||
PREFS = {"성사중시": 0.1, "가격중시": 0.9}
|
||
|
||
|
||
def main():
|
||
score = np_scorer_from_bundle(OUT_PATH)
|
||
z = np.load(OUT_PATH, allow_pickle=False)
|
||
numbers = [str(n) for n in z["card_numbers"]]
|
||
feats = z["card_feats"]
|
||
base = TenantConfigLoader().load("ktcommerce").reward
|
||
|
||
for phase, (turn, price) in PHASES.items():
|
||
print(f"\n=== {phase} (앵커 {int(ANCHOR):,} / 목표 {int(TARGET):,}) ===")
|
||
print(f"{'협력사':<12}" + "".join(f"{p:>16}" for p in PREFS))
|
||
for sup_name, sup in SUPPLIERS.items():
|
||
row = []
|
||
for _, p in PREFS.items():
|
||
tf = build_tenant_features(pref_config(base, p))
|
||
snap = NegotiationSnapshot(
|
||
revenue_amount=sup["revenue_amount"], distribution_code=sup["distribution_code"],
|
||
partner_count=sup["partner_count"],
|
||
acceptance_ratio=max(0.0, (575_000.0 - price) / 575_000.0),
|
||
input_price=price, anchor_price=ANCHOR, target_price=TARGET, round_number=turn,
|
||
)
|
||
sf = np.concatenate([build_state_features(snap), tf])
|
||
row.append(numbers[int(np.argmax(score(sf, feats)))])
|
||
print(f"{sup_name:<12}" + "".join(f"{c:>16}" for c in row))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|