@seanhogg/builderforce-memory-engine 2026.10.1 → 2026.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/evermind/delta_wire.d.ts +16 -1
- package/dist/evermind/delta_wire.d.ts.map +1 -1
- package/dist/evermind/delta_wire.js +22 -2
- package/dist/evermind/delta_wire.js.map +1 -1
- package/dist/evermind/index.d.ts +2 -2
- package/dist/evermind/index.d.ts.map +1 -1
- package/dist/evermind/index.js +1 -1
- package/dist/evermind/index.js.map +1 -1
- package/dist/evermind/merge.d.ts +5 -2
- package/dist/evermind/merge.d.ts.map +1 -1
- package/dist/evermind/merge.js +5 -2
- package/dist/evermind/merge.js.map +1 -1
- package/dist/evermind/ml_head.d.ts +28 -0
- package/dist/evermind/ml_head.d.ts.map +1 -0
- package/dist/evermind/ml_head.js +47 -0
- package/dist/evermind/ml_head.js.map +1 -0
- package/dist/evermind/package.d.ts +2 -2
- package/dist/evermind/package.d.ts.map +1 -1
- package/dist/evermind/package.js +1 -1
- package/dist/evermind/package.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/limbic/limbic_model.d.ts.map +1 -1
- package/dist/limbic/limbic_model.js +1 -3
- package/dist/limbic/limbic_model.js.map +1 -1
- package/dist/ml/classical/anomaly_composite.d.ts +32 -0
- package/dist/ml/classical/anomaly_composite.d.ts.map +1 -0
- package/dist/ml/classical/anomaly_composite.js +29 -0
- package/dist/ml/classical/anomaly_composite.js.map +1 -0
- package/dist/ml/classical/ewma.d.ts +6 -1
- package/dist/ml/classical/ewma.d.ts.map +1 -1
- package/dist/ml/classical/ewma.js +5 -3
- package/dist/ml/classical/ewma.js.map +1 -1
- package/dist/ml/classical/gbm.d.ts.map +1 -1
- package/dist/ml/classical/gbm.js +2 -1
- package/dist/ml/classical/gbm.js.map +1 -1
- package/dist/ml/classical/index.d.ts +4 -0
- package/dist/ml/classical/index.d.ts.map +1 -1
- package/dist/ml/classical/index.js +4 -0
- package/dist/ml/classical/index.js.map +1 -1
- package/dist/ml/classical/kmeans.d.ts.map +1 -1
- package/dist/ml/classical/kmeans.js +23 -4
- package/dist/ml/classical/kmeans.js.map +1 -1
- package/dist/ml/classical/knn.d.ts +4 -1
- package/dist/ml/classical/knn.d.ts.map +1 -1
- package/dist/ml/classical/knn.js +2 -1
- package/dist/ml/classical/knn.js.map +1 -1
- package/dist/ml/classical/logistic.d.ts +1 -9
- package/dist/ml/classical/logistic.d.ts.map +1 -1
- package/dist/ml/classical/logistic.js +17 -9
- package/dist/ml/classical/logistic.js.map +1 -1
- package/dist/ml/classical/naive_bayes.d.ts +5 -1
- package/dist/ml/classical/naive_bayes.d.ts.map +1 -1
- package/dist/ml/classical/naive_bayes.js +8 -3
- package/dist/ml/classical/naive_bayes.js.map +1 -1
- package/dist/ml/classical/robust_z.d.ts +9 -1
- package/dist/ml/classical/robust_z.d.ts.map +1 -1
- package/dist/ml/classical/robust_z.js +2 -1
- package/dist/ml/classical/robust_z.js.map +1 -1
- package/dist/ml/eval_loss.d.ts +15 -0
- package/dist/ml/eval_loss.d.ts.map +1 -0
- package/dist/ml/eval_loss.js +20 -0
- package/dist/ml/eval_loss.js.map +1 -0
- package/dist/ml/federate.d.ts +11 -25
- package/dist/ml/federate.d.ts.map +1 -1
- package/dist/ml/federate.js +15 -38
- package/dist/ml/federate.js.map +1 -1
- package/dist/ml/head_trainer.d.ts +4 -2
- package/dist/ml/head_trainer.d.ts.map +1 -1
- package/dist/ml/head_trainer.js +19 -19
- package/dist/ml/head_trainer.js.map +1 -1
- package/dist/ml/run.d.ts +11 -2
- package/dist/ml/run.d.ts.map +1 -1
- package/dist/ml/run.js +56 -17
- package/dist/ml/run.js.map +1 -1
- package/dist/ml/tasks/experience.d.ts +42 -5
- package/dist/ml/tasks/experience.d.ts.map +1 -1
- package/dist/ml/tasks/experience.js +76 -19
- package/dist/ml/tasks/experience.js.map +1 -1
- package/dist/ml/tasks/run_anomaly.d.ts +20 -19
- package/dist/ml/tasks/run_anomaly.d.ts.map +1 -1
- package/dist/ml/tasks/run_anomaly.js +20 -30
- package/dist/ml/tasks/run_anomaly.js.map +1 -1
- package/dist/ml/train.d.ts +2 -7
- package/dist/ml/train.d.ts.map +1 -1
- package/dist/ml/train.js +2 -19
- package/dist/ml/train.js.map +1 -1
- package/dist/ml/types.d.ts +6 -3
- package/dist/ml/types.d.ts.map +1 -1
- package/dist/ml/types.js +3 -2
- package/dist/ml/types.js.map +1 -1
- package/package.json +1 -1
- package/src/evermind/delta_wire.ts +173 -148
- package/src/evermind/index.ts +36 -35
- package/src/evermind/merge.ts +10 -4
- package/src/evermind/ml_head.ts +63 -0
- package/src/evermind/package.ts +488 -488
- package/src/index.ts +2 -0
- package/src/limbic/limbic_model.ts +1 -4
- package/src/limbic/limbic_trainer.ts +91 -91
- package/src/ml/classical/anomaly_composite.ts +46 -0
- package/src/ml/classical/ewma.ts +7 -1
- package/src/ml/classical/gbm.ts +144 -143
- package/src/ml/classical/index.ts +6 -0
- package/src/ml/classical/kmeans.ts +22 -4
- package/src/ml/classical/knn.ts +6 -2
- package/src/ml/classical/logistic.ts +9 -9
- package/src/ml/classical/math.ts +93 -93
- package/src/ml/classical/naive_bayes.ts +10 -3
- package/src/ml/classical/robust_z.ts +11 -2
- package/src/ml/eval_loss.ts +27 -0
- package/src/ml/federate.ts +22 -52
- package/src/ml/head_trainer.ts +21 -42
- package/src/ml/registry.ts +44 -44
- package/src/ml/run.ts +57 -13
- package/src/ml/tasks/experience.ts +98 -19
- package/src/ml/tasks/run_anomaly.ts +30 -36
- package/src/ml/train.ts +3 -19
- package/src/ml/types.ts +135 -132
- package/src/training/cpu_ops.ts +141 -141
|
@@ -57,11 +57,17 @@ export function fitKMeans(samples: readonly MlSample[], opts: KMeansOptions = {}
|
|
|
57
57
|
const total = nearest.reduce((a, b) => a + b, 0);
|
|
58
58
|
let pick = 0;
|
|
59
59
|
if (total > 0) {
|
|
60
|
+
// Walk only points with weight: a zero-distance point (already a centroid) can
|
|
61
|
+
// never be picked, even when rnd() returns 0 and r starts at 0.
|
|
60
62
|
let r = rnd() * total;
|
|
61
|
-
|
|
63
|
+
let last = -1;
|
|
64
|
+
for (pick = 0; pick < X.length; pick++) {
|
|
65
|
+
if (!(nearest[pick]! > 0)) continue;
|
|
66
|
+
last = pick;
|
|
62
67
|
r -= nearest[pick]!;
|
|
63
68
|
if (r <= 0) break;
|
|
64
69
|
}
|
|
70
|
+
if (pick >= X.length) pick = last; // float round-off left r > 0: take the last weighted point
|
|
65
71
|
} else {
|
|
66
72
|
pick = centroids.length; // every point coincides with a centroid; take the next index
|
|
67
73
|
}
|
|
@@ -71,8 +77,8 @@ export function fitKMeans(samples: readonly MlSample[], opts: KMeansOptions = {}
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
const assign = new Int32Array(X.length).fill(-1);
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
/** Assign every point to its nearest current centroid; true when any assignment moved. */
|
|
81
|
+
const reassign = (): boolean => {
|
|
76
82
|
let changed = false;
|
|
77
83
|
const model = new KMeansModel(centroids, 0);
|
|
78
84
|
X.forEach((x, i) => {
|
|
@@ -82,7 +88,15 @@ export function fitKMeans(samples: readonly MlSample[], opts: KMeansOptions = {}
|
|
|
82
88
|
changed = true;
|
|
83
89
|
}
|
|
84
90
|
});
|
|
85
|
-
|
|
91
|
+
return changed;
|
|
92
|
+
};
|
|
93
|
+
const maxIt = opts.maxIterations ?? 100;
|
|
94
|
+
let converged = false;
|
|
95
|
+
for (let it = 0; it < maxIt; it++) {
|
|
96
|
+
if (!reassign()) {
|
|
97
|
+
converged = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
86
100
|
const sums = centroids.map(() => new Float64Array(d));
|
|
87
101
|
const counts = new Array<number>(k).fill(0);
|
|
88
102
|
X.forEach((x, i) => {
|
|
@@ -96,6 +110,10 @@ export function fitKMeans(samples: readonly MlSample[], opts: KMeansOptions = {}
|
|
|
96
110
|
});
|
|
97
111
|
}
|
|
98
112
|
|
|
113
|
+
// Stopped on maxIterations: the centroids moved after the last assignment, so
|
|
114
|
+
// assign once more — inertia (and the stored model) must describe the same state.
|
|
115
|
+
if (!converged) reassign();
|
|
116
|
+
|
|
99
117
|
let inertia = 0;
|
|
100
118
|
X.forEach((x, i) => (inertia += squaredDistance(x, centroids[assign[i]!]!)));
|
|
101
119
|
return new KMeansModel(centroids, inertia);
|
package/src/ml/classical/knn.ts
CHANGED
|
@@ -11,7 +11,10 @@ import { classCount, cosine, featureDim, squaredDistance, topProbability, type V
|
|
|
11
11
|
export type KnnMetric = "cosine" | "euclidean";
|
|
12
12
|
|
|
13
13
|
export interface KnnOptions {
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Neighbours that vote. Default min(5, ⌊√n⌋), at least 1: with a small set a fixed
|
|
16
|
+
* 5 makes EVERY point vote, and the answer collapses to the class prior.
|
|
17
|
+
*/
|
|
15
18
|
k?: number;
|
|
16
19
|
metric?: KnnMetric;
|
|
17
20
|
/** Number of classes (default: inferred from the labels, at least 2). */
|
|
@@ -92,7 +95,8 @@ export class KnnModel implements MlPredictor {
|
|
|
92
95
|
export function fitKnn(samples: readonly MlSample[], opts: KnnOptions = {}): KnnModel {
|
|
93
96
|
featureDim(samples.map((s) => s.x));
|
|
94
97
|
const classes = opts.classes ?? classCount(samples.map((s) => s.y));
|
|
95
|
-
const
|
|
98
|
+
const k = opts.k ?? Math.min(5, Math.max(1, Math.floor(Math.sqrt(samples.length))));
|
|
99
|
+
const model = new KnnModel(Math.min(k, samples.length), opts.metric ?? "cosine", classes);
|
|
96
100
|
for (const s of samples) model.add(s.x, s.y, s.weight ?? 1);
|
|
97
101
|
return model;
|
|
98
102
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* features on a stable scale, so a weight means the same thing on every machine —
|
|
7
7
|
* which is what lets trained weights FEDERATE (`exportParams` / `importParams`).
|
|
8
8
|
*/
|
|
9
|
+
import { mlEvalLoss } from "../eval_loss.js";
|
|
9
10
|
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
10
11
|
import { classCount, featureDim, softmax, topProbability } from "./math.js";
|
|
11
12
|
|
|
@@ -50,16 +51,9 @@ export class LogisticModel implements MlPredictor {
|
|
|
50
51
|
return { scores, confidence: topProbability(scores) };
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
/** Weighted mean cross-entropy over `samples`
|
|
54
|
+
/** Weighted mean cross-entropy over `samples` — exactly the eval-gate loss ({@link mlEvalLoss}); 0 for an empty set. */
|
|
54
55
|
loss(samples: readonly MlSample[]): number {
|
|
55
|
-
|
|
56
|
-
let wSum = 0;
|
|
57
|
-
for (const s of samples) {
|
|
58
|
-
const w = s.weight ?? 1;
|
|
59
|
-
sum += -w * Math.log(Math.max(this.proba(s.x)[s.y] ?? 0, 1e-12));
|
|
60
|
-
wSum += w;
|
|
61
|
-
}
|
|
62
|
-
return wSum > 0 ? sum / wSum : 0;
|
|
56
|
+
return mlEvalLoss("classify", this, samples) ?? 0;
|
|
63
57
|
}
|
|
64
58
|
|
|
65
59
|
/** One full-batch gradient step; returns the pre-step mean loss. */
|
|
@@ -115,6 +109,12 @@ export function fitLogistic(
|
|
|
115
109
|
): LogisticModel {
|
|
116
110
|
const d = featureDim(samples.map((s) => s.x));
|
|
117
111
|
const k = opts.classes ?? init?.classes ?? classCount(samples.map((s) => s.y));
|
|
112
|
+
// A label at or above `classes` has no row of weights: its probability would be
|
|
113
|
+
// read as undefined and every loss and gradient would turn NaN, silently.
|
|
114
|
+
const top = classCount(samples.map((s) => s.y));
|
|
115
|
+
if (top > k && samples.some((s) => s.y >= k)) {
|
|
116
|
+
throw new Error(`fitLogistic: labels go up to ${top - 1} but the model has ${k} classes`);
|
|
117
|
+
}
|
|
118
118
|
const model = init ?? new LogisticModel(d, k);
|
|
119
119
|
if (model.dim !== d || model.classes !== k) throw new Error(`fitLogistic: init is ${model.dim}×${model.classes}, data is ${d}×${k}`);
|
|
120
120
|
const lr = opts.learningRate ?? 0.5;
|
package/src/ml/classical/math.ts
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ml/classical/math.ts — the small vector helpers every classical algorithm shares.
|
|
3
|
-
* Pure, allocation-light, deterministic.
|
|
4
|
-
*/
|
|
5
|
-
import { SeededRng } from "../../utils/rng.js";
|
|
6
|
-
|
|
7
|
-
/** The engine's ONE sigmoid / softmax (`training/cpu_ops.ts`). */
|
|
8
|
-
export { sigmoid, softmax } from "../../training/cpu_ops.js";
|
|
9
|
-
|
|
10
|
-
export type Vec = ArrayLike<number>;
|
|
11
|
-
|
|
12
|
-
export function dot(a: Vec, b: Vec): number {
|
|
13
|
-
const n = Math.min(a.length, b.length);
|
|
14
|
-
let s = 0;
|
|
15
|
-
for (let i = 0; i < n; i++) s += a[i]! * b[i]!;
|
|
16
|
-
return s;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function norm(a: Vec): number {
|
|
20
|
-
return Math.sqrt(dot(a, a));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Cosine similarity in [-1, 1]; 0 when either vector is zero. */
|
|
24
|
-
export function cosine(a: Vec, b: Vec): number {
|
|
25
|
-
const na = norm(a);
|
|
26
|
-
const nb = norm(b);
|
|
27
|
-
return na > 0 && nb > 0 ? dot(a, b) / (na * nb) : 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function squaredDistance(a: Vec, b: Vec): number {
|
|
31
|
-
const n = Math.min(a.length, b.length);
|
|
32
|
-
let s = 0;
|
|
33
|
-
for (let i = 0; i < n; i++) {
|
|
34
|
-
const d = a[i]! - b[i]!;
|
|
35
|
-
s += d * d;
|
|
36
|
-
}
|
|
37
|
-
return s;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Median of a numeric list (copy-sorted). NaN for an empty list. */
|
|
41
|
-
export function median(values: readonly number[]): number {
|
|
42
|
-
if (values.length === 0) return Number.NaN;
|
|
43
|
-
const s = [...values].sort((a, b) => a - b);
|
|
44
|
-
const m = s.length >> 1;
|
|
45
|
-
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Index of the largest value (first on ties). */
|
|
49
|
-
export function argmax(values: ArrayLike<number>): number {
|
|
50
|
-
let best = 0;
|
|
51
|
-
for (let i = 1; i < values.length; i++) if (values[i]! > values[best]!) best = i;
|
|
52
|
-
return best;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Number of distinct classes implied by integer labels 0..k-1. */
|
|
56
|
-
export function classCount(labels: readonly number[]): number {
|
|
57
|
-
let k = 0;
|
|
58
|
-
for (const y of labels) {
|
|
59
|
-
if (!Number.isInteger(y) || y < 0) throw new Error(`class label must be a non-negative integer, got ${y}`);
|
|
60
|
-
k = Math.max(k, y + 1);
|
|
61
|
-
}
|
|
62
|
-
return Math.max(k, 2);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Feature dimension of a non-empty sample matrix; throws on ragged rows. */
|
|
66
|
-
export function featureDim(X: readonly Vec[]): number {
|
|
67
|
-
if (X.length === 0) throw new Error("no samples");
|
|
68
|
-
const d = X[0]!.length;
|
|
69
|
-
for (const x of X) if (x.length !== d) throw new Error(`ragged features: expected ${d}, got ${x.length}`);
|
|
70
|
-
return d;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Confidence of a probability vector: its top probability. */
|
|
74
|
-
export function topProbability(p: readonly number[]): number {
|
|
75
|
-
return p.length ? Math.max(...p) : 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** A deterministic [0,1) source for algorithms that sample — the engine's ONE seeded RNG. */
|
|
79
|
-
export function seededUniform(seed: number): () => number {
|
|
80
|
-
const rng = new SeededRng(seed);
|
|
81
|
-
return () => rng.next();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Average path length of an unsuccessful BST search over n points — the
|
|
86
|
-
* isolation-forest normaliser c(n).
|
|
87
|
-
*/
|
|
88
|
-
export function averagePathLength(n: number): number {
|
|
89
|
-
if (n <= 1) return 0;
|
|
90
|
-
if (n === 2) return 1;
|
|
91
|
-
const harmonic = Math.log(n - 1) + 0.5772156649;
|
|
92
|
-
return 2 * harmonic - (2 * (n - 1)) / n;
|
|
93
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/math.ts — the small vector helpers every classical algorithm shares.
|
|
3
|
+
* Pure, allocation-light, deterministic.
|
|
4
|
+
*/
|
|
5
|
+
import { SeededRng } from "../../utils/rng.js";
|
|
6
|
+
|
|
7
|
+
/** The engine's ONE sigmoid / softmax (`training/cpu_ops.ts`). */
|
|
8
|
+
export { sigmoid, softmax } from "../../training/cpu_ops.js";
|
|
9
|
+
|
|
10
|
+
export type Vec = ArrayLike<number>;
|
|
11
|
+
|
|
12
|
+
export function dot(a: Vec, b: Vec): number {
|
|
13
|
+
const n = Math.min(a.length, b.length);
|
|
14
|
+
let s = 0;
|
|
15
|
+
for (let i = 0; i < n; i++) s += a[i]! * b[i]!;
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function norm(a: Vec): number {
|
|
20
|
+
return Math.sqrt(dot(a, a));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Cosine similarity in [-1, 1]; 0 when either vector is zero. */
|
|
24
|
+
export function cosine(a: Vec, b: Vec): number {
|
|
25
|
+
const na = norm(a);
|
|
26
|
+
const nb = norm(b);
|
|
27
|
+
return na > 0 && nb > 0 ? dot(a, b) / (na * nb) : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function squaredDistance(a: Vec, b: Vec): number {
|
|
31
|
+
const n = Math.min(a.length, b.length);
|
|
32
|
+
let s = 0;
|
|
33
|
+
for (let i = 0; i < n; i++) {
|
|
34
|
+
const d = a[i]! - b[i]!;
|
|
35
|
+
s += d * d;
|
|
36
|
+
}
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Median of a numeric list (copy-sorted). NaN for an empty list. */
|
|
41
|
+
export function median(values: readonly number[]): number {
|
|
42
|
+
if (values.length === 0) return Number.NaN;
|
|
43
|
+
const s = [...values].sort((a, b) => a - b);
|
|
44
|
+
const m = s.length >> 1;
|
|
45
|
+
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Index of the largest value (first on ties). */
|
|
49
|
+
export function argmax(values: ArrayLike<number>): number {
|
|
50
|
+
let best = 0;
|
|
51
|
+
for (let i = 1; i < values.length; i++) if (values[i]! > values[best]!) best = i;
|
|
52
|
+
return best;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Number of distinct classes implied by integer labels 0..k-1. */
|
|
56
|
+
export function classCount(labels: readonly number[]): number {
|
|
57
|
+
let k = 0;
|
|
58
|
+
for (const y of labels) {
|
|
59
|
+
if (!Number.isInteger(y) || y < 0) throw new Error(`class label must be a non-negative integer, got ${y}`);
|
|
60
|
+
k = Math.max(k, y + 1);
|
|
61
|
+
}
|
|
62
|
+
return Math.max(k, 2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Feature dimension of a non-empty sample matrix; throws on ragged rows. */
|
|
66
|
+
export function featureDim(X: readonly Vec[]): number {
|
|
67
|
+
if (X.length === 0) throw new Error("no samples");
|
|
68
|
+
const d = X[0]!.length;
|
|
69
|
+
for (const x of X) if (x.length !== d) throw new Error(`ragged features: expected ${d}, got ${x.length}`);
|
|
70
|
+
return d;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Confidence of a probability vector: its top probability. */
|
|
74
|
+
export function topProbability(p: readonly number[]): number {
|
|
75
|
+
return p.length ? Math.max(...p) : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A deterministic [0,1) source for algorithms that sample — the engine's ONE seeded RNG. */
|
|
79
|
+
export function seededUniform(seed: number): () => number {
|
|
80
|
+
const rng = new SeededRng(seed);
|
|
81
|
+
return () => rng.next();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Average path length of an unsuccessful BST search over n points — the
|
|
86
|
+
* isolation-forest normaliser c(n).
|
|
87
|
+
*/
|
|
88
|
+
export function averagePathLength(n: number): number {
|
|
89
|
+
if (n <= 1) return 0;
|
|
90
|
+
if (n === 2) return 1;
|
|
91
|
+
const harmonic = Math.log(n - 1) + 0.5772156649;
|
|
92
|
+
return 2 * harmonic - (2 * (n - 1)) / n;
|
|
93
|
+
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Per class: a weighted prior, and an independent Gaussian per feature. Variance
|
|
5
5
|
* is smoothed by a fraction of the largest feature variance (as scikit-learn's
|
|
6
6
|
* `var_smoothing`), so a constant feature never divides by zero.
|
|
7
|
+
*
|
|
8
|
+
* A class with no training weight has prior −∞ and probability exactly 0: its
|
|
9
|
+
* Gaussian is a placeholder (mean 0, variance ≈ the smoothing floor), which would
|
|
10
|
+
* otherwise out-score every real class on an input near zero.
|
|
7
11
|
*/
|
|
8
12
|
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
9
13
|
import { classCount, featureDim, softmax, topProbability } from "./math.js";
|
|
@@ -21,11 +25,12 @@ export class NaiveBayesModel implements MlPredictor {
|
|
|
21
25
|
readonly variance: Float64Array[],
|
|
22
26
|
) {}
|
|
23
27
|
|
|
24
|
-
/** Per-class joint log-likelihood log P(c) + Σ log N(x_i | μ, σ²). */
|
|
28
|
+
/** Per-class joint log-likelihood log P(c) + Σ log N(x_i | μ, σ²); −∞ for an unseen class. */
|
|
25
29
|
jointLogLikelihood(x: ArrayLike<number>): number[] {
|
|
26
30
|
return this.mean.map((mu, c) => {
|
|
27
31
|
const v = this.variance[c]!;
|
|
28
32
|
let ll = this.logPrior[c]!;
|
|
33
|
+
if (ll === -Infinity) return ll;
|
|
29
34
|
for (let i = 0; i < mu.length; i++) {
|
|
30
35
|
const d = (x[i] ?? 0) - mu[i]!;
|
|
31
36
|
ll += -0.5 * Math.log(2 * Math.PI * v[i]!) - (d * d) / (2 * v[i]!);
|
|
@@ -71,7 +76,9 @@ export function fitNaiveBayes(samples: readonly MlSample[], opts: NaiveBayesOpti
|
|
|
71
76
|
for (let c = 0; c < k; c++) for (let i = 0; i < d; i++) variance[c]![i] = variance[c]![i]! + eps;
|
|
72
77
|
|
|
73
78
|
const total = wSum.reduce((a, b) => a + b, 0);
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
if (!(total > 0)) throw new Error("fitNaiveBayes: the samples carry no weight");
|
|
80
|
+
// An unseen class is masked out (−∞, so softmax gives it exactly 0). At least one
|
|
81
|
+
// class has weight, so softmax always has a finite maximum.
|
|
82
|
+
const logPrior = Float64Array.from(wSum, (w) => (w > 0 ? Math.log(w / total) : -Infinity));
|
|
76
83
|
return new NaiveBayesModel(logPrior, mean, variance);
|
|
77
84
|
}
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Per feature: z = |x − median| / (1.4826 · MAD). Median and MAD are not dragged
|
|
5
5
|
* by the very outliers being looked for, unlike mean and standard deviation. When
|
|
6
6
|
* MAD is 0 (more than half the history identical) the mean absolute deviation
|
|
7
|
-
* stands in, then
|
|
7
|
+
* stands in, then two floors: a fraction of |median| (relative) and `minScale`
|
|
8
|
+
* (absolute, in the feature's own units). Without the absolute one a history that
|
|
9
|
+
* is all zeros — no tool failures, ever — gets a scale of ~1e-9, and the first
|
|
10
|
+
* small non-zero value scores astronomically.
|
|
8
11
|
*
|
|
9
12
|
* Prediction: `scores[0] = max_i z_i / threshold` — ≥ 1 is anomalous.
|
|
10
13
|
*/
|
|
@@ -21,6 +24,11 @@ export interface RobustZOptions {
|
|
|
21
24
|
threshold?: number;
|
|
22
25
|
/** Smallest scale per feature, as a fraction of |median| (plus 1e-9). Default 0.01. */
|
|
23
26
|
minScaleFraction?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Smallest scale per feature in absolute feature units. Default 0 — the algorithm
|
|
29
|
+
* cannot know the units; a task on a fixed scale sets it (`run.anomaly` does).
|
|
30
|
+
*/
|
|
31
|
+
minScale?: number;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export class RobustZModel implements MlPredictor {
|
|
@@ -50,6 +58,7 @@ export function fitRobustZ(samples: readonly MlSample[], opts: RobustZOptions =
|
|
|
50
58
|
const X = samples.map((s) => s.x);
|
|
51
59
|
const d = featureDim(X);
|
|
52
60
|
const minFrac = opts.minScaleFraction ?? 0.01;
|
|
61
|
+
const minAbs = opts.minScale ?? 0;
|
|
53
62
|
const center = new Float64Array(d);
|
|
54
63
|
const scale = new Float64Array(d);
|
|
55
64
|
for (let i = 0; i < d; i++) {
|
|
@@ -59,7 +68,7 @@ export function fitRobustZ(samples: readonly MlSample[], opts: RobustZOptions =
|
|
|
59
68
|
let s = MAD_TO_SD * median(dev);
|
|
60
69
|
if (!(s > 0)) s = MEANAD_TO_SD * (dev.reduce((a, b) => a + b, 0) / dev.length);
|
|
61
70
|
center[i] = med;
|
|
62
|
-
scale[i] = Math.max(s, minFrac * Math.abs(med) + 1e-9);
|
|
71
|
+
scale[i] = Math.max(s, minFrac * Math.abs(med) + 1e-9, minAbs);
|
|
63
72
|
}
|
|
64
73
|
return new RobustZModel(center, scale, opts.threshold ?? 3.5);
|
|
65
74
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/eval_loss.ts — the ONE held-out loss for catalog models.
|
|
3
|
+
*
|
|
4
|
+
* What the trainer reports as `holdoutLoss`, what a federated merge is gated on, and
|
|
5
|
+
* what `LogisticModel.loss` returns. A leaf (types only), so a classical algorithm can
|
|
6
|
+
* use it without importing the trainer that imports the algorithms.
|
|
7
|
+
*/
|
|
8
|
+
import type { MlPredictor, MlSample, MlTaskKind } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mean loss of `predictor` on `samples` for a task of `kind` — classify: weighted
|
|
12
|
+
* cross-entropy of the true class; regress: weighted ½·squared error. Null for
|
|
13
|
+
* kinds without a supervised loss, or an empty (or zero-weight) set.
|
|
14
|
+
*/
|
|
15
|
+
export function mlEvalLoss(kind: MlTaskKind, predictor: MlPredictor, samples: readonly MlSample[]): number | null {
|
|
16
|
+
if (samples.length === 0 || (kind !== "classify" && kind !== "regress")) return null;
|
|
17
|
+
let sum = 0;
|
|
18
|
+
let wSum = 0;
|
|
19
|
+
for (const s of samples) {
|
|
20
|
+
const w = s.weight ?? 1;
|
|
21
|
+
const p = predictor.predict(s.x);
|
|
22
|
+
const loss = kind === "classify" ? -Math.log(Math.max(p.scores[s.y] ?? 0, 1e-12)) : 0.5 * (p.scores[0]! - s.y) ** 2;
|
|
23
|
+
sum += w * loss;
|
|
24
|
+
wSum += w;
|
|
25
|
+
}
|
|
26
|
+
return wSum > 0 ? sum / wSum : null;
|
|
27
|
+
}
|
package/src/ml/federate.ts
CHANGED
|
@@ -8,62 +8,26 @@
|
|
|
8
8
|
* `target: "ml:<task>"`, and the writer folds N of them with the existing FedAvg
|
|
9
9
|
* (`mergeCheckpointDiffsGated` → `mergeCheckpointDiffs`), refusing a merge whose
|
|
10
10
|
* held-out loss regresses. No second wire, no second merge.
|
|
11
|
+
*
|
|
12
|
+
* The head's manifest entry and checkpoint format live with the artifact
|
|
13
|
+
* (`evermind/ml_head.ts`); this module only imports evermind/, never the reverse.
|
|
11
14
|
*/
|
|
12
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
buildDeltaLearnPayload,
|
|
17
|
+
decodeDeltaPayload,
|
|
18
|
+
deltaTargetMismatch,
|
|
19
|
+
deltaUnusableReason,
|
|
20
|
+
mlHeadTarget,
|
|
21
|
+
type DeltaLearnPayload,
|
|
22
|
+
} from "../evermind/delta_wire.js";
|
|
13
23
|
import { mergeCheckpointDiffsGated, type GatedMergeResult, type MergeGateOptions } from "../evermind/merge.js";
|
|
24
|
+
import { headCheckpoint, headParams, type EvermindMlHead } from "../evermind/ml_head.js";
|
|
14
25
|
import type { EvermindModelPackage } from "../evermind/package.js";
|
|
15
26
|
import { base64ToBytes, bytesToBase64 } from "../utils/base64.js";
|
|
16
|
-
import { appendCrcTrailer, verifyCrcTrailer } from "../utils/crc32.js";
|
|
17
27
|
import { diffCheckpoints } from "../utils/delta.js";
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* it rides in the manifest, and versioned, so it federates — a delta against it is
|
|
22
|
-
* `target: "ml:<task>"`, `baseVersion: version` on the delta wire.
|
|
23
|
-
*/
|
|
24
|
-
export interface EvermindMlHead {
|
|
25
|
-
/** Catalog task id, e.g. `run.local-success`. */
|
|
26
|
-
task: string;
|
|
27
|
-
/** Engine key, e.g. `local:classical:logistic`. */
|
|
28
|
-
engine: string;
|
|
29
|
-
/** Integer head version — what a federated delta for it is diffed against. */
|
|
30
|
-
version: number;
|
|
31
|
-
/** What a loader needs to rebuild the predictor (e.g. `{ dim, classes }`). */
|
|
32
|
-
shape: Record<string, number>;
|
|
33
|
-
/** base64 of the CRC-trailed f32 checkpoint of the head's flat parameters. */
|
|
34
|
-
params: string;
|
|
35
|
-
/** Held-out loss of this version, when measured. */
|
|
36
|
-
evalLoss?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** A head's flat parameters as a CRC-trailed f32 checkpoint. */
|
|
40
|
-
export function headCheckpoint(params: ArrayLike<number>): ArrayBuffer {
|
|
41
|
-
const body = new ArrayBuffer(params.length * 4);
|
|
42
|
-
new Float32Array(body).set(Array.from(params));
|
|
43
|
-
return appendCrcTrailer(body);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** The flat parameters of a head checkpoint. Throws on a failed CRC. */
|
|
47
|
-
export function headParams(checkpoint: ArrayBuffer): Float32Array {
|
|
48
|
-
const crc = verifyCrcTrailer(checkpoint);
|
|
49
|
-
if (!crc.ok) throw new Error("headParams: checkpoint failed its CRC check");
|
|
50
|
-
if (crc.body.byteLength % 4 !== 0) throw new Error("headParams: checkpoint is not f32");
|
|
51
|
-
return new Float32Array(crc.body);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Why a manifest head entry is unusable, or null (the package's validate() calls this). */
|
|
55
|
-
export function mlHeadUnusableReason(h: EvermindMlHead): string | null {
|
|
56
|
-
if (!h || typeof h.task !== "string" || typeof h.engine !== "string") return "task and engine are required";
|
|
57
|
-
if (!Number.isInteger(h.version) || h.version < 0) return "version must be a non-negative integer";
|
|
58
|
-
try {
|
|
59
|
-
const crc = verifyCrcTrailer(base64ToBytes(h.params));
|
|
60
|
-
if (!crc.hasTrailer || !crc.ok) return "params checkpoint failed its CRC check";
|
|
61
|
-
if (crc.body.byteLength % 4 !== 0) return "params checkpoint is not f32";
|
|
62
|
-
} catch {
|
|
63
|
-
return "params is not base64";
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
29
|
+
export { headCheckpoint, headParams, mlHeadUnusableReason } from "../evermind/ml_head.js";
|
|
30
|
+
export type { EvermindMlHead } from "../evermind/ml_head.js";
|
|
67
31
|
|
|
68
32
|
/** A manifest entry for a head's current parameters. */
|
|
69
33
|
export function mlHeadEntry(
|
|
@@ -121,6 +85,11 @@ export interface MlHeadMergeOutcome {
|
|
|
121
85
|
* target, stale base or a malformed diff costs only itself), FedAvg the rest, and
|
|
122
86
|
* keep the result only if `score` (held-out loss of a parameter vector, lower is
|
|
123
87
|
* better) does not regress.
|
|
88
|
+
*
|
|
89
|
+
* Fails CLOSED: `requireEval` defaults to true here, so a round `score` cannot judge
|
|
90
|
+
* (null — e.g. `mlEvalLoss` for an anomaly/cluster head or an empty holdout — or a
|
|
91
|
+
* non-finite loss) is refused. A caller that federates an unscorable head on purpose
|
|
92
|
+
* passes `{ requireEval: false }` explicitly.
|
|
124
93
|
*/
|
|
125
94
|
export function mergeMlHeadDeltas(
|
|
126
95
|
base: EvermindMlHead,
|
|
@@ -134,7 +103,8 @@ export function mergeMlHeadDeltas(
|
|
|
134
103
|
const weights: number[] = [];
|
|
135
104
|
const rejected: { index: number; reason: string }[] = [];
|
|
136
105
|
payloads.forEach((p, index) => {
|
|
137
|
-
|
|
106
|
+
const wrongTarget = deltaTargetMismatch(p.target, target);
|
|
107
|
+
if (wrongTarget) return void rejected.push({ index, reason: wrongTarget });
|
|
138
108
|
if (p.baseVersion !== base.version) return void rejected.push({ index, reason: `stale base version ${p.baseVersion} (head is ${base.version})` });
|
|
139
109
|
let diff: ArrayBuffer;
|
|
140
110
|
try {
|
|
@@ -148,7 +118,7 @@ export function mergeMlHeadDeltas(
|
|
|
148
118
|
weights.push(p.weight ?? 1);
|
|
149
119
|
});
|
|
150
120
|
|
|
151
|
-
const merge = mergeCheckpointDiffsGated(baseCp, diffs, weights, (cp) => score(headParams(cp)), opts);
|
|
121
|
+
const merge = mergeCheckpointDiffsGated(baseCp, diffs, weights, (cp) => score(headParams(cp)), { ...opts, requireEval: opts.requireEval ?? true });
|
|
152
122
|
if (!merge.accepted) return { head: base, merge, rejected };
|
|
153
123
|
const head: EvermindMlHead = {
|
|
154
124
|
...base,
|
package/src/ml/head_trainer.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Full-batch gradient descent with AdamW. When a GPUDevice is supplied the
|
|
7
7
|
* optimiser step runs on the GPU via the shared WEIGHT_UPDATE_WGSL kernel; with
|
|
8
|
-
* no device it
|
|
9
|
-
* everywhere (CI, Node without
|
|
8
|
+
* no device it runs the kernel's CPU mirror, `adamwUpdateInPlace` (`optim/adamw.ts`,
|
|
9
|
+
* NaN/Inf guard included), so training works everywhere (CI, Node without
|
|
10
|
+
* @webgpu/node, a laptop with no GPU). Neither path bounds the step (maxDelta 0).
|
|
10
11
|
*
|
|
11
12
|
* The head owns its forward/backward and its loss (MSE for regression, BCE /
|
|
12
13
|
* softmax cross-entropy for classification); this file owns only the loop:
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
cdiv,
|
|
24
25
|
} from "../utils/gpu_utils.js";
|
|
25
26
|
import { WEIGHT_UPDATE_WGSL } from "../kernels/weight_update.js";
|
|
27
|
+
import { adamwUpdateInPlace, type AdamWKernelStep } from "../optim/adamw.js";
|
|
26
28
|
|
|
27
29
|
/** A named trainable tensor (flat row-major Float32Array). */
|
|
28
30
|
export interface HeadParam {
|
|
@@ -63,29 +65,19 @@ interface AdamMoment {
|
|
|
63
65
|
v: Float32Array;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
weightDecay: number;
|
|
69
|
-
beta1: number;
|
|
70
|
-
beta2: number;
|
|
71
|
-
eps: number;
|
|
72
|
-
beta1_t: number;
|
|
73
|
-
beta2_t: number;
|
|
74
|
-
}
|
|
68
|
+
/** Byte size of WEIGHT_UPDATE_WGSL's `AdamParams` uniform: 1 u32 + 8 f32 + 3 f32 pad = 48. */
|
|
69
|
+
const ADAM_PARAMS_BYTES = 48;
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
beta1_t: number,
|
|
84
|
-
beta2_t: number,
|
|
85
|
-
): ArrayBuffer {
|
|
86
|
-
const buf = new ArrayBuffer(32);
|
|
71
|
+
/**
|
|
72
|
+
* Pack `AdamParams` exactly as WEIGHT_UPDATE_WGSL declares it:
|
|
73
|
+
* `num_elements: u32, lr, beta1, beta2, eps, weight_decay, beta1_t, beta2_t,
|
|
74
|
+
* max_delta, _pad0, _pad1, _pad2` (all f32 after the first) — 48 bytes.
|
|
75
|
+
*/
|
|
76
|
+
function packAdamParams(numElements: number, hp: AdamWKernelStep): ArrayBuffer {
|
|
77
|
+
const buf = new ArrayBuffer(ADAM_PARAMS_BYTES);
|
|
87
78
|
new Uint32Array(buf, 0, 1).set([numElements]);
|
|
88
|
-
|
|
79
|
+
// Pads stay 0 (fresh ArrayBuffer).
|
|
80
|
+
new Float32Array(buf, 4, 8).set([hp.lr, hp.beta1, hp.beta2, hp.eps, hp.weightDecay, hp.beta1_t, hp.beta2_t, hp.maxDelta]);
|
|
89
81
|
return buf;
|
|
90
82
|
}
|
|
91
83
|
|
|
@@ -159,7 +151,7 @@ export class HeadTrainer<S> {
|
|
|
159
151
|
this._step++;
|
|
160
152
|
const beta1_t = Math.pow(beta1, this._step);
|
|
161
153
|
const beta2_t = Math.pow(beta2, this._step);
|
|
162
|
-
const hp:
|
|
154
|
+
const hp: AdamWKernelStep = { lr: learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t, maxDelta: 0 };
|
|
163
155
|
if (this.gpuTraining) {
|
|
164
156
|
await this._adamwStepGpu(grads, hp);
|
|
165
157
|
} else {
|
|
@@ -181,30 +173,20 @@ export class HeadTrainer<S> {
|
|
|
181
173
|
return total / samples.length;
|
|
182
174
|
}
|
|
183
175
|
|
|
184
|
-
|
|
176
|
+
/** AdamW on the CPU via the kernel's shared mirror (`adamwUpdateInPlace`). */
|
|
177
|
+
private _adamwStepCpu(grads: HeadParam[], hp: AdamWKernelStep): void {
|
|
185
178
|
const params = this.head.parameters();
|
|
186
|
-
const { learningRate: lr, weightDecay: wd, beta1, beta2, eps, beta1_t, beta2_t } = hp;
|
|
187
179
|
for (let pi = 0; pi < params.length; pi++) {
|
|
188
|
-
const p = params[pi]!.data;
|
|
189
|
-
const g = grads[pi]!.data;
|
|
190
180
|
const mom = this._moments![pi]!;
|
|
191
|
-
|
|
192
|
-
const gi = g[i]!;
|
|
193
|
-
mom.m[i] = beta1 * mom.m[i]! + (1 - beta1) * gi;
|
|
194
|
-
mom.v[i] = beta2 * mom.v[i]! + (1 - beta2) * gi * gi;
|
|
195
|
-
const mHat = mom.m[i]! / (1 - beta1_t);
|
|
196
|
-
const vHat = mom.v[i]! / (1 - beta2_t);
|
|
197
|
-
p[i] = p[i]! * (1 - lr * wd) - (lr * mHat) / (Math.sqrt(vHat) + eps);
|
|
198
|
-
}
|
|
181
|
+
adamwUpdateInPlace(params[pi]!.data, grads[pi]!.data, mom.m, mom.v, hp);
|
|
199
182
|
}
|
|
200
183
|
}
|
|
201
184
|
|
|
202
185
|
/** AdamW on the GPU via the shared WEIGHT_UPDATE_WGSL kernel. Awaited per step. */
|
|
203
|
-
private async _adamwStepGpu(grads: HeadParam[], hp:
|
|
186
|
+
private async _adamwStepGpu(grads: HeadParam[], hp: AdamWKernelStep): Promise<void> {
|
|
204
187
|
const device = this.device!;
|
|
205
188
|
const pipeline = this._adamwPipeline!;
|
|
206
189
|
const params = this.head.parameters();
|
|
207
|
-
const { learningRate: lr, weightDecay: wd, beta1, beta2, eps, beta1_t, beta2_t } = hp;
|
|
208
190
|
|
|
209
191
|
for (let pi = 0; pi < params.length; pi++) {
|
|
210
192
|
const p = params[pi]!;
|
|
@@ -213,10 +195,7 @@ export class HeadTrainer<S> {
|
|
|
213
195
|
const gradBuf = createStorageBuffer(device, grads[pi]!.data, false);
|
|
214
196
|
const mBuf = createStorageBuffer(device, mom.m, true);
|
|
215
197
|
const vBuf = createStorageBuffer(device, mom.v, true);
|
|
216
|
-
const uni = createUniformBuffer(
|
|
217
|
-
device,
|
|
218
|
-
packAdamParams(p.numel, lr, beta1, beta2, eps, wd, beta1_t, beta2_t),
|
|
219
|
-
);
|
|
198
|
+
const uni = createUniformBuffer(device, packAdamParams(p.numel, hp));
|
|
220
199
|
const bg = createBindGroup(device, pipeline, [uni, paramBuf, gradBuf, mBuf, vBuf]);
|
|
221
200
|
dispatchKernel(device, pipeline, bg, [cdiv(p.numel, 256), 1, 1]);
|
|
222
201
|
|