""" KestrelNet Fraud Classifier — Gradio Space Interactive demo for a 1,059-parameter fraud detection model. Pure NumPy inference, no GPU required. """ import json import math import time import gradio as gr import numpy as np # ── Inference-only model (self-contained, no source code dependency) ───────── def _softmax(logits): m = np.max(logits) e = np.exp(logits - m) return e / e.sum() class KestrelNet: def __init__(self, input_dim, hidden_dims, output_dim): self.input_dim = input_dim self.hidden_dims = list(hidden_dims) self.output_dim = output_dim dims = [input_dim] + list(hidden_dims) + [output_dim] self.layer_dims = list(zip(dims[:-1], dims[1:])) self.n_layers = len(self.layer_dims) self.Ws = [np.zeros((fo, fi)) for fi, fo in self.layer_dims] self.bs = [np.zeros(fo) for _, fo in self.layer_dims] def load(self, path): with open(path) as f: params = np.array([float(x) for x in f.read().split()]) idx = 0 for i, (fi, fo) in enumerate(self.layer_dims): n = fo * fi self.Ws[i] = params[idx:idx + n].reshape(fo, fi) idx += n self.bs[i] = params[idx:idx + fo].copy() idx += fo return self def predict_proba(self, x): h = np.asarray(x, dtype=np.float64) for i, (W, b) in enumerate(zip(self.Ws, self.bs)): h = W @ h + b if i < self.n_layers - 1: h = np.maximum(0.0, h) return _softmax(h) def param_count(self): return sum(W.size + b.size for W, b in zip(self.Ws, self.bs)) # ── Load model ────────────────────────────────────────────────────────────── model = KestrelNet(14, [32, 16], 3).load("weights.txt") # ── Feature normalization ─────────────────────────────────────────────────── def normalize(amount_ratio, hour, day_of_week, location_delta, velocity_1h, velocity_24h, merchant_risk, international, card_present, device_match, account_age_days, prev_fraud_score): h_rad = 2 * math.pi * hour / 24 d_rad = 2 * math.pi * day_of_week / 7 return np.array([ amount_ratio, math.sin(h_rad), math.cos(h_rad), math.sin(d_rad), math.cos(d_rad), location_delta, min(velocity_1h / 10.0, 1.0), min(velocity_24h / 30.0, 1.0), merchant_risk, 1.0 if international else 0.0, 1.0 if card_present else 0.0, 1.0 if device_match else 0.0, min(account_age_days / 3650.0, 1.0), prev_fraud_score, ], dtype=np.float64) # ── Classification logic ─────────────────────────────────────────────────── THRESHOLDS = { "Standard": {"review": 0.30, "fraudulent": 0.55}, "Conservative": {"review": 0.20, "fraudulent": 0.40}, "Strict": {"review": 0.45, "fraudulent": 0.70}, } def classify(amount_ratio, hour, day_of_week, location_delta, velocity_1h, velocity_24h, merchant_risk, international, card_present, device_match, account_age_days, prev_fraud_score, threshold_mode): x = normalize(amount_ratio, hour, day_of_week, location_delta, velocity_1h, velocity_24h, merchant_risk, international, card_present, device_match, account_age_days, prev_fraud_score) t0 = time.perf_counter() proba = model.predict_proba(x) latency_us = (time.perf_counter() - t0) * 1e6 p_legit, p_review, p_fraud = proba t = THRESHOLDS[threshold_mode] if p_fraud >= t["fraudulent"]: verdict = "FRAUDULENT" elif p_fraud >= t["review"] or p_review >= 0.45: verdict = "REVIEW" else: verdict = "LEGITIMATE" # Verdict display colors = {"LEGITIMATE": "#22c55e", "REVIEW": "#eab308", "FRAUDULENT": "#ef4444"} color = colors[verdict] verdict_html = f"""
{verdict}
{latency_us:.1f}μs inference  ·  {model.param_count():,} params  ·  pure NumPy
""" scores = { "Legitimate": float(p_legit), "Review": float(p_review), "Fraudulent": float(p_fraud), } return verdict_html, scores # ── Presets ────────────────────────────────────────────────────────────────── PRESETS = { "Normal purchase": dict( amount_ratio=1.0, hour=14, day_of_week=2, location_delta=0.1, velocity_1h=1, velocity_24h=3, merchant_risk=0.05, international=False, card_present=True, device_match=True, account_age_days=1200, prev_fraud_score=0.0, ), "Suspicious transaction": dict( amount_ratio=8.5, hour=3, day_of_week=6, location_delta=3.2, velocity_1h=12, velocity_24h=45, merchant_risk=0.9, international=True, card_present=False, device_match=False, account_age_days=15, prev_fraud_score=0.4, ), "Borderline case": dict( amount_ratio=3.0, hour=22, day_of_week=5, location_delta=1.5, velocity_1h=4, velocity_24h=12, merchant_risk=0.4, international=True, card_present=False, device_match=True, account_age_days=200, prev_fraud_score=0.1, ), } def apply_preset(preset_name): if preset_name not in PRESETS: return [gr.update()] * 12 p = PRESETS[preset_name] return [ p["amount_ratio"], p["hour"], p["day_of_week"], p["location_delta"], p["velocity_1h"], p["velocity_24h"], p["merchant_risk"], p["international"], p["card_present"], p["device_match"], p["account_age_days"], p["prev_fraud_score"], ] # ── Gradio UI ─────────────────────────────────────────────────────────────── DESCRIPTION = """

