@seanhogg/builderforce-memory-engine 2026.9.34 → 2026.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/evermind/delta_wire.d.ts +26 -2
- package/dist/evermind/delta_wire.d.ts.map +1 -1
- package/dist/evermind/delta_wire.js +46 -4
- package/dist/evermind/delta_wire.js.map +1 -1
- package/dist/evermind/index.d.ts +5 -5
- 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 +28 -0
- package/dist/evermind/merge.d.ts.map +1 -1
- package/dist/evermind/merge.js +33 -0
- 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 +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_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/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/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 +37 -0
- package/dist/ml/classical/ewma.d.ts.map +1 -0
- package/dist/ml/classical/ewma.js +48 -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 +117 -0
- package/dist/ml/classical/gbm.js.map +1 -0
- package/dist/ml/classical/index.d.ts +52 -0
- package/dist/ml/classical/index.d.ts.map +1 -0
- package/dist/ml/classical/index.js +49 -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 +112 -0
- package/dist/ml/classical/kmeans.js.map +1 -0
- package/dist/ml/classical/knn.d.ts +51 -0
- package/dist/ml/classical/knn.d.ts.map +1 -0
- package/dist/ml/classical/knn.js +69 -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 +31 -0
- package/dist/ml/classical/logistic.d.ts.map +1 -0
- package/dist/ml/classical/logistic.js +111 -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 +28 -0
- package/dist/ml/classical/naive_bayes.d.ts.map +1 -0
- package/dist/ml/classical/naive_bayes.js +72 -0
- package/dist/ml/classical/naive_bayes.js.map +1 -0
- package/dist/ml/classical/robust_z.d.ts +38 -0
- package/dist/ml/classical/robust_z.d.ts.map +1 -0
- package/dist/ml/classical/robust_z.js +47 -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/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/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 +56 -0
- package/dist/ml/federate.d.ts.map +1 -0
- package/dist/ml/federate.js +99 -0
- package/dist/ml/federate.js.map +1 -0
- package/dist/ml/head_trainer.d.ts +78 -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 +103 -0
- package/dist/ml/run.d.ts.map +1 -0
- package/dist/ml/run.js +169 -0
- package/dist/ml/run.js.map +1 -0
- package/dist/ml/tasks/experience.d.ts +127 -0
- package/dist/ml/tasks/experience.d.ts.map +1 -0
- package/dist/ml/tasks/experience.js +213 -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 +52 -0
- package/dist/ml/tasks/run_anomaly.d.ts.map +1 -0
- package/dist/ml/tasks/run_anomaly.js +50 -0
- package/dist/ml/tasks/run_anomaly.js.map +1 -0
- package/dist/ml/train.d.ts +59 -0
- package/dist/ml/train.d.ts.map +1 -0
- package/dist/ml/train.js +84 -0
- package/dist/ml/train.js.map +1 -0
- package/dist/ml/types.d.ts +115 -0
- package/dist/ml/types.d.ts.map +1 -0
- package/dist/ml/types.js +24 -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 +63 -4
- package/src/evermind/index.ts +7 -3
- package/src/evermind/merge.ts +59 -0
- package/src/evermind/ml_head.ts +63 -0
- package/src/evermind/package.ts +24 -0
- package/src/index.ts +17 -1
- package/src/limbic/index.ts +5 -0
- package/src/limbic/limbic_model.ts +1 -4
- package/src/limbic/limbic_task.ts +62 -0
- package/src/limbic/limbic_trainer.ts +41 -208
- package/src/ml/classical/anomaly_composite.ts +46 -0
- package/src/ml/classical/ewma.ts +74 -0
- package/src/ml/classical/gbm.ts +144 -0
- package/src/ml/classical/index.ts +82 -0
- package/src/ml/classical/isolation_forest.ts +88 -0
- package/src/ml/classical/kmeans.ts +120 -0
- package/src/ml/classical/knn.ts +102 -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 +84 -0
- package/src/ml/classical/robust_z.ts +74 -0
- package/src/ml/dense_head.ts +157 -0
- package/src/ml/eval_loss.ts +27 -0
- package/src/ml/features.ts +61 -0
- package/src/ml/federate.ts +130 -0
- package/src/ml/head_trainer.ts +247 -0
- package/src/ml/index.ts +57 -0
- package/src/ml/registry.ts +44 -0
- package/src/ml/run.ts +222 -0
- package/src/ml/tasks/experience.ts +293 -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 +84 -0
- package/src/ml/train.ts +120 -0
- package/src/ml/types.ts +135 -0
- package/src/moe/moe_model.ts +1 -13
- package/src/training/cpu_ops.ts +14 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical — pure-TypeScript algorithms, one file each, and the ONE dispatch
|
|
3
|
+
* from a {@link ClassicalAlgorithm} name to its fit.
|
|
4
|
+
*/
|
|
5
|
+
import type { ClassicalAlgorithm, MlPredictor, MlSample } from "../types.js";
|
|
6
|
+
import { fitKnn, type KnnOptions } from "./knn.js";
|
|
7
|
+
import { fitNaiveBayes, type NaiveBayesOptions } from "./naive_bayes.js";
|
|
8
|
+
import { fitLogistic, type LogisticOptions } from "./logistic.js";
|
|
9
|
+
import { fitLinear, type LinearOptions } from "./linear.js";
|
|
10
|
+
import { fitKMeans, type KMeansOptions } from "./kmeans.js";
|
|
11
|
+
import { fitRobustZ, type RobustZOptions } from "./robust_z.js";
|
|
12
|
+
import { fitEwma, type EwmaOptions } from "./ewma.js";
|
|
13
|
+
import { fitAnomalyComposite, type AnomalyCompositeOptions } from "./anomaly_composite.js";
|
|
14
|
+
import { fitIsolationForest, type IsolationForestOptions } from "./isolation_forest.js";
|
|
15
|
+
import { fitGbm, type GbmOptions } from "./gbm.js";
|
|
16
|
+
|
|
17
|
+
export { KnnModel, fitKnn } from "./knn.js";
|
|
18
|
+
export type { KnnOptions, KnnMetric, Neighbour } from "./knn.js";
|
|
19
|
+
export { NaiveBayesModel, fitNaiveBayes } from "./naive_bayes.js";
|
|
20
|
+
export type { NaiveBayesOptions } from "./naive_bayes.js";
|
|
21
|
+
export { LogisticModel, fitLogistic } from "./logistic.js";
|
|
22
|
+
export type { LogisticOptions } from "./logistic.js";
|
|
23
|
+
export { LinearModel, fitLinear, solveLinearSystem } from "./linear.js";
|
|
24
|
+
export type { LinearOptions } from "./linear.js";
|
|
25
|
+
export { KMeansModel, fitKMeans } from "./kmeans.js";
|
|
26
|
+
export type { KMeansOptions } from "./kmeans.js";
|
|
27
|
+
export { RobustZModel, fitRobustZ, anomalyConfidence } from "./robust_z.js";
|
|
28
|
+
export type { RobustZOptions } from "./robust_z.js";
|
|
29
|
+
export { EwmaModel, fitEwma } from "./ewma.js";
|
|
30
|
+
export type { EwmaOptions } from "./ewma.js";
|
|
31
|
+
export { AnomalyCompositeModel, fitAnomalyComposite } from "./anomaly_composite.js";
|
|
32
|
+
export type { AnomalyCompositeOptions } from "./anomaly_composite.js";
|
|
33
|
+
export { IsolationForestModel, fitIsolationForest } from "./isolation_forest.js";
|
|
34
|
+
export type { IsolationForestOptions } from "./isolation_forest.js";
|
|
35
|
+
export { GbmModel, fitGbm } from "./gbm.js";
|
|
36
|
+
export type { GbmOptions, GbmObjective } from "./gbm.js";
|
|
37
|
+
export { cosine, dot } from "./math.js";
|
|
38
|
+
|
|
39
|
+
/** Per-algorithm options, keyed by algorithm name. */
|
|
40
|
+
export interface ClassicalOptions {
|
|
41
|
+
knn?: KnnOptions;
|
|
42
|
+
"naive-bayes"?: NaiveBayesOptions;
|
|
43
|
+
logistic?: LogisticOptions;
|
|
44
|
+
linear?: LinearOptions;
|
|
45
|
+
kmeans?: KMeansOptions;
|
|
46
|
+
"robust-z"?: RobustZOptions;
|
|
47
|
+
ewma?: EwmaOptions;
|
|
48
|
+
"anomaly-composite"?: AnomalyCompositeOptions;
|
|
49
|
+
"isolation-forest"?: IsolationForestOptions;
|
|
50
|
+
"gbm-small"?: GbmOptions;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Fit `algorithm` on `samples`. Unsupervised algorithms ignore `y`. */
|
|
54
|
+
export function fitClassical(
|
|
55
|
+
algorithm: ClassicalAlgorithm,
|
|
56
|
+
samples: readonly MlSample[],
|
|
57
|
+
opts: ClassicalOptions = {},
|
|
58
|
+
): MlPredictor {
|
|
59
|
+
if (samples.length === 0) throw new Error(`fitClassical(${algorithm}): no samples`);
|
|
60
|
+
switch (algorithm) {
|
|
61
|
+
case "knn":
|
|
62
|
+
return fitKnn(samples, opts.knn);
|
|
63
|
+
case "naive-bayes":
|
|
64
|
+
return fitNaiveBayes(samples, opts["naive-bayes"]);
|
|
65
|
+
case "logistic":
|
|
66
|
+
return fitLogistic(samples, opts.logistic);
|
|
67
|
+
case "linear":
|
|
68
|
+
return fitLinear(samples, opts.linear);
|
|
69
|
+
case "kmeans":
|
|
70
|
+
return fitKMeans(samples, opts.kmeans);
|
|
71
|
+
case "robust-z":
|
|
72
|
+
return fitRobustZ(samples, opts["robust-z"]);
|
|
73
|
+
case "ewma":
|
|
74
|
+
return fitEwma(samples, opts.ewma);
|
|
75
|
+
case "anomaly-composite":
|
|
76
|
+
return fitAnomalyComposite(samples, opts["anomaly-composite"]);
|
|
77
|
+
case "isolation-forest":
|
|
78
|
+
return fitIsolationForest(samples, opts["isolation-forest"]);
|
|
79
|
+
case "gbm-small":
|
|
80
|
+
return fitGbm(samples, opts["gbm-small"]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/isolation_forest.ts — Isolation Forest (Liu, Ting & Zhou 2008).
|
|
3
|
+
*
|
|
4
|
+
* Anomalies are few and different, so random axis-aligned splits isolate them in
|
|
5
|
+
* fewer steps. Score s(x) = 2^(−E[h(x)] / c(ψ)): near 1 = anomalous, ≤ 0.5 =
|
|
6
|
+
* normal. Prediction: `scores[0] = s / threshold` — ≥ 1 is anomalous.
|
|
7
|
+
*/
|
|
8
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
9
|
+
import { averagePathLength, featureDim, seededUniform, type Vec } from "./math.js";
|
|
10
|
+
import { anomalyConfidence } from "./robust_z.js";
|
|
11
|
+
|
|
12
|
+
export interface IsolationForestOptions {
|
|
13
|
+
trees?: number;
|
|
14
|
+
/** Subsample size ψ per tree. Default min(256, n). */
|
|
15
|
+
sampleSize?: number;
|
|
16
|
+
/** Raw score at which a point is anomalous. Default 0.6. */
|
|
17
|
+
threshold?: number;
|
|
18
|
+
seed?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ITree = { size: number } | { feature: number; split: number; left: ITree; right: ITree };
|
|
22
|
+
|
|
23
|
+
function buildTree(X: Vec[], depth: number, limit: number, rnd: () => number): ITree {
|
|
24
|
+
if (depth >= limit || X.length <= 1) return { size: X.length };
|
|
25
|
+
const d = X[0]!.length;
|
|
26
|
+
// Only features that still vary can split; none left = an isolated leaf.
|
|
27
|
+
const varying: { f: number; lo: number; hi: number }[] = [];
|
|
28
|
+
for (let f = 0; f < d; f++) {
|
|
29
|
+
let lo = Infinity;
|
|
30
|
+
let hi = -Infinity;
|
|
31
|
+
for (const x of X) {
|
|
32
|
+
lo = Math.min(lo, x[f]!);
|
|
33
|
+
hi = Math.max(hi, x[f]!);
|
|
34
|
+
}
|
|
35
|
+
if (hi > lo) varying.push({ f, lo, hi });
|
|
36
|
+
}
|
|
37
|
+
if (varying.length === 0) return { size: X.length };
|
|
38
|
+
const { f, lo, hi } = varying[Math.floor(rnd() * varying.length)]!;
|
|
39
|
+
const split = lo + rnd() * (hi - lo);
|
|
40
|
+
const left = X.filter((x) => x[f]! < split);
|
|
41
|
+
const right = X.filter((x) => x[f]! >= split);
|
|
42
|
+
return { feature: f, split, left: buildTree(left, depth + 1, limit, rnd), right: buildTree(right, depth + 1, limit, rnd) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pathLength(tree: ITree, x: Vec, depth = 0): number {
|
|
46
|
+
if ("size" in tree) return depth + averagePathLength(tree.size);
|
|
47
|
+
return pathLength(x[tree.feature]! < tree.split ? tree.left : tree.right, x, depth + 1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class IsolationForestModel implements MlPredictor {
|
|
51
|
+
constructor(
|
|
52
|
+
readonly trees: readonly ITree[],
|
|
53
|
+
readonly sampleSize: number,
|
|
54
|
+
readonly threshold: number,
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
/** The raw isolation score in (0, 1]. */
|
|
58
|
+
score(x: Vec): number {
|
|
59
|
+
const c = averagePathLength(this.sampleSize);
|
|
60
|
+
if (c === 0) return 0.5;
|
|
61
|
+
const mean = this.trees.reduce((s, t) => s + pathLength(t, x), 0) / this.trees.length;
|
|
62
|
+
return Math.pow(2, -mean / c);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
predict(x: Float32Array): MlPrediction {
|
|
66
|
+
const s = this.score(x) / this.threshold;
|
|
67
|
+
return { scores: [s], confidence: anomalyConfidence(s) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function fitIsolationForest(samples: readonly MlSample[], opts: IsolationForestOptions = {}): IsolationForestModel {
|
|
72
|
+
const X = samples.map((s) => s.x);
|
|
73
|
+
featureDim(X);
|
|
74
|
+
const rnd = seededUniform(opts.seed ?? 0x15f0);
|
|
75
|
+
const psi = Math.min(opts.sampleSize ?? 256, X.length);
|
|
76
|
+
const limit = Math.ceil(Math.log2(Math.max(psi, 2)));
|
|
77
|
+
const trees: ITree[] = [];
|
|
78
|
+
for (let t = 0; t < (opts.trees ?? 100); t++) {
|
|
79
|
+
// Sample ψ points without replacement (partial Fisher–Yates over indices).
|
|
80
|
+
const idx = X.map((_, i) => i);
|
|
81
|
+
for (let i = 0; i < psi; i++) {
|
|
82
|
+
const j = i + Math.floor(rnd() * (idx.length - i));
|
|
83
|
+
[idx[i], idx[j]] = [idx[j]!, idx[i]!];
|
|
84
|
+
}
|
|
85
|
+
trees.push(buildTree(idx.slice(0, psi).map((i) => X[i]!), 0, limit, rnd));
|
|
86
|
+
}
|
|
87
|
+
return new IsolationForestModel(trees, psi, opts.threshold ?? 0.6);
|
|
88
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/kmeans.ts — k-means clustering (k-means++ seeding, Lloyd iterations).
|
|
3
|
+
*
|
|
4
|
+
* Deterministic for a given seed. A prediction is the nearest centroid; its
|
|
5
|
+
* confidence is how much closer that centroid is than the runner-up.
|
|
6
|
+
*/
|
|
7
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
8
|
+
import { featureDim, seededUniform, squaredDistance, type Vec } from "./math.js";
|
|
9
|
+
|
|
10
|
+
export interface KMeansOptions {
|
|
11
|
+
/** Clusters. Default round(√(n/2)), at least 1. */
|
|
12
|
+
k?: number;
|
|
13
|
+
maxIterations?: number;
|
|
14
|
+
seed?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class KMeansModel implements MlPredictor {
|
|
18
|
+
constructor(
|
|
19
|
+
readonly centroids: Float32Array[],
|
|
20
|
+
/** Sum of squared distances of the fitted points to their centroid. */
|
|
21
|
+
readonly inertia: number,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
/** Nearest centroid index and the two smallest squared distances. */
|
|
25
|
+
assign(x: Vec): { cluster: number; d1: number; d2: number } {
|
|
26
|
+
let cluster = 0;
|
|
27
|
+
let d1 = Infinity;
|
|
28
|
+
let d2 = Infinity;
|
|
29
|
+
this.centroids.forEach((c, i) => {
|
|
30
|
+
const d = squaredDistance(x, c);
|
|
31
|
+
if (d < d1) {
|
|
32
|
+
d2 = d1;
|
|
33
|
+
d1 = d;
|
|
34
|
+
cluster = i;
|
|
35
|
+
} else if (d < d2) d2 = d;
|
|
36
|
+
});
|
|
37
|
+
return { cluster, d1, d2 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
predict(x: Float32Array): MlPrediction {
|
|
41
|
+
const { cluster, d1, d2 } = this.assign(x);
|
|
42
|
+
const confidence = Number.isFinite(d2) && d2 > 0 ? 1 - Math.sqrt(d1) / Math.sqrt(d2) : 1;
|
|
43
|
+
return { scores: [cluster], confidence: Math.max(0, Math.min(1, confidence)) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function fitKMeans(samples: readonly MlSample[], opts: KMeansOptions = {}): KMeansModel {
|
|
48
|
+
const X = samples.map((s) => s.x);
|
|
49
|
+
const d = featureDim(X);
|
|
50
|
+
const k = Math.min(X.length, Math.max(1, opts.k ?? Math.round(Math.sqrt(X.length / 2))));
|
|
51
|
+
const rnd = seededUniform(opts.seed ?? 0x6b6d);
|
|
52
|
+
|
|
53
|
+
// k-means++: first centroid uniformly, the rest ∝ squared distance to the nearest chosen one.
|
|
54
|
+
const centroids: Float32Array[] = [Float32Array.from(X[Math.floor(rnd() * X.length)]!)];
|
|
55
|
+
const nearest = X.map((x) => squaredDistance(x, centroids[0]!));
|
|
56
|
+
while (centroids.length < k) {
|
|
57
|
+
const total = nearest.reduce((a, b) => a + b, 0);
|
|
58
|
+
let pick = 0;
|
|
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.
|
|
62
|
+
let r = rnd() * total;
|
|
63
|
+
let last = -1;
|
|
64
|
+
for (pick = 0; pick < X.length; pick++) {
|
|
65
|
+
if (!(nearest[pick]! > 0)) continue;
|
|
66
|
+
last = pick;
|
|
67
|
+
r -= nearest[pick]!;
|
|
68
|
+
if (r <= 0) break;
|
|
69
|
+
}
|
|
70
|
+
if (pick >= X.length) pick = last; // float round-off left r > 0: take the last weighted point
|
|
71
|
+
} else {
|
|
72
|
+
pick = centroids.length; // every point coincides with a centroid; take the next index
|
|
73
|
+
}
|
|
74
|
+
const c = Float32Array.from(X[pick]!);
|
|
75
|
+
centroids.push(c);
|
|
76
|
+
X.forEach((x, i) => (nearest[i] = Math.min(nearest[i]!, squaredDistance(x, c))));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const assign = new Int32Array(X.length).fill(-1);
|
|
80
|
+
/** Assign every point to its nearest current centroid; true when any assignment moved. */
|
|
81
|
+
const reassign = (): boolean => {
|
|
82
|
+
let changed = false;
|
|
83
|
+
const model = new KMeansModel(centroids, 0);
|
|
84
|
+
X.forEach((x, i) => {
|
|
85
|
+
const c = model.assign(x).cluster;
|
|
86
|
+
if (assign[i] !== c) {
|
|
87
|
+
assign[i] = c;
|
|
88
|
+
changed = true;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
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
|
+
}
|
|
100
|
+
const sums = centroids.map(() => new Float64Array(d));
|
|
101
|
+
const counts = new Array<number>(k).fill(0);
|
|
102
|
+
X.forEach((x, i) => {
|
|
103
|
+
const c = assign[i]!;
|
|
104
|
+
counts[c] = counts[c]! + 1;
|
|
105
|
+
for (let j = 0; j < d; j++) sums[c]![j] = sums[c]![j]! + x[j]!;
|
|
106
|
+
});
|
|
107
|
+
// An emptied cluster keeps its previous centroid.
|
|
108
|
+
centroids.forEach((c, ci) => {
|
|
109
|
+
if (counts[ci]! > 0) for (let j = 0; j < d; j++) c[j] = sums[ci]![j]! / counts[ci]!;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
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
|
+
|
|
117
|
+
let inertia = 0;
|
|
118
|
+
X.forEach((x, i) => (inertia += squaredDistance(x, centroids[assign[i]!]!)));
|
|
119
|
+
return new KMeansModel(centroids, inertia);
|
|
120
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/knn.ts — k-nearest-neighbours classification over feature vectors.
|
|
3
|
+
*
|
|
4
|
+
* Votes are similarity-weighted (cosine similarity, or 1/(1+distance) for the
|
|
5
|
+
* euclidean metric), so a close neighbour counts for more than a far one. The
|
|
6
|
+
* same index answers "what is my nearest neighbour?" — the de-duplication query.
|
|
7
|
+
*/
|
|
8
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
9
|
+
import { classCount, cosine, featureDim, squaredDistance, topProbability, type Vec } from "./math.js";
|
|
10
|
+
|
|
11
|
+
export type KnnMetric = "cosine" | "euclidean";
|
|
12
|
+
|
|
13
|
+
export interface KnnOptions {
|
|
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
|
+
*/
|
|
18
|
+
k?: number;
|
|
19
|
+
metric?: KnnMetric;
|
|
20
|
+
/** Number of classes (default: inferred from the labels, at least 2). */
|
|
21
|
+
classes?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Neighbour {
|
|
25
|
+
index: number;
|
|
26
|
+
/** Higher = closer: cosine similarity, or 1/(1+distance) for euclidean. */
|
|
27
|
+
similarity: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class KnnModel implements MlPredictor {
|
|
31
|
+
private readonly _x: Float32Array[] = [];
|
|
32
|
+
private readonly _y: number[] = [];
|
|
33
|
+
private readonly _w: number[] = [];
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
readonly k: number,
|
|
37
|
+
readonly metric: KnnMetric,
|
|
38
|
+
readonly classes: number,
|
|
39
|
+
) {
|
|
40
|
+
if (!(k >= 1)) throw new Error(`KnnModel: k must be ≥ 1 (got ${k})`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** How many points the index holds. */
|
|
44
|
+
get size(): number {
|
|
45
|
+
return this._x.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Add one labelled point (online — the index never needs a refit). */
|
|
49
|
+
add(x: Vec, y = 0, weight = 1): void {
|
|
50
|
+
this._x.push(Float32Array.from(x));
|
|
51
|
+
this._y.push(y);
|
|
52
|
+
this._w.push(weight);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
similarity(a: Vec, b: Vec): number {
|
|
56
|
+
return this.metric === "cosine" ? cosine(a, b) : 1 / (1 + Math.sqrt(squaredDistance(a, b)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The `k` closest points, closest first. */
|
|
60
|
+
neighbours(x: Vec, k: number = this.k): Neighbour[] {
|
|
61
|
+
const all = this._x.map((p, index) => ({ index, similarity: this.similarity(x, p) }));
|
|
62
|
+
all.sort((a, b) => b.similarity - a.similarity || a.index - b.index);
|
|
63
|
+
return all.slice(0, Math.min(k, all.length));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The single closest point, or null when the index is empty. */
|
|
67
|
+
nearest(x: Vec): Neighbour | null {
|
|
68
|
+
return this.neighbours(x, 1)[0] ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The label of an indexed point. */
|
|
72
|
+
labelOf(index: number): number {
|
|
73
|
+
return this._y[index]!;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Class probabilities from similarity-weighted votes (uniform when empty). */
|
|
77
|
+
proba(x: Vec): number[] {
|
|
78
|
+
const votes = new Array<number>(this.classes).fill(0);
|
|
79
|
+
for (const n of this.neighbours(x)) {
|
|
80
|
+
// Shift cosine into (0, 2] so an orthogonal neighbour still casts a small vote.
|
|
81
|
+
const w = (this.metric === "cosine" ? n.similarity + 1 : n.similarity) * this._w[n.index]! + 1e-9;
|
|
82
|
+
votes[this._y[n.index]!]! += w;
|
|
83
|
+
}
|
|
84
|
+
const sum = votes.reduce((a, b) => a + b, 0);
|
|
85
|
+
return sum > 0 ? votes.map((v) => v / sum) : votes.map(() => 1 / this.classes);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
predict(x: Float32Array): MlPrediction {
|
|
89
|
+
const scores = this.proba(x);
|
|
90
|
+
return { scores, confidence: topProbability(scores) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Index `samples` for kNN classification. */
|
|
95
|
+
export function fitKnn(samples: readonly MlSample[], opts: KnnOptions = {}): KnnModel {
|
|
96
|
+
featureDim(samples.map((s) => s.x));
|
|
97
|
+
const classes = opts.classes ?? classCount(samples.map((s) => s.y));
|
|
98
|
+
const k = opts.k ?? Math.min(5, Math.max(1, Math.floor(Math.sqrt(samples.length))));
|
|
99
|
+
const model = new KnnModel(Math.min(k, samples.length), opts.metric ?? "cosine", classes);
|
|
100
|
+
for (const s of samples) model.add(s.x, s.y, s.weight ?? 1);
|
|
101
|
+
return model;
|
|
102
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/linear.ts — ridge linear regression, closed form.
|
|
3
|
+
*
|
|
4
|
+
* Solves (XᵀWX + λI)w = XᵀWy with an unpenalised intercept by Gaussian
|
|
5
|
+
* elimination with partial pivoting. Small dims only (the catalog's numeric
|
|
6
|
+
* tasks), which is exactly where a closed form beats iterating.
|
|
7
|
+
*/
|
|
8
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
9
|
+
import { featureDim } from "./math.js";
|
|
10
|
+
|
|
11
|
+
export interface LinearOptions {
|
|
12
|
+
/** L2 penalty on weights (not the intercept). Default 1e-6. */
|
|
13
|
+
l2?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class LinearModel implements MlPredictor {
|
|
17
|
+
constructor(
|
|
18
|
+
readonly weights: Float32Array,
|
|
19
|
+
public intercept: number,
|
|
20
|
+
/** Coefficient of determination on the fitted data, clamped to [0, 1]. */
|
|
21
|
+
readonly r2: number,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
value(x: ArrayLike<number>): number {
|
|
25
|
+
let s = this.intercept;
|
|
26
|
+
for (let i = 0; i < this.weights.length; i++) s += this.weights[i]! * (x[i] ?? 0);
|
|
27
|
+
return s;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
predict(x: Float32Array): MlPrediction {
|
|
31
|
+
return { scores: [this.value(x)], confidence: this.r2 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
exportParams(): Float32Array {
|
|
35
|
+
const out = new Float32Array(this.weights.length + 1);
|
|
36
|
+
out.set(this.weights, 0);
|
|
37
|
+
out[this.weights.length] = this.intercept;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
importParams(params: ArrayLike<number>): void {
|
|
42
|
+
if (params.length !== this.weights.length + 1) {
|
|
43
|
+
throw new Error(`LinearModel.importParams: expected ${this.weights.length + 1} values, got ${params.length}`);
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i < this.weights.length; i++) this.weights[i] = params[i]!;
|
|
46
|
+
this.intercept = params[this.weights.length]!;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Solve A·x = b in place (A is n×n row-major). Throws when singular. */
|
|
51
|
+
export function solveLinearSystem(A: Float64Array, b: Float64Array, n: number): Float64Array {
|
|
52
|
+
for (let col = 0; col < n; col++) {
|
|
53
|
+
let piv = col;
|
|
54
|
+
for (let r = col + 1; r < n; r++) if (Math.abs(A[r * n + col]!) > Math.abs(A[piv * n + col]!)) piv = r;
|
|
55
|
+
if (Math.abs(A[piv * n + col]!) < 1e-12) throw new Error("solveLinearSystem: singular matrix");
|
|
56
|
+
if (piv !== col) {
|
|
57
|
+
for (let c = 0; c < n; c++) {
|
|
58
|
+
const t = A[col * n + c]!;
|
|
59
|
+
A[col * n + c] = A[piv * n + c]!;
|
|
60
|
+
A[piv * n + c] = t;
|
|
61
|
+
}
|
|
62
|
+
const t = b[col]!;
|
|
63
|
+
b[col] = b[piv]!;
|
|
64
|
+
b[piv] = t;
|
|
65
|
+
}
|
|
66
|
+
for (let r = col + 1; r < n; r++) {
|
|
67
|
+
const f = A[r * n + col]! / A[col * n + col]!;
|
|
68
|
+
if (f === 0) continue;
|
|
69
|
+
for (let c = col; c < n; c++) A[r * n + c] = A[r * n + c]! - f * A[col * n + c]!;
|
|
70
|
+
b[r] = b[r]! - f * b[col]!;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const x = new Float64Array(n);
|
|
74
|
+
for (let r = n - 1; r >= 0; r--) {
|
|
75
|
+
let s = b[r]!;
|
|
76
|
+
for (let c = r + 1; c < n; c++) s -= A[r * n + c]! * x[c]!;
|
|
77
|
+
x[r] = s / A[r * n + r]!;
|
|
78
|
+
}
|
|
79
|
+
return x;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function fitLinear(samples: readonly MlSample[], opts: LinearOptions = {}): LinearModel {
|
|
83
|
+
const d = featureDim(samples.map((s) => s.x));
|
|
84
|
+
const n = d + 1; // last column = intercept
|
|
85
|
+
const A = new Float64Array(n * n);
|
|
86
|
+
const b = new Float64Array(n);
|
|
87
|
+
const row = new Float64Array(n);
|
|
88
|
+
for (const s of samples) {
|
|
89
|
+
const w = s.weight ?? 1;
|
|
90
|
+
for (let i = 0; i < d; i++) row[i] = s.x[i]!;
|
|
91
|
+
row[d] = 1;
|
|
92
|
+
for (let i = 0; i < n; i++) {
|
|
93
|
+
b[i] = b[i]! + w * row[i]! * s.y;
|
|
94
|
+
for (let j = 0; j < n; j++) A[i * n + j] = A[i * n + j]! + w * row[i]! * row[j]!;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const l2 = opts.l2 ?? 1e-6;
|
|
98
|
+
for (let i = 0; i < d; i++) A[i * n + i] = A[i * n + i]! + l2;
|
|
99
|
+
const sol = solveLinearSystem(A, b, n);
|
|
100
|
+
const model = new LinearModel(Float32Array.from(sol.subarray(0, d)), sol[d]!, 0);
|
|
101
|
+
|
|
102
|
+
// R² on the fitted data — the confidence the runner reports.
|
|
103
|
+
let wSum = 0;
|
|
104
|
+
let mean = 0;
|
|
105
|
+
for (const s of samples) {
|
|
106
|
+
wSum += s.weight ?? 1;
|
|
107
|
+
mean += (s.weight ?? 1) * s.y;
|
|
108
|
+
}
|
|
109
|
+
mean /= wSum;
|
|
110
|
+
let ssRes = 0;
|
|
111
|
+
let ssTot = 0;
|
|
112
|
+
for (const s of samples) {
|
|
113
|
+
const w = s.weight ?? 1;
|
|
114
|
+
ssRes += w * (s.y - model.value(s.x)) ** 2;
|
|
115
|
+
ssTot += w * (s.y - mean) ** 2;
|
|
116
|
+
}
|
|
117
|
+
const r2 = ssTot > 0 ? Math.min(1, Math.max(0, 1 - ssRes / ssTot)) : ssRes < 1e-12 ? 1 : 0;
|
|
118
|
+
return new LinearModel(model.weights, model.intercept, r2);
|
|
119
|
+
}
|
|
@@ -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 { mlEvalLoss } from "../eval_loss.js";
|
|
10
|
+
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
11
|
+
import { classCount, featureDim, softmax, topProbability } from "./math.js";
|
|
12
|
+
|
|
13
|
+
export interface LogisticOptions {
|
|
14
|
+
classes?: number;
|
|
15
|
+
learningRate?: number;
|
|
16
|
+
epochs?: number;
|
|
17
|
+
/** L2 penalty on weights (not biases). Default 1e-3. */
|
|
18
|
+
l2?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class LogisticModel implements MlPredictor {
|
|
22
|
+
/** Row-major weights [classes × dim]. */
|
|
23
|
+
readonly weights: Float32Array;
|
|
24
|
+
readonly bias: Float32Array;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
readonly dim: number,
|
|
28
|
+
readonly classes: number,
|
|
29
|
+
) {
|
|
30
|
+
this.weights = new Float32Array(classes * dim);
|
|
31
|
+
this.bias = new Float32Array(classes);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
logits(x: ArrayLike<number>): number[] {
|
|
35
|
+
const z: number[] = [];
|
|
36
|
+
for (let c = 0; c < this.classes; c++) {
|
|
37
|
+
let s = this.bias[c]!;
|
|
38
|
+
const off = c * this.dim;
|
|
39
|
+
for (let i = 0; i < this.dim; i++) s += this.weights[off + i]! * (x[i] ?? 0);
|
|
40
|
+
z.push(s);
|
|
41
|
+
}
|
|
42
|
+
return z;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
proba(x: ArrayLike<number>): number[] {
|
|
46
|
+
return Array.from(softmax(this.logits(x)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
predict(x: Float32Array): MlPrediction {
|
|
50
|
+
const scores = this.proba(x);
|
|
51
|
+
return { scores, confidence: topProbability(scores) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Weighted mean cross-entropy over `samples` — exactly the eval-gate loss ({@link mlEvalLoss}); 0 for an empty set. */
|
|
55
|
+
loss(samples: readonly MlSample[]): number {
|
|
56
|
+
return mlEvalLoss("classify", this, samples) ?? 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One full-batch gradient step; returns the pre-step mean loss. */
|
|
60
|
+
step(samples: readonly MlSample[], learningRate: number, l2: number): number {
|
|
61
|
+
const gW = new Float64Array(this.weights.length);
|
|
62
|
+
const gB = new Float64Array(this.classes);
|
|
63
|
+
let loss = 0;
|
|
64
|
+
let wSum = 0;
|
|
65
|
+
for (const s of samples) {
|
|
66
|
+
const w = s.weight ?? 1;
|
|
67
|
+
const p = this.proba(s.x);
|
|
68
|
+
loss += -w * Math.log(Math.max(p[s.y]!, 1e-12));
|
|
69
|
+
wSum += w;
|
|
70
|
+
for (let c = 0; c < this.classes; c++) {
|
|
71
|
+
const g = w * (p[c]! - (c === s.y ? 1 : 0));
|
|
72
|
+
gB[c] = gB[c]! + g;
|
|
73
|
+
const off = c * this.dim;
|
|
74
|
+
for (let i = 0; i < this.dim; i++) gW[off + i] = gW[off + i]! + g * s.x[i]!;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const inv = wSum > 0 ? 1 / wSum : 0;
|
|
78
|
+
for (let j = 0; j < this.weights.length; j++) {
|
|
79
|
+
this.weights[j] = this.weights[j]! - learningRate * (gW[j]! * inv + l2 * this.weights[j]!);
|
|
80
|
+
}
|
|
81
|
+
for (let c = 0; c < this.classes; c++) this.bias[c] = this.bias[c]! - learningRate * gB[c]! * inv;
|
|
82
|
+
return loss * inv;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
exportParams(): Float32Array {
|
|
86
|
+
const out = new Float32Array(this.weights.length + this.bias.length);
|
|
87
|
+
out.set(this.weights, 0);
|
|
88
|
+
out.set(this.bias, this.weights.length);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
importParams(params: ArrayLike<number>): void {
|
|
93
|
+
if (params.length !== this.weights.length + this.bias.length) {
|
|
94
|
+
throw new Error(`LogisticModel.importParams: expected ${this.weights.length + this.bias.length} values, got ${params.length}`);
|
|
95
|
+
}
|
|
96
|
+
for (let j = 0; j < this.weights.length; j++) this.weights[j] = params[j]!;
|
|
97
|
+
for (let c = 0; c < this.bias.length; c++) this.bias[c] = params[this.weights.length + c]!;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Fit logistic regression. Pass `init` to continue from existing weights (a local
|
|
103
|
+
* round on top of a merged federated head).
|
|
104
|
+
*/
|
|
105
|
+
export function fitLogistic(
|
|
106
|
+
samples: readonly MlSample[],
|
|
107
|
+
opts: LogisticOptions = {},
|
|
108
|
+
init?: LogisticModel,
|
|
109
|
+
): LogisticModel {
|
|
110
|
+
const d = featureDim(samples.map((s) => s.x));
|
|
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
|
+
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
|
+
}
|