kestrelnet-benchmarks / inference.py
Frodo
5 benchmark models: ECG, EEG emotions, eye state, seizure, HAR β€” verified on Kaggle
844b533
"""
inference.py β€” Self-contained model loader for KestrelNet/GoshawkNet benchmarks.
Pure NumPy. No framework dependencies. Supports both standard FC (KestrelNet)
and multivector product (GoshawkNet) architectures.
Usage:
from inference import load_model
model = load_model("ecg-heartbeat")
proba = model.predict_proba(x)
"""
import numpy as np
from pathlib import Path
ROOT = Path(__file__).resolve().parent
# ── Model configs (architecture used for each benchmark) ────────────────────
CONFIGS = {
"ecg-heartbeat": {
"input_dim": 187,
"hidden_dims": [16, 8],
"output_dim": 5,
"algebra": (0, 2), # Cl(0,2) quaternion
"class_names": ["Normal", "Supraventricular", "Ventricular", "Fusion", "Unknown"],
},
"eeg-emotions": {
"input_dim": 2548,
"hidden_dims": [16, 8],
"output_dim": 3,
"algebra": (0, 2),
"class_names": ["Negative", "Neutral", "Positive"],
},
"eye-state": {
"input_dim": 14,
"hidden_dims": [16, 8],
"output_dim": 2,
"algebra": (0, 2),
"class_names": ["Eyes Open", "Eyes Closed"],
},
"seizure-prediction": {
"input_dim": 178,
"hidden_dims": [16, 8],
"output_dim": 2,
"algebra": (0, 2),
"class_names": ["Non-seizure", "Seizure"],
},
"har-smartphones": {
"input_dim": 228,
"hidden_dims": [16, 8],
"output_dim": 6,
"algebra": (0, 2),
"class_names": ["Walking", "Walking Upstairs", "Walking Downstairs",
"Sitting", "Standing", "Laying"],
},
}
# ── Clifford algebra (inference-only, minimal) ─────────────────────────────
class _CliffordAlgebra:
"""Minimal Cl(p,q) for inference. Precomputes Cayley tensor."""
def __init__(self, p, q):
self.p, self.q = p, q
self.n = p + q
self.dim = 1 << self.n
self.cayley = np.zeros((self.dim, self.dim, self.dim), dtype=np.float64)
for i in range(self.dim):
for j in range(self.dim):
sign, k = self._blade_product(i, j)
self.cayley[k, i, j] = sign
self.cayley_flat = self.cayley.reshape(self.dim * self.dim, self.dim)
def _blade_product(self, a, b):
n_swaps = 0
temp = a >> 1
while temp:
n_swaps += bin(temp & b).count('1')
temp >>= 1
sign = -1 if n_swaps % 2 else 1
common = a & b
for i in range(self.n):
if (common >> i) & 1 and i >= self.p:
sign = -sign
return sign, a ^ b
# ── Softmax ────────────────────────────────────────────────────────────────
def _softmax(logits):
m = np.max(logits)
e = np.exp(logits - m)
return e / e.sum()
# ── GoshawkNet (inference-only) ────────────────────────────────────────────
class GoshawkNet:
"""Multivector product neural network β€” inference only."""
def __init__(self, input_dim, hidden_dims, output_dim, p=0, q=2):
self.input_dim = input_dim
self.hidden_dims = list(hidden_dims)
self.output_dim = output_dim
self.algebra = _CliffordAlgebra(p, q)
self.D = self.algebra.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, self.D)) for fi, fo in self.layer_dims]
self.bs = [np.zeros((fo, self.D)) for _, fo in self.layer_dims]
def set_params(self, v):
idx = 0
for l, (fi, fo) in enumerate(self.layer_dims):
n_W = fo * fi * self.D
self.Ws[l] = v[idx:idx + n_W].reshape(fo, fi, self.D)
idx += n_W
n_b = fo * self.D
self.bs[l] = v[idx:idx + n_b].reshape(fo, self.D)
idx += n_b
def predict_proba(self, x):
x = np.asarray(x, dtype=np.float64)
D = self.D
cf = self.algebra.cayley_flat
# Lift input to scalar multivectors
h = np.zeros((self.input_dim, D))
h[:, 0] = x
for l in range(self.n_layers):
W, b = self.Ws[l], self.bs[l]
fo, fi = W.shape[0], W.shape[1]
Rh = (h @ cf.T).reshape(fi, D, D)
Rh_mat = Rh.transpose(0, 2, 1).reshape(fi * D, D)
W_mat = W.reshape(fo, fi * D)
z = W_mat @ Rh_mat + b
if l < self.n_layers - 1:
h = np.maximum(0.0, z)
else:
h = z
return _softmax(h[:, 0])
def predict(self, x):
return int(np.argmax(self.predict_proba(x)))
def param_count(self):
return sum(W.size + b.size for W, b in zip(self.Ws, self.bs))
def __repr__(self):
dims = [self.input_dim] + self.hidden_dims + [self.output_dim]
arch = ' > '.join(str(d) for d in dims)
return f'GoshawkNet({arch}, Cl({self.algebra.p},{self.algebra.q}), {self.param_count():,} params)'
# ── Loader ─────────────────────────────────────────────────────────────────
def load_model(name):
"""
Load a benchmark model by name.
Parameters
----------
name : str
One of: 'ecg-heartbeat', 'eeg-emotions', 'eye-state',
'seizure-prediction', 'har-smartphones'
Returns
-------
model : GoshawkNet with loaded weights
"""
if name not in CONFIGS:
available = ', '.join(sorted(CONFIGS.keys()))
raise ValueError(f"Unknown model '{name}'. Available: {available}")
cfg = CONFIGS[name]
p, q = cfg["algebra"]
model = GoshawkNet(
input_dim=cfg["input_dim"],
hidden_dims=cfg["hidden_dims"],
output_dim=cfg["output_dim"],
p=p, q=q,
)
weights_path = ROOT / name / "weights.txt"
with open(weights_path) as f:
params = np.array([float(x) for x in f.read().split()])
model.set_params(params)
model.class_names = cfg["class_names"]
return model
def list_models():
"""List available benchmark models with their configs."""
for name, cfg in CONFIGS.items():
p, q = cfg["algebra"]
model = GoshawkNet(cfg["input_dim"], cfg["hidden_dims"],
cfg["output_dim"], p=p, q=q)
print(f" {name:<25} {model} classes={cfg['output_dim']}")
if __name__ == "__main__":
print("Available models:\n")
list_models()
print("\n\nQuick test β€” loading all models:\n")
for name in CONFIGS:
model = load_model(name)
x = np.random.randn(model.input_dim)
proba = model.predict_proba(x)
top = model.class_names[np.argmax(proba)]
print(f" {name:<25} {top:<20} (prob={proba.max():.3f}, params={model.param_count():,})")