@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.
Files changed (149) hide show
  1. package/dist/evermind/adapt.d.ts +7 -0
  2. package/dist/evermind/adapt.d.ts.map +1 -1
  3. package/dist/evermind/adapt.js +1 -1
  4. package/dist/evermind/adapt.js.map +1 -1
  5. package/dist/evermind/delta_wire.d.ts +16 -1
  6. package/dist/evermind/delta_wire.d.ts.map +1 -1
  7. package/dist/evermind/delta_wire.js +22 -2
  8. package/dist/evermind/delta_wire.js.map +1 -1
  9. package/dist/evermind/index.d.ts +2 -2
  10. package/dist/evermind/index.d.ts.map +1 -1
  11. package/dist/evermind/index.js +1 -1
  12. package/dist/evermind/index.js.map +1 -1
  13. package/dist/evermind/merge.d.ts +5 -2
  14. package/dist/evermind/merge.d.ts.map +1 -1
  15. package/dist/evermind/merge.js +5 -2
  16. package/dist/evermind/merge.js.map +1 -1
  17. package/dist/evermind/ml_head.d.ts +28 -0
  18. package/dist/evermind/ml_head.d.ts.map +1 -0
  19. package/dist/evermind/ml_head.js +47 -0
  20. package/dist/evermind/ml_head.js.map +1 -0
  21. package/dist/evermind/package.d.ts +2 -2
  22. package/dist/evermind/package.d.ts.map +1 -1
  23. package/dist/evermind/package.js +1 -1
  24. package/dist/evermind/package.js.map +1 -1
  25. package/dist/experience/index.d.ts +2 -2
  26. package/dist/experience/index.d.ts.map +1 -1
  27. package/dist/experience/index.js.map +1 -1
  28. package/dist/experience/store.d.ts +19 -1
  29. package/dist/experience/store.d.ts.map +1 -1
  30. package/dist/experience/store.js +14 -2
  31. package/dist/experience/store.js.map +1 -1
  32. package/dist/experience/types.d.ts +18 -0
  33. package/dist/experience/types.d.ts.map +1 -1
  34. package/dist/experience/types.js.map +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/kernels/weight_update.d.ts.map +1 -1
  40. package/dist/kernels/weight_update.js +12 -0
  41. package/dist/kernels/weight_update.js.map +1 -1
  42. package/dist/limbic/limbic_model.d.ts.map +1 -1
  43. package/dist/limbic/limbic_model.js +1 -3
  44. package/dist/limbic/limbic_model.js.map +1 -1
  45. package/dist/ml/classical/anomaly_composite.d.ts +32 -0
  46. package/dist/ml/classical/anomaly_composite.d.ts.map +1 -0
  47. package/dist/ml/classical/anomaly_composite.js +29 -0
  48. package/dist/ml/classical/anomaly_composite.js.map +1 -0
  49. package/dist/ml/classical/ewma.d.ts +7 -1
  50. package/dist/ml/classical/ewma.d.ts.map +1 -1
  51. package/dist/ml/classical/ewma.js +6 -4
  52. package/dist/ml/classical/ewma.js.map +1 -1
  53. package/dist/ml/classical/gbm.d.ts.map +1 -1
  54. package/dist/ml/classical/gbm.js +2 -1
  55. package/dist/ml/classical/gbm.js.map +1 -1
  56. package/dist/ml/classical/index.d.ts +6 -2
  57. package/dist/ml/classical/index.d.ts.map +1 -1
  58. package/dist/ml/classical/index.js +5 -1
  59. package/dist/ml/classical/index.js.map +1 -1
  60. package/dist/ml/classical/kmeans.d.ts.map +1 -1
  61. package/dist/ml/classical/kmeans.js +23 -4
  62. package/dist/ml/classical/kmeans.js.map +1 -1
  63. package/dist/ml/classical/knn.d.ts +15 -5
  64. package/dist/ml/classical/knn.d.ts.map +1 -1
  65. package/dist/ml/classical/knn.js +28 -7
  66. package/dist/ml/classical/knn.js.map +1 -1
  67. package/dist/ml/classical/logistic.d.ts +1 -9
  68. package/dist/ml/classical/logistic.d.ts.map +1 -1
  69. package/dist/ml/classical/logistic.js +17 -9
  70. package/dist/ml/classical/logistic.js.map +1 -1
  71. package/dist/ml/classical/naive_bayes.d.ts +5 -1
  72. package/dist/ml/classical/naive_bayes.d.ts.map +1 -1
  73. package/dist/ml/classical/naive_bayes.js +8 -3
  74. package/dist/ml/classical/naive_bayes.js.map +1 -1
  75. package/dist/ml/classical/robust_z.d.ts +15 -1
  76. package/dist/ml/classical/robust_z.d.ts.map +1 -1
  77. package/dist/ml/classical/robust_z.js +6 -1
  78. package/dist/ml/classical/robust_z.js.map +1 -1
  79. package/dist/ml/eval_loss.d.ts +15 -0
  80. package/dist/ml/eval_loss.d.ts.map +1 -0
  81. package/dist/ml/eval_loss.js +20 -0
  82. package/dist/ml/eval_loss.js.map +1 -0
  83. package/dist/ml/federate.d.ts +11 -25
  84. package/dist/ml/federate.d.ts.map +1 -1
  85. package/dist/ml/federate.js +15 -38
  86. package/dist/ml/federate.js.map +1 -1
  87. package/dist/ml/head_trainer.d.ts +16 -2
  88. package/dist/ml/head_trainer.d.ts.map +1 -1
  89. package/dist/ml/head_trainer.js +64 -27
  90. package/dist/ml/head_trainer.js.map +1 -1
  91. package/dist/ml/run.d.ts +11 -2
  92. package/dist/ml/run.d.ts.map +1 -1
  93. package/dist/ml/run.js +56 -17
  94. package/dist/ml/run.js.map +1 -1
  95. package/dist/ml/tasks/experience.d.ts +95 -13
  96. package/dist/ml/tasks/experience.d.ts.map +1 -1
  97. package/dist/ml/tasks/experience.js +172 -20
  98. package/dist/ml/tasks/experience.js.map +1 -1
  99. package/dist/ml/tasks/run_anomaly.d.ts +37 -19
  100. package/dist/ml/tasks/run_anomaly.d.ts.map +1 -1
  101. package/dist/ml/tasks/run_anomaly.js +50 -31
  102. package/dist/ml/tasks/run_anomaly.js.map +1 -1
  103. package/dist/ml/train.d.ts +8 -7
  104. package/dist/ml/train.d.ts.map +1 -1
  105. package/dist/ml/train.js +21 -30
  106. package/dist/ml/train.js.map +1 -1
  107. package/dist/ml/types.d.ts +6 -3
  108. package/dist/ml/types.d.ts.map +1 -1
  109. package/dist/ml/types.js +3 -2
  110. package/dist/ml/types.js.map +1 -1
  111. package/dist/optim/adamw.d.ts +11 -5
  112. package/dist/optim/adamw.d.ts.map +1 -1
  113. package/dist/optim/adamw.js +19 -4
  114. package/dist/optim/adamw.js.map +1 -1
  115. package/package.json +1 -1
  116. package/src/evermind/adapt.ts +8 -2
  117. package/src/evermind/delta_wire.ts +173 -148
  118. package/src/evermind/index.ts +36 -35
  119. package/src/evermind/merge.ts +10 -4
  120. package/src/evermind/ml_head.ts +63 -0
  121. package/src/evermind/package.ts +488 -488
  122. package/src/experience/index.ts +2 -1
  123. package/src/experience/store.ts +35 -3
  124. package/src/experience/types.ts +19 -0
  125. package/src/index.ts +2 -0
  126. package/src/kernels/weight_update.ts +12 -0
  127. package/src/limbic/limbic_model.ts +1 -4
  128. package/src/limbic/limbic_trainer.ts +91 -91
  129. package/src/ml/classical/anomaly_composite.ts +46 -0
  130. package/src/ml/classical/ewma.ts +8 -2
  131. package/src/ml/classical/gbm.ts +144 -143
  132. package/src/ml/classical/index.ts +8 -2
  133. package/src/ml/classical/kmeans.ts +22 -4
  134. package/src/ml/classical/knn.ts +31 -8
  135. package/src/ml/classical/logistic.ts +9 -9
  136. package/src/ml/classical/math.ts +93 -93
  137. package/src/ml/classical/naive_bayes.ts +10 -3
  138. package/src/ml/classical/robust_z.ts +21 -2
  139. package/src/ml/eval_loss.ts +27 -0
  140. package/src/ml/federate.ts +22 -52
  141. package/src/ml/head_trainer.ts +66 -48
  142. package/src/ml/registry.ts +44 -44
  143. package/src/ml/run.ts +57 -13
  144. package/src/ml/tasks/experience.ts +222 -23
  145. package/src/ml/tasks/run_anomaly.ts +62 -37
  146. package/src/ml/train.ts +34 -31
  147. package/src/ml/types.ts +135 -132
  148. package/src/optim/adamw.ts +21 -5
  149. package/src/training/cpu_ops.ts +141 -141
