CoolFace
Apppublic

ableman82/FDS-Generation-Demo-Test

sourceHugging Faceapache-2.0updated 4mo agoView on Hugging Face
1likes
app.py480 linesDownload Raw Back to root
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)