o2o-negosium-original/agent/tools/probe_serving_dqn.py
jwkim d4acdd0ac5 [feat] agent: 카드 선택 DQN 서빙 전환 (action-as-feature)
- 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>
2026-07-10 16:56:24 +09:00

62 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()