1,059 parameters  ·  5μs inference  ·  pure NumPy  ·  no GPU
A fully-connected classifier smaller than most models' bias vectors.

""" BENCHMARKS_HTML = """

Verified on Public Kaggle Datasets

All datasets publicly available. Results independently reproducible. Pure NumPy, CPU only.

Dataset Task Accuracy F1 / AUC Params Latency
ECG Heartbeat
MIT-BIH Arrhythmia
5-class arrhythmia 97.2% F1 0.853 12,756 56 μs
EEG Emotions
Brainwave Sentiment
3-class emotion 99.1% F1 0.991 163,788 1.3 ms
EEG Eye State
Roesler / UCI
Binary open/closed 94.2% AUC 0.986 1,576 17 μs
Seizure Prediction
Bonn University EEG
Binary seizure 97.1% AUC 0.988 12,072
HAR Smartphones
UCI Activity Recognition
6-class activity 94.9% F1 0.949 15,416 70 μs
Fraud Detection
Proprietary
3-class fraud 91.6% 1,059 5 μs

Parameter Efficiency vs Typical Models

Dataset Typical CNN/LSTM Ours Reduction
ECG Heartbeat 500K – 2M 12,756 40–160x smaller
EEG Emotions 1M+ 163,788 6x smaller
EEG Eye State 100K+ 1,576 63x smaller
HAR Smartphones 200K – 1M 15,416 13–65x smaller

Two model families: KestrelNet (standard FC, minimal params) and GoshawkNet (multivector products, deeper pattern capture).
Named after raptors — bird size matches model size, hunting style matches classification style.

""" with gr.Blocks( title="KestrelNet Fraud Classifier", theme=gr.themes.Base( primary_hue=gr.themes.colors.orange, neutral_hue=gr.themes.colors.gray, font=gr.themes.GoogleFont("Inter"), ), css=""" .gradio-container { max-width: 860px !important; } footer { display: none !important; } """, ) as demo: gr.Markdown("# KestrelNet", elem_id="title") gr.HTML(DESCRIPTION) with gr.Tabs(): with gr.TabItem("Classify"): with gr.Row(): preset = gr.Dropdown( choices=list(PRESETS.keys()), label="Load preset", interactive=True, scale=2, ) threshold = gr.Radio( choices=["Standard", "Conservative", "Strict"], value="Standard", label="Threshold mode", scale=3, ) with gr.Row(): with gr.Column(scale=3): gr.Markdown("### Transaction Features") with gr.Row(): amount_ratio = gr.Number(label="Amount / 90-day avg", value=1.0, minimum=0, maximum=100) hour = gr.Slider(label="Hour", value=14, minimum=0, maximum=23, step=1) day_of_week = gr.Slider(label="Day of week (0=Mon)", value=2, minimum=0, maximum=6, step=1) with gr.Row(): location_delta = gr.Number(label="Location delta (σ)", value=0.1, minimum=0, maximum=10) velocity_1h = gr.Slider(label="Txns past hour", value=1, minimum=0, maximum=99, step=1) velocity_24h = gr.Slider(label="Txns past 24h", value=3, minimum=0, maximum=999, step=1) with gr.Row(): merchant_risk = gr.Slider(label="Merchant risk", value=0.05, minimum=0, maximum=1, step=0.01) account_age_days = gr.Number(label="Account age (days)", value=1200, minimum=0, maximum=36500) prev_fraud_score = gr.Slider(label="Prev fraud score", value=0.0, minimum=0, maximum=1, step=0.01) with gr.Row(): international = gr.Checkbox(label="International", value=False) card_present = gr.Checkbox(label="Card present", value=True) device_match = gr.Checkbox(label="Device match", value=True) with gr.Column(scale=2): gr.Markdown("### Result") verdict_output = gr.HTML() scores_output = gr.Label(label="Class probabilities", num_top_classes=3) feature_inputs = [ amount_ratio, hour, day_of_week, location_delta, velocity_1h, velocity_24h, merchant_risk, international, card_present, device_match, account_age_days, prev_fraud_score, ] all_inputs = feature_inputs + [threshold] # Preset loader preset.change(fn=apply_preset, inputs=[preset], outputs=feature_inputs) # Auto-classify on any change for inp in all_inputs: inp.change(fn=classify, inputs=all_inputs, outputs=[verdict_output, scores_output]) # Classify button as fallback btn = gr.Button("Classify", variant="primary") btn.click(fn=classify, inputs=all_inputs, outputs=[verdict_output, scores_output]) # Run once on load demo.load(fn=classify, inputs=all_inputs, outputs=[verdict_output, scores_output]) with gr.TabItem("Benchmarks"): gr.HTML(BENCHMARKS_HTML) gr.Markdown(""" ---
Architecture: 14 → 32 → 16 → 3  ·  ReLU activations  ·  Softmax output  ·  Analytic backprop training
No PyTorch. No TensorFlow. No ONNX. Just NumPy.

Model Card  ·  Website
""") if __name__ == "__main__": demo.launch()