@@ -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
- const yMean = ySum / wSum;
127
- const base = objective === "binary" ? Math.log(Math.max(yMean, 1e-6) / Math.max(1 - yMean, 1e-6)) : yMean;
128
- const margins = samples.map(() => base);
129
- const trees: RTree[] = [];
130
- for (let t = 0; t < (opts.trees ?? 50); t++) {
131
- const rows: Row[] = samples.map((s, i) => {
132
- if (objective === "binary") {
133
- const p = sigmoid(margins[i]!);
134
- return { x: s.x, g: p - s.y, h: Math.max(p * (1 - p), 1e-6), w: s.weight ?? 1 };
135
- }
136
- return { x: s.x, g: margins[i]! - s.y, h: 1, w: s.weight ?? 1 };
137
- });
138
- const tree = buildTree(rows, 0, treeOpts);
139
- trees.push(tree);
140
- samples.forEach((s, i) => (margins[i] = margins[i]! + lr * treeValue(tree, s.x)));
141
- }
142
- return new GbmModel(objective, base, trees, lr);
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
- for (pick = 0; pick < X.length - 1; pick++) {
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
- const maxIt = opts.maxIterations ?? 100;
75
- for (let it = 0; it < maxIt; it++) {
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
- if (!changed) break;
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);
@@ -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
- /** Neighbours that vote. Default 5 (capped at the sample count). */
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 = this._x.map((p, index) => ({ index, similarity: this.similarity(x, p) }));
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
- /** Class probabilities from similarity-weighted votes (uniform when empty). */
74
- proba(x: Vec): number[] {
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 model = new KnnModel(Math.min(opts.k ?? 5, samples.length), opts.metric ?? "cosine", classes);
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` (the eval-gate loss). */
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
- 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;
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;