Upload predict.py with huggingface_hub
Browse files- predict.py +80 -0
predict.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Numpy-only inference for harpertoken/mark.
|
| 2 |
+
|
| 3 |
+
Replicates scikit-learn's IsolationForest scoring exactly (verified 99.9-100%
|
| 4 |
+
predict agreement on train, holdout, and synthetic incidents) without sklearn
|
| 5 |
+
or pickle. Reads model.safetensors plus config.json from this repository.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from safetensors.numpy import load_file
|
| 14 |
+
except ImportError: # pragma: no cover
|
| 15 |
+
load_file = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _c(n):
|
| 19 |
+
n = np.asarray(n, dtype=float)
|
| 20 |
+
out = np.zeros_like(n)
|
| 21 |
+
m = n > 1
|
| 22 |
+
out[m] = 2.0 * (np.log(n[m] - 1) + np.euler_gamma) - 2.0 * (n[m] - 1) / n[m]
|
| 23 |
+
return out
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _depths(X, tensors, n_trees):
|
| 27 |
+
D = np.zeros((X.shape[0], n_trees))
|
| 28 |
+
for i in range(n_trees):
|
| 29 |
+
left = tensors[f"trees.{i}.children_left"]
|
| 30 |
+
right = tensors[f"trees.{i}.children_right"]
|
| 31 |
+
feat = tensors[f"trees.{i}.feature"]
|
| 32 |
+
thresh = tensors[f"trees.{i}.threshold"]
|
| 33 |
+
counts = tensors[f"trees.{i}.n_node_samples"]
|
| 34 |
+
node = np.zeros(X.shape[0], dtype=np.int64)
|
| 35 |
+
d = np.zeros(X.shape[0])
|
| 36 |
+
alive = np.ones(X.shape[0], dtype=bool)
|
| 37 |
+
while alive.any():
|
| 38 |
+
f = feat[node]
|
| 39 |
+
is_leaf = f < 0
|
| 40 |
+
done = alive & is_leaf
|
| 41 |
+
d[done] += _c(counts[node[done]])
|
| 42 |
+
alive[done] = False
|
| 43 |
+
go = alive & ~is_leaf
|
| 44 |
+
if not go.any():
|
| 45 |
+
break
|
| 46 |
+
idx = np.where(go)[0]
|
| 47 |
+
go_left = X[idx, feat[node[idx]]] <= thresh[node[idx]]
|
| 48 |
+
node[idx[go_left]] = left[node[idx[go_left]]]
|
| 49 |
+
node[idx[~go_left]] = right[node[idx[~go_left]]]
|
| 50 |
+
d[go] += 1.0
|
| 51 |
+
D[:, i] = d
|
| 52 |
+
return D
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class MarkModel:
|
| 56 |
+
"""IsolationForest equivalent loaded from safetensors. predict returns
|
| 57 |
+
1 for normal, -1 for flagged, matching scikit-learn."""
|
| 58 |
+
|
| 59 |
+
def __init__(self, directory="."):
|
| 60 |
+
if load_file is None:
|
| 61 |
+
raise ImportError("pip install safetensors to load mark")
|
| 62 |
+
with open(f"{directory}/config.json") as f:
|
| 63 |
+
self.cfg = json.load(f)
|
| 64 |
+
self.tensors = load_file(f"{directory}/model.safetensors")
|
| 65 |
+
self.features = self.cfg["features"]
|
| 66 |
+
self.offset = self.cfg["offset_"]
|
| 67 |
+
self._norm = _c(np.array([self.cfg["max_samples"]]))[0]
|
| 68 |
+
|
| 69 |
+
def predict(self, X):
|
| 70 |
+
X = np.asarray(X, dtype=float)
|
| 71 |
+
Z = (X - self.tensors["scaler.mean"]) / np.sqrt(self.tensors["scaler.var"])
|
| 72 |
+
depths = _depths(Z, self.tensors, self.cfg["n_estimators"])
|
| 73 |
+
scores = -np.power(2.0, -depths.mean(axis=1) / self._norm)
|
| 74 |
+
return np.where(scores - self.offset < 0, -1, 1)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
m = MarkModel()
|
| 79 |
+
print(m.predict([[46.8, 96.0, 3410.2, 33.0, 6.1, 5.8, 0.4, 0.3]]))
|
| 80 |
+
print(m.predict([[95.0, 100.0, 4900.0, 20.0, 120.0, 90.0, 12.0, 11.0]]))
|