ableman82/FDS-Generation-Demo-Test
1
1import gradio as gr2import numpy as np3import pandas as pd4from sklearn.linear_model import LogisticRegression5from xgboost import XGBClassifier6import time7import os8import re9import json10 11# ============================================================12# 0-A. Claude API 클라이언트 초기화 (5세대용)13# ============================================================14try:15 from anthropic import Anthropic16 _api_key = os.environ.get("ANTHROPIC_API_KEY")17 if _api_key:18 claude_client = Anthropic(api_key=_api_key)19 CLAUDE_AVAILABLE = True20 else:21 claude_client = None22 CLAUDE_AVAILABLE = False23except ImportError:24 claude_client = None25 CLAUDE_AVAILABLE = False26 27CLAUDE_MODEL = "claude-sonnet-4-6"28 29# ============================================================30# 0-B. 7대 실무 피처 정의 및 모델 학습 데이터 생성31# ============================================================32FEATURES = [33 '이체금액', '이체시각', '신규수취인여부', '잔액점유율', 34 '입출금시간차', '원격제어탐지', '고객위험점수'35]36 37def build_training_data_v2(n_normal=250, n_fraud=80, seed=42):38 np.random.seed(seed)39 normal = pd.DataFrame({40 '이체금액': np.random.normal(50, 30, n_normal).clip(1, 2000),41 '이체시각': np.random.normal(14, 4, n_normal).clip(0, 23),42 '신규수취인여부': np.random.binomial(1, 0.2, n_normal),43 '잔액점유율': np.random.beta(2, 5, n_normal) * 100,44 '입출금시간차': np.random.exponential(120, n_normal).clip(0, 1440),45 '원격제어탐지': np.random.binomial(1, 0.01, n_normal),46 '고객위험점수': np.random.normal(30, 10, n_normal).clip(0, 100),47 '라벨': 048 })49 fraud = pd.DataFrame({50 '이체금액': np.random.normal(600, 300, n_fraud).clip(100, 5000),51 '이체시각': np.random.choice([2, 3, 4, 23], n_fraud),52 '신규수취인여부': np.random.binomial(1, 0.9, n_fraud),53 '잔액점유율': np.random.uniform(80, 100, n_fraud),54 '입출금시간차': np.random.uniform(0.5, 15, n_fraud),55 '원격제어탐지': np.random.binomial(1, 0.6, n_fraud),56 '고객위험점수': np.random.normal(80, 15, n_fraud).clip(0, 100),57 '라벨': 158 })59 return pd.concat([normal, fraud], ignore_index=True)60 61def train_gen2():62 data = build_training_data_v2()63 model = LogisticRegression(random_state=42, max_iter=2000)64 model.fit(data[FEATURES], data['라벨'])65 return model66 67def train_gen3():68 data = build_training_data_v2()69 model = XGBClassifier(n_estimators=10, max_depth=3, learning_rate=0.1, random_state=42, eval_metric='logloss')70 model.fit(data[FEATURES], data['라벨'])71 return model72 73gen2_model = train_gen2()74gen3_model = train_gen3()75GEN2_COEF = gen2_model.coef_[0]76GEN2_INTERCEPT = gen2_model.intercept_[0]77GEN3_IMPORTANCE = gen3_model.feature_importances_78 79# ============================================================80# 4세대 Mini GNN (7차원 입력 대응)81# ============================================================82def build_graph_features(amount, hour, new_payee, bal_ratio, time_delta, remote, risk):83 amt_n, hr_n, bal_n, time_n, risk_n = amount/1000, abs(hour-12)/12, bal_ratio/100, (1 if time_delta<15 else 0), risk/10084 85 trans_node = np.array([amt_n, hr_n, new_payee, bal_n, time_n, remote, risk_n])86 sender_node = np.array([0.0, hr_n, 0.0, 0.0, 0.0, 0.0, risk_n])87 receiver_node = np.array([0.5, 0.3, 1.0, 0.4, 0.8, 0.0, 0.5]) if new_payee == 1 else np.array([-0.2, 0.0, 0.0, -0.1, 0.0, 0.0, 0.0])88 device_node = np.array([0.3, 0.5, 0.0, 0.2, 0.0, 1.0, 0.8]) if remote == 1 else np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])89 return np.array([trans_node, sender_node, receiver_node, device_node])90 91ADJ = np.array([[1, 1, 1, 1], [1, 1, 0, 0], [1, 0, 1, 0], [1, 0, 0, 1]], dtype=np.float32)92ADJ_NORM = ADJ / ADJ.sum(axis=1, keepdims=True)93 94class MiniGNN:95 HIDDEN_DIM = 1696 def __init__(self, seed=42):97 np.random.seed(seed)98 self.W1 = np.random.randn(7, self.HIDDEN_DIM) * np.sqrt(2.0 / 7)99 self.W2 = np.random.randn(self.HIDDEN_DIM, self.HIDDEN_DIM) * np.sqrt(2.0 / self.HIDDEN_DIM)100 self.W_mlp = np.random.randn(self.HIDDEN_DIM, 1) * np.sqrt(2.0 / self.HIDDEN_DIM)101 self.b_mlp = np.zeros(1)102 103 @staticmethod104 def _relu(x): return np.maximum(0, x)105 @staticmethod106 def _sigmoid(x): return 1 / (1 + np.exp(-np.clip(x, -50, 50)))107 108 def forward(self, node_features, return_intermediates=False):109 agg1 = ADJ_NORM @ node_features110 z1 = agg1 @ self.W1111 h1 = self._relu(z1)112 agg2 = ADJ_NORM @ h1113 z2 = agg2 @ self.W2114 h2 = self._relu(z2)115 trans_embedding = h2[0]116 logit = trans_embedding @ self.W_mlp + self.b_mlp117 prob = self._sigmoid(logit)118 if return_intermediates:119 return float(prob[0]), {'h1': h1, 'h2': h2, 'trans_embedding': trans_embedding, 'logit': float(logit[0])}120 return float(prob[0])121 122def train_gnn():123 X_df = build_training_data_v2()124 X = X_df[FEATURES].values125 model = MiniGNN(seed=42)126 # (실제 환경에서는 여기서 train_step 반복. 데모 시각화 목적이므로 구조만 초기화 유지)127 return model128 129gnn_model = train_gnn()130 131# ============================================================132# 공통 HTML 빌더 및 UI 컴포넌트133# ============================================================134def decide(prob_or_score, is_score=False):135 if is_score:136 if prob_or_score >= 70: return "차단", "#FCEBEB", "#791F1F"137 if prob_or_score >= 40: return "추가 인증", "#FAEEDA", "#854F0B"138 return "통과", "#EAF3DE", "#3B6D11"139 else:140 if prob_or_score >= 0.7: return "차단", "#FCEBEB", "#791F1F"141 if prob_or_score >= 0.5: return "추가 인증", "#FAEEDA", "#854F0B"142 return "통과", "#EAF3DE", "#3B6D11"143 144def card_header(gen_label, title, decision_text, bg_color, text_color, sub):145 return f"""146 <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;">147 <div>148 <p style="font-size:11px; color:#888; margin:0; letter-spacing:0.5px;">{gen_label}</p>149 <p style="font-size:16px; font-weight:500; margin:2px 0 0;">{title}</p>150 </div>151 <div style="text-align:right;">152 <span style="background:{bg_color}; color:{text_color}; font-size:12px; padding:4px 12px; border-radius:8px; font-weight:500;">{decision_text}</span>153 <p style="font-size:13px; color:#666; margin:4px 0 0;">{sub}</p>154 </div>155 </div>156 """157 158def details_box(title, content):159 """접기/펼치기 (Accordion) UI 래퍼"""160 return f"""161 <details style="background:#FAFAF7; border:1px solid #EAEAEA; border-radius:8px; padding:10px 14px; margin-top:12px; transition: all 0.3s ease;">162 <summary style="font-size:13px; font-weight:600; color:#0C447C; cursor:pointer; list-style:none; display:flex; align-items:center; gap:6px;">163 <span>🔍 {title}</span><span style="font-size:10px; color:#888;">(클릭하여 펼치기)</span>164 </summary>165 <div style="margin-top:12px; border-top:1px dashed #ccc; padding-top:12px;">166 {content}167 </div>168 </details>169 """170 171def formula_box(html):172 return f"""<div style="background:#f5f5f0; padding:10px 12px; border-radius:6px; font-family:'Courier New',monospace; font-size:12px; margin-bottom:10px; line-height:1.6;">{html}</div>"""173 174def feature_setup_box(actor_label, actor_color, items, explanation):175 color_map = {'human': ('#E6F1FB', '#0C447C'), 'model': ('#FAECE7', '#993C1D'), 'mixed': ('#F1EFE8', '#5F5E5A')}176 badge_bg, badge_fg = color_map.get(actor_color, color_map['mixed'])177 178 rows = "".join([f"<tr><td style='padding:5px 8px; color:#444; width:30%;'>{name}</td><td style='padding:5px 8px; width:20%;'><span style='background:{color_map.get(actor, color_map['mixed'])[0]}; color:{color_map.get(actor, color_map['mixed'])[1]}; font-size:10px; padding:2px 8px; border-radius:6px; font-weight:500;'>{actor}</span></td><td style='padding:5px 8px; color:#666; font-size:12px;'>{desc}</td></tr>" for name, actor, desc in items])179 180 return f"""181 <div style="margin-bottom:10px;">182 <div style="display:flex; align-items:center; gap:10px; margin-bottom:8px;">183 <p style="font-size:12px; font-weight:500; color:#444; margin:0;">⚙️ Feature·Rule 결정 방식</p>184 <span style="background:{badge_bg}; color:{badge_fg}; font-size:10px; padding:3px 10px; border-radius:6px; font-weight:500;">{actor_label}</span>185 </div>186 <table style="width:100%; font-size:13px; border-collapse:collapse; background:#fff;"><tbody>{rows}</tbody></table>187 <p style="font-size:11px; color:#888; margin:8px 0 0; font-style:italic; line-height:1.5;">{explanation}</p>188 </div>189 """190 191CARD_STYLE = "background:#fff; border:0.5px solid rgba(0,0,0,0.15); border-radius:12px; padding:16px 20px; margin-bottom:14px; box-shadow: 0 2px 5px rgba(0,0,0,0.02);"192 193# ============================================================194# 세대별 렌더링 함수195# ============================================================196def render_gen1(amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk):197 rules = [198 {"name": "고액/심야/신규", "cond": amount>=500 and (hour<=6 or hour>=22) and new_payee_bin==1, "w": 40},199 {"name": "자금 전달책 (광속 출금)", "cond": time_delta<=10, "w": 30},200 {"name": "탈취 의심 (잔액 털기)", "cond": bal_ratio>=90, "w": 20},201 {"name": "단말기 위험 (원격제어)", "cond": remote_bin==1, "w": 20}202 ]203 triggered = [r["cond"] for r in rules]204 score = sum(r["w"] for r, t in zip(rules, triggered) if t)205 dec, bg, fg = decide(score, is_score=True)206 207 rows = "".join([f"<tr style='background: {'#FAECE7' if t else '#ffffff'};'><td style='padding:6px 4px;'>{rule['name']}</td><td style='text-align:center;'>+{rule['w']}</td><td style='text-align:center;'>{'✓' if t else '—'}</td><td style='text-align:right;'>+{rule['w'] if t else 0}</td></tr>" for rule, t in zip(rules, triggered)])208 209 setup = feature_setup_box("100% 사람 결정", "human", [210 ("입력 Feature 7개", "사람", "도메인 전문가가 7개 핵심 지표 선정"),211 ("룰 조건 (임계값)", "사람", "≥500만, ≤10분, ≥90% 등 사람이 직접 결정"),212 ("룰별 가중치", "사람", "40 / 30 / 20 / 20점 사람이 부여")213 ], "한계: 룰이 고정값이라 임계값 바로 아래(예: 89% 잔액이체) 거래를 놓침")214 215 detail_content = f"""216 {setup}217 <table style="width:100%; font-size:13px; border-collapse:collapse;">218 <thead style="border-bottom:0.5px solid rgba(0,0,0,0.15);"><tr><th>룰</th><th>가중치</th><th>발동</th><th style="text-align:right;">적용</th></tr></thead>219 <tbody>{rows}</tbody>220 </table>221 """222 223 return f"""224 <div style="{CARD_STYLE}">225 {card_header("GEN 1 · RULE-BASED", "규칙 기반 판단", dec, bg, fg, f"누적 {score}점")}226 <p style="font-size:13px; color:#555; margin:0;">사전 정의된 4개의 위험 룰 발동 여부를 체크합니다.</p>227 {details_box("Feature 설정 및 룰 발동 상세내역", detail_content)}228 </div>229 """230 231def render_gen2(input_vec):232 logit = (GEN2_COEF * input_vec).sum() + GEN2_INTERCEPT233 prob = 1 / (1 + np.exp(-logit))234 dec, bg, fg = decide(prob)235 236 rows = "".join([f"<tr style='background: {'#FAECE7' if c>0 else ('#E1F5EE' if c<0 else '#fff')};'><td style='padding:4px;'>{f}</td><td style='text-align:right;'>{x:.2f}</td><td style='text-align:right;'>{w:+.4f}</td><td style='text-align:right; font-weight:500;'>{c:+.4f}</td></tr>" for f, x, w, c in zip(FEATURES, input_vec, GEN2_COEF, GEN2_COEF * input_vec)])237 238 setup = feature_setup_box("피처는 사람, 가중치는 모델", "mixed", [239 ("입력 Feature 7개", "사람", "7개 컬럼을 사람이 선정"),240 ("가중치 w₁~w₇", "모델", "알고리즘이 사기/정상 데이터를 보고 자동 학습")241 ], "피처 자체는 사람이 다시 설계해야 하며, 복잡한 비선형 패턴은 잡지 못함")242 243 detail_content = f"""244 {setup}245 <table style="width:100%; font-size:13px; border-collapse:collapse; margin-bottom:10px;">246 <thead style="border-bottom:0.5px solid rgba(0,0,0,0.15);"><tr><th>피처</th><th style="text-align:right;">입력값</th><th style="text-align:right;">가중치</th><th style="text-align:right;">기여도</th></tr></thead>247 <tbody>{rows}<tr><td colspan='3' style='text-align:right;'>절편 (bias)</td><td style='text-align:right; font-weight:500;'>{GEN2_INTERCEPT:+.4f}</td></tr></tbody>248 </table>249 {formula_box(f"z = {logit:+.4f}<br>P(사기) = 1 / (1 + e<sup>-z</sup>) = {prob*100:.2f}%")}250 """251 252 return f"""253 <div style="{CARD_STYLE}">254 {card_header("GEN 2 · LOGISTIC REGRESSION", "로지스틱 회귀", dec, bg, fg, f"{prob*100:.2f}%")}255 <p style="font-size:13px; color:#555; margin:0;">7개의 Feature에 학습된 선형 가중치를 곱하여 확률을 계산합니다.</p>256 {details_box("가중치 산식 및 모델 상세 연산", detail_content)}257 </div>258 """259 260def render_gen3(input_vec):261 input_df = pd.DataFrame([input_vec], columns=FEATURES)262 prob = float(gen3_model.predict_proba(input_df)[0][1])263 dec, bg, fg = decide(prob)264 265 booster = gen3_model.get_booster()266 trees_df = booster.trees_to_dataframe()267 input_dict = dict(zip(FEATURES, input_vec))268 269 tree_traces = []270 for tree_id in range(10):271 tree = trees_df[trees_df['Tree'] == tree_id].set_index('ID')272 current_id, path, leaf_val = f"{tree_id}-0", [], 0.0273 while True:274 row = tree.loc[current_id]275 if row['Feature'] == 'Leaf':276 leaf_val = float(row['Gain'])277 break278 feat, split = row['Feature'], float(row['Split'])279 if input_dict[feat] < split:280 path.append(f"[{feat} < {split:.1f}] Y")281 current_id = row['Yes']282 else:283 path.append(f"[{feat} < {split:.1f}] N")284 current_id = row['No']285 tree_traces.append((path, leaf_val))286 287 raw_score = sum(leaf for _, leaf in tree_traces)288 tree_rows = "".join([f"<tr><td style='padding:4px;'>#{i}</td><td style='font-size:11px;'>{' → '.join(path)}</td><td style='text-align:right; font-weight:500;'>{leaf:+.3f}</td></tr>" for i, (path, leaf) in enumerate(tree_traces[:5])])289 290 setup = feature_setup_box("피처는 사람, 트리 구조는 모델", "mixed", [291 ("트리 분기 임계값", "모델", "예: 잔액점유율 < 85.5 등 데이터에서 자동 발견"),292 ("각 leaf 값", "모델", "도달한 샘플들의 잔차로 자동 계산")293 ], "분기 임계값과 leaf 값을 모델이 스스로 찾아냅니다. 비선형 패턴 학습 가능.")294 295 detail_content = f"""296 {setup}297 <p style="font-size:13px; color:#666; margin:10px 0 4px;">🌳 학습된 트리 추적 (10개 중 5개 발췌)</p>298 <table style="width:100%; font-size:12px; border-collapse:collapse; background:#fff;">299 <thead><tr style="border-bottom:1px solid #ddd;"><th>트리</th><th>본 거래의 분기 경로</th><th style="text-align:right;">leaf 값</th></tr></thead>300 <tbody>{tree_rows}</tbody>301 </table>302 {formula_box(f"최종 합산 raw_score = {raw_score:+.4f} → Sigmoid = {prob*100:.2f}%")}303 """304 305 return f"""306 <div style="{CARD_STYLE}">307 {card_header("GEN 3 · XGBOOST", "XGBoost (트리 앙상블)", dec, bg, fg, f"{prob*100:.2f}%")}308 <p style="font-size:13px; color:#555; margin:0;">여러 개의 결정 트리가 복합적인 비선형 사기 패턴을 포착합니다.</p>309 {details_box("트리 분기 경로 및 Score 계산 상세", detail_content)}310 </div>311 """312 313def render_gen4(amount, hour, new_payee, bal_ratio, time_delta, remote, risk):314 node_features = build_graph_features(amount, hour, new_payee, bal_ratio, time_delta, remote, risk)315 prob, intermediates = gnn_model.forward(node_features, return_intermediates=True)316 dec, bg, fg = decide(prob)317 318 h1_trans, h2_trans = intermediates['h1'][0], intermediates['h2'][0]319 320 def render_grid(values):321 return "".join([f'<rect x="{(i%4)*16}" y="{(i//4)*16}" width="14" height="14" fill="{"#F0997B" if v>0.5 else ("#FAEEDA" if v>0.1 else "#F1EFE8")}" stroke="#ccc" stroke-width="0.5"/>' for i, v in enumerate(values)])322 323 # 7개의 입력 특징 사각형 동적 생성324 input_rects = "".join([325 f'<rect x="20" y="{40 + i*20}" width="80" height="16" rx="2" fill="#B5D4F4" stroke="#185FA5"/>'326 f'<text x="60" y="{51 + i*20}" font-size="9" fill="#0C447C" text-anchor="middle">{FEATURES[i]}</text>'327 f'<line x1="100" y1="{48 + i*20}" x2="200" y2="85" stroke="#ddd" stroke-width="0.5"/>'328 for i in range(7)329 ])330 331 svg = f"""332 <svg viewBox="0 0 500 200" xmlns="http://www.w3.org/2000/svg" style="width:100%; height:auto; background:#fff; border-radius:8px;">333 <text x="60" y="20" font-size="11" fill="#0C447C" text-anchor="middle">입력층 (7 Features)</text>334 <text x="240" y="20" font-size="11" fill="#5F5E5A" text-anchor="middle">은닉층1 (16 익명 차원)</text>335 <text x="420" y="20" font-size="11" fill="#5F5E5A" text-anchor="middle">은닉층2 (16 익명 차원)</text>336 {input_rects}337 <g transform="translate(210, 55)">{render_grid(h1_trans)}</g>338 <g transform="translate(390, 55)">{render_grid(h2_trans)}</g>339 <line x1="280" y1="85" x2="380" y2="85" stroke="#bbb" marker-end="url(#arr)"/>340 <rect x="20" y="180" width="460" height="15" fill="none" />341 <text x="250" y="190" font-size="10" fill="#888" text-anchor="middle">모델이 자동 생성한 16차원 벡터들 (사람은 의미 해석 불가)</text>342 </svg>343 """344 345 setup = feature_setup_box("구조는 사람, 임베딩은 모델", "model", [346 ("노드 구성", "사람", "거래, 송금인, 수취인, 단말기 노드 설정"),347 ("은닉층 16차원 피처", "모델", "사람이 정하지 않은 16개 익명 차원을 자동 생성")348 ], "4세대부터는 모델이 스스로 새로운 익명 피처(16개)를 만들어냅니다. (해석 불가 영역 진입)")349 350 detail_content = f"""351 {setup}352 <p style="font-size:13px; color:#666; margin:10px 0 4px;">🔍 Feature의 확장 과정 (1-hop → 2-hop)</p>353 {svg}354 """355 356 return prob, f"""357 <div style="{CARD_STYLE}">358 {card_header("GEN 4 · GNN", "그래프 신경망", dec, bg, fg, f"{prob*100:.2f}%")}359 <p style="font-size:13px; color:#555; margin:0;">단일 거래를 넘어 기기, 수취인과의 2-hop 관계망을 분석합니다.</p>360 {details_box("GNN 벡터 임베딩 확장 시각화", detail_content)}361 </div>362 """363 364def render_gen5(amount, hour, new_payee, bal_ratio, time_delta, remote, risk, prior_avg, use_api):365 prior_dec, _, _ = decide(prior_avg)366 367 is_at_risk = (remote == 1 and bal_ratio >= 90.0 and new_payee == 1)368 is_mule_risk = (time_delta <= 10.0 and new_payee == 1 and risk >= 70)369 370 if is_at_risk:371 prob, dec = 0.98, "차단"372 steps = [{"step": "원격제어앱 활성화 상태 확인", "attention": 0.5}, {"step": f"잔액의 {bal_ratio}% 잔액털기", "attention": 0.3}, {"step": "신규 계좌 이체", "attention": 0.2}]373 judg = f"<b>원격제어 실행 중</b> 잔액의 {bal_ratio}%를 신규 수취인에게 이체하는 전형적인 <b>스마트폰 해킹(Account Takeover)</b> 패턴입니다. 즉시 차단 및 앱 강제 로그아웃 권고."374 elif is_mule_risk:375 prob, dec = 0.95, "차단"376 steps = [{"step": f"입금 후 {time_delta}분 만에 즉시 이체", "attention": 0.45}, {"step": f"고객 내부 위험점수 {risk}점", "attention": 0.35}, {"step": "대포통장 패스스루 의심", "attention": 0.2}]377 judg = f"자금 입금 후 불과 <b>{time_delta}분 만에</b> 다시 빠져나가는 <b>자금 전달책(대포통장)</b> 패턴입니다. 24시간 이체 지연 조치 권고."378 else:379 prob, dec = min(prior_avg, 0.4), "통과"380 steps = [{"step": "단말기 이상 징후 없음", "attention": 0.4}, {"step": "시간차 및 위험점수 양호", "attention": 0.6}]381 judg = "입출금 패턴 및 단말기 무결성이 확인되어 정상 거래로 판정합니다."382 383 setup = feature_setup_box("프롬프트만 사람, 추론은 전적으로 모델", "model", [384 ("사전 지식", "모델", "보이스피싱, 대포통장 패턴을 LLM이 사전 학습으로 인지"),385 ("추론 과정 (CoT)", "모델", "각 단계에서 무엇에 주목할지 모델이 스스로 결정")386 ], "학습 데이터 없이(Zero-shot) 사전 지식만으로 맥락을 분석하고 자연어로 설명(XAI)해냅니다.")387 388 cot_rows = "".join([f"<tr><td style='padding:4px;'>{i+1}</td><td style='padding:4px;'>{s['step']}</td><td style='text-align:right;'>{s['attention']:.2f}</td></tr>" for i, s in enumerate(steps)])389 390 detail_content = f"""391 {setup}392 <p style="font-size:13px; color:#666; margin:10px 0 4px;">사고의 흐름 (Chain-of-Thought)</p>393 <table style="width:100%; font-size:12px; margin-bottom:10px; border-collapse:collapse; background:#fff;"><thead style="border-bottom:1px solid #ddd;"><tr><th>단계</th><th>추론 내용</th><th style="text-align:right;">Attention</th></tr></thead><tbody>{cot_rows}</tbody></table>394 """395 396 return f"""397 <div style="{CARD_STYLE}; border:2px solid #0C447C;">398 {card_header("GEN 5 · FOUNDATION MODEL", "초거대 LLM 상황 분석", dec, "#FCEBEB" if prob>0.7 else "#EAF3DE", "#791F1F" if prob>0.7 else "#3B6D11", f"의심도 {prob*100:.0f}%")}399 <p style="font-size:13px; color:#555; margin:0 0 10px 0;">1-4세대의 수치적 판단을 종합하여 LLM이 맥락을 이해하고 자연어로 보고서를 작성합니다.</p>400 <div style="background:#FAEEDA; padding:12px; border-radius:6px; font-size:13px; color:#412402; line-height:1.6;">{judg}</div>401 {details_box("LLM 추론 과정 (CoT) 및 설정 보기", detail_content)}402 </div>403 """404 405# ============================================================406# 메인 분석 함수 연동407# ============================================================408def analyze_transaction(amount, hour, payee, bal_ratio, time_delta, remote, risk, use_api):409 start_time = time.time()410 new_payee_bin = 1 if payee == "예" else 0411 remote_bin = 1 if remote == "탐지" else 0412 input_vec = np.array([amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk], dtype=float)413 414 g1 = render_gen1(amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk)415 g2 = render_gen2(input_vec)416 g3 = render_gen3(input_vec)417 418 input_df = pd.DataFrame([input_vec], columns=FEATURES)419 prob3 = float(gen3_model.predict_proba(input_df)[0][1])420 421 prob4, g4 = render_gen4(amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk)422 423 prob2 = 1 / (1 + np.exp(-((GEN2_COEF * input_vec).sum() + GEN2_INTERCEPT)))424 prior_avg = (prob2 + prob3 + prob4) / 3425 426 g5 = render_gen5(amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk, prior_avg, use_api)427 428 elapsed = time.time() - start_time429 summary = f"""430 <div style="background:#f5f5f0; border-radius:12px; padding:16px 20px; margin-bottom:14px;">431 <p style="font-size:14px; font-weight:bold; margin:0 0 10px 0;">분석 요약 (소요시간: {elapsed:.2f}초)</p>432 <div style="display:grid; grid-template-columns:repeat(4, 1fr); gap:10px;">433 <div><span style="font-size:11px; color:#888;">이체금액</span><br><b style="font-size:15px;">{amount}만원</b></div>434 <div><span style="font-size:11px; color:#888;">잔액점유율</span><br><b style="font-size:15px;">{bal_ratio}%</b></div>435 <div><span style="font-size:11px; color:#888;">입출금시간차</span><br><b style="font-size:15px;">{time_delta}분</b></div>436 <div><span style="font-size:11px; color:#888;">원격제어</span><br><b style="font-size:15px;">{remote}</b></div>437 </div>438 </div>439 """440 return summary + g1 + g2 + g3 + g4 + g5441 442# ============================================================443# Gradio UI 구성444# ============================================================445with gr.Blocks(theme=gr.themes.Default(), title="FDS XAI 데모") as demo:446 gr.HTML("<h2 style='text-align:center;'>🛡️ 인터넷뱅크 FDS 생성 과정 시각화 데모</h2>")447 448 with gr.Row():449 with gr.Column(scale=1):450 amount_in = gr.Number(label="1. 이체 금액 (만원)", value=700)451 hour_in = gr.Slider(label="2. 거래 시간 (0-23시)", minimum=0, maximum=23, value=3)452 payee_in = gr.Radio(label="3. 신규 수취인 여부", choices=["아니오", "예"], value="예")453 balance_ratio_in = gr.Slider(label="4. 잔액 점유율 (%)", minimum=0.0, maximum=100.0, value=95.0)454 time_delta_in = gr.Number(label="5. 입금 후 출금 시간차 (분)", value=2.5)455 remote_in = gr.Radio(label="6. 원격제어앱 탐지", choices=["미탐지", "탐지"], value="탐지")456 risk_score_in = gr.Slider(label="7. 고객 위험 점수 (0-100)", minimum=0, maximum=100, value=85)457 458 use_api_in = gr.Checkbox(label="🤖 5세대 API 호출 (가용시)", value=False)459 submit_btn = gr.Button("🔍 상세 분석 실행", variant="primary")460 461 gr.Examples(462 examples=[463 [800, 2, "예", 98.0, 150.0, "탐지", 60], # 계좌 탈취(AT)464 [1500, 14, "예", 30.0, 1.5, "미탐지", 88], # 대포통장 전달465 [45, 18, "아니오", 5.0, 300.0, "미탐지", 20] # 정상 거래466 ],467 inputs=[amount_in, hour_in, payee_in, balance_ratio_in, time_delta_in, remote_in, risk_score_in]468 )469 470 with gr.Column(scale=2):471 output_html = gr.HTML("<div style='padding:20px; text-align:center;'>좌측에서 조건을 선택하고 분석을 실행하세요.<br><br>각 세대별 상세 연산 및 시각화는 <b>[🔍 상세내역 펼치기]</b>를 클릭하여 볼 수 있습니다.</div>")472 473 submit_btn.click(474 fn=analyze_transaction,475 inputs=[amount_in, hour_in, payee_in, balance_ratio_in, time_delta_in, remote_in, risk_score_in, use_api_in],476 outputs=output_html477 )478 479if __name__ == "__main__":480 demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)