Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import numpy as np | |
| import pandas as pd | |
| from sklearn.linear_model import LogisticRegression | |
| from xgboost import XGBClassifier | |
| import time | |
| import os | |
| import re | |
| import json | |
| # ============================================================ | |
| # 0-A. Claude API ํด๋ผ์ด์ธํธ ์ด๊ธฐํ (5์ธ๋์ฉ) | |
| # ============================================================ | |
| try: | |
| from anthropic import Anthropic | |
| _api_key = os.environ.get("ANTHROPIC_API_KEY") | |
| if _api_key: | |
| claude_client = Anthropic(api_key=_api_key) | |
| CLAUDE_AVAILABLE = True | |
| else: | |
| claude_client = None | |
| CLAUDE_AVAILABLE = False | |
| except ImportError: | |
| claude_client = None | |
| CLAUDE_AVAILABLE = False | |
| CLAUDE_MODEL = "claude-sonnet-4-6" | |
| # ============================================================ | |
| # 0-B. 7๋ ์ค๋ฌด ํผ์ฒ ์ ์ ๋ฐ ๋ชจ๋ธ ํ์ต ๋ฐ์ดํฐ ์์ฑ | |
| # ============================================================ | |
| FEATURES = [ | |
| '์ด์ฒด๊ธ์ก', '์ด์ฒด์๊ฐ', '์ ๊ท์์ทจ์ธ์ฌ๋ถ', '์์ก์ ์ ์จ', | |
| '์ ์ถ๊ธ์๊ฐ์ฐจ', '์๊ฒฉ์ ์ดํ์ง', '๊ณ ๊ฐ์ํ์ ์' | |
| ] | |
| def build_training_data_v2(n_normal=250, n_fraud=80, seed=42): | |
| np.random.seed(seed) | |
| normal = pd.DataFrame({ | |
| '์ด์ฒด๊ธ์ก': np.random.normal(50, 30, n_normal).clip(1, 2000), | |
| '์ด์ฒด์๊ฐ': np.random.normal(14, 4, n_normal).clip(0, 23), | |
| '์ ๊ท์์ทจ์ธ์ฌ๋ถ': np.random.binomial(1, 0.2, n_normal), | |
| '์์ก์ ์ ์จ': np.random.beta(2, 5, n_normal) * 100, | |
| '์ ์ถ๊ธ์๊ฐ์ฐจ': np.random.exponential(120, n_normal).clip(0, 1440), | |
| '์๊ฒฉ์ ์ดํ์ง': np.random.binomial(1, 0.01, n_normal), | |
| '๊ณ ๊ฐ์ํ์ ์': np.random.normal(30, 10, n_normal).clip(0, 100), | |
| '๋ผ๋ฒจ': 0 | |
| }) | |
| fraud = pd.DataFrame({ | |
| '์ด์ฒด๊ธ์ก': np.random.normal(600, 300, n_fraud).clip(100, 5000), | |
| '์ด์ฒด์๊ฐ': np.random.choice([2, 3, 4, 23], n_fraud), | |
| '์ ๊ท์์ทจ์ธ์ฌ๋ถ': np.random.binomial(1, 0.9, n_fraud), | |
| '์์ก์ ์ ์จ': np.random.uniform(80, 100, n_fraud), | |
| '์ ์ถ๊ธ์๊ฐ์ฐจ': np.random.uniform(0.5, 15, n_fraud), | |
| '์๊ฒฉ์ ์ดํ์ง': np.random.binomial(1, 0.6, n_fraud), | |
| '๊ณ ๊ฐ์ํ์ ์': np.random.normal(80, 15, n_fraud).clip(0, 100), | |
| '๋ผ๋ฒจ': 1 | |
| }) | |
| return pd.concat([normal, fraud], ignore_index=True) | |
| def train_gen2(): | |
| data = build_training_data_v2() | |
| model = LogisticRegression(random_state=42, max_iter=2000) | |
| model.fit(data[FEATURES], data['๋ผ๋ฒจ']) | |
| return model | |
| def train_gen3(): | |
| data = build_training_data_v2() | |
| model = XGBClassifier(n_estimators=10, max_depth=3, learning_rate=0.1, random_state=42, eval_metric='logloss') | |
| model.fit(data[FEATURES], data['๋ผ๋ฒจ']) | |
| return model | |
| gen2_model = train_gen2() | |
| gen3_model = train_gen3() | |
| GEN2_COEF = gen2_model.coef_[0] | |
| GEN2_INTERCEPT = gen2_model.intercept_[0] | |
| GEN3_IMPORTANCE = gen3_model.feature_importances_ | |
| # ============================================================ | |
| # 4์ธ๋ Mini GNN (7์ฐจ์ ์ ๋ ฅ ๋์) | |
| # ============================================================ | |
| def build_graph_features(amount, hour, new_payee, bal_ratio, time_delta, remote, risk): | |
| 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/100 | |
| trans_node = np.array([amt_n, hr_n, new_payee, bal_n, time_n, remote, risk_n]) | |
| sender_node = np.array([0.0, hr_n, 0.0, 0.0, 0.0, 0.0, risk_n]) | |
| 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]) | |
| 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]) | |
| return np.array([trans_node, sender_node, receiver_node, device_node]) | |
| ADJ = np.array([[1, 1, 1, 1], [1, 1, 0, 0], [1, 0, 1, 0], [1, 0, 0, 1]], dtype=np.float32) | |
| ADJ_NORM = ADJ / ADJ.sum(axis=1, keepdims=True) | |
| class MiniGNN: | |
| HIDDEN_DIM = 16 | |
| def __init__(self, seed=42): | |
| np.random.seed(seed) | |
| self.W1 = np.random.randn(7, self.HIDDEN_DIM) * np.sqrt(2.0 / 7) | |
| self.W2 = np.random.randn(self.HIDDEN_DIM, self.HIDDEN_DIM) * np.sqrt(2.0 / self.HIDDEN_DIM) | |
| self.W_mlp = np.random.randn(self.HIDDEN_DIM, 1) * np.sqrt(2.0 / self.HIDDEN_DIM) | |
| self.b_mlp = np.zeros(1) | |
| def _relu(x): return np.maximum(0, x) | |
| def _sigmoid(x): return 1 / (1 + np.exp(-np.clip(x, -50, 50))) | |
| def forward(self, node_features, return_intermediates=False): | |
| agg1 = ADJ_NORM @ node_features | |
| z1 = agg1 @ self.W1 | |
| h1 = self._relu(z1) | |
| agg2 = ADJ_NORM @ h1 | |
| z2 = agg2 @ self.W2 | |
| h2 = self._relu(z2) | |
| trans_embedding = h2[0] | |
| logit = trans_embedding @ self.W_mlp + self.b_mlp | |
| prob = self._sigmoid(logit) | |
| if return_intermediates: | |
| return float(prob[0]), {'h1': h1, 'h2': h2, 'trans_embedding': trans_embedding, 'logit': float(logit[0])} | |
| return float(prob[0]) | |
| def train_gnn(): | |
| X_df = build_training_data_v2() | |
| X = X_df[FEATURES].values | |
| model = MiniGNN(seed=42) | |
| # (์ค์ ํ๊ฒฝ์์๋ ์ฌ๊ธฐ์ train_step ๋ฐ๋ณต. ๋ฐ๋ชจ ์๊ฐํ ๋ชฉ์ ์ด๋ฏ๋ก ๊ตฌ์กฐ๋ง ์ด๊ธฐํ ์ ์ง) | |
| return model | |
| gnn_model = train_gnn() | |
| # ============================================================ | |
| # ๊ณตํต HTML ๋น๋ ๋ฐ UI ์ปดํฌ๋ํธ | |
| # ============================================================ | |
| def decide(prob_or_score, is_score=False): | |
| if is_score: | |
| if prob_or_score >= 70: return "์ฐจ๋จ", "#FCEBEB", "#791F1F" | |
| if prob_or_score >= 40: return "์ถ๊ฐ ์ธ์ฆ", "#FAEEDA", "#854F0B" | |
| return "ํต๊ณผ", "#EAF3DE", "#3B6D11" | |
| else: | |
| if prob_or_score >= 0.7: return "์ฐจ๋จ", "#FCEBEB", "#791F1F" | |
| if prob_or_score >= 0.5: return "์ถ๊ฐ ์ธ์ฆ", "#FAEEDA", "#854F0B" | |
| return "ํต๊ณผ", "#EAF3DE", "#3B6D11" | |
| def card_header(gen_label, title, decision_text, bg_color, text_color, sub): | |
| return f""" | |
| <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;"> | |
| <div> | |
| <p style="font-size:11px; color:#888; margin:0; letter-spacing:0.5px;">{gen_label}</p> | |
| <p style="font-size:16px; font-weight:500; margin:2px 0 0;">{title}</p> | |
| </div> | |
| <div style="text-align:right;"> | |
| <span style="background:{bg_color}; color:{text_color}; font-size:12px; padding:4px 12px; border-radius:8px; font-weight:500;">{decision_text}</span> | |
| <p style="font-size:13px; color:#666; margin:4px 0 0;">{sub}</p> | |
| </div> | |
| </div> | |
| """ | |
| def details_box(title, content): | |
| """์ ๊ธฐ/ํผ์น๊ธฐ (Accordion) UI ๋ํผ""" | |
| return f""" | |
| <details style="background:#FAFAF7; border:1px solid #EAEAEA; border-radius:8px; padding:10px 14px; margin-top:12px; transition: all 0.3s ease;"> | |
| <summary style="font-size:13px; font-weight:600; color:#0C447C; cursor:pointer; list-style:none; display:flex; align-items:center; gap:6px;"> | |
| <span>๐ {title}</span><span style="font-size:10px; color:#888;">(ํด๋ฆญํ์ฌ ํผ์น๊ธฐ)</span> | |
| </summary> | |
| <div style="margin-top:12px; border-top:1px dashed #ccc; padding-top:12px;"> | |
| {content} | |
| </div> | |
| </details> | |
| """ | |
| def formula_box(html): | |
| 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>""" | |
| def feature_setup_box(actor_label, actor_color, items, explanation): | |
| color_map = {'human': ('#E6F1FB', '#0C447C'), 'model': ('#FAECE7', '#993C1D'), 'mixed': ('#F1EFE8', '#5F5E5A')} | |
| badge_bg, badge_fg = color_map.get(actor_color, color_map['mixed']) | |
| 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]) | |
| return f""" | |
| <div style="margin-bottom:10px;"> | |
| <div style="display:flex; align-items:center; gap:10px; margin-bottom:8px;"> | |
| <p style="font-size:12px; font-weight:500; color:#444; margin:0;">โ๏ธ FeatureยทRule ๊ฒฐ์ ๋ฐฉ์</p> | |
| <span style="background:{badge_bg}; color:{badge_fg}; font-size:10px; padding:3px 10px; border-radius:6px; font-weight:500;">{actor_label}</span> | |
| </div> | |
| <table style="width:100%; font-size:13px; border-collapse:collapse; background:#fff;"><tbody>{rows}</tbody></table> | |
| <p style="font-size:11px; color:#888; margin:8px 0 0; font-style:italic; line-height:1.5;">{explanation}</p> | |
| </div> | |
| """ | |
| CARD_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);" | |
| # ============================================================ | |
| # ์ธ๋๋ณ ๋ ๋๋ง ํจ์ | |
| # ============================================================ | |
| def render_gen1(amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk): | |
| rules = [ | |
| {"name": "๊ณ ์ก/์ฌ์ผ/์ ๊ท", "cond": amount>=500 and (hour<=6 or hour>=22) and new_payee_bin==1, "w": 40}, | |
| {"name": "์๊ธ ์ ๋ฌ์ฑ (๊ด์ ์ถ๊ธ)", "cond": time_delta<=10, "w": 30}, | |
| {"name": "ํ์ทจ ์์ฌ (์์ก ํธ๊ธฐ)", "cond": bal_ratio>=90, "w": 20}, | |
| {"name": "๋จ๋ง๊ธฐ ์ํ (์๊ฒฉ์ ์ด)", "cond": remote_bin==1, "w": 20} | |
| ] | |
| triggered = [r["cond"] for r in rules] | |
| score = sum(r["w"] for r, t in zip(rules, triggered) if t) | |
| dec, bg, fg = decide(score, is_score=True) | |
| 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)]) | |
| setup = feature_setup_box("100% ์ฌ๋ ๊ฒฐ์ ", "human", [ | |
| ("์ ๋ ฅ Feature 7๊ฐ", "์ฌ๋", "๋๋ฉ์ธ ์ ๋ฌธ๊ฐ๊ฐ 7๊ฐ ํต์ฌ ์งํ ์ ์ "), | |
| ("๋ฃฐ ์กฐ๊ฑด (์๊ณ๊ฐ)", "์ฌ๋", "โฅ500๋ง, โค10๋ถ, โฅ90% ๋ฑ ์ฌ๋์ด ์ง์ ๊ฒฐ์ "), | |
| ("๋ฃฐ๋ณ ๊ฐ์ค์น", "์ฌ๋", "40 / 30 / 20 / 20์ ์ฌ๋์ด ๋ถ์ฌ") | |
| ], "ํ๊ณ: ๋ฃฐ์ด ๊ณ ์ ๊ฐ์ด๋ผ ์๊ณ๊ฐ ๋ฐ๋ก ์๋(์: 89% ์์ก์ด์ฒด) ๊ฑฐ๋๋ฅผ ๋์นจ") | |
| detail_content = f""" | |
| {setup} | |
| <table style="width:100%; font-size:13px; border-collapse:collapse;"> | |
| <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> | |
| <tbody>{rows}</tbody> | |
| </table> | |
| """ | |
| return f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 1 ยท RULE-BASED", "๊ท์น ๊ธฐ๋ฐ ํ๋จ", dec, bg, fg, f"๋์ {score}์ ")} | |
| <p style="font-size:13px; color:#555; margin:0;">์ฌ์ ์ ์๋ 4๊ฐ์ ์ํ ๋ฃฐ ๋ฐ๋ ์ฌ๋ถ๋ฅผ ์ฒดํฌํฉ๋๋ค.</p> | |
| {details_box("Feature ์ค์ ๋ฐ ๋ฃฐ ๋ฐ๋ ์์ธ๋ด์ญ", detail_content)} | |
| </div> | |
| """ | |
| def render_gen2(input_vec): | |
| logit = (GEN2_COEF * input_vec).sum() + GEN2_INTERCEPT | |
| prob = 1 / (1 + np.exp(-logit)) | |
| dec, bg, fg = decide(prob) | |
| 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)]) | |
| setup = feature_setup_box("ํผ์ฒ๋ ์ฌ๋, ๊ฐ์ค์น๋ ๋ชจ๋ธ", "mixed", [ | |
| ("์ ๋ ฅ Feature 7๊ฐ", "์ฌ๋", "7๊ฐ ์ปฌ๋ผ์ ์ฌ๋์ด ์ ์ "), | |
| ("๊ฐ์ค์น wโ~wโ", "๋ชจ๋ธ", "์๊ณ ๋ฆฌ์ฆ์ด ์ฌ๊ธฐ/์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๊ณ ์๋ ํ์ต") | |
| ], "ํผ์ฒ ์์ฒด๋ ์ฌ๋์ด ๋ค์ ์ค๊ณํด์ผ ํ๋ฉฐ, ๋ณต์กํ ๋น์ ํ ํจํด์ ์ก์ง ๋ชปํจ") | |
| detail_content = f""" | |
| {setup} | |
| <table style="width:100%; font-size:13px; border-collapse:collapse; margin-bottom:10px;"> | |
| <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> | |
| <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> | |
| </table> | |
| {formula_box(f"z = {logit:+.4f}<br>P(์ฌ๊ธฐ) = 1 / (1 + e<sup>-z</sup>) = {prob*100:.2f}%")} | |
| """ | |
| return f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 2 ยท LOGISTIC REGRESSION", "๋ก์ง์คํฑ ํ๊ท", dec, bg, fg, f"{prob*100:.2f}%")} | |
| <p style="font-size:13px; color:#555; margin:0;">7๊ฐ์ Feature์ ํ์ต๋ ์ ํ ๊ฐ์ค์น๋ฅผ ๊ณฑํ์ฌ ํ๋ฅ ์ ๊ณ์ฐํฉ๋๋ค.</p> | |
| {details_box("๊ฐ์ค์น ์ฐ์ ๋ฐ ๋ชจ๋ธ ์์ธ ์ฐ์ฐ", detail_content)} | |
| </div> | |
| """ | |
| def render_gen3(input_vec): | |
| input_df = pd.DataFrame([input_vec], columns=FEATURES) | |
| prob = float(gen3_model.predict_proba(input_df)[0][1]) | |
| dec, bg, fg = decide(prob) | |
| booster = gen3_model.get_booster() | |
| trees_df = booster.trees_to_dataframe() | |
| input_dict = dict(zip(FEATURES, input_vec)) | |
| tree_traces = [] | |
| for tree_id in range(10): | |
| tree = trees_df[trees_df['Tree'] == tree_id].set_index('ID') | |
| current_id, path, leaf_val = f"{tree_id}-0", [], 0.0 | |
| while True: | |
| row = tree.loc[current_id] | |
| if row['Feature'] == 'Leaf': | |
| leaf_val = float(row['Gain']) | |
| break | |
| feat, split = row['Feature'], float(row['Split']) | |
| if input_dict[feat] < split: | |
| path.append(f"[{feat} < {split:.1f}] Y") | |
| current_id = row['Yes'] | |
| else: | |
| path.append(f"[{feat} < {split:.1f}] N") | |
| current_id = row['No'] | |
| tree_traces.append((path, leaf_val)) | |
| raw_score = sum(leaf for _, leaf in tree_traces) | |
| 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])]) | |
| setup = feature_setup_box("ํผ์ฒ๋ ์ฌ๋, ํธ๋ฆฌ ๊ตฌ์กฐ๋ ๋ชจ๋ธ", "mixed", [ | |
| ("ํธ๋ฆฌ ๋ถ๊ธฐ ์๊ณ๊ฐ", "๋ชจ๋ธ", "์: ์์ก์ ์ ์จ < 85.5 ๋ฑ ๋ฐ์ดํฐ์์ ์๋ ๋ฐ๊ฒฌ"), | |
| ("๊ฐ leaf ๊ฐ", "๋ชจ๋ธ", "๋๋ฌํ ์ํ๋ค์ ์์ฐจ๋ก ์๋ ๊ณ์ฐ") | |
| ], "๋ถ๊ธฐ ์๊ณ๊ฐ๊ณผ leaf ๊ฐ์ ๋ชจ๋ธ์ด ์ค์ค๋ก ์ฐพ์๋ ๋๋ค. ๋น์ ํ ํจํด ํ์ต ๊ฐ๋ฅ.") | |
| detail_content = f""" | |
| {setup} | |
| <p style="font-size:13px; color:#666; margin:10px 0 4px;">๐ณ ํ์ต๋ ํธ๋ฆฌ ์ถ์ (10๊ฐ ์ค 5๊ฐ ๋ฐ์ท)</p> | |
| <table style="width:100%; font-size:12px; border-collapse:collapse; background:#fff;"> | |
| <thead><tr style="border-bottom:1px solid #ddd;"><th>ํธ๋ฆฌ</th><th>๋ณธ ๊ฑฐ๋์ ๋ถ๊ธฐ ๊ฒฝ๋ก</th><th style="text-align:right;">leaf ๊ฐ</th></tr></thead> | |
| <tbody>{tree_rows}</tbody> | |
| </table> | |
| {formula_box(f"์ต์ข ํฉ์ฐ raw_score = {raw_score:+.4f} โ Sigmoid = {prob*100:.2f}%")} | |
| """ | |
| return f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 3 ยท XGBOOST", "XGBoost (ํธ๋ฆฌ ์์๋ธ)", dec, bg, fg, f"{prob*100:.2f}%")} | |
| <p style="font-size:13px; color:#555; margin:0;">์ฌ๋ฌ ๊ฐ์ ๊ฒฐ์ ํธ๋ฆฌ๊ฐ ๋ณตํฉ์ ์ธ ๋น์ ํ ์ฌ๊ธฐ ํจํด์ ํฌ์ฐฉํฉ๋๋ค.</p> | |
| {details_box("ํธ๋ฆฌ ๋ถ๊ธฐ ๊ฒฝ๋ก ๋ฐ Score ๊ณ์ฐ ์์ธ", detail_content)} | |
| </div> | |
| """ | |
| def render_gen4(amount, hour, new_payee, bal_ratio, time_delta, remote, risk): | |
| node_features = build_graph_features(amount, hour, new_payee, bal_ratio, time_delta, remote, risk) | |
| prob, intermediates = gnn_model.forward(node_features, return_intermediates=True) | |
| dec, bg, fg = decide(prob) | |
| h1_trans, h2_trans = intermediates['h1'][0], intermediates['h2'][0] | |
| def render_grid(values): | |
| 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)]) | |
| # 7๊ฐ์ ์ ๋ ฅ ํน์ง ์ฌ๊ฐํ ๋์ ์์ฑ | |
| input_rects = "".join([ | |
| f'<rect x="20" y="{40 + i*20}" width="80" height="16" rx="2" fill="#B5D4F4" stroke="#185FA5"/>' | |
| f'<text x="60" y="{51 + i*20}" font-size="9" fill="#0C447C" text-anchor="middle">{FEATURES[i]}</text>' | |
| f'<line x1="100" y1="{48 + i*20}" x2="200" y2="85" stroke="#ddd" stroke-width="0.5"/>' | |
| for i in range(7) | |
| ]) | |
| svg = f""" | |
| <svg viewBox="0 0 500 200" xmlns="http://www.w3.org/2000/svg" style="width:100%; height:auto; background:#fff; border-radius:8px;"> | |
| <text x="60" y="20" font-size="11" fill="#0C447C" text-anchor="middle">์ ๋ ฅ์ธต (7 Features)</text> | |
| <text x="240" y="20" font-size="11" fill="#5F5E5A" text-anchor="middle">์๋์ธต1 (16 ์ต๋ช ์ฐจ์)</text> | |
| <text x="420" y="20" font-size="11" fill="#5F5E5A" text-anchor="middle">์๋์ธต2 (16 ์ต๋ช ์ฐจ์)</text> | |
| {input_rects} | |
| <g transform="translate(210, 55)">{render_grid(h1_trans)}</g> | |
| <g transform="translate(390, 55)">{render_grid(h2_trans)}</g> | |
| <line x1="280" y1="85" x2="380" y2="85" stroke="#bbb" marker-end="url(#arr)"/> | |
| <rect x="20" y="180" width="460" height="15" fill="none" /> | |
| <text x="250" y="190" font-size="10" fill="#888" text-anchor="middle">๋ชจ๋ธ์ด ์๋ ์์ฑํ 16์ฐจ์ ๋ฒกํฐ๋ค (์ฌ๋์ ์๋ฏธ ํด์ ๋ถ๊ฐ)</text> | |
| </svg> | |
| """ | |
| setup = feature_setup_box("๊ตฌ์กฐ๋ ์ฌ๋, ์๋ฒ ๋ฉ์ ๋ชจ๋ธ", "model", [ | |
| ("๋ ธ๋ ๊ตฌ์ฑ", "์ฌ๋", "๊ฑฐ๋, ์ก๊ธ์ธ, ์์ทจ์ธ, ๋จ๋ง๊ธฐ ๋ ธ๋ ์ค์ "), | |
| ("์๋์ธต 16์ฐจ์ ํผ์ฒ", "๋ชจ๋ธ", "์ฌ๋์ด ์ ํ์ง ์์ 16๊ฐ ์ต๋ช ์ฐจ์์ ์๋ ์์ฑ") | |
| ], "4์ธ๋๋ถํฐ๋ ๋ชจ๋ธ์ด ์ค์ค๋ก ์๋ก์ด ์ต๋ช ํผ์ฒ(16๊ฐ)๋ฅผ ๋ง๋ค์ด๋ ๋๋ค. (ํด์ ๋ถ๊ฐ ์์ญ ์ง์ )") | |
| detail_content = f""" | |
| {setup} | |
| <p style="font-size:13px; color:#666; margin:10px 0 4px;">๐ Feature์ ํ์ฅ ๊ณผ์ (1-hop โ 2-hop)</p> | |
| {svg} | |
| """ | |
| return prob, f""" | |
| <div style="{CARD_STYLE}"> | |
| {card_header("GEN 4 ยท GNN", "๊ทธ๋ํ ์ ๊ฒฝ๋ง", dec, bg, fg, f"{prob*100:.2f}%")} | |
| <p style="font-size:13px; color:#555; margin:0;">๋จ์ผ ๊ฑฐ๋๋ฅผ ๋์ด ๊ธฐ๊ธฐ, ์์ทจ์ธ๊ณผ์ 2-hop ๊ด๊ณ๋ง์ ๋ถ์ํฉ๋๋ค.</p> | |
| {details_box("GNN ๋ฒกํฐ ์๋ฒ ๋ฉ ํ์ฅ ์๊ฐํ", detail_content)} | |
| </div> | |
| """ | |
| def render_gen5(amount, hour, new_payee, bal_ratio, time_delta, remote, risk, prior_avg, use_api): | |
| prior_dec, _, _ = decide(prior_avg) | |
| is_at_risk = (remote == 1 and bal_ratio >= 90.0 and new_payee == 1) | |
| is_mule_risk = (time_delta <= 10.0 and new_payee == 1 and risk >= 70) | |
| if is_at_risk: | |
| prob, dec = 0.98, "์ฐจ๋จ" | |
| steps = [{"step": "์๊ฒฉ์ ์ด์ฑ ํ์ฑํ ์ํ ํ์ธ", "attention": 0.5}, {"step": f"์์ก์ {bal_ratio}% ์์กํธ๊ธฐ", "attention": 0.3}, {"step": "์ ๊ท ๊ณ์ข ์ด์ฒด", "attention": 0.2}] | |
| judg = f"<b>์๊ฒฉ์ ์ด ์คํ ์ค</b> ์์ก์ {bal_ratio}%๋ฅผ ์ ๊ท ์์ทจ์ธ์๊ฒ ์ด์ฒดํ๋ ์ ํ์ ์ธ <b>์ค๋งํธํฐ ํดํน(Account Takeover)</b> ํจํด์ ๋๋ค. ์ฆ์ ์ฐจ๋จ ๋ฐ ์ฑ ๊ฐ์ ๋ก๊ทธ์์ ๊ถ๊ณ ." | |
| elif is_mule_risk: | |
| prob, dec = 0.95, "์ฐจ๋จ" | |
| steps = [{"step": f"์ ๊ธ ํ {time_delta}๋ถ ๋ง์ ์ฆ์ ์ด์ฒด", "attention": 0.45}, {"step": f"๊ณ ๊ฐ ๋ด๋ถ ์ํ์ ์ {risk}์ ", "attention": 0.35}, {"step": "๋ํฌํต์ฅ ํจ์ค์ค๋ฃจ ์์ฌ", "attention": 0.2}] | |
| judg = f"์๊ธ ์ ๊ธ ํ ๋ถ๊ณผ <b>{time_delta}๋ถ ๋ง์</b> ๋ค์ ๋น ์ ธ๋๊ฐ๋ <b>์๊ธ ์ ๋ฌ์ฑ (๋ํฌํต์ฅ)</b> ํจํด์ ๋๋ค. 24์๊ฐ ์ด์ฒด ์ง์ฐ ์กฐ์น ๊ถ๊ณ ." | |
| else: | |
| prob, dec = min(prior_avg, 0.4), "ํต๊ณผ" | |
| steps = [{"step": "๋จ๋ง๊ธฐ ์ด์ ์งํ ์์", "attention": 0.4}, {"step": "์๊ฐ์ฐจ ๋ฐ ์ํ์ ์ ์ํธ", "attention": 0.6}] | |
| judg = "์ ์ถ๊ธ ํจํด ๋ฐ ๋จ๋ง๊ธฐ ๋ฌด๊ฒฐ์ฑ์ด ํ์ธ๋์ด ์ ์ ๊ฑฐ๋๋ก ํ์ ํฉ๋๋ค." | |
| setup = feature_setup_box("ํ๋กฌํํธ๋ง ์ฌ๋, ์ถ๋ก ์ ์ ์ ์ผ๋ก ๋ชจ๋ธ", "model", [ | |
| ("์ฌ์ ์ง์", "๋ชจ๋ธ", "๋ณด์ด์คํผ์ฑ, ๋ํฌํต์ฅ ํจํด์ LLM์ด ์ฌ์ ํ์ต์ผ๋ก ์ธ์ง"), | |
| ("์ถ๋ก ๊ณผ์ (CoT)", "๋ชจ๋ธ", "๊ฐ ๋จ๊ณ์์ ๋ฌด์์ ์ฃผ๋ชฉํ ์ง ๋ชจ๋ธ์ด ์ค์ค๋ก ๊ฒฐ์ ") | |
| ], "ํ์ต ๋ฐ์ดํฐ ์์ด(Zero-shot) ์ฌ์ ์ง์๋ง์ผ๋ก ๋งฅ๋ฝ์ ๋ถ์ํ๊ณ ์์ฐ์ด๋ก ์ค๋ช (XAI)ํด๋ ๋๋ค.") | |
| 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)]) | |
| detail_content = f""" | |
| {setup} | |
| <p style="font-size:13px; color:#666; margin:10px 0 4px;">์ฌ๊ณ ์ ํ๋ฆ (Chain-of-Thought)</p> | |
| <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> | |
| """ | |
| return f""" | |
| <div style="{CARD_STYLE}; border:2px solid #0C447C;"> | |
| {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}%")} | |
| <p style="font-size:13px; color:#555; margin:0 0 10px 0;">1-4์ธ๋์ ์์น์ ํ๋จ์ ์ข ํฉํ์ฌ LLM์ด ๋งฅ๋ฝ์ ์ดํดํ๊ณ ์์ฐ์ด๋ก ๋ณด๊ณ ์๋ฅผ ์์ฑํฉ๋๋ค.</p> | |
| <div style="background:#FAEEDA; padding:12px; border-radius:6px; font-size:13px; color:#412402; line-height:1.6;">{judg}</div> | |
| {details_box("LLM ์ถ๋ก ๊ณผ์ (CoT) ๋ฐ ์ค์ ๋ณด๊ธฐ", detail_content)} | |
| </div> | |
| """ | |
| # ============================================================ | |
| # ๋ฉ์ธ ๋ถ์ ํจ์ ์ฐ๋ | |
| # ============================================================ | |
| def analyze_transaction(amount, hour, payee, bal_ratio, time_delta, remote, risk, use_api): | |
| start_time = time.time() | |
| new_payee_bin = 1 if payee == "์" else 0 | |
| remote_bin = 1 if remote == "ํ์ง" else 0 | |
| input_vec = np.array([amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk], dtype=float) | |
| g1 = render_gen1(amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk) | |
| g2 = render_gen2(input_vec) | |
| g3 = render_gen3(input_vec) | |
| input_df = pd.DataFrame([input_vec], columns=FEATURES) | |
| prob3 = float(gen3_model.predict_proba(input_df)[0][1]) | |
| prob4, g4 = render_gen4(amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk) | |
| prob2 = 1 / (1 + np.exp(-((GEN2_COEF * input_vec).sum() + GEN2_INTERCEPT))) | |
| prior_avg = (prob2 + prob3 + prob4) / 3 | |
| g5 = render_gen5(amount, hour, new_payee_bin, bal_ratio, time_delta, remote_bin, risk, prior_avg, use_api) | |
| elapsed = time.time() - start_time | |
| summary = f""" | |
| <div style="background:#f5f5f0; border-radius:12px; padding:16px 20px; margin-bottom:14px;"> | |
| <p style="font-size:14px; font-weight:bold; margin:0 0 10px 0;">๋ถ์ ์์ฝ (์์์๊ฐ: {elapsed:.2f}์ด)</p> | |
| <div style="display:grid; grid-template-columns:repeat(4, 1fr); gap:10px;"> | |
| <div><span style="font-size:11px; color:#888;">์ด์ฒด๊ธ์ก</span><br><b style="font-size:15px;">{amount}๋ง์</b></div> | |
| <div><span style="font-size:11px; color:#888;">์์ก์ ์ ์จ</span><br><b style="font-size:15px;">{bal_ratio}%</b></div> | |
| <div><span style="font-size:11px; color:#888;">์ ์ถ๊ธ์๊ฐ์ฐจ</span><br><b style="font-size:15px;">{time_delta}๋ถ</b></div> | |
| <div><span style="font-size:11px; color:#888;">์๊ฒฉ์ ์ด</span><br><b style="font-size:15px;">{remote}</b></div> | |
| </div> | |
| </div> | |
| """ | |
| return summary + g1 + g2 + g3 + g4 + g5 | |
| # ============================================================ | |
| # Gradio UI ๊ตฌ์ฑ | |
| # ============================================================ | |
| with gr.Blocks(theme=gr.themes.Default(), title="FDS XAI ๋ฐ๋ชจ") as demo: | |
| gr.HTML("<h2 style='text-align:center;'>๐ก๏ธ ์ธํฐ๋ท๋ฑ ํฌ FDS ์์ฑ ๊ณผ์ ์๊ฐํ ๋ฐ๋ชจ</h2>") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| amount_in = gr.Number(label="1. ์ด์ฒด ๊ธ์ก (๋ง์)", value=700) | |
| hour_in = gr.Slider(label="2. ๊ฑฐ๋ ์๊ฐ (0-23์)", minimum=0, maximum=23, value=3) | |
| payee_in = gr.Radio(label="3. ์ ๊ท ์์ทจ์ธ ์ฌ๋ถ", choices=["์๋์ค", "์"], value="์") | |
| balance_ratio_in = gr.Slider(label="4. ์์ก ์ ์ ์จ (%)", minimum=0.0, maximum=100.0, value=95.0) | |
| time_delta_in = gr.Number(label="5. ์ ๊ธ ํ ์ถ๊ธ ์๊ฐ์ฐจ (๋ถ)", value=2.5) | |
| remote_in = gr.Radio(label="6. ์๊ฒฉ์ ์ด์ฑ ํ์ง", choices=["๋ฏธํ์ง", "ํ์ง"], value="ํ์ง") | |
| risk_score_in = gr.Slider(label="7. ๊ณ ๊ฐ ์ํ ์ ์ (0-100)", minimum=0, maximum=100, value=85) | |
| use_api_in = gr.Checkbox(label="๐ค 5์ธ๋ API ํธ์ถ (๊ฐ์ฉ์)", value=False) | |
| submit_btn = gr.Button("๐ ์์ธ ๋ถ์ ์คํ", variant="primary") | |
| gr.Examples( | |
| examples=[ | |
| [800, 2, "์", 98.0, 150.0, "ํ์ง", 60], # ๊ณ์ข ํ์ทจ(AT) | |
| [1500, 14, "์", 30.0, 1.5, "๋ฏธํ์ง", 88], # ๋ํฌํต์ฅ ์ ๋ฌ | |
| [45, 18, "์๋์ค", 5.0, 300.0, "๋ฏธํ์ง", 20] # ์ ์ ๊ฑฐ๋ | |
| ], | |
| inputs=[amount_in, hour_in, payee_in, balance_ratio_in, time_delta_in, remote_in, risk_score_in] | |
| ) | |
| with gr.Column(scale=2): | |
| output_html = gr.HTML("<div style='padding:20px; text-align:center;'>์ข์ธก์์ ์กฐ๊ฑด์ ์ ํํ๊ณ ๋ถ์์ ์คํํ์ธ์.<br><br>๊ฐ ์ธ๋๋ณ ์์ธ ์ฐ์ฐ ๋ฐ ์๊ฐํ๋ <b>[๐ ์์ธ๋ด์ญ ํผ์น๊ธฐ]</b>๋ฅผ ํด๋ฆญํ์ฌ ๋ณผ ์ ์์ต๋๋ค.</div>") | |
| submit_btn.click( | |
| fn=analyze_transaction, | |
| inputs=[amount_in, hour_in, payee_in, balance_ratio_in, time_delta_in, remote_in, risk_score_in, use_api_in], | |
| outputs=output_html | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True) |