@seanhogg/builderforce-memory-engine 2026.10.1 → 2026.10.3
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/adapt.d.ts +7 -0
- package/dist/evermind/adapt.d.ts.map +1 -1
- package/dist/evermind/adapt.js +1 -1
- package/dist/evermind/adapt.js.map +1 -1
- 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/experience/index.d.ts +2 -2
- package/dist/experience/index.d.ts.map +1 -1
- package/dist/experience/index.js.map +1 -1
- package/dist/experience/store.d.ts +19 -1
- package/dist/experience/store.d.ts.map +1 -1
- package/dist/experience/store.js +14 -2
- package/dist/experience/store.js.map +1 -1
- package/dist/experience/types.d.ts +18 -0
- package/dist/experience/types.d.ts.map +1 -1
- package/dist/experience/types.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/kernels/weight_update.d.ts.map +1 -1
- package/dist/kernels/weight_update.js +12 -0
- package/dist/kernels/weight_update.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 +7 -1
- package/dist/ml/classical/ewma.d.ts.map +1 -1
- package/dist/ml/classical/ewma.js +6 -4
- 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 +6 -2
- package/dist/ml/classical/index.d.ts.map +1 -1
- package/dist/ml/classical/index.js +5 -1
- 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 +15 -5
- package/dist/ml/classical/knn.d.ts.map +1 -1
- package/dist/ml/classical/knn.js +28 -7
- 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 +15 -1
- package/dist/ml/classical/robust_z.d.ts.map +1 -1
- package/dist/ml/classical/robust_z.js +6 -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 +16 -2
- package/dist/ml/head_trainer.d.ts.map +1 -1
- package/dist/ml/head_trainer.js +64 -27
- 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 +95 -13
- package/dist/ml/tasks/experience.d.ts.map +1 -1
- package/dist/ml/tasks/experience.js +172 -20
- package/dist/ml/tasks/experience.js.map +1 -1
- package/dist/ml/tasks/run_anomaly.d.ts +37 -19
- package/dist/ml/tasks/run_anomaly.d.ts.map +1 -1
- package/dist/ml/tasks/run_anomaly.js +50 -31
- package/dist/ml/tasks/run_anomaly.js.map +1 -1
- package/dist/ml/train.d.ts +8 -7
- package/dist/ml/train.d.ts.map +1 -1
- package/dist/ml/train.js +21 -30
- 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/dist/optim/adamw.d.ts +11 -5
- package/dist/optim/adamw.d.ts.map +1 -1
- package/dist/optim/adamw.js +19 -4
- package/dist/optim/adamw.js.map +1 -1
- package/package.json +1 -1
- package/src/evermind/adapt.ts +8 -2
- 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/experience/index.ts +2 -1
- package/src/experience/store.ts +35 -3
- package/src/experience/types.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernels/weight_update.ts +12 -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 +8 -2
- package/src/ml/classical/gbm.ts +144 -143
- package/src/ml/classical/index.ts +8 -2
- package/src/ml/classical/kmeans.ts +22 -4
- package/src/ml/classical/knn.ts +31 -8
- 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 +21 -2
- package/src/ml/eval_loss.ts +27 -0
- package/src/ml/federate.ts +22 -52
- package/src/ml/head_trainer.ts +66 -48
- package/src/ml/registry.ts +44 -44
- package/src/ml/run.ts +57 -13
- package/src/ml/tasks/experience.ts +222 -23
- package/src/ml/tasks/run_anomaly.ts +62 -37
- package/src/ml/train.ts +34 -31
- package/src/ml/types.ts +135 -132
- package/src/optim/adamw.ts +21 -5
- package/src/training/cpu_ops.ts +141 -141
package/src/ml/classical/gbm.ts
CHANGED
|
@@ -1,143 +1,144 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ml/classical/gbm.ts — a small gradient-boosted tree ensemble.
|
|
3
|
-
*
|
|
4
|
-
* Depth-limited regression trees fitted to the loss gradient, one after another,
|
|
5
|
-
* shrunk by a learning rate. Objectives: `binary` (log-loss, Newton leaf values)
|
|
6
|
-
* and `regression` (squared error). Exhaustive split search over midpoints —
|
|
7
|
-
* fine for the catalog's small numeric feature sets, not for wide embeddings.
|
|
8
|
-
*/
|
|
9
|
-
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
10
|
-
import { featureDim, sigmoid, type Vec } from "./math.js";
|
|
11
|
-
|
|
12
|
-
export type GbmObjective = "binary" | "regression";
|
|
13
|
-
|
|
14
|
-
export interface GbmOptions {
|
|
15
|
-
objective?: GbmObjective;
|
|
16
|
-
trees?: number;
|
|
17
|
-
maxDepth?: number;
|
|
18
|
-
learningRate?: number;
|
|
19
|
-
/** Minimum summed sample weight in a leaf. Default 1. */
|
|
20
|
-
minLeafWeight?: number;
|
|
21
|
-
/** L2 on leaf values (Newton denominator). Default 1. */
|
|
22
|
-
lambda?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type RTree = { value: number } | { feature: number; split: number; left: RTree; right: RTree };
|
|
26
|
-
|
|
27
|
-
interface Row {
|
|
28
|
-
x: Vec;
|
|
29
|
-
g: number; // gradient
|
|
30
|
-
h: number; // hessian
|
|
31
|
-
w: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function leafValue(rows: readonly Row[], lambda: number): number {
|
|
35
|
-
let g = 0;
|
|
36
|
-
let h = 0;
|
|
37
|
-
for (const r of rows) {
|
|
38
|
-
g += r.w * r.g;
|
|
39
|
-
h += r.w * r.h;
|
|
40
|
-
}
|
|
41
|
-
return -g / (h + lambda);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** XGBoost-style structure score G²/(H+λ) of a node. */
|
|
45
|
-
function gain(g: number, h: number, lambda: number): number {
|
|
46
|
-
return (g * g) / (h + lambda);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function buildTree(rows: Row[], depth: number, opts: Required<Omit<GbmOptions, "objective" | "trees" | "learningRate">>): RTree {
|
|
50
|
-
if (depth >= opts.maxDepth || rows.length < 2) return { value: leafValue(rows, opts.lambda) };
|
|
51
|
-
let G = 0;
|
|
52
|
-
let H = 0;
|
|
53
|
-
let W = 0;
|
|
54
|
-
for (const r of rows) {
|
|
55
|
-
G += r.w * r.g;
|
|
56
|
-
H += r.w * r.h;
|
|
57
|
-
W += r.w;
|
|
58
|
-
}
|
|
59
|
-
const parent = gain(G, H, opts.lambda);
|
|
60
|
-
let best: { feature: number; split: number; score: number } | null = null;
|
|
61
|
-
const d = rows[0]!.x.length;
|
|
62
|
-
for (let f = 0; f < d; f++) {
|
|
63
|
-
const sorted = [...rows].sort((a, b) => a.x[f]! - b.x[f]!);
|
|
64
|
-
let gl = 0;
|
|
65
|
-
let hl = 0;
|
|
66
|
-
let wl = 0;
|
|
67
|
-
for (let i = 0; i < sorted.length - 1; i++) {
|
|
68
|
-
const r = sorted[i]!;
|
|
69
|
-
gl += r.w * r.g;
|
|
70
|
-
hl += r.w * r.h;
|
|
71
|
-
wl += r.w;
|
|
72
|
-
const a = r.x[f]!;
|
|
73
|
-
const b = sorted[i + 1]!.x[f]!;
|
|
74
|
-
if (a === b) continue;
|
|
75
|
-
if (wl < opts.minLeafWeight || W - wl < opts.minLeafWeight) continue;
|
|
76
|
-
const score = gain(gl, hl, opts.lambda) + gain(G - gl, H - hl, opts.lambda) - parent;
|
|
77
|
-
if (score > 1e-12 && (!best || score > best.score)) best = { feature: f, split: (a + b) / 2, score };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!best) return { value: leafValue(rows, opts.lambda) };
|
|
81
|
-
const left = rows.filter((r) => r.x[best!.feature]! < best!.split);
|
|
82
|
-
const right = rows.filter((r) => r.x[best!.feature]! >= best!.split);
|
|
83
|
-
return { feature: best.feature, split: best.split, left: buildTree(left, depth + 1, opts), right: buildTree(right, depth + 1, opts) };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function treeValue(t: RTree, x: Vec): number {
|
|
87
|
-
while (!("value" in t)) t = x[t.feature]! < t.split ? t.left : t.right;
|
|
88
|
-
return t.value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export class GbmModel implements MlPredictor {
|
|
92
|
-
constructor(
|
|
93
|
-
readonly objective: GbmObjective,
|
|
94
|
-
readonly base: number,
|
|
95
|
-
readonly trees: readonly RTree[],
|
|
96
|
-
readonly learningRate: number,
|
|
97
|
-
) {}
|
|
98
|
-
|
|
99
|
-
/** The raw additive score (log-odds for `binary`). */
|
|
100
|
-
margin(x: Vec): number {
|
|
101
|
-
let s = this.base;
|
|
102
|
-
for (const t of this.trees) s += this.learningRate * treeValue(t, x);
|
|
103
|
-
return s;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
predict(x: Float32Array): MlPrediction {
|
|
107
|
-
const m = this.margin(x);
|
|
108
|
-
if (this.objective === "regression") return { scores: [m], confidence: 1 };
|
|
109
|
-
const p = sigmoid(m);
|
|
110
|
-
return { scores: [1 - p, p], confidence: Math.max(p, 1 - p) };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function fitGbm(samples: readonly MlSample[], opts: GbmOptions = {}): GbmModel {
|
|
115
|
-
featureDim(samples.map((s) => s.x));
|
|
116
|
-
const objective = opts.objective ?? "binary";
|
|
117
|
-
const lr = opts.learningRate ?? 0.3;
|
|
118
|
-
const treeOpts = { maxDepth: opts.maxDepth ?? 2, minLeafWeight: opts.minLeafWeight ?? 1, lambda: opts.lambda ?? 1 };
|
|
119
|
-
|
|
120
|
-
let wSum = 0;
|
|
121
|
-
let ySum = 0;
|
|
122
|
-
for (const s of samples) {
|
|
123
|
-
wSum += s.weight ?? 1;
|
|
124
|
-
ySum += (s.weight ?? 1) * s.y;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/gbm.ts — a small gradient-boosted tree ensemble.
|
|
3
|
+
*
|
|
4
|
+
* Depth-limited regression trees fitted to the loss gradient, one after another,
|
|
5
|
+
* shrunk by a learning rate. Objectives: `binary` (log-loss, Newton leaf values)
|
|
6
|
+
* and `regression` (squared error). Exhaustive split search over midpoints —
|
|
7
|
+
* fine for the catalog's small numeric feature sets, not for wide embeddings.
|
|
8
|
+
*/
|
|
9
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
10
|
+
import { featureDim, sigmoid, type Vec } from "./math.js";
|
|
11
|
+
|
|
12
|
+
export type GbmObjective = "binary" | "regression";
|
|
13
|
+
|
|
14
|
+
export interface GbmOptions {
|
|
15
|
+
objective?: GbmObjective;
|
|
16
|
+
trees?: number;
|
|
17
|
+
maxDepth?: number;
|
|
18
|
+
learningRate?: number;
|
|
19
|
+
/** Minimum summed sample weight in a leaf. Default 1. */
|
|
20
|
+
minLeafWeight?: number;
|
|
21
|
+
/** L2 on leaf values (Newton denominator). Default 1. */
|
|
22
|
+
lambda?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type RTree = { value: number } | { feature: number; split: number; left: RTree; right: RTree };
|
|
26
|
+
|
|
27
|
+
interface Row {
|
|
28
|
+
x: Vec;
|
|
29
|
+
g: number; // gradient
|
|
30
|
+
h: number; // hessian
|
|
31
|
+
w: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function leafValue(rows: readonly Row[], lambda: number): number {
|
|
35
|
+
let g = 0;
|
|
36
|
+
let h = 0;
|
|
37
|
+
for (const r of rows) {
|
|
38
|
+
g += r.w * r.g;
|
|
39
|
+
h += r.w * r.h;
|
|
40
|
+
}
|
|
41
|
+
return -g / (h + lambda);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** XGBoost-style structure score G²/(H+λ) of a node. */
|
|
45
|
+
function gain(g: number, h: number, lambda: number): number {
|
|
46
|
+
return (g * g) / (h + lambda);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildTree(rows: Row[], depth: number, opts: Required<Omit<GbmOptions, "objective" | "trees" | "learningRate">>): RTree {
|
|
50
|
+
if (depth >= opts.maxDepth || rows.length < 2) return { value: leafValue(rows, opts.lambda) };
|
|
51
|
+
let G = 0;
|
|
52
|
+
let H = 0;
|
|
53
|
+
let W = 0;
|
|
54
|
+
for (const r of rows) {
|
|
55
|
+
G += r.w * r.g;
|
|
56
|
+
H += r.w * r.h;
|
|
57
|
+
W += r.w;
|
|
58
|
+
}
|
|
59
|
+
const parent = gain(G, H, opts.lambda);
|
|
60
|
+
let best: { feature: number; split: number; score: number } | null = null;
|
|
61
|
+
const d = rows[0]!.x.length;
|
|
62
|
+
for (let f = 0; f < d; f++) {
|
|
63
|
+
const sorted = [...rows].sort((a, b) => a.x[f]! - b.x[f]!);
|
|
64
|
+
let gl = 0;
|
|
65
|
+
let hl = 0;
|
|
66
|
+
let wl = 0;
|
|
67
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
68
|
+
const r = sorted[i]!;
|
|
69
|
+
gl += r.w * r.g;
|
|
70
|
+
hl += r.w * r.h;
|
|
71
|
+
wl += r.w;
|
|
72
|
+
const a = r.x[f]!;
|
|
73
|
+
const b = sorted[i + 1]!.x[f]!;
|
|
74
|
+
if (a === b) continue;
|
|
75
|
+
if (wl < opts.minLeafWeight || W - wl < opts.minLeafWeight) continue;
|
|
76
|
+
const score = gain(gl, hl, opts.lambda) + gain(G - gl, H - hl, opts.lambda) - parent;
|
|
77
|
+
if (score > 1e-12 && (!best || score > best.score)) best = { feature: f, split: (a + b) / 2, score };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!best) return { value: leafValue(rows, opts.lambda) };
|
|
81
|
+
const left = rows.filter((r) => r.x[best!.feature]! < best!.split);
|
|
82
|
+
const right = rows.filter((r) => r.x[best!.feature]! >= best!.split);
|
|
83
|
+
return { feature: best.feature, split: best.split, left: buildTree(left, depth + 1, opts), right: buildTree(right, depth + 1, opts) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function treeValue(t: RTree, x: Vec): number {
|
|
87
|
+
while (!("value" in t)) t = x[t.feature]! < t.split ? t.left : t.right;
|
|
88
|
+
return t.value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class GbmModel implements MlPredictor {
|
|
92
|
+
constructor(
|
|
93
|
+
readonly objective: GbmObjective,
|
|
94
|
+
readonly base: number,
|
|
95
|
+
readonly trees: readonly RTree[],
|
|
96
|
+
readonly learningRate: number,
|
|
97
|
+
) {}
|
|
98
|
+
|
|
99
|
+
/** The raw additive score (log-odds for `binary`). */
|
|
100
|
+
margin(x: Vec): number {
|
|
101
|
+
let s = this.base;
|
|
102
|
+
for (const t of this.trees) s += this.learningRate * treeValue(t, x);
|
|
103
|
+
return s;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
predict(x: Float32Array): MlPrediction {
|
|
107
|
+
const m = this.margin(x);
|
|
108
|
+
if (this.objective === "regression") return { scores: [m], confidence: 1 };
|
|
109
|
+
const p = sigmoid(m);
|
|
110
|
+
return { scores: [1 - p, p], confidence: Math.max(p, 1 - p) };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function fitGbm(samples: readonly MlSample[], opts: GbmOptions = {}): GbmModel {
|
|
115
|
+
featureDim(samples.map((s) => s.x));
|
|
116
|
+
const objective = opts.objective ?? "binary";
|
|
117
|
+
const lr = opts.learningRate ?? 0.3;
|
|
118
|
+
const treeOpts = { maxDepth: opts.maxDepth ?? 2, minLeafWeight: opts.minLeafWeight ?? 1, lambda: opts.lambda ?? 1 };
|
|
119
|
+
|
|
120
|
+
let wSum = 0;
|
|
121
|
+
let ySum = 0;
|
|
122
|
+
for (const s of samples) {
|
|
123
|
+
wSum += s.weight ?? 1;
|
|
124
|
+
ySum += (s.weight ?? 1) * s.y;
|
|
125
|
+
}
|
|
126
|
+
// All-zero weights would make this 0/0: fall back to the unweighted mean.
|
|
127
|
+
const yMean = wSum > 0 ? ySum / wSum : samples.reduce((a, s) => a + s.y, 0) / samples.length;
|
|
128
|
+
const base = objective === "binary" ? Math.log(Math.max(yMean, 1e-6) / Math.max(1 - yMean, 1e-6)) : yMean;
|
|
129
|
+
const margins = samples.map(() => base);
|
|
130
|
+
const trees: RTree[] = [];
|
|
131
|
+
for (let t = 0; t < (opts.trees ?? 50); t++) {
|
|
132
|
+
const rows: Row[] = samples.map((s, i) => {
|
|
133
|
+
if (objective === "binary") {
|
|
134
|
+
const p = sigmoid(margins[i]!);
|
|
135
|
+
return { x: s.x, g: p - s.y, h: Math.max(p * (1 - p), 1e-6), w: s.weight ?? 1 };
|
|
136
|
+
}
|
|
137
|
+
return { x: s.x, g: margins[i]! - s.y, h: 1, w: s.weight ?? 1 };
|
|
138
|
+
});
|
|
139
|
+
const tree = buildTree(rows, 0, treeOpts);
|
|
140
|
+
trees.push(tree);
|
|
141
|
+
samples.forEach((s, i) => (margins[i] = margins[i]! + lr * treeValue(tree, s.x)));
|
|
142
|
+
}
|
|
143
|
+
return new GbmModel(objective, base, trees, lr);
|
|
144
|
+
}
|
|
@@ -10,6 +10,7 @@ import { fitLinear, type LinearOptions } from "./linear.js";
|
|
|
10
10
|
import { fitKMeans, type KMeansOptions } from "./kmeans.js";
|
|
11
11
|
import { fitRobustZ, type RobustZOptions } from "./robust_z.js";
|
|
12
12
|
import { fitEwma, type EwmaOptions } from "./ewma.js";
|
|
13
|
+
import { fitAnomalyComposite, type AnomalyCompositeOptions } from "./anomaly_composite.js";
|
|
13
14
|
import { fitIsolationForest, type IsolationForestOptions } from "./isolation_forest.js";
|
|
14
15
|
import { fitGbm, type GbmOptions } from "./gbm.js";
|
|
15
16
|
|
|
@@ -23,10 +24,12 @@ export { LinearModel, fitLinear, solveLinearSystem } from "./linear.js";
|
|
|
23
24
|
export type { LinearOptions } from "./linear.js";
|
|
24
25
|
export { KMeansModel, fitKMeans } from "./kmeans.js";
|
|
25
26
|
export type { KMeansOptions } from "./kmeans.js";
|
|
26
|
-
export { RobustZModel, fitRobustZ, anomalyConfidence } from "./robust_z.js";
|
|
27
|
-
export type { RobustZOptions } from "./robust_z.js";
|
|
27
|
+
export { RobustZModel, fitRobustZ, anomalyConfidence, minScaleAt } from "./robust_z.js";
|
|
28
|
+
export type { RobustZOptions, MinScale } from "./robust_z.js";
|
|
28
29
|
export { EwmaModel, fitEwma } from "./ewma.js";
|
|
29
30
|
export type { EwmaOptions } from "./ewma.js";
|
|
31
|
+
export { AnomalyCompositeModel, fitAnomalyComposite } from "./anomaly_composite.js";
|
|
32
|
+
export type { AnomalyCompositeOptions } from "./anomaly_composite.js";
|
|
30
33
|
export { IsolationForestModel, fitIsolationForest } from "./isolation_forest.js";
|
|
31
34
|
export type { IsolationForestOptions } from "./isolation_forest.js";
|
|
32
35
|
export { GbmModel, fitGbm } from "./gbm.js";
|
|
@@ -42,6 +45,7 @@ export interface ClassicalOptions {
|
|
|
42
45
|
kmeans?: KMeansOptions;
|
|
43
46
|
"robust-z"?: RobustZOptions;
|
|
44
47
|
ewma?: EwmaOptions;
|
|
48
|
+
"anomaly-composite"?: AnomalyCompositeOptions;
|
|
45
49
|
"isolation-forest"?: IsolationForestOptions;
|
|
46
50
|
"gbm-small"?: GbmOptions;
|
|
47
51
|
}
|
|
@@ -68,6 +72,8 @@ export function fitClassical(
|
|
|
68
72
|
return fitRobustZ(samples, opts["robust-z"]);
|
|
69
73
|
case "ewma":
|
|
70
74
|
return fitEwma(samples, opts.ewma);
|
|
75
|
+
case "anomaly-composite":
|
|
76
|
+
return fitAnomalyComposite(samples, opts["anomaly-composite"]);
|
|
71
77
|
case "isolation-forest":
|
|
72
78
|
return fitIsolationForest(samples, opts["isolation-forest"]);
|
|
73
79
|
case "gbm-small":
|
|
@@ -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). */
|
|
@@ -53,9 +56,12 @@ export class KnnModel implements MlPredictor {
|
|
|
53
56
|
return this.metric === "cosine" ? cosine(a, b) : 1 / (1 + Math.sqrt(squaredDistance(a, b)));
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
/** The `k` closest points, closest first. */
|
|
57
|
-
neighbours(x: Vec, k: number = this.k): Neighbour[] {
|
|
58
|
-
const all
|
|
59
|
+
/** The `k` closest points, closest first — never the point at `exclude` (leave-one-out). */
|
|
60
|
+
neighbours(x: Vec, k: number = this.k, exclude?: number): Neighbour[] {
|
|
61
|
+
const all: Neighbour[] = [];
|
|
62
|
+
for (let index = 0; index < this._x.length; index++) {
|
|
63
|
+
if (index !== exclude) all.push({ index, similarity: this.similarity(x, this._x[index]!) });
|
|
64
|
+
}
|
|
59
65
|
all.sort((a, b) => b.similarity - a.similarity || a.index - b.index);
|
|
60
66
|
return all.slice(0, Math.min(k, all.length));
|
|
61
67
|
}
|
|
@@ -70,10 +76,15 @@ export class KnnModel implements MlPredictor {
|
|
|
70
76
|
return this._y[index]!;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Class probabilities from similarity-weighted votes (uniform when empty). With
|
|
81
|
+
* `exclude`, the point at that index does not vote — the leave-one-out score of a
|
|
82
|
+
* point that is itself in the index, which would otherwise vote for itself at
|
|
83
|
+
* similarity 1.
|
|
84
|
+
*/
|
|
85
|
+
proba(x: Vec, exclude?: number): number[] {
|
|
75
86
|
const votes = new Array<number>(this.classes).fill(0);
|
|
76
|
-
for (const n of this.neighbours(x)) {
|
|
87
|
+
for (const n of this.neighbours(x, this.k, exclude)) {
|
|
77
88
|
// Shift cosine into (0, 2] so an orthogonal neighbour still casts a small vote.
|
|
78
89
|
const w = (this.metric === "cosine" ? n.similarity + 1 : n.similarity) * this._w[n.index]! + 1e-9;
|
|
79
90
|
votes[this._y[n.index]!]! += w;
|
|
@@ -86,13 +97,25 @@ export class KnnModel implements MlPredictor {
|
|
|
86
97
|
const scores = this.proba(x);
|
|
87
98
|
return { scores, confidence: topProbability(scores) };
|
|
88
99
|
}
|
|
100
|
+
|
|
101
|
+
/** This index as a predictor that leaves the point at `index` out of every vote. */
|
|
102
|
+
excluding(index: number): MlPredictor {
|
|
103
|
+
if (!(index >= 0 && index < this._x.length)) throw new Error(`KnnModel.excluding: no point ${index} (size ${this._x.length})`);
|
|
104
|
+
return {
|
|
105
|
+
predict: (x: Float32Array): MlPrediction => {
|
|
106
|
+
const scores = this.proba(x, index);
|
|
107
|
+
return { scores, confidence: topProbability(scores) };
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
/** Index `samples` for kNN classification. */
|
|
92
114
|
export function fitKnn(samples: readonly MlSample[], opts: KnnOptions = {}): KnnModel {
|
|
93
115
|
featureDim(samples.map((s) => s.x));
|
|
94
116
|
const classes = opts.classes ?? classCount(samples.map((s) => s.y));
|
|
95
|
-
const
|
|
117
|
+
const k = opts.k ?? Math.min(5, Math.max(1, Math.floor(Math.sqrt(samples.length))));
|
|
118
|
+
const model = new KnnModel(Math.min(k, samples.length), opts.metric ?? "cosine", classes);
|
|
96
119
|
for (const s of samples) model.add(s.x, s.y, s.weight ?? 1);
|
|
97
120
|
return model;
|
|
98
121
|
}
|
|
@@ -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;
|