@seanhogg/builderforce-memory-engine 2026.9.34 → 2026.10.1
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 +10 -1
- package/dist/evermind/delta_wire.d.ts.map +1 -1
- package/dist/evermind/delta_wire.js +25 -3
- package/dist/evermind/delta_wire.js.map +1 -1
- package/dist/evermind/index.d.ts +4 -4
- package/dist/evermind/index.d.ts.map +1 -1
- package/dist/evermind/index.js +2 -2
- package/dist/evermind/index.js.map +1 -1
- package/dist/evermind/merge.d.ts +25 -0
- package/dist/evermind/merge.d.ts.map +1 -1
- package/dist/evermind/merge.js +30 -0
- package/dist/evermind/merge.js.map +1 -1
- package/dist/evermind/package.d.ts +9 -0
- package/dist/evermind/package.d.ts.map +1 -1
- package/dist/evermind/package.js +20 -0
- package/dist/evermind/package.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/limbic/index.d.ts +3 -0
- package/dist/limbic/index.d.ts.map +1 -1
- package/dist/limbic/index.js +3 -0
- package/dist/limbic/index.js.map +1 -1
- package/dist/limbic/limbic_task.d.ts +22 -0
- package/dist/limbic/limbic_task.d.ts.map +1 -0
- package/dist/limbic/limbic_task.js +45 -0
- package/dist/limbic/limbic_task.js.map +1 -0
- package/dist/limbic/limbic_trainer.d.ts +17 -30
- package/dist/limbic/limbic_trainer.d.ts.map +1 -1
- package/dist/limbic/limbic_trainer.js +38 -144
- package/dist/limbic/limbic_trainer.js.map +1 -1
- package/dist/ml/classical/ewma.d.ts +32 -0
- package/dist/ml/classical/ewma.d.ts.map +1 -0
- package/dist/ml/classical/ewma.js +46 -0
- package/dist/ml/classical/ewma.js.map +1 -0
- package/dist/ml/classical/gbm.d.ts +42 -0
- package/dist/ml/classical/gbm.d.ts.map +1 -0
- package/dist/ml/classical/gbm.js +116 -0
- package/dist/ml/classical/gbm.js.map +1 -0
- package/dist/ml/classical/index.d.ts +48 -0
- package/dist/ml/classical/index.d.ts.map +1 -0
- package/dist/ml/classical/index.js +45 -0
- package/dist/ml/classical/index.js.map +1 -0
- package/dist/ml/classical/isolation_forest.d.ts +37 -0
- package/dist/ml/classical/isolation_forest.d.ts.map +1 -0
- package/dist/ml/classical/isolation_forest.js +72 -0
- package/dist/ml/classical/isolation_forest.js.map +1 -0
- package/dist/ml/classical/kmeans.d.ts +31 -0
- package/dist/ml/classical/kmeans.d.ts.map +1 -0
- package/dist/ml/classical/kmeans.js +93 -0
- package/dist/ml/classical/kmeans.js.map +1 -0
- package/dist/ml/classical/knn.d.ts +48 -0
- package/dist/ml/classical/knn.d.ts.map +1 -0
- package/dist/ml/classical/knn.js +68 -0
- package/dist/ml/classical/knn.js.map +1 -0
- package/dist/ml/classical/linear.d.ts +29 -0
- package/dist/ml/classical/linear.d.ts.map +1 -0
- package/dist/ml/classical/linear.js +114 -0
- package/dist/ml/classical/linear.js.map +1 -0
- package/dist/ml/classical/logistic.d.ts +39 -0
- package/dist/ml/classical/logistic.d.ts.map +1 -0
- package/dist/ml/classical/logistic.js +103 -0
- package/dist/ml/classical/logistic.js.map +1 -0
- package/dist/ml/classical/math.d.ts +26 -0
- package/dist/ml/classical/math.d.ts.map +1 -0
- package/dist/ml/classical/math.js +90 -0
- package/dist/ml/classical/math.js.map +1 -0
- package/dist/ml/classical/naive_bayes.d.ts +24 -0
- package/dist/ml/classical/naive_bayes.d.ts.map +1 -0
- package/dist/ml/classical/naive_bayes.js +67 -0
- package/dist/ml/classical/naive_bayes.js.map +1 -0
- package/dist/ml/classical/robust_z.d.ts +30 -0
- package/dist/ml/classical/robust_z.d.ts.map +1 -0
- package/dist/ml/classical/robust_z.js +46 -0
- package/dist/ml/classical/robust_z.js.map +1 -0
- package/dist/ml/dense_head.d.ts +33 -0
- package/dist/ml/dense_head.d.ts.map +1 -0
- package/dist/ml/dense_head.js +140 -0
- package/dist/ml/dense_head.js.map +1 -0
- package/dist/ml/features.d.ts +25 -0
- package/dist/ml/features.d.ts.map +1 -0
- package/dist/ml/features.js +46 -0
- package/dist/ml/features.js.map +1 -0
- package/dist/ml/federate.d.ts +70 -0
- package/dist/ml/federate.d.ts.map +1 -0
- package/dist/ml/federate.js +122 -0
- package/dist/ml/federate.js.map +1 -0
- package/dist/ml/head_trainer.d.ts +76 -0
- package/dist/ml/head_trainer.d.ts.map +1 -0
- package/dist/ml/head_trainer.js +182 -0
- package/dist/ml/head_trainer.js.map +1 -0
- package/dist/ml/index.d.ts +23 -0
- package/dist/ml/index.d.ts.map +1 -0
- package/dist/ml/index.js +11 -0
- package/dist/ml/index.js.map +1 -0
- package/dist/ml/registry.d.ts +20 -0
- package/dist/ml/registry.d.ts.map +1 -0
- package/dist/ml/registry.js +43 -0
- package/dist/ml/registry.js.map +1 -0
- package/dist/ml/run.d.ts +94 -0
- package/dist/ml/run.d.ts.map +1 -0
- package/dist/ml/run.js +130 -0
- package/dist/ml/run.js.map +1 -0
- package/dist/ml/tasks/experience.d.ts +90 -0
- package/dist/ml/tasks/experience.d.ts.map +1 -0
- package/dist/ml/tasks/experience.js +156 -0
- package/dist/ml/tasks/experience.js.map +1 -0
- package/dist/ml/tasks/index.d.ts +10 -0
- package/dist/ml/tasks/index.d.ts.map +1 -0
- package/dist/ml/tasks/index.js +28 -0
- package/dist/ml/tasks/index.js.map +1 -0
- package/dist/ml/tasks/local_success.d.ts +55 -0
- package/dist/ml/tasks/local_success.d.ts.map +1 -0
- package/dist/ml/tasks/local_success.js +77 -0
- package/dist/ml/tasks/local_success.js.map +1 -0
- package/dist/ml/tasks/run_anomaly.d.ts +51 -0
- package/dist/ml/tasks/run_anomaly.d.ts.map +1 -0
- package/dist/ml/tasks/run_anomaly.js +60 -0
- package/dist/ml/tasks/run_anomaly.js.map +1 -0
- package/dist/ml/train.d.ts +64 -0
- package/dist/ml/train.d.ts.map +1 -0
- package/dist/ml/train.js +101 -0
- package/dist/ml/train.js.map +1 -0
- package/dist/ml/types.d.ts +112 -0
- package/dist/ml/types.d.ts.map +1 -0
- package/dist/ml/types.js +23 -0
- package/dist/ml/types.js.map +1 -0
- package/dist/moe/moe_model.d.ts.map +1 -1
- package/dist/moe/moe_model.js +1 -16
- package/dist/moe/moe_model.js.map +1 -1
- package/dist/training/cpu_ops.d.ts +2 -0
- package/dist/training/cpu_ops.d.ts.map +1 -1
- package/dist/training/cpu_ops.js +16 -0
- package/dist/training/cpu_ops.js.map +1 -1
- package/package.json +1 -1
- package/src/evermind/delta_wire.ts +148 -114
- package/src/evermind/index.ts +35 -32
- package/src/evermind/merge.ts +53 -0
- package/src/evermind/package.ts +488 -464
- package/src/index.ts +15 -1
- package/src/limbic/index.ts +5 -0
- package/src/limbic/limbic_task.ts +62 -0
- package/src/limbic/limbic_trainer.ts +91 -258
- package/src/ml/classical/ewma.ts +68 -0
- package/src/ml/classical/gbm.ts +143 -0
- package/src/ml/classical/index.ts +76 -0
- package/src/ml/classical/isolation_forest.ts +88 -0
- package/src/ml/classical/kmeans.ts +102 -0
- package/src/ml/classical/knn.ts +98 -0
- package/src/ml/classical/linear.ts +119 -0
- package/src/ml/classical/logistic.ts +125 -0
- package/src/ml/classical/math.ts +93 -0
- package/src/ml/classical/naive_bayes.ts +77 -0
- package/src/ml/classical/robust_z.ts +65 -0
- package/src/ml/dense_head.ts +157 -0
- package/src/ml/features.ts +61 -0
- package/src/ml/federate.ts +160 -0
- package/src/ml/head_trainer.ts +268 -0
- package/src/ml/index.ts +57 -0
- package/src/ml/registry.ts +44 -0
- package/src/ml/run.ts +178 -0
- package/src/ml/tasks/experience.ts +214 -0
- package/src/ml/tasks/index.ts +30 -0
- package/src/ml/tasks/local_success.ts +118 -0
- package/src/ml/tasks/run_anomaly.ts +90 -0
- package/src/ml/train.ts +136 -0
- package/src/ml/types.ts +132 -0
- package/src/moe/moe_model.ts +1 -13
- package/src/training/cpu_ops.ts +141 -127
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/logistic.ts — (multinomial) logistic regression.
|
|
3
|
+
*
|
|
4
|
+
* Softmax regression with L2, fitted by full-batch gradient descent; two classes
|
|
5
|
+
* is ordinary logistic regression. No internal feature scaling: a task supplies
|
|
6
|
+
* features on a stable scale, so a weight means the same thing on every machine —
|
|
7
|
+
* which is what lets trained weights FEDERATE (`exportParams` / `importParams`).
|
|
8
|
+
*/
|
|
9
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
10
|
+
import { classCount, featureDim, softmax, topProbability } from "./math.js";
|
|
11
|
+
|
|
12
|
+
export interface LogisticOptions {
|
|
13
|
+
classes?: number;
|
|
14
|
+
learningRate?: number;
|
|
15
|
+
epochs?: number;
|
|
16
|
+
/** L2 penalty on weights (not biases). Default 1e-3. */
|
|
17
|
+
l2?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class LogisticModel implements MlPredictor {
|
|
21
|
+
/** Row-major weights [classes × dim]. */
|
|
22
|
+
readonly weights: Float32Array;
|
|
23
|
+
readonly bias: Float32Array;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
readonly dim: number,
|
|
27
|
+
readonly classes: number,
|
|
28
|
+
) {
|
|
29
|
+
this.weights = new Float32Array(classes * dim);
|
|
30
|
+
this.bias = new Float32Array(classes);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
logits(x: ArrayLike<number>): number[] {
|
|
34
|
+
const z: number[] = [];
|
|
35
|
+
for (let c = 0; c < this.classes; c++) {
|
|
36
|
+
let s = this.bias[c]!;
|
|
37
|
+
const off = c * this.dim;
|
|
38
|
+
for (let i = 0; i < this.dim; i++) s += this.weights[off + i]! * (x[i] ?? 0);
|
|
39
|
+
z.push(s);
|
|
40
|
+
}
|
|
41
|
+
return z;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
proba(x: ArrayLike<number>): number[] {
|
|
45
|
+
return Array.from(softmax(this.logits(x)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
predict(x: Float32Array): MlPrediction {
|
|
49
|
+
const scores = this.proba(x);
|
|
50
|
+
return { scores, confidence: topProbability(scores) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Weighted mean cross-entropy over `samples` (the eval-gate loss). */
|
|
54
|
+
loss(samples: readonly MlSample[]): number {
|
|
55
|
+
let sum = 0;
|
|
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;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One full-batch gradient step; returns the pre-step mean loss. */
|
|
66
|
+
step(samples: readonly MlSample[], learningRate: number, l2: number): number {
|
|
67
|
+
const gW = new Float64Array(this.weights.length);
|
|
68
|
+
const gB = new Float64Array(this.classes);
|
|
69
|
+
let loss = 0;
|
|
70
|
+
let wSum = 0;
|
|
71
|
+
for (const s of samples) {
|
|
72
|
+
const w = s.weight ?? 1;
|
|
73
|
+
const p = this.proba(s.x);
|
|
74
|
+
loss += -w * Math.log(Math.max(p[s.y]!, 1e-12));
|
|
75
|
+
wSum += w;
|
|
76
|
+
for (let c = 0; c < this.classes; c++) {
|
|
77
|
+
const g = w * (p[c]! - (c === s.y ? 1 : 0));
|
|
78
|
+
gB[c] = gB[c]! + g;
|
|
79
|
+
const off = c * this.dim;
|
|
80
|
+
for (let i = 0; i < this.dim; i++) gW[off + i] = gW[off + i]! + g * s.x[i]!;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const inv = wSum > 0 ? 1 / wSum : 0;
|
|
84
|
+
for (let j = 0; j < this.weights.length; j++) {
|
|
85
|
+
this.weights[j] = this.weights[j]! - learningRate * (gW[j]! * inv + l2 * this.weights[j]!);
|
|
86
|
+
}
|
|
87
|
+
for (let c = 0; c < this.classes; c++) this.bias[c] = this.bias[c]! - learningRate * gB[c]! * inv;
|
|
88
|
+
return loss * inv;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
exportParams(): Float32Array {
|
|
92
|
+
const out = new Float32Array(this.weights.length + this.bias.length);
|
|
93
|
+
out.set(this.weights, 0);
|
|
94
|
+
out.set(this.bias, this.weights.length);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
importParams(params: ArrayLike<number>): void {
|
|
99
|
+
if (params.length !== this.weights.length + this.bias.length) {
|
|
100
|
+
throw new Error(`LogisticModel.importParams: expected ${this.weights.length + this.bias.length} values, got ${params.length}`);
|
|
101
|
+
}
|
|
102
|
+
for (let j = 0; j < this.weights.length; j++) this.weights[j] = params[j]!;
|
|
103
|
+
for (let c = 0; c < this.bias.length; c++) this.bias[c] = params[this.weights.length + c]!;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fit logistic regression. Pass `init` to continue from existing weights (a local
|
|
109
|
+
* round on top of a merged federated head).
|
|
110
|
+
*/
|
|
111
|
+
export function fitLogistic(
|
|
112
|
+
samples: readonly MlSample[],
|
|
113
|
+
opts: LogisticOptions = {},
|
|
114
|
+
init?: LogisticModel,
|
|
115
|
+
): LogisticModel {
|
|
116
|
+
const d = featureDim(samples.map((s) => s.x));
|
|
117
|
+
const k = opts.classes ?? init?.classes ?? classCount(samples.map((s) => s.y));
|
|
118
|
+
const model = init ?? new LogisticModel(d, k);
|
|
119
|
+
if (model.dim !== d || model.classes !== k) throw new Error(`fitLogistic: init is ${model.dim}×${model.classes}, data is ${d}×${k}`);
|
|
120
|
+
const lr = opts.learningRate ?? 0.5;
|
|
121
|
+
const l2 = opts.l2 ?? 1e-3;
|
|
122
|
+
const epochs = opts.epochs ?? 300;
|
|
123
|
+
for (let e = 0; e < epochs; e++) model.step(samples, lr, l2);
|
|
124
|
+
return model;
|
|
125
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/naive_bayes.ts — Gaussian naive Bayes.
|
|
3
|
+
*
|
|
4
|
+
* Per class: a weighted prior, and an independent Gaussian per feature. Variance
|
|
5
|
+
* is smoothed by a fraction of the largest feature variance (as scikit-learn's
|
|
6
|
+
* `var_smoothing`), so a constant feature never divides by zero.
|
|
7
|
+
*/
|
|
8
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
9
|
+
import { classCount, featureDim, softmax, topProbability } from "./math.js";
|
|
10
|
+
|
|
11
|
+
export interface NaiveBayesOptions {
|
|
12
|
+
classes?: number;
|
|
13
|
+
/** Fraction of the largest feature variance added to every variance. Default 1e-9. */
|
|
14
|
+
varSmoothing?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class NaiveBayesModel implements MlPredictor {
|
|
18
|
+
constructor(
|
|
19
|
+
readonly logPrior: Float64Array,
|
|
20
|
+
readonly mean: Float64Array[],
|
|
21
|
+
readonly variance: Float64Array[],
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
/** Per-class joint log-likelihood log P(c) + Σ log N(x_i | μ, σ²). */
|
|
25
|
+
jointLogLikelihood(x: ArrayLike<number>): number[] {
|
|
26
|
+
return this.mean.map((mu, c) => {
|
|
27
|
+
const v = this.variance[c]!;
|
|
28
|
+
let ll = this.logPrior[c]!;
|
|
29
|
+
for (let i = 0; i < mu.length; i++) {
|
|
30
|
+
const d = (x[i] ?? 0) - mu[i]!;
|
|
31
|
+
ll += -0.5 * Math.log(2 * Math.PI * v[i]!) - (d * d) / (2 * v[i]!);
|
|
32
|
+
}
|
|
33
|
+
return ll;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
predict(x: Float32Array): MlPrediction {
|
|
38
|
+
const scores = Array.from(softmax(this.jointLogLikelihood(x)));
|
|
39
|
+
return { scores, confidence: topProbability(scores) };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function fitNaiveBayes(samples: readonly MlSample[], opts: NaiveBayesOptions = {}): NaiveBayesModel {
|
|
44
|
+
const d = featureDim(samples.map((s) => s.x));
|
|
45
|
+
const k = opts.classes ?? classCount(samples.map((s) => s.y));
|
|
46
|
+
const wSum = new Float64Array(k);
|
|
47
|
+
const mean = Array.from({ length: k }, () => new Float64Array(d));
|
|
48
|
+
const variance = Array.from({ length: k }, () => new Float64Array(d));
|
|
49
|
+
|
|
50
|
+
for (const s of samples) {
|
|
51
|
+
const w = s.weight ?? 1;
|
|
52
|
+
wSum[s.y] = wSum[s.y]! + w;
|
|
53
|
+
for (let i = 0; i < d; i++) mean[s.y]![i] = mean[s.y]![i]! + w * s.x[i]!;
|
|
54
|
+
}
|
|
55
|
+
for (let c = 0; c < k; c++) if (wSum[c]! > 0) for (let i = 0; i < d; i++) mean[c]![i] = mean[c]![i]! / wSum[c]!;
|
|
56
|
+
for (const s of samples) {
|
|
57
|
+
const w = s.weight ?? 1;
|
|
58
|
+
for (let i = 0; i < d; i++) {
|
|
59
|
+
const diff = s.x[i]! - mean[s.y]![i]!;
|
|
60
|
+
variance[s.y]![i] = variance[s.y]![i]! + w * diff * diff;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let maxVar = 0;
|
|
64
|
+
for (let c = 0; c < k; c++) {
|
|
65
|
+
for (let i = 0; i < d; i++) {
|
|
66
|
+
variance[c]![i] = wSum[c]! > 0 ? variance[c]![i]! / wSum[c]! : 0;
|
|
67
|
+
maxVar = Math.max(maxVar, variance[c]![i]!);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const eps = (opts.varSmoothing ?? 1e-9) * Math.max(maxVar, 1e-12) + 1e-12;
|
|
71
|
+
for (let c = 0; c < k; c++) for (let i = 0; i < d; i++) variance[c]![i] = variance[c]![i]! + eps;
|
|
72
|
+
|
|
73
|
+
const total = wSum.reduce((a, b) => a + b, 0);
|
|
74
|
+
// An unseen class keeps a vanishing (not -∞) prior so softmax stays defined.
|
|
75
|
+
const logPrior = Float64Array.from(wSum, (w) => Math.log(Math.max(w, 1e-12) / total));
|
|
76
|
+
return new NaiveBayesModel(logPrior, mean, variance);
|
|
77
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/robust_z.ts — robust (median/MAD) z-scores for anomaly detection.
|
|
3
|
+
*
|
|
4
|
+
* Per feature: z = |x − median| / (1.4826 · MAD). Median and MAD are not dragged
|
|
5
|
+
* by the very outliers being looked for, unlike mean and standard deviation. When
|
|
6
|
+
* MAD is 0 (more than half the history identical) the mean absolute deviation
|
|
7
|
+
* stands in, then a floor, so a constant history still scores a change.
|
|
8
|
+
*
|
|
9
|
+
* Prediction: `scores[0] = max_i z_i / threshold` — ≥ 1 is anomalous.
|
|
10
|
+
*/
|
|
11
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
12
|
+
import { featureDim, median } from "./math.js";
|
|
13
|
+
|
|
14
|
+
/** MAD → standard deviation for normally-distributed data. */
|
|
15
|
+
const MAD_TO_SD = 1.4826;
|
|
16
|
+
/** Mean absolute deviation → standard deviation for normally-distributed data. */
|
|
17
|
+
const MEANAD_TO_SD = 1.2533;
|
|
18
|
+
|
|
19
|
+
export interface RobustZOptions {
|
|
20
|
+
/** z at which a feature counts as anomalous. Default 3.5 (Iglewicz & Hoaglin). */
|
|
21
|
+
threshold?: number;
|
|
22
|
+
/** Smallest scale per feature, as a fraction of |median| (plus 1e-9). Default 0.01. */
|
|
23
|
+
minScaleFraction?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RobustZModel implements MlPredictor {
|
|
27
|
+
constructor(
|
|
28
|
+
readonly center: Float64Array,
|
|
29
|
+
readonly scale: Float64Array,
|
|
30
|
+
readonly threshold: number,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
/** Per-feature robust z-scores (absolute). */
|
|
34
|
+
zScores(x: ArrayLike<number>): number[] {
|
|
35
|
+
return Array.from(this.center, (c, i) => Math.abs((x[i] ?? 0) - c) / this.scale[i]!);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
predict(x: Float32Array): MlPrediction {
|
|
39
|
+
const score = Math.max(0, ...this.zScores(x)) / this.threshold;
|
|
40
|
+
return { scores: [score], confidence: anomalyConfidence(score) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Confidence of an anomaly score normalised so 1 is the threshold: distance from it, capped. */
|
|
45
|
+
export function anomalyConfidence(score: number): number {
|
|
46
|
+
return Math.max(0, Math.min(1, Math.abs(score - 1)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function fitRobustZ(samples: readonly MlSample[], opts: RobustZOptions = {}): RobustZModel {
|
|
50
|
+
const X = samples.map((s) => s.x);
|
|
51
|
+
const d = featureDim(X);
|
|
52
|
+
const minFrac = opts.minScaleFraction ?? 0.01;
|
|
53
|
+
const center = new Float64Array(d);
|
|
54
|
+
const scale = new Float64Array(d);
|
|
55
|
+
for (let i = 0; i < d; i++) {
|
|
56
|
+
const col = X.map((x) => x[i]!);
|
|
57
|
+
const med = median(col);
|
|
58
|
+
const dev = col.map((v) => Math.abs(v - med));
|
|
59
|
+
let s = MAD_TO_SD * median(dev);
|
|
60
|
+
if (!(s > 0)) s = MEANAD_TO_SD * (dev.reduce((a, b) => a + b, 0) / dev.length);
|
|
61
|
+
center[i] = med;
|
|
62
|
+
scale[i] = Math.max(s, minFrac * Math.abs(med) + 1e-9);
|
|
63
|
+
}
|
|
64
|
+
return new RobustZModel(center, scale, opts.threshold ?? 3.5);
|
|
65
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/dense_head.ts — the catalog's trainable head: linear, or one tanh hidden layer,
|
|
3
|
+
* over a feature vector (usually an experience embedding).
|
|
4
|
+
*
|
|
5
|
+
* It is a {@link TrainableHead} (so the ONE {@link HeadTrainer} fits it, on the
|
|
6
|
+
* GPU when there is one) and an {@link MlPredictor} (so the runner answers with
|
|
7
|
+
* it). Its parameters flatten to one vector — the unit that federates.
|
|
8
|
+
*/
|
|
9
|
+
import { gaussianArray, SeededRng } from "../utils/rng.js";
|
|
10
|
+
import { softmax } from "../training/cpu_ops.js";
|
|
11
|
+
import { assignParams, flattenParams, type HeadParam, type TrainableHead } from "./head_trainer.js";
|
|
12
|
+
import type { MlPrediction, MlPredictor, MlSample } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export interface DenseHeadConfig {
|
|
15
|
+
inputDim: number;
|
|
16
|
+
/** Hidden width; 0 = a linear head. */
|
|
17
|
+
hiddenDim: number;
|
|
18
|
+
/** Classes (softmax) or 1 (mse regression). */
|
|
19
|
+
outputDim: number;
|
|
20
|
+
/** `softmax` = classification over `outputDim` classes; `mse` = scalar regression. */
|
|
21
|
+
objective: "softmax" | "mse";
|
|
22
|
+
seed?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DenseHead implements TrainableHead<MlSample>, MlPredictor {
|
|
26
|
+
readonly config: Required<DenseHeadConfig>;
|
|
27
|
+
private readonly _params: HeadParam[];
|
|
28
|
+
private readonly _grads: HeadParam[];
|
|
29
|
+
|
|
30
|
+
constructor(config: DenseHeadConfig) {
|
|
31
|
+
this.config = { seed: 0x4e4d, ...config };
|
|
32
|
+
const { inputDim, hiddenDim, outputDim, objective } = this.config;
|
|
33
|
+
if (objective === "mse" && outputDim !== 1) throw new Error("DenseHead: mse objective needs outputDim 1");
|
|
34
|
+
if (objective === "softmax" && outputDim < 2) throw new Error("DenseHead: softmax objective needs outputDim ≥ 2");
|
|
35
|
+
const rng = new SeededRng(this.config.seed);
|
|
36
|
+
const next = () => rng.next();
|
|
37
|
+
const inner = hiddenDim > 0 ? hiddenDim : inputDim;
|
|
38
|
+
const shapes: [string, number, number][] = [
|
|
39
|
+
...(hiddenDim > 0
|
|
40
|
+
? ([
|
|
41
|
+
["w1", hiddenDim * inputDim, 1 / Math.sqrt(inputDim)],
|
|
42
|
+
["b1", hiddenDim, 0],
|
|
43
|
+
] as [string, number, number][])
|
|
44
|
+
: []),
|
|
45
|
+
["w2", outputDim * inner, 1 / Math.sqrt(inner)],
|
|
46
|
+
["b2", outputDim, 0],
|
|
47
|
+
];
|
|
48
|
+
this._params = shapes.map(([name, n, std]) => ({ name, numel: n, data: std > 0 ? gaussianArray(n, std, next) : new Float32Array(n) }));
|
|
49
|
+
this._grads = shapes.map(([name, n]) => ({ name, numel: n, data: new Float32Array(n) }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private _p(name: string): Float32Array {
|
|
53
|
+
return this._params.find((p) => p.name === name)!.data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private _g(name: string): Float32Array {
|
|
57
|
+
return this._grads.find((p) => p.name === name)!.data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
parameters(): HeadParam[] {
|
|
61
|
+
return this._params;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
gradients(): HeadParam[] {
|
|
65
|
+
return this._grads;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
zeroGrad(): void {
|
|
69
|
+
for (const g of this._grads) g.data.fill(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Hidden activation (or the input itself for a linear head) and the output logits/value. */
|
|
73
|
+
private _forward(x: ArrayLike<number>): { h: Float32Array; z: Float32Array } {
|
|
74
|
+
const { inputDim, hiddenDim, outputDim } = this.config;
|
|
75
|
+
let h: Float32Array;
|
|
76
|
+
if (hiddenDim > 0) {
|
|
77
|
+
const w1 = this._p("w1");
|
|
78
|
+
const b1 = this._p("b1");
|
|
79
|
+
h = new Float32Array(hiddenDim);
|
|
80
|
+
for (let j = 0; j < hiddenDim; j++) {
|
|
81
|
+
let s = b1[j]!;
|
|
82
|
+
for (let i = 0; i < inputDim; i++) s += w1[j * inputDim + i]! * (x[i] ?? 0);
|
|
83
|
+
h[j] = Math.tanh(s);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
h = Float32Array.from({ length: inputDim }, (_, i) => x[i] ?? 0);
|
|
87
|
+
}
|
|
88
|
+
const w2 = this._p("w2");
|
|
89
|
+
const b2 = this._p("b2");
|
|
90
|
+
const z = new Float32Array(outputDim);
|
|
91
|
+
for (let k = 0; k < outputDim; k++) {
|
|
92
|
+
let s = b2[k]!;
|
|
93
|
+
for (let j = 0; j < h.length; j++) s += w2[k * h.length + j]! * h[j]!;
|
|
94
|
+
z[k] = s;
|
|
95
|
+
}
|
|
96
|
+
return { h, z };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Loss and dLoss/dz of one sample (unweighted). */
|
|
100
|
+
private _lossGrad(z: Float32Array, y: number): { loss: number; dz: Float32Array } {
|
|
101
|
+
if (this.config.objective === "mse") {
|
|
102
|
+
const d = z[0]! - y;
|
|
103
|
+
return { loss: 0.5 * d * d, dz: Float32Array.of(d) };
|
|
104
|
+
}
|
|
105
|
+
const p = softmax(z);
|
|
106
|
+
const dz = Float32Array.from(p, (v, k) => v - (k === y ? 1 : 0));
|
|
107
|
+
return { loss: -Math.log(Math.max(p[y] ?? 0, 1e-12)), dz };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
loss(s: MlSample): number {
|
|
111
|
+
return (s.weight ?? 1) * this._lossGrad(this._forward(s.x).z, s.y).loss;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
accumulate(s: MlSample): number {
|
|
115
|
+
const w = s.weight ?? 1;
|
|
116
|
+
const { inputDim, hiddenDim, outputDim } = this.config;
|
|
117
|
+
const { h, z } = this._forward(s.x);
|
|
118
|
+
const { loss, dz } = this._lossGrad(z, s.y);
|
|
119
|
+
const w2 = this._p("w2");
|
|
120
|
+
const gW2 = this._g("w2");
|
|
121
|
+
const gB2 = this._g("b2");
|
|
122
|
+
const dh = new Float32Array(h.length);
|
|
123
|
+
for (let k = 0; k < outputDim; k++) {
|
|
124
|
+
const g = w * dz[k]!;
|
|
125
|
+
gB2[k] = gB2[k]! + g;
|
|
126
|
+
for (let j = 0; j < h.length; j++) {
|
|
127
|
+
gW2[k * h.length + j] = gW2[k * h.length + j]! + g * h[j]!;
|
|
128
|
+
dh[j] = dh[j]! + g * w2[k * h.length + j]!;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (hiddenDim > 0) {
|
|
132
|
+
const gW1 = this._g("w1");
|
|
133
|
+
const gB1 = this._g("b1");
|
|
134
|
+
for (let j = 0; j < hiddenDim; j++) {
|
|
135
|
+
const dpre = dh[j]! * (1 - h[j]! * h[j]!);
|
|
136
|
+
gB1[j] = gB1[j]! + dpre;
|
|
137
|
+
for (let i = 0; i < inputDim; i++) gW1[j * inputDim + i] = gW1[j * inputDim + i]! + dpre * (s.x[i] ?? 0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return w * loss;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
predict(x: Float32Array): MlPrediction {
|
|
144
|
+
const { z } = this._forward(x);
|
|
145
|
+
if (this.config.objective === "mse") return { scores: [z[0]!], confidence: 1 };
|
|
146
|
+
const scores = Array.from(softmax(z));
|
|
147
|
+
return { scores, confidence: Math.max(...scores) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
exportParams(): Float32Array {
|
|
151
|
+
return flattenParams(this._params);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
importParams(params: ArrayLike<number>): void {
|
|
155
|
+
assignParams(this._params, params);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/features.ts — how an input becomes a feature vector.
|
|
3
|
+
*
|
|
4
|
+
* Text goes through the existing CPU embedder seam ({@link TextEmbedder}, which
|
|
5
|
+
* `EvermindTextEmbedder` implements: the person's own Evermind turns text into
|
|
6
|
+
* an L2-normalised vector with no GPU and no network). Numeric fields are
|
|
7
|
+
* appended as the task supplies them — on a FIXED scale (see {@link logScale}),
|
|
8
|
+
* never one fitted to local data, so a federated weight means the same thing on
|
|
9
|
+
* every machine.
|
|
10
|
+
*/
|
|
11
|
+
import type { TextEmbedder } from "../lm/text_embedder.js";
|
|
12
|
+
import type { FeatureSpec } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export type { TextEmbedder } from "../lm/text_embedder.js";
|
|
15
|
+
|
|
16
|
+
/** Feature vector of one input: `[embed(text) …, numeric …]`. */
|
|
17
|
+
export async function extractFeatures<I>(spec: FeatureSpec<I>, input: I, embedder?: TextEmbedder): Promise<Float32Array> {
|
|
18
|
+
const parts: ArrayLike<number>[] = [];
|
|
19
|
+
if (spec.text) {
|
|
20
|
+
if (!embedder) throw new Error("extractFeatures: this task embeds text but no embedder was supplied");
|
|
21
|
+
parts.push(await embedder.embed(spec.text(input)));
|
|
22
|
+
}
|
|
23
|
+
if (spec.numeric) {
|
|
24
|
+
const nums = spec.numeric(input);
|
|
25
|
+
for (const v of nums) if (!Number.isFinite(v)) throw new Error(`extractFeatures: non-finite numeric feature ${v}`);
|
|
26
|
+
parts.push(nums);
|
|
27
|
+
}
|
|
28
|
+
if (parts.length === 0) throw new Error("extractFeatures: the feature spec has neither text nor numeric fields");
|
|
29
|
+
const out = new Float32Array(parts.reduce((n, p) => n + p.length, 0));
|
|
30
|
+
let o = 0;
|
|
31
|
+
for (const p of parts) {
|
|
32
|
+
out.set(p, o);
|
|
33
|
+
o += p.length;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Feature vectors of many inputs, in order. */
|
|
39
|
+
export async function extractFeatureMatrix<I>(
|
|
40
|
+
spec: FeatureSpec<I>,
|
|
41
|
+
inputs: readonly I[],
|
|
42
|
+
embedder?: TextEmbedder,
|
|
43
|
+
): Promise<Float32Array[]> {
|
|
44
|
+
const out: Float32Array[] = [];
|
|
45
|
+
for (const i of inputs) out.push(await extractFeatures(spec, i, embedder));
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A count or size on a fixed log scale: log1p(max(v, 0)) / log1p(ref). `ref` is
|
|
51
|
+
* the value that maps to 1 (e.g. 100k tokens), so the scale is the same everywhere.
|
|
52
|
+
*/
|
|
53
|
+
export function logScale(v: number, ref: number): number {
|
|
54
|
+
return Math.log1p(Math.max(0, v)) / Math.log1p(Math.max(ref, 1));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A ratio clamped to [0, max] — fill fractions, success rates. */
|
|
58
|
+
export function clampRatio(numerator: number, denominator: number, max = 4): number {
|
|
59
|
+
if (!(denominator > 0)) return 0;
|
|
60
|
+
return Math.max(0, Math.min(max, numerator / denominator));
|
|
61
|
+
}
|