@dniskav/neuron 0.2.7 → 0.3.0
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/README.md +456 -195
- package/dist/index.d.mts +470 -1
- package/dist/index.d.ts +470 -1
- package/dist/index.js +3023 -2
- package/dist/index.mjs +2985 -2
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -2430,12 +2430,12 @@ var Trainer = class {
|
|
|
2430
2430
|
precisions.push(colSum > 0 ? tp / colSum : 0);
|
|
2431
2431
|
recalls.push(rowSum > 0 ? tp / rowSum : 0);
|
|
2432
2432
|
}
|
|
2433
|
-
const
|
|
2433
|
+
const accuracy2 = totalSamples > 0 ? totalCorrect / totalSamples : 0;
|
|
2434
2434
|
const macroPrecision = precisions.reduce((a, b) => a + b, 0) / nClasses;
|
|
2435
2435
|
const macroRecall = recalls.reduce((a, b) => a + b, 0) / nClasses;
|
|
2436
2436
|
const f1 = macroPrecision + macroRecall > 0 ? 2 * macroPrecision * macroRecall / (macroPrecision + macroRecall) : 0;
|
|
2437
2437
|
return {
|
|
2438
|
-
accuracy,
|
|
2438
|
+
accuracy: accuracy2,
|
|
2439
2439
|
precision: macroPrecision,
|
|
2440
2440
|
recall: macroRecall,
|
|
2441
2441
|
f1
|
|
@@ -2598,22 +2598,2982 @@ var ModelSaver = class _ModelSaver {
|
|
|
2598
2598
|
_ModelSaver.fromJSON(model, json);
|
|
2599
2599
|
}
|
|
2600
2600
|
};
|
|
2601
|
+
|
|
2602
|
+
// src/Perceptron.ts
|
|
2603
|
+
var Perceptron = class {
|
|
2604
|
+
// ─── Constructor ─────────────────────────────────────────────────────────────
|
|
2605
|
+
// All weights and bias start at 0. The perceptron learning rule does not
|
|
2606
|
+
// require random initialization because the step function already breaks
|
|
2607
|
+
// symmetry when any misclassification occurs.
|
|
2608
|
+
constructor(nInputs) {
|
|
2609
|
+
if (!Number.isInteger(nInputs) || nInputs <= 0) {
|
|
2610
|
+
throw new Error(
|
|
2611
|
+
`Perceptron: nInputs must be a positive integer, got ${nInputs}`
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
this.weights = new Array(nInputs).fill(0);
|
|
2615
|
+
this.bias = 0;
|
|
2616
|
+
}
|
|
2617
|
+
// ─── Forward pass ────────────────────────────────────────────────────────────
|
|
2618
|
+
// Computes z = Σ(wᵢ·xᵢ) + bias, then applies the Heaviside step function.
|
|
2619
|
+
// Returns 1 if z > 0, else 0.
|
|
2620
|
+
predict(inputs) {
|
|
2621
|
+
validateArray(inputs, this.weights.length, "Perceptron.predict");
|
|
2622
|
+
let z = this.bias;
|
|
2623
|
+
for (let i = 0; i < this.weights.length; i++) {
|
|
2624
|
+
z += this.weights[i] * inputs[i];
|
|
2625
|
+
}
|
|
2626
|
+
return z > 0 ? 1 : 0;
|
|
2627
|
+
}
|
|
2628
|
+
// ─── Training step ───────────────────────────────────────────────────────────
|
|
2629
|
+
// Applies the perceptron update rule for a single (input, target) pair.
|
|
2630
|
+
//
|
|
2631
|
+
// error = target − output (0 on correct prediction → no update)
|
|
2632
|
+
// wᵢ ← wᵢ + lr · error · xᵢ
|
|
2633
|
+
// bias ← bias + lr · error
|
|
2634
|
+
//
|
|
2635
|
+
// Returns the error (useful for tracking convergence).
|
|
2636
|
+
train(inputs, target, lr) {
|
|
2637
|
+
validateArray(inputs, this.weights.length, "Perceptron.train");
|
|
2638
|
+
validateNumber(target, "Perceptron.train");
|
|
2639
|
+
validateNumber(lr, "Perceptron.train");
|
|
2640
|
+
if (target !== 0 && target !== 1) {
|
|
2641
|
+
throw new Error(
|
|
2642
|
+
`Perceptron.train: target must be 0 or 1, got ${target}`
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
if (lr <= 0) {
|
|
2646
|
+
throw new Error(
|
|
2647
|
+
`Perceptron.train: learning rate must be positive, got ${lr}`
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
const output = this.predict(inputs);
|
|
2651
|
+
const error = target - output;
|
|
2652
|
+
if (error !== 0) {
|
|
2653
|
+
for (let i = 0; i < this.weights.length; i++) {
|
|
2654
|
+
this.weights[i] += lr * error * inputs[i];
|
|
2655
|
+
}
|
|
2656
|
+
this.bias += lr * error;
|
|
2657
|
+
}
|
|
2658
|
+
return error;
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
// src/LinearRegression.ts
|
|
2663
|
+
function matMul2(A, B) {
|
|
2664
|
+
const m = A.length;
|
|
2665
|
+
const k = A[0].length;
|
|
2666
|
+
const n = B[0].length;
|
|
2667
|
+
const C = Array.from({ length: m }, () => new Array(n).fill(0));
|
|
2668
|
+
for (let i = 0; i < m; i++) {
|
|
2669
|
+
for (let j = 0; j < n; j++) {
|
|
2670
|
+
let sum = 0;
|
|
2671
|
+
for (let p = 0; p < k; p++) sum += A[i][p] * B[p][j];
|
|
2672
|
+
C[i][j] = sum;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
return C;
|
|
2676
|
+
}
|
|
2677
|
+
function transpose2(A) {
|
|
2678
|
+
const m = A.length;
|
|
2679
|
+
const n = A[0].length;
|
|
2680
|
+
const T = Array.from({ length: n }, () => new Array(m).fill(0));
|
|
2681
|
+
for (let i = 0; i < m; i++) {
|
|
2682
|
+
for (let j = 0; j < n; j++) {
|
|
2683
|
+
T[j][i] = A[i][j];
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return T;
|
|
2687
|
+
}
|
|
2688
|
+
function invertMatrix(M) {
|
|
2689
|
+
const n = M.length;
|
|
2690
|
+
const aug = M.map((row, i) => {
|
|
2691
|
+
const id = new Array(n).fill(0);
|
|
2692
|
+
id[i] = 1;
|
|
2693
|
+
return [...row, ...id];
|
|
2694
|
+
});
|
|
2695
|
+
for (let col = 0; col < n; col++) {
|
|
2696
|
+
let maxRow = col;
|
|
2697
|
+
let maxVal = Math.abs(aug[col][col]);
|
|
2698
|
+
for (let row = col + 1; row < n; row++) {
|
|
2699
|
+
if (Math.abs(aug[row][col]) > maxVal) {
|
|
2700
|
+
maxVal = Math.abs(aug[row][col]);
|
|
2701
|
+
maxRow = row;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
[aug[col], aug[maxRow]] = [aug[maxRow], aug[col]];
|
|
2705
|
+
const pivot = aug[col][col];
|
|
2706
|
+
if (Math.abs(pivot) < 1e-12) return null;
|
|
2707
|
+
for (let j = 0; j < 2 * n; j++) aug[col][j] /= pivot;
|
|
2708
|
+
for (let row = 0; row < n; row++) {
|
|
2709
|
+
if (row === col) continue;
|
|
2710
|
+
const factor = aug[row][col];
|
|
2711
|
+
for (let j = 0; j < 2 * n; j++) {
|
|
2712
|
+
aug[row][j] -= factor * aug[col][j];
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
return aug.map((row) => row.slice(n));
|
|
2717
|
+
}
|
|
2718
|
+
function augment(X) {
|
|
2719
|
+
return X.map((row) => [...row, 1]);
|
|
2720
|
+
}
|
|
2721
|
+
var LinearRegression = class {
|
|
2722
|
+
constructor() {
|
|
2723
|
+
// weights = [w₁, w₂, ..., wₙ, bias]
|
|
2724
|
+
this.weights = [];
|
|
2725
|
+
this._nFeatures = 0;
|
|
2726
|
+
}
|
|
2727
|
+
// ─── Normal Equation ───────────────────────────────────────────────────────
|
|
2728
|
+
// W = (XᵀX)⁻¹Xᵀy — exact solution in one matrix operation.
|
|
2729
|
+
// Augments X with a bias column so the bias is solved jointly.
|
|
2730
|
+
fitNormal(X, y) {
|
|
2731
|
+
if (X.length === 0) throw new Error("LinearRegression.fitNormal: X is empty");
|
|
2732
|
+
if (X.length !== y.length) {
|
|
2733
|
+
throw new Error(
|
|
2734
|
+
`LinearRegression.fitNormal: X has ${X.length} rows but y has ${y.length} elements`
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
this._nFeatures = X[0].length;
|
|
2738
|
+
const Xa = augment(X);
|
|
2739
|
+
const XaT = transpose2(Xa);
|
|
2740
|
+
const XaTXa = matMul2(XaT, Xa);
|
|
2741
|
+
const XaTXaInv = invertMatrix(XaTXa);
|
|
2742
|
+
if (XaTXaInv === null) {
|
|
2743
|
+
throw new Error(
|
|
2744
|
+
"LinearRegression.fitNormal: X\u1D40X is singular \u2014 features may be linearly dependent"
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
const yCol = y.map((v) => [v]);
|
|
2748
|
+
const XaTy = matMul2(XaT, yCol);
|
|
2749
|
+
const W = matMul2(XaTXaInv, XaTy);
|
|
2750
|
+
this.weights = W.map((row) => row[0]);
|
|
2751
|
+
}
|
|
2752
|
+
// ─── Gradient Descent ──────────────────────────────────────────────────────
|
|
2753
|
+
// Minimises MSE = (1/m) Σ (ŷᵢ − yᵢ)² iteratively.
|
|
2754
|
+
//
|
|
2755
|
+
// ŷ = Xa · W
|
|
2756
|
+
// dW = (2/m) · Xaᵀ · (ŷ − y)
|
|
2757
|
+
// W ← W − lr · dW
|
|
2758
|
+
//
|
|
2759
|
+
// Returns the loss (MSE) at every epoch for convergence diagnostics.
|
|
2760
|
+
fitGD(X, y, lr, epochs) {
|
|
2761
|
+
if (X.length === 0) throw new Error("LinearRegression.fitGD: X is empty");
|
|
2762
|
+
if (X.length !== y.length) {
|
|
2763
|
+
throw new Error(
|
|
2764
|
+
`LinearRegression.fitGD: X has ${X.length} rows but y has ${y.length} elements`
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
validateNumber(lr, "LinearRegression.fitGD");
|
|
2768
|
+
if (lr <= 0) throw new Error("LinearRegression.fitGD: lr must be positive");
|
|
2769
|
+
if (!Number.isInteger(epochs) || epochs <= 0) {
|
|
2770
|
+
throw new Error("LinearRegression.fitGD: epochs must be a positive integer");
|
|
2771
|
+
}
|
|
2772
|
+
this._nFeatures = X[0].length;
|
|
2773
|
+
const m = X.length;
|
|
2774
|
+
const Xa = augment(X);
|
|
2775
|
+
this.weights = new Array(this._nFeatures + 1).fill(0);
|
|
2776
|
+
const lossHistory = [];
|
|
2777
|
+
for (let epoch = 0; epoch < epochs; epoch++) {
|
|
2778
|
+
const yHat = Xa.map(
|
|
2779
|
+
(row) => row.reduce((s, x, j) => s + x * this.weights[j], 0)
|
|
2780
|
+
);
|
|
2781
|
+
const residuals = yHat.map((yh, i) => yh - y[i]);
|
|
2782
|
+
const mse2 = residuals.reduce((s, r) => s + r * r, 0) / m;
|
|
2783
|
+
lossHistory.push(mse2);
|
|
2784
|
+
for (let j = 0; j < this.weights.length; j++) {
|
|
2785
|
+
let grad = 0;
|
|
2786
|
+
for (let i = 0; i < m; i++) {
|
|
2787
|
+
grad += Xa[i][j] * residuals[i];
|
|
2788
|
+
}
|
|
2789
|
+
this.weights[j] -= lr * (2 / m) * grad;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
return lossHistory;
|
|
2793
|
+
}
|
|
2794
|
+
// ─── Inference ─────────────────────────────────────────────────────────────
|
|
2795
|
+
// ŷ = Σ wᵢ·xᵢ + bias (bias = weights[last])
|
|
2796
|
+
predict(x) {
|
|
2797
|
+
if (this.weights.length === 0) {
|
|
2798
|
+
throw new Error("LinearRegression.predict: model has not been fitted yet");
|
|
2799
|
+
}
|
|
2800
|
+
if (x.length !== this._nFeatures) {
|
|
2801
|
+
throw new Error(
|
|
2802
|
+
`LinearRegression.predict: expected ${this._nFeatures} features, got ${x.length}`
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
let out = this.weights[this._nFeatures];
|
|
2806
|
+
for (let i = 0; i < this._nFeatures; i++) {
|
|
2807
|
+
out += this.weights[i] * x[i];
|
|
2808
|
+
}
|
|
2809
|
+
return out;
|
|
2810
|
+
}
|
|
2811
|
+
// ─── Introspection ─────────────────────────────────────────────────────────
|
|
2812
|
+
getCoefficients() {
|
|
2813
|
+
if (this.weights.length === 0) {
|
|
2814
|
+
throw new Error(
|
|
2815
|
+
"LinearRegression.getCoefficients: model has not been fitted yet"
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2818
|
+
return {
|
|
2819
|
+
weights: this.weights.slice(0, this._nFeatures),
|
|
2820
|
+
bias: this.weights[this._nFeatures]
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
|
|
2825
|
+
// src/LogisticRegression.ts
|
|
2826
|
+
function sigmoid5(z) {
|
|
2827
|
+
return 1 / (1 + Math.exp(-z));
|
|
2828
|
+
}
|
|
2829
|
+
function bce(target, pred) {
|
|
2830
|
+
const eps = 1e-15;
|
|
2831
|
+
const p = Math.max(eps, Math.min(1 - eps, pred));
|
|
2832
|
+
return -(target * Math.log(p) + (1 - target) * Math.log(1 - p));
|
|
2833
|
+
}
|
|
2834
|
+
var LogisticRegression = class {
|
|
2835
|
+
constructor() {
|
|
2836
|
+
this.weights = [];
|
|
2837
|
+
this.bias = 0;
|
|
2838
|
+
this._nFeatures = 0;
|
|
2839
|
+
}
|
|
2840
|
+
// ─── Train ────────────────────────────────────────────────────────────────
|
|
2841
|
+
// Online SGD over the full dataset for `epochs` passes.
|
|
2842
|
+
// Updates are applied after each sample (stochastic gradient descent).
|
|
2843
|
+
//
|
|
2844
|
+
// Returns the mean BCE loss per epoch for convergence monitoring.
|
|
2845
|
+
train(X, y, lr, epochs) {
|
|
2846
|
+
if (X.length === 0) throw new Error("LogisticRegression.train: X is empty");
|
|
2847
|
+
if (X.length !== y.length) {
|
|
2848
|
+
throw new Error(
|
|
2849
|
+
`LogisticRegression.train: X has ${X.length} rows but y has ${y.length} labels`
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
validateNumber(lr, "LogisticRegression.train");
|
|
2853
|
+
if (lr <= 0) throw new Error("LogisticRegression.train: lr must be positive");
|
|
2854
|
+
if (!Number.isInteger(epochs) || epochs <= 0) {
|
|
2855
|
+
throw new Error("LogisticRegression.train: epochs must be a positive integer");
|
|
2856
|
+
}
|
|
2857
|
+
this._nFeatures = X[0].length;
|
|
2858
|
+
if (this.weights.length !== this._nFeatures) {
|
|
2859
|
+
const limit = Math.sqrt(2 / this._nFeatures);
|
|
2860
|
+
this.weights = Array.from(
|
|
2861
|
+
{ length: this._nFeatures },
|
|
2862
|
+
() => (Math.random() * 2 - 1) * limit
|
|
2863
|
+
);
|
|
2864
|
+
this.bias = 0;
|
|
2865
|
+
}
|
|
2866
|
+
const lossHistory = [];
|
|
2867
|
+
for (let epoch = 0; epoch < epochs; epoch++) {
|
|
2868
|
+
let epochLoss = 0;
|
|
2869
|
+
for (let i = 0; i < X.length; i++) {
|
|
2870
|
+
const xi = X[i];
|
|
2871
|
+
const yi = y[i];
|
|
2872
|
+
let z = this.bias;
|
|
2873
|
+
for (let j = 0; j < this._nFeatures; j++) z += this.weights[j] * xi[j];
|
|
2874
|
+
const yHat = sigmoid5(z);
|
|
2875
|
+
epochLoss += bce(yi, yHat);
|
|
2876
|
+
const delta = yi - yHat;
|
|
2877
|
+
for (let j = 0; j < this._nFeatures; j++) {
|
|
2878
|
+
this.weights[j] += lr * delta * xi[j];
|
|
2879
|
+
}
|
|
2880
|
+
this.bias += lr * delta;
|
|
2881
|
+
}
|
|
2882
|
+
lossHistory.push(epochLoss / X.length);
|
|
2883
|
+
}
|
|
2884
|
+
return lossHistory;
|
|
2885
|
+
}
|
|
2886
|
+
// ─── Predict (probability) ────────────────────────────────────────────────
|
|
2887
|
+
// Returns P(y=1|x) ∈ [0, 1].
|
|
2888
|
+
predict(x) {
|
|
2889
|
+
if (this.weights.length === 0) {
|
|
2890
|
+
throw new Error("LogisticRegression.predict: model has not been trained yet");
|
|
2891
|
+
}
|
|
2892
|
+
validateArray(x, this._nFeatures, "LogisticRegression.predict");
|
|
2893
|
+
let z = this.bias;
|
|
2894
|
+
for (let j = 0; j < this._nFeatures; j++) z += this.weights[j] * x[j];
|
|
2895
|
+
return sigmoid5(z);
|
|
2896
|
+
}
|
|
2897
|
+
// ─── Classify (hard label) ────────────────────────────────────────────────
|
|
2898
|
+
// Returns 0 or 1 using 0.5 as the decision threshold.
|
|
2899
|
+
classify(x) {
|
|
2900
|
+
return this.predict(x) >= 0.5 ? 1 : 0;
|
|
2901
|
+
}
|
|
2902
|
+
};
|
|
2903
|
+
var SoftmaxRegression = class {
|
|
2904
|
+
constructor() {
|
|
2905
|
+
// weights[k][j] = weight for class k, feature j
|
|
2906
|
+
this.weights = [];
|
|
2907
|
+
// biases[k] = bias for class k
|
|
2908
|
+
this.biases = [];
|
|
2909
|
+
this._nFeatures = 0;
|
|
2910
|
+
this._nClasses = 0;
|
|
2911
|
+
}
|
|
2912
|
+
// ─── Softmax helper ──────────────────────────────────────────────────────
|
|
2913
|
+
_softmax(scores) {
|
|
2914
|
+
const maxScore = Math.max(...scores);
|
|
2915
|
+
const exps = scores.map((s) => Math.exp(s - maxScore));
|
|
2916
|
+
const sum = exps.reduce((a, b) => a + b, 0);
|
|
2917
|
+
return exps.map((e) => e / sum);
|
|
2918
|
+
}
|
|
2919
|
+
// ─── Train ────────────────────────────────────────────────────────────────
|
|
2920
|
+
// y must contain integer class labels 0..K-1.
|
|
2921
|
+
// Returns mean cross-entropy loss per epoch.
|
|
2922
|
+
train(X, y, lr, epochs) {
|
|
2923
|
+
if (X.length === 0) throw new Error("SoftmaxRegression.train: X is empty");
|
|
2924
|
+
if (X.length !== y.length) {
|
|
2925
|
+
throw new Error(
|
|
2926
|
+
`SoftmaxRegression.train: X has ${X.length} rows but y has ${y.length} labels`
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
validateNumber(lr, "SoftmaxRegression.train");
|
|
2930
|
+
if (lr <= 0) throw new Error("SoftmaxRegression.train: lr must be positive");
|
|
2931
|
+
if (!Number.isInteger(epochs) || epochs <= 0) {
|
|
2932
|
+
throw new Error("SoftmaxRegression.train: epochs must be a positive integer");
|
|
2933
|
+
}
|
|
2934
|
+
this._nFeatures = X[0].length;
|
|
2935
|
+
this._nClasses = Math.max(...y) + 1;
|
|
2936
|
+
if (this._nClasses < 2) {
|
|
2937
|
+
throw new Error("SoftmaxRegression.train: need at least 2 classes in y");
|
|
2938
|
+
}
|
|
2939
|
+
const limit = Math.sqrt(2 / this._nFeatures);
|
|
2940
|
+
this.weights = Array.from(
|
|
2941
|
+
{ length: this._nClasses },
|
|
2942
|
+
() => Array.from({ length: this._nFeatures }, () => (Math.random() * 2 - 1) * limit)
|
|
2943
|
+
);
|
|
2944
|
+
this.biases = new Array(this._nClasses).fill(0);
|
|
2945
|
+
const lossHistory = [];
|
|
2946
|
+
for (let epoch = 0; epoch < epochs; epoch++) {
|
|
2947
|
+
let epochLoss = 0;
|
|
2948
|
+
for (let i = 0; i < X.length; i++) {
|
|
2949
|
+
const xi = X[i];
|
|
2950
|
+
const trueClass = y[i];
|
|
2951
|
+
const scores = this.weights.map((wk, k) => {
|
|
2952
|
+
let s = this.biases[k];
|
|
2953
|
+
for (let j = 0; j < this._nFeatures; j++) s += wk[j] * xi[j];
|
|
2954
|
+
return s;
|
|
2955
|
+
});
|
|
2956
|
+
const probs = this._softmax(scores);
|
|
2957
|
+
epochLoss += -Math.log(Math.max(probs[trueClass], 1e-15));
|
|
2958
|
+
for (let k = 0; k < this._nClasses; k++) {
|
|
2959
|
+
const delta = probs[k] - (k === trueClass ? 1 : 0);
|
|
2960
|
+
for (let j = 0; j < this._nFeatures; j++) {
|
|
2961
|
+
this.weights[k][j] -= lr * delta * xi[j];
|
|
2962
|
+
}
|
|
2963
|
+
this.biases[k] -= lr * delta;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
lossHistory.push(epochLoss / X.length);
|
|
2967
|
+
}
|
|
2968
|
+
return lossHistory;
|
|
2969
|
+
}
|
|
2970
|
+
// ─── Predict (class probabilities) ───────────────────────────────────────
|
|
2971
|
+
predictProba(x) {
|
|
2972
|
+
if (this.weights.length === 0) {
|
|
2973
|
+
throw new Error("SoftmaxRegression.predictProba: model has not been trained yet");
|
|
2974
|
+
}
|
|
2975
|
+
validateArray(x, this._nFeatures, "SoftmaxRegression.predictProba");
|
|
2976
|
+
const scores = this.weights.map((wk, k) => {
|
|
2977
|
+
let s = this.biases[k];
|
|
2978
|
+
for (let j = 0; j < this._nFeatures; j++) s += wk[j] * x[j];
|
|
2979
|
+
return s;
|
|
2980
|
+
});
|
|
2981
|
+
return this._softmax(scores);
|
|
2982
|
+
}
|
|
2983
|
+
// ─── Classify (argmax) ────────────────────────────────────────────────────
|
|
2984
|
+
predict(x) {
|
|
2985
|
+
const probs = this.predictProba(x);
|
|
2986
|
+
return probs.indexOf(Math.max(...probs));
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
|
|
2990
|
+
// src/NaiveBayes.ts
|
|
2991
|
+
var GaussianNaiveBayes = class {
|
|
2992
|
+
constructor() {
|
|
2993
|
+
// Per-class, per-feature statistics
|
|
2994
|
+
this._means = /* @__PURE__ */ new Map();
|
|
2995
|
+
this._variances = /* @__PURE__ */ new Map();
|
|
2996
|
+
// Log prior: log P(class)
|
|
2997
|
+
this._logPriors = /* @__PURE__ */ new Map();
|
|
2998
|
+
this._classes = [];
|
|
2999
|
+
this._nFeatures = 0;
|
|
3000
|
+
}
|
|
3001
|
+
// ─── Fit ───────────────────────────────────────────────────────────────────
|
|
3002
|
+
// Scans the data once to compute μ, σ², and π per class.
|
|
3003
|
+
// Variance is clamped to a minimum of 1e-9 to prevent division by zero
|
|
3004
|
+
// when a feature is perfectly constant within a class.
|
|
3005
|
+
fit(X, y) {
|
|
3006
|
+
if (X.length === 0) throw new Error("GaussianNaiveBayes.fit: X is empty");
|
|
3007
|
+
if (X.length !== y.length) {
|
|
3008
|
+
throw new Error(
|
|
3009
|
+
`GaussianNaiveBayes.fit: X has ${X.length} rows but y has ${y.length} labels`
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
3012
|
+
this._nFeatures = X[0].length;
|
|
3013
|
+
const m = X.length;
|
|
3014
|
+
this._classes = [...new Set(y)].sort((a, b) => a - b);
|
|
3015
|
+
for (const c of this._classes) {
|
|
3016
|
+
const rows = X.filter((_, i) => y[i] === c);
|
|
3017
|
+
const count = rows.length;
|
|
3018
|
+
if (count === 0) continue;
|
|
3019
|
+
this._logPriors.set(c, Math.log(count / m));
|
|
3020
|
+
const means = new Array(this._nFeatures).fill(0);
|
|
3021
|
+
for (const row of rows) {
|
|
3022
|
+
for (let j = 0; j < this._nFeatures; j++) means[j] += row[j];
|
|
3023
|
+
}
|
|
3024
|
+
for (let j = 0; j < this._nFeatures; j++) means[j] /= count;
|
|
3025
|
+
this._means.set(c, means);
|
|
3026
|
+
const variances = new Array(this._nFeatures).fill(0);
|
|
3027
|
+
for (const row of rows) {
|
|
3028
|
+
for (let j = 0; j < this._nFeatures; j++) {
|
|
3029
|
+
const diff = row[j] - means[j];
|
|
3030
|
+
variances[j] += diff * diff;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
for (let j = 0; j < this._nFeatures; j++) {
|
|
3034
|
+
variances[j] = Math.max(variances[j] / count, 1e-9);
|
|
3035
|
+
}
|
|
3036
|
+
this._variances.set(c, variances);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
// ─── Log-likelihood of a single feature value under a Gaussian ─────────────
|
|
3040
|
+
// log P(x | μ, σ²) = −0.5·log(2πσ²) − (x−μ)² / (2σ²)
|
|
3041
|
+
_logGaussian(x, mean, variance) {
|
|
3042
|
+
return -0.5 * Math.log(2 * Math.PI * variance) - (x - mean) ** 2 / (2 * variance);
|
|
3043
|
+
}
|
|
3044
|
+
// ─── Log-scores per class ────────────────────────────────────────────────
|
|
3045
|
+
// log P(class|x) ∝ log P(class) + Σⱼ log P(xⱼ|class)
|
|
3046
|
+
_logScores(x) {
|
|
3047
|
+
if (this._classes.length === 0) {
|
|
3048
|
+
throw new Error("GaussianNaiveBayes: model has not been fitted yet");
|
|
3049
|
+
}
|
|
3050
|
+
if (x.length !== this._nFeatures) {
|
|
3051
|
+
throw new Error(
|
|
3052
|
+
`GaussianNaiveBayes: expected ${this._nFeatures} features, got ${x.length}`
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
const scores = /* @__PURE__ */ new Map();
|
|
3056
|
+
for (const c of this._classes) {
|
|
3057
|
+
const means = this._means.get(c);
|
|
3058
|
+
const variances = this._variances.get(c);
|
|
3059
|
+
const logPrior = this._logPriors.get(c);
|
|
3060
|
+
let logLikelihood = 0;
|
|
3061
|
+
for (let j = 0; j < this._nFeatures; j++) {
|
|
3062
|
+
logLikelihood += this._logGaussian(x[j], means[j], variances[j]);
|
|
3063
|
+
}
|
|
3064
|
+
scores.set(c, logPrior + logLikelihood);
|
|
3065
|
+
}
|
|
3066
|
+
return scores;
|
|
3067
|
+
}
|
|
3068
|
+
// ─── Predict (argmax class) ──────────────────────────────────────────────
|
|
3069
|
+
// Returns the class with the highest log-posterior.
|
|
3070
|
+
// No exp() needed — argmax is order-preserving.
|
|
3071
|
+
predict(x) {
|
|
3072
|
+
const scores = this._logScores(x);
|
|
3073
|
+
let bestClass = this._classes[0];
|
|
3074
|
+
let bestScore = -Infinity;
|
|
3075
|
+
for (const [c, s] of scores) {
|
|
3076
|
+
if (s > bestScore) {
|
|
3077
|
+
bestScore = s;
|
|
3078
|
+
bestClass = c;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
return bestClass;
|
|
3082
|
+
}
|
|
3083
|
+
// ─── Predict probabilities ────────────────────────────────────────────────
|
|
3084
|
+
// Converts log-scores to actual probabilities using the log-sum-exp trick
|
|
3085
|
+
// to avoid numerical overflow:
|
|
3086
|
+
//
|
|
3087
|
+
// log Σₖ exp(sₖ) = maxScore + log Σₖ exp(sₖ − maxScore)
|
|
3088
|
+
//
|
|
3089
|
+
// Then P(c|x) = exp(score[c] − log Σ exp).
|
|
3090
|
+
predictProba(x) {
|
|
3091
|
+
const logScores = this._logScores(x);
|
|
3092
|
+
const scores = [...logScores.values()];
|
|
3093
|
+
const maxScore = Math.max(...scores);
|
|
3094
|
+
const logSumExp = maxScore + Math.log(
|
|
3095
|
+
scores.reduce((sum, s) => sum + Math.exp(s - maxScore), 0)
|
|
3096
|
+
);
|
|
3097
|
+
const proba = /* @__PURE__ */ new Map();
|
|
3098
|
+
for (const [c, s] of logScores) {
|
|
3099
|
+
proba.set(c, Math.exp(s - logSumExp));
|
|
3100
|
+
}
|
|
3101
|
+
return proba;
|
|
3102
|
+
}
|
|
3103
|
+
};
|
|
3104
|
+
|
|
3105
|
+
// src/DecisionTree.ts
|
|
3106
|
+
var DecisionTree = class {
|
|
3107
|
+
constructor(options) {
|
|
3108
|
+
this._root = null;
|
|
3109
|
+
this._maxDepth = options?.maxDepth ?? 10;
|
|
3110
|
+
this._minSamplesSplit = options?.minSamplesSplit ?? 2;
|
|
3111
|
+
this._task = options?.task ?? "classification";
|
|
3112
|
+
if (this._maxDepth <= 0) {
|
|
3113
|
+
throw new Error("DecisionTree: maxDepth must be positive");
|
|
3114
|
+
}
|
|
3115
|
+
if (this._minSamplesSplit < 2) {
|
|
3116
|
+
throw new Error("DecisionTree: minSamplesSplit must be at least 2");
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
// ─── Gini impurity ─────────────────────────────────────────────────────────
|
|
3120
|
+
// G = 1 − Σₖ pₖ²
|
|
3121
|
+
// G = 0 when all samples share one class (perfectly pure node).
|
|
3122
|
+
// G ≈ 0.5 for a binary node with equal class distribution.
|
|
3123
|
+
_gini(y) {
|
|
3124
|
+
if (y.length === 0) return 0;
|
|
3125
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3126
|
+
for (const label of y) counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
3127
|
+
let g = 1;
|
|
3128
|
+
for (const count of counts.values()) {
|
|
3129
|
+
const p = count / y.length;
|
|
3130
|
+
g -= p * p;
|
|
3131
|
+
}
|
|
3132
|
+
return g;
|
|
3133
|
+
}
|
|
3134
|
+
// ─── Mean Squared Error (regression impurity) ─────────────────────────────
|
|
3135
|
+
// MSE = (1/n) Σ (yᵢ − ȳ)²
|
|
3136
|
+
_mse(y) {
|
|
3137
|
+
if (y.length === 0) return 0;
|
|
3138
|
+
const mean = y.reduce((a, b) => a + b, 0) / y.length;
|
|
3139
|
+
return y.reduce((acc, v) => acc + (v - mean) ** 2, 0) / y.length;
|
|
3140
|
+
}
|
|
3141
|
+
// ─── Impurity selector ─────────────────────────────────────────────────────
|
|
3142
|
+
_impurity(y) {
|
|
3143
|
+
return this._task === "classification" ? this._gini(y) : this._mse(y);
|
|
3144
|
+
}
|
|
3145
|
+
// ─── Leaf value ────────────────────────────────────────────────────────────
|
|
3146
|
+
// Classification: majority class. Regression: mean.
|
|
3147
|
+
_leafValue(y) {
|
|
3148
|
+
if (this._task === "regression") {
|
|
3149
|
+
return y.reduce((a, b) => a + b, 0) / y.length;
|
|
3150
|
+
}
|
|
3151
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3152
|
+
for (const label of y) counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
3153
|
+
let bestClass = y[0];
|
|
3154
|
+
let bestCount = 0;
|
|
3155
|
+
for (const [cls, cnt] of counts) {
|
|
3156
|
+
if (cnt > bestCount) {
|
|
3157
|
+
bestCount = cnt;
|
|
3158
|
+
bestClass = cls;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
return bestClass;
|
|
3162
|
+
}
|
|
3163
|
+
// ─── Best split search ─────────────────────────────────────────────────────
|
|
3164
|
+
// Brute-force: try every feature × every unique threshold.
|
|
3165
|
+
// Returns the split that minimises weighted impurity (or null if none helps).
|
|
3166
|
+
_bestSplit(X, y) {
|
|
3167
|
+
const nFeatures = X[0].length;
|
|
3168
|
+
const n = y.length;
|
|
3169
|
+
let bestImpurity = Infinity;
|
|
3170
|
+
let bestSplit = null;
|
|
3171
|
+
const parentImpurity = this._impurity(y);
|
|
3172
|
+
for (let j = 0; j < nFeatures; j++) {
|
|
3173
|
+
const values = [...new Set(X.map((row) => row[j]))].sort((a, b) => a - b);
|
|
3174
|
+
for (let vi = 0; vi < values.length - 1; vi++) {
|
|
3175
|
+
const threshold = (values[vi] + values[vi + 1]) / 2;
|
|
3176
|
+
const leftY = [];
|
|
3177
|
+
const rightY = [];
|
|
3178
|
+
for (let i = 0; i < n; i++) {
|
|
3179
|
+
if (X[i][j] <= threshold) leftY.push(y[i]);
|
|
3180
|
+
else rightY.push(y[i]);
|
|
3181
|
+
}
|
|
3182
|
+
if (leftY.length === 0 || rightY.length === 0) continue;
|
|
3183
|
+
const weightedImpurity = leftY.length / n * this._impurity(leftY) + rightY.length / n * this._impurity(rightY);
|
|
3184
|
+
if (weightedImpurity < bestImpurity && weightedImpurity < parentImpurity) {
|
|
3185
|
+
bestImpurity = weightedImpurity;
|
|
3186
|
+
bestSplit = { featureIndex: j, threshold };
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return bestSplit;
|
|
3191
|
+
}
|
|
3192
|
+
// ─── Recursive tree builder ────────────────────────────────────────────────
|
|
3193
|
+
_buildNode(X, y, depth) {
|
|
3194
|
+
const allSame = y.every((v) => v === y[0]);
|
|
3195
|
+
if (depth >= this._maxDepth || y.length < this._minSamplesSplit || allSame) {
|
|
3196
|
+
return { isLeaf: true, value: this._leafValue(y) };
|
|
3197
|
+
}
|
|
3198
|
+
const split = this._bestSplit(X, y);
|
|
3199
|
+
if (split === null) {
|
|
3200
|
+
return { isLeaf: true, value: this._leafValue(y) };
|
|
3201
|
+
}
|
|
3202
|
+
const { featureIndex, threshold } = split;
|
|
3203
|
+
const leftX = [];
|
|
3204
|
+
const leftY = [];
|
|
3205
|
+
const rightX = [];
|
|
3206
|
+
const rightY = [];
|
|
3207
|
+
for (let i = 0; i < y.length; i++) {
|
|
3208
|
+
if (X[i][featureIndex] <= threshold) {
|
|
3209
|
+
leftX.push(X[i]);
|
|
3210
|
+
leftY.push(y[i]);
|
|
3211
|
+
} else {
|
|
3212
|
+
rightX.push(X[i]);
|
|
3213
|
+
rightY.push(y[i]);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
return {
|
|
3217
|
+
isLeaf: false,
|
|
3218
|
+
featureIndex,
|
|
3219
|
+
threshold,
|
|
3220
|
+
left: this._buildNode(leftX, leftY, depth + 1),
|
|
3221
|
+
right: this._buildNode(rightX, rightY, depth + 1)
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
// ─── Fit ──────────────────────────────────────────────────────────────────
|
|
3225
|
+
fit(X, y) {
|
|
3226
|
+
if (X.length === 0) throw new Error("DecisionTree.fit: X is empty");
|
|
3227
|
+
if (X.length !== y.length) {
|
|
3228
|
+
throw new Error(
|
|
3229
|
+
`DecisionTree.fit: X has ${X.length} rows but y has ${y.length} labels`
|
|
3230
|
+
);
|
|
3231
|
+
}
|
|
3232
|
+
this._root = this._buildNode(X, y, 0);
|
|
3233
|
+
}
|
|
3234
|
+
// ─── Traverse a single sample ─────────────────────────────────────────────
|
|
3235
|
+
_traverse(node, x) {
|
|
3236
|
+
if (node.isLeaf) return node.value;
|
|
3237
|
+
if (x[node.featureIndex] <= node.threshold) {
|
|
3238
|
+
return this._traverse(node.left, x);
|
|
3239
|
+
}
|
|
3240
|
+
return this._traverse(node.right, x);
|
|
3241
|
+
}
|
|
3242
|
+
// ─── Predict single sample ────────────────────────────────────────────────
|
|
3243
|
+
predict(x) {
|
|
3244
|
+
if (this._root === null) {
|
|
3245
|
+
throw new Error("DecisionTree.predict: model has not been fitted yet");
|
|
3246
|
+
}
|
|
3247
|
+
return this._traverse(this._root, x);
|
|
3248
|
+
}
|
|
3249
|
+
// ─── Predict batch ────────────────────────────────────────────────────────
|
|
3250
|
+
predictBatch(X) {
|
|
3251
|
+
return X.map((x) => this.predict(x));
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
|
|
3255
|
+
// src/KMeans.ts
|
|
3256
|
+
var KMeans = class {
|
|
3257
|
+
constructor(k, options = {}) {
|
|
3258
|
+
if (!Number.isInteger(k) || k < 1) {
|
|
3259
|
+
throw new Error(`KMeans: k must be a positive integer, got ${k}`);
|
|
3260
|
+
}
|
|
3261
|
+
this._k = k;
|
|
3262
|
+
this._maxIter = options.maxIter ?? 300;
|
|
3263
|
+
this.centroids = [];
|
|
3264
|
+
if (options.seed !== void 0) {
|
|
3265
|
+
let s = options.seed >>> 0;
|
|
3266
|
+
this._rng = () => {
|
|
3267
|
+
s += 1831565813;
|
|
3268
|
+
let t = Math.imul(s ^ s >>> 15, 1 | s);
|
|
3269
|
+
t ^= t + Math.imul(t ^ t >>> 7, 61 | t);
|
|
3270
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
3271
|
+
};
|
|
3272
|
+
} else {
|
|
3273
|
+
this._rng = () => Math.random();
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
// ── fit ────────────────────────────────────────────────────────────────────
|
|
3277
|
+
// Runs K-Means++ init then Lloyd iterations until centroids stop moving or
|
|
3278
|
+
// maxIter is reached.
|
|
3279
|
+
fit(X) {
|
|
3280
|
+
if (!X || X.length === 0) {
|
|
3281
|
+
throw new Error("KMeans.fit: dataset X must be non-empty");
|
|
3282
|
+
}
|
|
3283
|
+
const n = X.length;
|
|
3284
|
+
const d = X[0].length;
|
|
3285
|
+
if (this._k > n) {
|
|
3286
|
+
throw new Error(`KMeans.fit: k (${this._k}) cannot exceed number of samples (${n})`);
|
|
3287
|
+
}
|
|
3288
|
+
this.centroids = [];
|
|
3289
|
+
const firstIdx = Math.floor(this._rng() * n);
|
|
3290
|
+
this.centroids.push([...X[firstIdx]]);
|
|
3291
|
+
for (let c = 1; c < this._k; c++) {
|
|
3292
|
+
const dists = X.map((x) => this._minDistSq(x));
|
|
3293
|
+
const total = dists.reduce((s, v) => s + v, 0);
|
|
3294
|
+
let threshold = this._rng() * total;
|
|
3295
|
+
let chosen = n - 1;
|
|
3296
|
+
for (let i = 0; i < n; i++) {
|
|
3297
|
+
threshold -= dists[i];
|
|
3298
|
+
if (threshold <= 0) {
|
|
3299
|
+
chosen = i;
|
|
3300
|
+
break;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
this.centroids.push([...X[chosen]]);
|
|
3304
|
+
}
|
|
3305
|
+
const assignments = new Int32Array(n);
|
|
3306
|
+
for (let iter = 0; iter < this._maxIter; iter++) {
|
|
3307
|
+
for (let i = 0; i < n; i++) {
|
|
3308
|
+
assignments[i] = this._nearestCentroid(X[i]);
|
|
3309
|
+
}
|
|
3310
|
+
const sums = Array.from({ length: this._k }, () => new Array(d).fill(0));
|
|
3311
|
+
const counts = new Int32Array(this._k);
|
|
3312
|
+
for (let i = 0; i < n; i++) {
|
|
3313
|
+
const c = assignments[i];
|
|
3314
|
+
counts[c]++;
|
|
3315
|
+
for (let j = 0; j < d; j++) sums[c][j] += X[i][j];
|
|
3316
|
+
}
|
|
3317
|
+
let moved = false;
|
|
3318
|
+
for (let c = 0; c < this._k; c++) {
|
|
3319
|
+
if (counts[c] === 0) continue;
|
|
3320
|
+
for (let j = 0; j < d; j++) {
|
|
3321
|
+
const newVal = sums[c][j] / counts[c];
|
|
3322
|
+
if (Math.abs(newVal - this.centroids[c][j]) > 1e-10) moved = true;
|
|
3323
|
+
this.centroids[c][j] = newVal;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
if (!moved) break;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
// ── predict ────────────────────────────────────────────────────────────────
|
|
3330
|
+
// Returns the index of the nearest centroid for a single point.
|
|
3331
|
+
predict(x) {
|
|
3332
|
+
if (this.centroids.length === 0) {
|
|
3333
|
+
throw new Error("KMeans.predict: call fit() before predict()");
|
|
3334
|
+
}
|
|
3335
|
+
return this._nearestCentroid(x);
|
|
3336
|
+
}
|
|
3337
|
+
// ── predictBatch ──────────────────────────────────────────────────────────
|
|
3338
|
+
// Assigns each point in X to a cluster. Returns array of cluster indices.
|
|
3339
|
+
predictBatch(X) {
|
|
3340
|
+
return X.map((x) => this.predict(x));
|
|
3341
|
+
}
|
|
3342
|
+
// ── inertia ───────────────────────────────────────────────────────────────
|
|
3343
|
+
// J = Σᵢ d(xᵢ, μ_{cᵢ})²
|
|
3344
|
+
// Lower inertia = tighter clusters. Use the elbow method to pick K:
|
|
3345
|
+
// run fit() for K = 1..10 and plot inertia — the elbow is your optimal K.
|
|
3346
|
+
inertia(X) {
|
|
3347
|
+
if (this.centroids.length === 0) {
|
|
3348
|
+
throw new Error("KMeans.inertia: call fit() before inertia()");
|
|
3349
|
+
}
|
|
3350
|
+
return X.reduce((sum, x) => sum + this._minDistSq(x), 0);
|
|
3351
|
+
}
|
|
3352
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
3353
|
+
_euclideanSq(a, b) {
|
|
3354
|
+
let s = 0;
|
|
3355
|
+
for (let i = 0; i < a.length; i++) s += (a[i] - b[i]) ** 2;
|
|
3356
|
+
return s;
|
|
3357
|
+
}
|
|
3358
|
+
_minDistSq(x) {
|
|
3359
|
+
let min = Infinity;
|
|
3360
|
+
for (const c of this.centroids) {
|
|
3361
|
+
const d = this._euclideanSq(x, c);
|
|
3362
|
+
if (d < min) min = d;
|
|
3363
|
+
}
|
|
3364
|
+
return min;
|
|
3365
|
+
}
|
|
3366
|
+
_nearestCentroid(x) {
|
|
3367
|
+
let best = 0;
|
|
3368
|
+
let bestDist = Infinity;
|
|
3369
|
+
for (let c = 0; c < this.centroids.length; c++) {
|
|
3370
|
+
const d = this._euclideanSq(x, this.centroids[c]);
|
|
3371
|
+
if (d < bestDist) {
|
|
3372
|
+
bestDist = d;
|
|
3373
|
+
best = c;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
return best;
|
|
3377
|
+
}
|
|
3378
|
+
};
|
|
3379
|
+
|
|
3380
|
+
// src/PCA.ts
|
|
3381
|
+
var PCA = class {
|
|
3382
|
+
constructor(nComponents) {
|
|
3383
|
+
if (!Number.isInteger(nComponents) || nComponents < 1) {
|
|
3384
|
+
throw new Error(`PCA: nComponents must be a positive integer, got ${nComponents}`);
|
|
3385
|
+
}
|
|
3386
|
+
this._nComponents = nComponents;
|
|
3387
|
+
this.components = [];
|
|
3388
|
+
this.explainedVariance = [];
|
|
3389
|
+
this.mean = [];
|
|
3390
|
+
}
|
|
3391
|
+
// ── fit ────────────────────────────────────────────────────────────────────
|
|
3392
|
+
// Computes the mean and the top nComponents principal components from X.
|
|
3393
|
+
fit(X) {
|
|
3394
|
+
const n = X.length;
|
|
3395
|
+
if (n < 2) throw new Error("PCA.fit: need at least 2 samples");
|
|
3396
|
+
const p = X[0].length;
|
|
3397
|
+
if (this._nComponents > p) {
|
|
3398
|
+
throw new Error(
|
|
3399
|
+
`PCA: nComponents (${this._nComponents}) cannot exceed number of features (${p})`
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
this.mean = new Array(p).fill(0);
|
|
3403
|
+
for (const row of X) for (let j = 0; j < p; j++) this.mean[j] += row[j];
|
|
3404
|
+
for (let j = 0; j < p; j++) this.mean[j] /= n;
|
|
3405
|
+
const Xc = X.map((row) => row.map((v, j) => v - this.mean[j]));
|
|
3406
|
+
let cov = this._covMatrix(Xc, n, p);
|
|
3407
|
+
this.components = [];
|
|
3408
|
+
this.explainedVariance = [];
|
|
3409
|
+
for (let c = 0; c < this._nComponents; c++) {
|
|
3410
|
+
const { eigenvector, eigenvalue } = this._powerIteration(cov, p);
|
|
3411
|
+
this.components.push(eigenvector);
|
|
3412
|
+
this.explainedVariance.push(eigenvalue);
|
|
3413
|
+
for (let i = 0; i < p; i++) {
|
|
3414
|
+
for (let j = 0; j < p; j++) {
|
|
3415
|
+
cov[i][j] -= eigenvalue * eigenvector[i] * eigenvector[j];
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
// ── transform ──────────────────────────────────────────────────────────────
|
|
3421
|
+
// Z = (X - μ) · Vᵀ shape [n × nComponents]
|
|
3422
|
+
transform(X) {
|
|
3423
|
+
if (this.components.length === 0) {
|
|
3424
|
+
throw new Error("PCA.transform: call fit() before transform()");
|
|
3425
|
+
}
|
|
3426
|
+
return X.map((row) => {
|
|
3427
|
+
const centered = row.map((v, j) => v - this.mean[j]);
|
|
3428
|
+
return this.components.map(
|
|
3429
|
+
(pc) => pc.reduce((s, w, j) => s + w * centered[j], 0)
|
|
3430
|
+
);
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
// ── fitTransform ───────────────────────────────────────────────────────────
|
|
3434
|
+
// Convenience: fit() then transform() in a single call.
|
|
3435
|
+
fitTransform(X) {
|
|
3436
|
+
this.fit(X);
|
|
3437
|
+
return this.transform(X);
|
|
3438
|
+
}
|
|
3439
|
+
// ── inverseTransform ───────────────────────────────────────────────────────
|
|
3440
|
+
// X̂ = Z · V + μ shape [n × nFeatures] (approximate reconstruction)
|
|
3441
|
+
inverseTransform(Z) {
|
|
3442
|
+
if (this.components.length === 0) {
|
|
3443
|
+
throw new Error("PCA.inverseTransform: call fit() before inverseTransform()");
|
|
3444
|
+
}
|
|
3445
|
+
const p = this.mean.length;
|
|
3446
|
+
return Z.map((z) => {
|
|
3447
|
+
const row = new Array(p).fill(0);
|
|
3448
|
+
for (let c = 0; c < this._nComponents; c++) {
|
|
3449
|
+
for (let j = 0; j < p; j++) {
|
|
3450
|
+
row[j] += z[c] * this.components[c][j];
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
return row.map((v, j) => v + this.mean[j]);
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
// ── explainedVarianceRatio ─────────────────────────────────────────────────
|
|
3457
|
+
// rₖ = λₖ / Σⱼ λⱼ
|
|
3458
|
+
// Sum of all ratios ≤ 1. If you chose nComponents = p, the sum is exactly 1.
|
|
3459
|
+
explainedVarianceRatio() {
|
|
3460
|
+
const total = this.explainedVariance.reduce((s, v) => s + v, 0);
|
|
3461
|
+
if (total === 0) return this.explainedVariance.map(() => 0);
|
|
3462
|
+
return this.explainedVariance.map((v) => v / total);
|
|
3463
|
+
}
|
|
3464
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
3465
|
+
// Build the [p×p] covariance matrix from a centered matrix Xc.
|
|
3466
|
+
_covMatrix(Xc, n, p) {
|
|
3467
|
+
const cov = Array.from({ length: p }, () => new Array(p).fill(0));
|
|
3468
|
+
for (const row of Xc) {
|
|
3469
|
+
for (let i = 0; i < p; i++) {
|
|
3470
|
+
for (let j = i; j < p; j++) {
|
|
3471
|
+
cov[i][j] += row[i] * row[j];
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
for (let i = 0; i < p; i++) {
|
|
3476
|
+
cov[i][i] /= n;
|
|
3477
|
+
for (let j = i + 1; j < p; j++) {
|
|
3478
|
+
cov[i][j] /= n;
|
|
3479
|
+
cov[j][i] = cov[i][j];
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
return cov;
|
|
3483
|
+
}
|
|
3484
|
+
// Power iteration: find the dominant eigenvector of a symmetric matrix.
|
|
3485
|
+
// v ← M·v / ‖M·v‖ (repeated until ‖v_new - v_old‖ < tol)
|
|
3486
|
+
// Returns both the eigenvector (unit length) and its eigenvalue λ = vᵀ·M·v.
|
|
3487
|
+
_powerIteration(M, p, maxIter = 1e3, tol = 1e-10) {
|
|
3488
|
+
let v = Array.from({ length: p }, () => Math.random() - 0.5);
|
|
3489
|
+
v = this._normalize(v);
|
|
3490
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
3491
|
+
const Mv2 = this._matvec(M, v);
|
|
3492
|
+
const vNew = this._normalize(Mv2);
|
|
3493
|
+
const dot = v.reduce((s, vi, i) => s + vi * vNew[i], 0);
|
|
3494
|
+
v = vNew;
|
|
3495
|
+
if (Math.abs(Math.abs(dot) - 1) < tol) break;
|
|
3496
|
+
}
|
|
3497
|
+
const Mv = this._matvec(M, v);
|
|
3498
|
+
const eigenvalue = v.reduce((s, vi, i) => s + vi * Mv[i], 0);
|
|
3499
|
+
return { eigenvector: v, eigenvalue: Math.max(0, eigenvalue) };
|
|
3500
|
+
}
|
|
3501
|
+
_matvec(M, v) {
|
|
3502
|
+
return M.map((row) => row.reduce((s, mij, j) => s + mij * v[j], 0));
|
|
3503
|
+
}
|
|
3504
|
+
_normalize(v) {
|
|
3505
|
+
const norm = Math.sqrt(v.reduce((s, vi) => s + vi * vi, 0));
|
|
3506
|
+
if (norm < 1e-14) return v;
|
|
3507
|
+
return v.map((vi) => vi / norm);
|
|
3508
|
+
}
|
|
3509
|
+
};
|
|
3510
|
+
|
|
3511
|
+
// src/SOM.ts
|
|
3512
|
+
var SOM = class {
|
|
3513
|
+
constructor(rows, cols, inputSize, options = {}) {
|
|
3514
|
+
if (rows < 1 || cols < 1 || inputSize < 1) {
|
|
3515
|
+
throw new Error(
|
|
3516
|
+
`SOM: rows, cols and inputSize must be positive integers, got ${rows}\xD7${cols}\xD7${inputSize}`
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
this._rows = rows;
|
|
3520
|
+
this._cols = cols;
|
|
3521
|
+
this._inputSize = inputSize;
|
|
3522
|
+
this._initialLr = options.initialLr ?? 0.5;
|
|
3523
|
+
this._finalLr = options.finalLr ?? 0.01;
|
|
3524
|
+
this._initialSigma = options.initialSigma ?? Math.max(rows, cols) / 2;
|
|
3525
|
+
this._finalSigma = options.finalSigma ?? 1;
|
|
3526
|
+
this.weights = Array.from(
|
|
3527
|
+
{ length: rows },
|
|
3528
|
+
() => Array.from(
|
|
3529
|
+
{ length: cols },
|
|
3530
|
+
() => Array.from({ length: inputSize }, () => Math.random())
|
|
3531
|
+
)
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
// ── train ──────────────────────────────────────────────────────────────────
|
|
3535
|
+
// Iterates over the dataset `epochs` times, presenting each sample and
|
|
3536
|
+
// performing a BMU search + neighborhood weight update.
|
|
3537
|
+
train(X, epochs) {
|
|
3538
|
+
if (!X || X.length === 0) {
|
|
3539
|
+
throw new Error("SOM.train: dataset X must be non-empty");
|
|
3540
|
+
}
|
|
3541
|
+
if (X[0].length !== this._inputSize) {
|
|
3542
|
+
throw new Error(
|
|
3543
|
+
`SOM.train: expected input size ${this._inputSize}, got ${X[0].length}`
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3546
|
+
const totalSteps = epochs * X.length;
|
|
3547
|
+
let step = 0;
|
|
3548
|
+
for (let epoch = 0; epoch < epochs; epoch++) {
|
|
3549
|
+
const indices = this._shuffle(X.length);
|
|
3550
|
+
for (const idx of indices) {
|
|
3551
|
+
const x = X[idx];
|
|
3552
|
+
const t = step / totalSteps;
|
|
3553
|
+
const lr = this._initialLr * Math.pow(this._finalLr / this._initialLr, t);
|
|
3554
|
+
const sigma = this._initialSigma * Math.pow(this._finalSigma / this._initialSigma, t);
|
|
3555
|
+
const sigma2 = 2 * sigma * sigma;
|
|
3556
|
+
const [bmuR, bmuC] = this.getBMU(x);
|
|
3557
|
+
for (let r = 0; r < this._rows; r++) {
|
|
3558
|
+
for (let c = 0; c < this._cols; c++) {
|
|
3559
|
+
const dr = r - bmuR;
|
|
3560
|
+
const dc = c - bmuC;
|
|
3561
|
+
const gridDistSq = dr * dr + dc * dc;
|
|
3562
|
+
const h = Math.exp(-gridDistSq / sigma2);
|
|
3563
|
+
if (h < 1e-6) continue;
|
|
3564
|
+
const w = this.weights[r][c];
|
|
3565
|
+
for (let i = 0; i < this._inputSize; i++) {
|
|
3566
|
+
w[i] += lr * h * (x[i] - w[i]);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
step++;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
// ── getBMU ─────────────────────────────────────────────────────────────────
|
|
3575
|
+
// Returns [row, col] of the Best Matching Unit for input x.
|
|
3576
|
+
// BMU = argmin_{r,c} ‖x − w[r][c]‖²
|
|
3577
|
+
getBMU(x) {
|
|
3578
|
+
if (x.length !== this._inputSize) {
|
|
3579
|
+
throw new Error(
|
|
3580
|
+
`SOM.getBMU: expected input of length ${this._inputSize}, got ${x.length}`
|
|
3581
|
+
);
|
|
3582
|
+
}
|
|
3583
|
+
let bestR = 0;
|
|
3584
|
+
let bestC = 0;
|
|
3585
|
+
let bestDist = Infinity;
|
|
3586
|
+
for (let r = 0; r < this._rows; r++) {
|
|
3587
|
+
for (let c = 0; c < this._cols; c++) {
|
|
3588
|
+
const dist = this._distSq(x, this.weights[r][c]);
|
|
3589
|
+
if (dist < bestDist) {
|
|
3590
|
+
bestDist = dist;
|
|
3591
|
+
bestR = r;
|
|
3592
|
+
bestC = c;
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
return [bestR, bestC];
|
|
3597
|
+
}
|
|
3598
|
+
// ── predict ────────────────────────────────────────────────────────────────
|
|
3599
|
+
// Alias for getBMU — returns [row, col] of the winning neuron.
|
|
3600
|
+
predict(x) {
|
|
3601
|
+
return this.getBMU(x);
|
|
3602
|
+
}
|
|
3603
|
+
// ── quantizationError ─────────────────────────────────────────────────────
|
|
3604
|
+
// QE = (1/n) · Σᵢ ‖xᵢ − w[BMU(xᵢ)]‖
|
|
3605
|
+
// Measures how well the prototypes represent the data. Lower is better.
|
|
3606
|
+
quantizationError(X) {
|
|
3607
|
+
let total = 0;
|
|
3608
|
+
for (const x of X) {
|
|
3609
|
+
const [r, c] = this.getBMU(x);
|
|
3610
|
+
total += Math.sqrt(this._distSq(x, this.weights[r][c]));
|
|
3611
|
+
}
|
|
3612
|
+
return total / X.length;
|
|
3613
|
+
}
|
|
3614
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
3615
|
+
_distSq(a, b) {
|
|
3616
|
+
let s = 0;
|
|
3617
|
+
for (let i = 0; i < a.length; i++) s += (a[i] - b[i]) ** 2;
|
|
3618
|
+
return s;
|
|
3619
|
+
}
|
|
3620
|
+
// Fisher-Yates shuffle — returns an array of shuffled indices.
|
|
3621
|
+
_shuffle(n) {
|
|
3622
|
+
const arr = Array.from({ length: n }, (_, i) => i);
|
|
3623
|
+
for (let i = n - 1; i > 0; i--) {
|
|
3624
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
3625
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
3626
|
+
}
|
|
3627
|
+
return arr;
|
|
3628
|
+
}
|
|
3629
|
+
};
|
|
3630
|
+
|
|
3631
|
+
// src/HopfieldNetwork.ts
|
|
3632
|
+
var HopfieldNetwork = class {
|
|
3633
|
+
constructor(n) {
|
|
3634
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
3635
|
+
throw new Error(`HopfieldNetwork: n must be a positive integer, got ${n}`);
|
|
3636
|
+
}
|
|
3637
|
+
this.n = n;
|
|
3638
|
+
this.storedPatterns = 0;
|
|
3639
|
+
this.weights = Array.from({ length: n }, () => new Array(n).fill(0));
|
|
3640
|
+
}
|
|
3641
|
+
// ── store ──────────────────────────────────────────────────────────────────
|
|
3642
|
+
// Adds a pattern to the network's memory using the Hebbian learning rule:
|
|
3643
|
+
// W ← W + (1/N) · p · pᵀ (diagonal stays 0)
|
|
3644
|
+
//
|
|
3645
|
+
// The pattern must be bipolar: each element must be +1 or −1.
|
|
3646
|
+
store(pattern) {
|
|
3647
|
+
if (pattern.length !== this.n) {
|
|
3648
|
+
throw new Error(
|
|
3649
|
+
`HopfieldNetwork.store: pattern length ${pattern.length} does not match network size ${this.n}`
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
for (let i = 0; i < this.n; i++) {
|
|
3653
|
+
if (pattern[i] !== 1 && pattern[i] !== -1) {
|
|
3654
|
+
throw new Error(
|
|
3655
|
+
`HopfieldNetwork.store: pattern values must be +1 or -1, got ${pattern[i]} at index ${i}. Use HopfieldNetwork.binarize() to convert 0/1 arrays.`
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
const scale = 1 / this.n;
|
|
3660
|
+
for (let i = 0; i < this.n; i++) {
|
|
3661
|
+
for (let j = 0; j < this.n; j++) {
|
|
3662
|
+
if (i !== j) {
|
|
3663
|
+
this.weights[i][j] += scale * pattern[i] * pattern[j];
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
this.storedPatterns++;
|
|
3668
|
+
if (this.storedPatterns > Math.floor(0.138 * this.n)) {
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
// ── recall ─────────────────────────────────────────────────────────────────
|
|
3672
|
+
// Starting from `input` (a noisy/partial copy of a stored pattern), runs
|
|
3673
|
+
// asynchronous updates until convergence or maxIter is reached.
|
|
3674
|
+
// hᵢ = Σⱼ Wᵢⱼ · sⱼ
|
|
3675
|
+
// sᵢ ← sign(hᵢ) (+1, −1; unchanged when hᵢ = 0)
|
|
3676
|
+
// Returns the converged state vector.
|
|
3677
|
+
recall(input, maxIter = 20 * this.n) {
|
|
3678
|
+
if (input.length !== this.n) {
|
|
3679
|
+
throw new Error(
|
|
3680
|
+
`HopfieldNetwork.recall: input length ${input.length} does not match network size ${this.n}`
|
|
3681
|
+
);
|
|
3682
|
+
}
|
|
3683
|
+
const s = [...input];
|
|
3684
|
+
const order = Array.from({ length: this.n }, (_, i) => i);
|
|
3685
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
3686
|
+
this._shuffleInPlace(order);
|
|
3687
|
+
let changed = false;
|
|
3688
|
+
for (const i of order) {
|
|
3689
|
+
let h = 0;
|
|
3690
|
+
const row = this.weights[i];
|
|
3691
|
+
for (let j = 0; j < this.n; j++) h += row[j] * s[j];
|
|
3692
|
+
const newSi = h > 0 ? 1 : h < 0 ? -1 : s[i];
|
|
3693
|
+
if (newSi !== s[i]) {
|
|
3694
|
+
s[i] = newSi;
|
|
3695
|
+
changed = true;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
if (!changed) break;
|
|
3699
|
+
}
|
|
3700
|
+
return s;
|
|
3701
|
+
}
|
|
3702
|
+
// ── energy ─────────────────────────────────────────────────────────────────
|
|
3703
|
+
// E(s) = −½ · Σᵢⱼ Wᵢⱼ · sᵢ · sⱼ
|
|
3704
|
+
// Stored patterns are local minima. Updates always push E downward (or keep
|
|
3705
|
+
// it constant), so the network is guaranteed to converge.
|
|
3706
|
+
energy(state) {
|
|
3707
|
+
if (state.length !== this.n) {
|
|
3708
|
+
throw new Error(
|
|
3709
|
+
`HopfieldNetwork.energy: state length ${state.length} does not match network size ${this.n}`
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
let e = 0;
|
|
3713
|
+
for (let i = 0; i < this.n; i++) {
|
|
3714
|
+
for (let j = 0; j < this.n; j++) {
|
|
3715
|
+
e += this.weights[i][j] * state[i] * state[j];
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
return -0.5 * e;
|
|
3719
|
+
}
|
|
3720
|
+
// ── binarize ───────────────────────────────────────────────────────────────
|
|
3721
|
+
// Converts a 0/1 array to bipolar −1/+1.
|
|
3722
|
+
// 0 → −1, 1 → +1
|
|
3723
|
+
static binarize(arr) {
|
|
3724
|
+
return arr.map((v) => v === 0 ? -1 : 1);
|
|
3725
|
+
}
|
|
3726
|
+
// ── unbinarize ─────────────────────────────────────────────────────────────
|
|
3727
|
+
// Converts a bipolar −1/+1 array back to 0/1.
|
|
3728
|
+
// −1 → 0, +1 → 1
|
|
3729
|
+
static unbinarize(arr) {
|
|
3730
|
+
return arr.map((v) => v === -1 ? 0 : 1);
|
|
3731
|
+
}
|
|
3732
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
3733
|
+
_shuffleInPlace(arr) {
|
|
3734
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
3735
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
3736
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
};
|
|
3740
|
+
|
|
3741
|
+
// src/Autoencoder.ts
|
|
3742
|
+
var Autoencoder = class {
|
|
3743
|
+
constructor(inputSize, encoderHidden, latentSize, decoderHidden, options = {}) {
|
|
3744
|
+
if (inputSize < 1) {
|
|
3745
|
+
throw new Error(`Autoencoder: inputSize must be \u2265 1, got ${inputSize}`);
|
|
3746
|
+
}
|
|
3747
|
+
if (latentSize < 1) {
|
|
3748
|
+
throw new Error(`Autoencoder: latentSize must be \u2265 1, got ${latentSize}`);
|
|
3749
|
+
}
|
|
3750
|
+
if (latentSize >= inputSize) {
|
|
3751
|
+
}
|
|
3752
|
+
this._inputSize = inputSize;
|
|
3753
|
+
this._latentSize = latentSize;
|
|
3754
|
+
const encoderStructure = [inputSize, ...encoderHidden, latentSize];
|
|
3755
|
+
const encoderOptions = { ...options };
|
|
3756
|
+
if (options.activations) {
|
|
3757
|
+
const nEncoderLayers = encoderStructure.length - 1;
|
|
3758
|
+
if (options.activations.length >= nEncoderLayers) {
|
|
3759
|
+
encoderOptions.activations = options.activations.slice(0, nEncoderLayers);
|
|
3760
|
+
} else {
|
|
3761
|
+
encoderOptions.activations = void 0;
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
this.encoder = new NetworkN(encoderStructure, encoderOptions);
|
|
3765
|
+
const decoderStructure = [latentSize, ...decoderHidden, inputSize];
|
|
3766
|
+
const decoderOptions = { ...options };
|
|
3767
|
+
if (options.activations) {
|
|
3768
|
+
const nEncoderLayers = encoderStructure.length - 1;
|
|
3769
|
+
const nDecoderLayers = decoderStructure.length - 1;
|
|
3770
|
+
const remaining = options.activations.slice(nEncoderLayers);
|
|
3771
|
+
if (remaining.length >= nDecoderLayers) {
|
|
3772
|
+
decoderOptions.activations = remaining.slice(0, nDecoderLayers);
|
|
3773
|
+
} else {
|
|
3774
|
+
decoderOptions.activations = void 0;
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
this.decoder = new NetworkN(decoderStructure, decoderOptions);
|
|
3778
|
+
}
|
|
3779
|
+
// ── encode ─────────────────────────────────────────────────────────────────
|
|
3780
|
+
// Maps an input vector to its latent representation.
|
|
3781
|
+
// z = encoder(x) ∈ ℝ^latentSize
|
|
3782
|
+
encode(x) {
|
|
3783
|
+
if (x.length !== this._inputSize) {
|
|
3784
|
+
throw new Error(
|
|
3785
|
+
`Autoencoder.encode: expected input of length ${this._inputSize}, got ${x.length}`
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
return this.encoder.predict(x);
|
|
3789
|
+
}
|
|
3790
|
+
// ── decode ─────────────────────────────────────────────────────────────────
|
|
3791
|
+
// Reconstructs an input from its latent code.
|
|
3792
|
+
// x̂ = decoder(z) ∈ ℝ^inputSize
|
|
3793
|
+
decode(z) {
|
|
3794
|
+
if (z.length !== this._latentSize) {
|
|
3795
|
+
throw new Error(
|
|
3796
|
+
`Autoencoder.decode: expected latent vector of length ${this._latentSize}, got ${z.length}`
|
|
3797
|
+
);
|
|
3798
|
+
}
|
|
3799
|
+
return this.decoder.predict(z);
|
|
3800
|
+
}
|
|
3801
|
+
// ── reconstruct ───────────────────────────────────────────────────────────
|
|
3802
|
+
// Convenience: encode then decode in a single call.
|
|
3803
|
+
// x̂ = decode(encode(x))
|
|
3804
|
+
reconstruct(x) {
|
|
3805
|
+
return this.decode(this.encode(x));
|
|
3806
|
+
}
|
|
3807
|
+
// ── train ──────────────────────────────────────────────────────────────────
|
|
3808
|
+
// Trains on a single example using backpropagation through both sub-networks.
|
|
3809
|
+
//
|
|
3810
|
+
// Gradient flow:
|
|
3811
|
+
// 1. Forward: z = encoder(x), x̂ = decoder(z)
|
|
3812
|
+
// 2. Compute MSE output deltas at x̂: δᵢ = (xᵢ − x̂ᵢ) · act'(x̂ᵢ)
|
|
3813
|
+
// 3. Walk backward through decoder layers to get ∂L/∂z (BEFORE updating weights)
|
|
3814
|
+
// 4. Update decoder weights via trainWithDeltas(z, outputDeltas, lr)
|
|
3815
|
+
// 5. Update encoder weights via trainWithDeltas(x, dLdz, lr)
|
|
3816
|
+
//
|
|
3817
|
+
// Returns the MSE reconstruction loss: (1/d) · Σᵢ (xᵢ − x̂ᵢ)².
|
|
3818
|
+
train(x, lr) {
|
|
3819
|
+
if (x.length !== this._inputSize) {
|
|
3820
|
+
throw new Error(
|
|
3821
|
+
`Autoencoder.train: expected input of length ${this._inputSize}, got ${x.length}`
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
3824
|
+
const z = this.encoder.predict(x, true);
|
|
3825
|
+
const xHat = this.decoder.predict(z, true);
|
|
3826
|
+
const loss = mse(xHat, x);
|
|
3827
|
+
const decoderOutAct = this.decoder.layers[this.decoder.layers.length - 1].neurons[0].activation;
|
|
3828
|
+
const outputDeltas = xHat.map((xh, i) => (x[i] - xh) * decoderOutAct.dfn(xh));
|
|
3829
|
+
const decoderLayers = this.decoder.layers;
|
|
3830
|
+
const decoderActVals = [z];
|
|
3831
|
+
let cur = [...z];
|
|
3832
|
+
for (const layer of decoderLayers) {
|
|
3833
|
+
cur = layer.predict(cur);
|
|
3834
|
+
decoderActVals.push(cur);
|
|
3835
|
+
}
|
|
3836
|
+
let deltas = outputDeltas;
|
|
3837
|
+
for (let l = decoderLayers.length - 1; l >= 0; l--) {
|
|
3838
|
+
const layer = decoderLayers[l];
|
|
3839
|
+
const prevAct = decoderActVals[l];
|
|
3840
|
+
const prevLayerActivation = l > 0 ? decoderLayers[l - 1].neurons[0].activation : null;
|
|
3841
|
+
const prevDeltas = prevAct.map((out, j) => {
|
|
3842
|
+
const errProp = layer.neurons.reduce((s, n, k) => s + deltas[k] * n.weights[j], 0);
|
|
3843
|
+
return prevLayerActivation ? errProp * prevLayerActivation.dfn(out) : errProp;
|
|
3844
|
+
});
|
|
3845
|
+
deltas = prevDeltas;
|
|
3846
|
+
}
|
|
3847
|
+
const dLdz = deltas;
|
|
3848
|
+
this.decoder.trainWithDeltas(z, outputDeltas, lr);
|
|
3849
|
+
this.encoder.trainWithDeltas(x, dLdz, lr);
|
|
3850
|
+
return loss;
|
|
3851
|
+
}
|
|
3852
|
+
// ── trainBatch ────────────────────────────────────────────────────────────
|
|
3853
|
+
// Trains on a batch of examples and returns the mean reconstruction MSE.
|
|
3854
|
+
trainBatch(X, lr) {
|
|
3855
|
+
if (X.length === 0) {
|
|
3856
|
+
throw new Error("Autoencoder.trainBatch: batch X must be non-empty");
|
|
3857
|
+
}
|
|
3858
|
+
let totalLoss = 0;
|
|
3859
|
+
for (const x of X) totalLoss += this.train(x, lr);
|
|
3860
|
+
return totalLoss / X.length;
|
|
3861
|
+
}
|
|
3862
|
+
};
|
|
3863
|
+
|
|
3864
|
+
// src/Conv2D.ts
|
|
3865
|
+
var Conv2D = class {
|
|
3866
|
+
constructor(inputHeight, inputWidth, channels, kernelSize, filters, options) {
|
|
3867
|
+
// [filters]
|
|
3868
|
+
this._input = null;
|
|
3869
|
+
this._padded = null;
|
|
3870
|
+
const [kH, kW] = Array.isArray(kernelSize) ? kernelSize : [kernelSize, kernelSize];
|
|
3871
|
+
if (inputHeight <= 0 || inputWidth <= 0 || channels <= 0 || filters <= 0) {
|
|
3872
|
+
throw new Error("Conv2D: dimensions and filters must be positive");
|
|
3873
|
+
}
|
|
3874
|
+
if (kH <= 0 || kW <= 0) {
|
|
3875
|
+
throw new Error("Conv2D: kernelSize must be positive");
|
|
3876
|
+
}
|
|
3877
|
+
this.inputHeight = inputHeight;
|
|
3878
|
+
this.inputWidth = inputWidth;
|
|
3879
|
+
this.channels = channels;
|
|
3880
|
+
this.kH = kH;
|
|
3881
|
+
this.kW = kW;
|
|
3882
|
+
this.filters = filters;
|
|
3883
|
+
this.stride = options?.stride ?? 1;
|
|
3884
|
+
this.padding = options?.padding ?? "valid";
|
|
3885
|
+
const optimizerFactory = options?.optimizerFactory ?? (() => new SGD());
|
|
3886
|
+
const limit = Math.sqrt(2 / (kH * kW * channels));
|
|
3887
|
+
this.kernels = Array.from(
|
|
3888
|
+
{ length: filters },
|
|
3889
|
+
() => Array.from(
|
|
3890
|
+
{ length: kH },
|
|
3891
|
+
() => Array.from(
|
|
3892
|
+
{ length: kW },
|
|
3893
|
+
() => Array.from({ length: channels }, () => (Math.random() * 2 - 1) * limit)
|
|
3894
|
+
)
|
|
3895
|
+
)
|
|
3896
|
+
);
|
|
3897
|
+
this.biases = new Array(filters).fill(0);
|
|
3898
|
+
this._kOpts = Array.from(
|
|
3899
|
+
{ length: filters },
|
|
3900
|
+
() => Array.from(
|
|
3901
|
+
{ length: kH },
|
|
3902
|
+
() => Array.from(
|
|
3903
|
+
{ length: kW },
|
|
3904
|
+
() => Array.from({ length: channels }, () => optimizerFactory())
|
|
3905
|
+
)
|
|
3906
|
+
)
|
|
3907
|
+
);
|
|
3908
|
+
this._bOpts = Array.from({ length: filters }, () => optimizerFactory());
|
|
3909
|
+
}
|
|
3910
|
+
// ── Padding helper ────────────────────────────────────────────────────────
|
|
3911
|
+
_pad(input) {
|
|
3912
|
+
if (this.padding === "valid") return input;
|
|
3913
|
+
const padH = Math.floor(this.kH / 2);
|
|
3914
|
+
const padW = Math.floor(this.kW / 2);
|
|
3915
|
+
const H = input.length;
|
|
3916
|
+
const W = input[0].length;
|
|
3917
|
+
const C = this.channels;
|
|
3918
|
+
const paddedH = H + 2 * padH;
|
|
3919
|
+
const paddedW = W + 2 * padW;
|
|
3920
|
+
const out = Array.from(
|
|
3921
|
+
{ length: paddedH },
|
|
3922
|
+
() => Array.from({ length: paddedW }, () => new Array(C).fill(0))
|
|
3923
|
+
);
|
|
3924
|
+
for (let h = 0; h < H; h++) {
|
|
3925
|
+
for (let w = 0; w < W; w++) {
|
|
3926
|
+
for (let c = 0; c < C; c++) {
|
|
3927
|
+
out[h + padH][w + padW][c] = input[h][w][c];
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
return out;
|
|
3932
|
+
}
|
|
3933
|
+
// ── Output shape ──────────────────────────────────────────────────────────
|
|
3934
|
+
outputShape() {
|
|
3935
|
+
const padH = this.padding === "same" ? Math.floor(this.kH / 2) : 0;
|
|
3936
|
+
const padW = this.padding === "same" ? Math.floor(this.kW / 2) : 0;
|
|
3937
|
+
const outH = Math.floor((this.inputHeight - this.kH + 2 * padH) / this.stride) + 1;
|
|
3938
|
+
const outW = Math.floor((this.inputWidth - this.kW + 2 * padW) / this.stride) + 1;
|
|
3939
|
+
return [outH, outW, this.filters];
|
|
3940
|
+
}
|
|
3941
|
+
// ── Forward pass ──────────────────────────────────────────────────────────
|
|
3942
|
+
// output[h][w][f] = bias[f] + Σ_{kh,kw,c} kernel[f][kh][kw][c] · input[h·s+kh][w·s+kw][c]
|
|
3943
|
+
forward(input) {
|
|
3944
|
+
if (input.length !== this.inputHeight) {
|
|
3945
|
+
throw new Error(`Conv2D.forward: expected height ${this.inputHeight}, got ${input.length}`);
|
|
3946
|
+
}
|
|
3947
|
+
if (input[0].length !== this.inputWidth) {
|
|
3948
|
+
throw new Error(`Conv2D.forward: expected width ${this.inputWidth}, got ${input[0].length}`);
|
|
3949
|
+
}
|
|
3950
|
+
this._input = input;
|
|
3951
|
+
this._padded = this._pad(input);
|
|
3952
|
+
const padded = this._padded;
|
|
3953
|
+
const padH = this.padding === "same" ? Math.floor(this.kH / 2) : 0;
|
|
3954
|
+
const padW = this.padding === "same" ? Math.floor(this.kW / 2) : 0;
|
|
3955
|
+
const outH = Math.floor((this.inputHeight - this.kH + 2 * padH) / this.stride) + 1;
|
|
3956
|
+
const outW = Math.floor((this.inputWidth - this.kW + 2 * padW) / this.stride) + 1;
|
|
3957
|
+
const output = Array.from(
|
|
3958
|
+
{ length: outH },
|
|
3959
|
+
() => Array.from({ length: outW }, () => new Array(this.filters).fill(0))
|
|
3960
|
+
);
|
|
3961
|
+
for (let f = 0; f < this.filters; f++) {
|
|
3962
|
+
for (let h = 0; h < outH; h++) {
|
|
3963
|
+
for (let w = 0; w < outW; w++) {
|
|
3964
|
+
let sum = this.biases[f];
|
|
3965
|
+
for (let kh = 0; kh < this.kH; kh++) {
|
|
3966
|
+
for (let kw = 0; kw < this.kW; kw++) {
|
|
3967
|
+
for (let c = 0; c < this.channels; c++) {
|
|
3968
|
+
sum += this.kernels[f][kh][kw][c] * padded[h * this.stride + kh][w * this.stride + kw][c];
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
output[h][w][f] = sum;
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
return output;
|
|
3977
|
+
}
|
|
3978
|
+
// ── Backward pass ─────────────────────────────────────────────────────────
|
|
3979
|
+
// dOutput: number[][][] of shape [outH][outW][filters]
|
|
3980
|
+
// Returns dInput: number[][][] of shape [H][W][channels]
|
|
3981
|
+
backward(dOutput, lr) {
|
|
3982
|
+
if (!this._padded || !this._input) {
|
|
3983
|
+
throw new Error("Conv2D.backward: call forward() first");
|
|
3984
|
+
}
|
|
3985
|
+
const padded = this._padded;
|
|
3986
|
+
const outH = dOutput.length;
|
|
3987
|
+
const outW = dOutput[0].length;
|
|
3988
|
+
const dKernels = Array.from(
|
|
3989
|
+
{ length: this.filters },
|
|
3990
|
+
() => Array.from(
|
|
3991
|
+
{ length: this.kH },
|
|
3992
|
+
() => Array.from({ length: this.kW }, () => new Array(this.channels).fill(0))
|
|
3993
|
+
)
|
|
3994
|
+
);
|
|
3995
|
+
const dBiases = new Array(this.filters).fill(0);
|
|
3996
|
+
const dPadded = Array.from(
|
|
3997
|
+
{ length: padded.length },
|
|
3998
|
+
() => Array.from({ length: padded[0].length }, () => new Array(this.channels).fill(0))
|
|
3999
|
+
);
|
|
4000
|
+
for (let f = 0; f < this.filters; f++) {
|
|
4001
|
+
for (let h = 0; h < outH; h++) {
|
|
4002
|
+
for (let w = 0; w < outW; w++) {
|
|
4003
|
+
const dv = dOutput[h][w][f];
|
|
4004
|
+
dBiases[f] += dv;
|
|
4005
|
+
for (let kh = 0; kh < this.kH; kh++) {
|
|
4006
|
+
for (let kw = 0; kw < this.kW; kw++) {
|
|
4007
|
+
for (let c = 0; c < this.channels; c++) {
|
|
4008
|
+
const ph = h * this.stride + kh;
|
|
4009
|
+
const pw = w * this.stride + kw;
|
|
4010
|
+
dKernels[f][kh][kw][c] += dv * padded[ph][pw][c];
|
|
4011
|
+
dPadded[ph][pw][c] += dv * this.kernels[f][kh][kw][c];
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
for (let f = 0; f < this.filters; f++) {
|
|
4019
|
+
for (let kh = 0; kh < this.kH; kh++) {
|
|
4020
|
+
for (let kw = 0; kw < this.kW; kw++) {
|
|
4021
|
+
for (let c = 0; c < this.channels; c++) {
|
|
4022
|
+
this.kernels[f][kh][kw][c] = this._kOpts[f][kh][kw][c].step(
|
|
4023
|
+
this.kernels[f][kh][kw][c],
|
|
4024
|
+
dKernels[f][kh][kw][c],
|
|
4025
|
+
lr
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
this.biases[f] = this._bOpts[f].step(this.biases[f], dBiases[f], lr);
|
|
4031
|
+
}
|
|
4032
|
+
if (this.padding === "same") {
|
|
4033
|
+
const padH = Math.floor(this.kH / 2);
|
|
4034
|
+
const padW = Math.floor(this.kW / 2);
|
|
4035
|
+
return dPadded.slice(padH, padH + this.inputHeight).map((row) => row.slice(padW, padW + this.inputWidth));
|
|
4036
|
+
}
|
|
4037
|
+
return dPadded.slice(0, this.inputHeight).map((row) => row.slice(0, this.inputWidth));
|
|
4038
|
+
}
|
|
4039
|
+
// ── Weight serialization ──────────────────────────────────────────────────
|
|
4040
|
+
getWeights() {
|
|
4041
|
+
const w = [];
|
|
4042
|
+
for (const kf of this.kernels)
|
|
4043
|
+
for (const kh of kf)
|
|
4044
|
+
for (const kw of kh)
|
|
4045
|
+
for (const v of kw)
|
|
4046
|
+
w.push(v);
|
|
4047
|
+
w.push(...this.biases);
|
|
4048
|
+
return w;
|
|
4049
|
+
}
|
|
4050
|
+
setWeights(weights) {
|
|
4051
|
+
let idx = 0;
|
|
4052
|
+
for (let f = 0; f < this.filters; f++)
|
|
4053
|
+
for (let kh = 0; kh < this.kH; kh++)
|
|
4054
|
+
for (let kw = 0; kw < this.kW; kw++)
|
|
4055
|
+
for (let c = 0; c < this.channels; c++)
|
|
4056
|
+
this.kernels[f][kh][kw][c] = weights[idx++];
|
|
4057
|
+
for (let f = 0; f < this.filters; f++)
|
|
4058
|
+
this.biases[f] = weights[idx++];
|
|
4059
|
+
}
|
|
4060
|
+
};
|
|
4061
|
+
|
|
4062
|
+
// src/MaxPool2D.ts
|
|
4063
|
+
var MaxPool2D = class {
|
|
4064
|
+
constructor(poolSize, stride) {
|
|
4065
|
+
// Mask stored during forward pass for backprop:
|
|
4066
|
+
// _maxMask[h][w][c] = true if input[h][w][c] was the maximum in its window
|
|
4067
|
+
this._maxMask = null;
|
|
4068
|
+
this._inputH = 0;
|
|
4069
|
+
this._inputW = 0;
|
|
4070
|
+
this._inputC = 0;
|
|
4071
|
+
if (poolSize <= 0) {
|
|
4072
|
+
throw new Error("MaxPool2D: poolSize must be positive");
|
|
4073
|
+
}
|
|
4074
|
+
this.poolSize = poolSize;
|
|
4075
|
+
this.stride = stride ?? poolSize;
|
|
4076
|
+
}
|
|
4077
|
+
// ── Output shape ──────────────────────────────────────────────────────────
|
|
4078
|
+
outputShape(inputH, inputW, channels) {
|
|
4079
|
+
const outH = Math.floor((inputH - this.poolSize) / this.stride) + 1;
|
|
4080
|
+
const outW = Math.floor((inputW - this.poolSize) / this.stride) + 1;
|
|
4081
|
+
return [outH, outW, channels];
|
|
4082
|
+
}
|
|
4083
|
+
// ── Forward pass ──────────────────────────────────────────────────────────
|
|
4084
|
+
// output[oh][ow][c] = max over ph in [0..poolSize), pw in [0..poolSize) of
|
|
4085
|
+
// input[oh·stride + ph][ow·stride + pw][c]
|
|
4086
|
+
forward(input) {
|
|
4087
|
+
const H = input.length;
|
|
4088
|
+
const W = input[0].length;
|
|
4089
|
+
const C = input[0][0].length;
|
|
4090
|
+
this._inputH = H;
|
|
4091
|
+
this._inputW = W;
|
|
4092
|
+
this._inputC = C;
|
|
4093
|
+
const [outH, outW] = this.outputShape(H, W, C);
|
|
4094
|
+
const output = Array.from(
|
|
4095
|
+
{ length: outH },
|
|
4096
|
+
() => Array.from({ length: outW }, () => new Array(C).fill(-Infinity))
|
|
4097
|
+
);
|
|
4098
|
+
this._maxMask = Array.from(
|
|
4099
|
+
{ length: H },
|
|
4100
|
+
() => Array.from({ length: W }, () => new Array(C).fill(false))
|
|
4101
|
+
);
|
|
4102
|
+
for (let oh = 0; oh < outH; oh++) {
|
|
4103
|
+
for (let ow = 0; ow < outW; ow++) {
|
|
4104
|
+
for (let c = 0; c < C; c++) {
|
|
4105
|
+
let maxVal = -Infinity;
|
|
4106
|
+
let maxPH = 0;
|
|
4107
|
+
let maxPW = 0;
|
|
4108
|
+
for (let ph = 0; ph < this.poolSize; ph++) {
|
|
4109
|
+
for (let pw = 0; pw < this.poolSize; pw++) {
|
|
4110
|
+
const val = input[oh * this.stride + ph][ow * this.stride + pw][c];
|
|
4111
|
+
if (val > maxVal) {
|
|
4112
|
+
maxVal = val;
|
|
4113
|
+
maxPH = ph;
|
|
4114
|
+
maxPW = pw;
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
output[oh][ow][c] = maxVal;
|
|
4119
|
+
this._maxMask[oh * this.stride + maxPH][ow * this.stride + maxPW][c] = true;
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
return output;
|
|
4124
|
+
}
|
|
4125
|
+
// ── Backward pass ─────────────────────────────────────────────────────────
|
|
4126
|
+
// dOutput: number[][][] of shape [outH][outW][C]
|
|
4127
|
+
// Returns dInput: number[][][] of shape [H][W][C]
|
|
4128
|
+
// Gradient is routed only to the max position; all others get 0.
|
|
4129
|
+
backward(dOutput) {
|
|
4130
|
+
if (!this._maxMask) {
|
|
4131
|
+
throw new Error("MaxPool2D.backward: call forward() first");
|
|
4132
|
+
}
|
|
4133
|
+
const dInput = Array.from(
|
|
4134
|
+
{ length: this._inputH },
|
|
4135
|
+
() => Array.from({ length: this._inputW }, () => new Array(this._inputC).fill(0))
|
|
4136
|
+
);
|
|
4137
|
+
const outH = dOutput.length;
|
|
4138
|
+
const outW = dOutput[0].length;
|
|
4139
|
+
const C = this._inputC;
|
|
4140
|
+
for (let oh = 0; oh < outH; oh++) {
|
|
4141
|
+
for (let ow = 0; ow < outW; ow++) {
|
|
4142
|
+
for (let c = 0; c < C; c++) {
|
|
4143
|
+
for (let ph = 0; ph < this.poolSize; ph++) {
|
|
4144
|
+
for (let pw = 0; pw < this.poolSize; pw++) {
|
|
4145
|
+
const ih = oh * this.stride + ph;
|
|
4146
|
+
const iw = ow * this.stride + pw;
|
|
4147
|
+
if (this._maxMask[ih][iw][c]) {
|
|
4148
|
+
dInput[ih][iw][c] += dOutput[oh][ow][c];
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
return dInput;
|
|
4156
|
+
}
|
|
4157
|
+
};
|
|
4158
|
+
|
|
4159
|
+
// src/Flatten.ts
|
|
4160
|
+
var Flatten = class {
|
|
4161
|
+
constructor() {
|
|
4162
|
+
this.inputShape = null;
|
|
4163
|
+
}
|
|
4164
|
+
// [H, W, C]
|
|
4165
|
+
// ── Forward pass ──────────────────────────────────────────────────────────
|
|
4166
|
+
// Flattens input[h][w][c] into a 1D array in row-major, channel-last order.
|
|
4167
|
+
forward(input) {
|
|
4168
|
+
const H = input.length;
|
|
4169
|
+
const W = input[0].length;
|
|
4170
|
+
const C = input[0][0].length;
|
|
4171
|
+
this.inputShape = [H, W, C];
|
|
4172
|
+
const flat = new Array(H * W * C);
|
|
4173
|
+
let idx = 0;
|
|
4174
|
+
for (let h = 0; h < H; h++) {
|
|
4175
|
+
for (let w = 0; w < W; w++) {
|
|
4176
|
+
for (let c = 0; c < C; c++) {
|
|
4177
|
+
flat[idx++] = input[h][w][c];
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
return flat;
|
|
4182
|
+
}
|
|
4183
|
+
// ── Backward pass ─────────────────────────────────────────────────────────
|
|
4184
|
+
// Reshapes a flat gradient vector back into [H][W][C] using the saved shape.
|
|
4185
|
+
backward(dOutput) {
|
|
4186
|
+
if (!this.inputShape) {
|
|
4187
|
+
throw new Error("Flatten.backward: call forward() first");
|
|
4188
|
+
}
|
|
4189
|
+
const [H, W, C] = this.inputShape;
|
|
4190
|
+
if (dOutput.length !== H * W * C) {
|
|
4191
|
+
throw new Error(
|
|
4192
|
+
`Flatten.backward: expected gradient of length ${H * W * C}, got ${dOutput.length}`
|
|
4193
|
+
);
|
|
4194
|
+
}
|
|
4195
|
+
const dInput = Array.from(
|
|
4196
|
+
{ length: H },
|
|
4197
|
+
() => Array.from({ length: W }, () => new Array(C).fill(0))
|
|
4198
|
+
);
|
|
4199
|
+
let idx = 0;
|
|
4200
|
+
for (let h = 0; h < H; h++) {
|
|
4201
|
+
for (let w = 0; w < W; w++) {
|
|
4202
|
+
for (let c = 0; c < C; c++) {
|
|
4203
|
+
dInput[h][w][c] = dOutput[idx++];
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
return dInput;
|
|
4208
|
+
}
|
|
4209
|
+
};
|
|
4210
|
+
|
|
4211
|
+
// src/RNN.ts
|
|
4212
|
+
function tanh3(x) {
|
|
4213
|
+
const e = Math.exp(2 * x);
|
|
4214
|
+
return (e - 1) / (e + 1);
|
|
4215
|
+
}
|
|
4216
|
+
var RNN = class {
|
|
4217
|
+
constructor(inputSize, hiddenSize, outputSize, optimizerFactory = () => new SGD()) {
|
|
4218
|
+
// Trajectory stored during forward for BPTT
|
|
4219
|
+
this._traj = [];
|
|
4220
|
+
this._outputs = [];
|
|
4221
|
+
if (inputSize <= 0 || hiddenSize <= 0 || outputSize <= 0) {
|
|
4222
|
+
throw new Error("RNN: all sizes must be positive");
|
|
4223
|
+
}
|
|
4224
|
+
this.inputSize = inputSize;
|
|
4225
|
+
this.hiddenSize = hiddenSize;
|
|
4226
|
+
this.outputSize = outputSize;
|
|
4227
|
+
const limXH = Math.sqrt(2 / inputSize);
|
|
4228
|
+
const limHH = Math.sqrt(2 / hiddenSize);
|
|
4229
|
+
const limHY = Math.sqrt(2 / hiddenSize);
|
|
4230
|
+
this.Wxh = Array.from(
|
|
4231
|
+
{ length: hiddenSize },
|
|
4232
|
+
() => Array.from({ length: inputSize }, () => (Math.random() * 2 - 1) * limXH)
|
|
4233
|
+
);
|
|
4234
|
+
this.Whh = Array.from(
|
|
4235
|
+
{ length: hiddenSize },
|
|
4236
|
+
() => Array.from({ length: hiddenSize }, () => (Math.random() * 2 - 1) * limHH)
|
|
4237
|
+
);
|
|
4238
|
+
this.Why = Array.from(
|
|
4239
|
+
{ length: outputSize },
|
|
4240
|
+
() => Array.from({ length: hiddenSize }, () => (Math.random() * 2 - 1) * limHY)
|
|
4241
|
+
);
|
|
4242
|
+
this.bh = new Array(hiddenSize).fill(0);
|
|
4243
|
+
this.by = new Array(outputSize).fill(0);
|
|
4244
|
+
this._h = new Array(hiddenSize).fill(0);
|
|
4245
|
+
this._opts = {
|
|
4246
|
+
Wxh: Array.from(
|
|
4247
|
+
{ length: hiddenSize },
|
|
4248
|
+
() => Array.from({ length: inputSize }, () => optimizerFactory())
|
|
4249
|
+
),
|
|
4250
|
+
Whh: Array.from(
|
|
4251
|
+
{ length: hiddenSize },
|
|
4252
|
+
() => Array.from({ length: hiddenSize }, () => optimizerFactory())
|
|
4253
|
+
),
|
|
4254
|
+
Why: Array.from(
|
|
4255
|
+
{ length: outputSize },
|
|
4256
|
+
() => Array.from({ length: hiddenSize }, () => optimizerFactory())
|
|
4257
|
+
),
|
|
4258
|
+
bh: Array.from({ length: hiddenSize }, () => optimizerFactory()),
|
|
4259
|
+
by: Array.from({ length: outputSize }, () => optimizerFactory())
|
|
4260
|
+
};
|
|
4261
|
+
}
|
|
4262
|
+
// ── Reset hidden state ────────────────────────────────────────────────────
|
|
4263
|
+
// Call at the start of each new sequence / episode.
|
|
4264
|
+
reset() {
|
|
4265
|
+
this._h = new Array(this.hiddenSize).fill(0);
|
|
4266
|
+
this._traj = [];
|
|
4267
|
+
this._outputs = [];
|
|
4268
|
+
}
|
|
4269
|
+
// ── Forward pass ──────────────────────────────────────────────────────────
|
|
4270
|
+
// sequence: number[][] of shape [T][inputSize]
|
|
4271
|
+
// Returns outputs and hidden states for all timesteps.
|
|
4272
|
+
forward(sequence) {
|
|
4273
|
+
this._traj = [];
|
|
4274
|
+
this._outputs = [];
|
|
4275
|
+
const outputs = [];
|
|
4276
|
+
const hiddens = [];
|
|
4277
|
+
let hPrev = [...this._h];
|
|
4278
|
+
for (const x of sequence) {
|
|
4279
|
+
const hRaw = this.bh.map(
|
|
4280
|
+
(b, i) => b + this.Wxh[i].reduce((s, w, j) => s + w * x[j], 0) + this.Whh[i].reduce((s, w, j) => s + w * hPrev[j], 0)
|
|
4281
|
+
);
|
|
4282
|
+
const h = hRaw.map(tanh3);
|
|
4283
|
+
const o = this.by.map(
|
|
4284
|
+
(b, i) => b + this.Why[i].reduce((s, w, j) => s + w * h[j], 0)
|
|
4285
|
+
);
|
|
4286
|
+
this._traj.push({ x: [...x], h: [...h], hRaw: [...hRaw], hPrev: [...hPrev] });
|
|
4287
|
+
this._outputs.push(o);
|
|
4288
|
+
outputs.push(o);
|
|
4289
|
+
hiddens.push(h);
|
|
4290
|
+
hPrev = h;
|
|
4291
|
+
}
|
|
4292
|
+
this._h = hPrev;
|
|
4293
|
+
return { outputs, hiddens };
|
|
4294
|
+
}
|
|
4295
|
+
// ── BPTT + weight update ──────────────────────────────────────────────────
|
|
4296
|
+
// targets: number[][] of shape [T][outputSize], paired with the last forward call.
|
|
4297
|
+
// Returns the mean squared error loss.
|
|
4298
|
+
backward(sequence, targets, lr) {
|
|
4299
|
+
this.reset();
|
|
4300
|
+
const { outputs } = this.forward(sequence);
|
|
4301
|
+
const T = this._traj.length;
|
|
4302
|
+
if (T === 0) return 0;
|
|
4303
|
+
let loss = 0;
|
|
4304
|
+
const dOutputs = outputs.map((o, t) => {
|
|
4305
|
+
return o.map((v, k) => {
|
|
4306
|
+
const diff = v - targets[t][k];
|
|
4307
|
+
loss += diff * diff;
|
|
4308
|
+
return 2 * diff / this.outputSize;
|
|
4309
|
+
});
|
|
4310
|
+
});
|
|
4311
|
+
loss /= T * this.outputSize;
|
|
4312
|
+
const dWxh = Array.from({ length: this.hiddenSize }, () => new Array(this.inputSize).fill(0));
|
|
4313
|
+
const dWhh = Array.from({ length: this.hiddenSize }, () => new Array(this.hiddenSize).fill(0));
|
|
4314
|
+
const dWhy = Array.from({ length: this.outputSize }, () => new Array(this.hiddenSize).fill(0));
|
|
4315
|
+
const dbh = new Array(this.hiddenSize).fill(0);
|
|
4316
|
+
const dby = new Array(this.outputSize).fill(0);
|
|
4317
|
+
let dhNext = new Array(this.hiddenSize).fill(0);
|
|
4318
|
+
for (let t = T - 1; t >= 0; t--) {
|
|
4319
|
+
const s = this._traj[t];
|
|
4320
|
+
const do_ = dOutputs[t];
|
|
4321
|
+
for (let i = 0; i < this.outputSize; i++) {
|
|
4322
|
+
for (let j = 0; j < this.hiddenSize; j++) {
|
|
4323
|
+
dWhy[i][j] += do_[i] * s.h[j];
|
|
4324
|
+
}
|
|
4325
|
+
dby[i] += do_[i];
|
|
4326
|
+
}
|
|
4327
|
+
const dh = this.hiddenSize > 0 ? Array.from(
|
|
4328
|
+
{ length: this.hiddenSize },
|
|
4329
|
+
(_, j) => this.Why.reduce((sum, row, i) => sum + row[j] * do_[i], 0) + dhNext[j]
|
|
4330
|
+
) : [];
|
|
4331
|
+
const dhRaw = dh.map((d, k) => d * (1 - s.h[k] ** 2));
|
|
4332
|
+
for (let i = 0; i < this.hiddenSize; i++) {
|
|
4333
|
+
for (let j = 0; j < this.inputSize; j++) {
|
|
4334
|
+
dWxh[i][j] += dhRaw[i] * s.x[j];
|
|
4335
|
+
}
|
|
4336
|
+
dbh[i] += dhRaw[i];
|
|
4337
|
+
}
|
|
4338
|
+
for (let i = 0; i < this.hiddenSize; i++) {
|
|
4339
|
+
for (let j = 0; j < this.hiddenSize; j++) {
|
|
4340
|
+
dWhh[i][j] += dhRaw[i] * s.hPrev[j];
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
dhNext = Array.from(
|
|
4344
|
+
{ length: this.hiddenSize },
|
|
4345
|
+
(_, j) => this.Whh.reduce((sum, row, i) => sum + row[j] * dhRaw[i], 0)
|
|
4346
|
+
);
|
|
4347
|
+
}
|
|
4348
|
+
const scale = lr / T;
|
|
4349
|
+
for (let i = 0; i < this.hiddenSize; i++) {
|
|
4350
|
+
for (let j = 0; j < this.inputSize; j++) {
|
|
4351
|
+
this.Wxh[i][j] = this._opts.Wxh[i][j].step(this.Wxh[i][j], dWxh[i][j], scale);
|
|
4352
|
+
}
|
|
4353
|
+
for (let j = 0; j < this.hiddenSize; j++) {
|
|
4354
|
+
this.Whh[i][j] = this._opts.Whh[i][j].step(this.Whh[i][j], dWhh[i][j], scale);
|
|
4355
|
+
}
|
|
4356
|
+
this.bh[i] = this._opts.bh[i].step(this.bh[i], dbh[i], scale);
|
|
4357
|
+
}
|
|
4358
|
+
for (let i = 0; i < this.outputSize; i++) {
|
|
4359
|
+
for (let j = 0; j < this.hiddenSize; j++) {
|
|
4360
|
+
this.Why[i][j] = this._opts.Why[i][j].step(this.Why[i][j], dWhy[i][j], scale);
|
|
4361
|
+
}
|
|
4362
|
+
this.by[i] = this._opts.by[i].step(this.by[i], dby[i], scale);
|
|
4363
|
+
}
|
|
4364
|
+
this._traj = [];
|
|
4365
|
+
this._outputs = [];
|
|
4366
|
+
return loss;
|
|
4367
|
+
}
|
|
4368
|
+
};
|
|
4369
|
+
|
|
4370
|
+
// src/Seq2Seq.ts
|
|
4371
|
+
var Seq2Seq = class {
|
|
4372
|
+
// [outputSize]
|
|
4373
|
+
constructor(inputSize, hiddenSize, outputSize, options) {
|
|
4374
|
+
if (inputSize <= 0 || hiddenSize <= 0 || outputSize <= 0) {
|
|
4375
|
+
throw new Error("Seq2Seq: all sizes must be positive");
|
|
4376
|
+
}
|
|
4377
|
+
this.inputSize = inputSize;
|
|
4378
|
+
this.hiddenSize = hiddenSize;
|
|
4379
|
+
this.outputSize = outputSize;
|
|
4380
|
+
const factory = options?.optimizerFactory ?? (() => new SGD());
|
|
4381
|
+
this.encoder = new LSTMLayer(inputSize, hiddenSize, factory);
|
|
4382
|
+
this.decoder = new LSTMLayer(outputSize, hiddenSize, factory);
|
|
4383
|
+
const limit = Math.sqrt(2 / hiddenSize);
|
|
4384
|
+
this.W_out = Array.from(
|
|
4385
|
+
{ length: outputSize },
|
|
4386
|
+
() => Array.from({ length: hiddenSize }, () => (Math.random() * 2 - 1) * limit)
|
|
4387
|
+
);
|
|
4388
|
+
this.b_out = new Array(outputSize).fill(0);
|
|
4389
|
+
this._wOutOpts = Array.from(
|
|
4390
|
+
{ length: outputSize },
|
|
4391
|
+
() => Array.from({ length: hiddenSize }, () => factory())
|
|
4392
|
+
);
|
|
4393
|
+
this._bOutOpts = Array.from({ length: outputSize }, () => factory());
|
|
4394
|
+
}
|
|
4395
|
+
// ── Linear projection ─────────────────────────────────────────────────────
|
|
4396
|
+
_project(h) {
|
|
4397
|
+
return this.b_out.map(
|
|
4398
|
+
(b, i) => b + this.W_out[i].reduce((s, w, j) => s + w * h[j], 0)
|
|
4399
|
+
);
|
|
4400
|
+
}
|
|
4401
|
+
// ── Encode ────────────────────────────────────────────────────────────────
|
|
4402
|
+
// Runs the encoder over inputSequence and returns the final (h, c) pair.
|
|
4403
|
+
// The context vector summarizes the full input sequence.
|
|
4404
|
+
encode(inputSequence) {
|
|
4405
|
+
this.encoder.reset();
|
|
4406
|
+
for (const x of inputSequence) {
|
|
4407
|
+
this.encoder.predict(x);
|
|
4408
|
+
}
|
|
4409
|
+
return {
|
|
4410
|
+
h: [...this.encoder.h],
|
|
4411
|
+
c: [...this.encoder.c]
|
|
4412
|
+
};
|
|
4413
|
+
}
|
|
4414
|
+
// ── Decode ────────────────────────────────────────────────────────────────
|
|
4415
|
+
// Generates `steps` output tokens autoregressively.
|
|
4416
|
+
// The decoder starts from contextVector and uses its own previous output
|
|
4417
|
+
// as input at each step (greedy / free-running decoding).
|
|
4418
|
+
decode(contextVector, steps) {
|
|
4419
|
+
this.decoder.reset();
|
|
4420
|
+
this.decoder.h = [...contextVector.h];
|
|
4421
|
+
this.decoder.c = [...contextVector.c];
|
|
4422
|
+
const results = [];
|
|
4423
|
+
let prevOutput = new Array(this.outputSize).fill(0);
|
|
4424
|
+
for (let t = 0; t < steps; t++) {
|
|
4425
|
+
const hidden = this.decoder.predict(prevOutput);
|
|
4426
|
+
const output = this._project(hidden);
|
|
4427
|
+
results.push(output);
|
|
4428
|
+
prevOutput = output;
|
|
4429
|
+
}
|
|
4430
|
+
return results;
|
|
4431
|
+
}
|
|
4432
|
+
// ── Training step (teacher forcing) ──────────────────────────────────────
|
|
4433
|
+
// inputSeq: number[][] of shape [T_in][inputSize]
|
|
4434
|
+
// targetSeq: number[][] of shape [T_out][outputSize]
|
|
4435
|
+
// Returns the MSE loss for this step.
|
|
4436
|
+
trainStep(inputSeq, targetSeq, lr) {
|
|
4437
|
+
const T = targetSeq.length;
|
|
4438
|
+
if (T === 0) return 0;
|
|
4439
|
+
this.encoder.reset();
|
|
4440
|
+
for (const x of inputSeq) {
|
|
4441
|
+
this.encoder.predict(x);
|
|
4442
|
+
}
|
|
4443
|
+
const contextH = [...this.encoder.h];
|
|
4444
|
+
const contextC = [...this.encoder.c];
|
|
4445
|
+
this.decoder.reset();
|
|
4446
|
+
this.decoder.h = [...contextH];
|
|
4447
|
+
this.decoder.c = [...contextC];
|
|
4448
|
+
const hiddens = [];
|
|
4449
|
+
const projOuts = [];
|
|
4450
|
+
let prevTeacher = new Array(this.outputSize).fill(0);
|
|
4451
|
+
for (let t = 0; t < T; t++) {
|
|
4452
|
+
const h = this.decoder.predict(prevTeacher);
|
|
4453
|
+
const out = this._project(h);
|
|
4454
|
+
hiddens.push(h);
|
|
4455
|
+
projOuts.push(out);
|
|
4456
|
+
prevTeacher = targetSeq[t];
|
|
4457
|
+
}
|
|
4458
|
+
let loss = 0;
|
|
4459
|
+
const dProjOut = projOuts.map(
|
|
4460
|
+
(o, t) => o.map((v, k) => {
|
|
4461
|
+
const diff = v - targetSeq[t][k];
|
|
4462
|
+
loss += diff * diff;
|
|
4463
|
+
return 2 * diff / this.outputSize;
|
|
4464
|
+
})
|
|
4465
|
+
);
|
|
4466
|
+
loss /= T * this.outputSize;
|
|
4467
|
+
const dhSeq = Array.from(
|
|
4468
|
+
{ length: T },
|
|
4469
|
+
() => new Array(this.hiddenSize).fill(0)
|
|
4470
|
+
);
|
|
4471
|
+
const dWout = Array.from(
|
|
4472
|
+
{ length: this.outputSize },
|
|
4473
|
+
() => new Array(this.hiddenSize).fill(0)
|
|
4474
|
+
);
|
|
4475
|
+
const dbOut = new Array(this.outputSize).fill(0);
|
|
4476
|
+
for (let t = 0; t < T; t++) {
|
|
4477
|
+
for (let i = 0; i < this.outputSize; i++) {
|
|
4478
|
+
const dv = dProjOut[t][i];
|
|
4479
|
+
dbOut[i] += dv;
|
|
4480
|
+
for (let j = 0; j < this.hiddenSize; j++) {
|
|
4481
|
+
dWout[i][j] += dv * hiddens[t][j];
|
|
4482
|
+
dhSeq[t][j] += dv * this.W_out[i][j];
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
const scale = lr / T;
|
|
4487
|
+
for (let i = 0; i < this.outputSize; i++) {
|
|
4488
|
+
for (let j = 0; j < this.hiddenSize; j++) {
|
|
4489
|
+
this.W_out[i][j] = this._wOutOpts[i][j].step(this.W_out[i][j], dWout[i][j], scale);
|
|
4490
|
+
}
|
|
4491
|
+
this.b_out[i] = this._bOutOpts[i].step(this.b_out[i], dbOut[i], scale);
|
|
4492
|
+
}
|
|
4493
|
+
this.decoder.backprop(dhSeq, lr);
|
|
4494
|
+
const dContext = dhSeq[0];
|
|
4495
|
+
const encoderDhSeq = inputSeq.map(
|
|
4496
|
+
(_, t) => t === inputSeq.length - 1 ? [...dContext] : new Array(this.hiddenSize).fill(0)
|
|
4497
|
+
);
|
|
4498
|
+
this.encoder.backprop(encoderDhSeq, lr);
|
|
4499
|
+
return loss;
|
|
4500
|
+
}
|
|
4501
|
+
};
|
|
4502
|
+
|
|
4503
|
+
// src/TCN.ts
|
|
4504
|
+
var CausalConv1D = class {
|
|
4505
|
+
constructor(inputChannels, outputChannels, kernelSize, dilation, optimizerFactory = () => new SGD()) {
|
|
4506
|
+
// Cache for backward pass
|
|
4507
|
+
this._paddedInput = null;
|
|
4508
|
+
this._inputLen = 0;
|
|
4509
|
+
if (inputChannels <= 0 || outputChannels <= 0 || kernelSize <= 0 || dilation <= 0) {
|
|
4510
|
+
throw new Error("CausalConv1D: all dimensions must be positive");
|
|
4511
|
+
}
|
|
4512
|
+
this.inputChannels = inputChannels;
|
|
4513
|
+
this.outputChannels = outputChannels;
|
|
4514
|
+
this.kernelSize = kernelSize;
|
|
4515
|
+
this.dilation = dilation;
|
|
4516
|
+
const limit = Math.sqrt(2 / (kernelSize * inputChannels));
|
|
4517
|
+
this.kernels = Array.from(
|
|
4518
|
+
{ length: outputChannels },
|
|
4519
|
+
() => Array.from(
|
|
4520
|
+
{ length: kernelSize },
|
|
4521
|
+
() => Array.from({ length: inputChannels }, () => (Math.random() * 2 - 1) * limit)
|
|
4522
|
+
)
|
|
4523
|
+
);
|
|
4524
|
+
this.biases = new Array(outputChannels).fill(0);
|
|
4525
|
+
this._kOpts = Array.from(
|
|
4526
|
+
{ length: outputChannels },
|
|
4527
|
+
() => Array.from(
|
|
4528
|
+
{ length: kernelSize },
|
|
4529
|
+
() => Array.from({ length: inputChannels }, () => optimizerFactory())
|
|
4530
|
+
)
|
|
4531
|
+
);
|
|
4532
|
+
this._bOpts = Array.from({ length: outputChannels }, () => optimizerFactory());
|
|
4533
|
+
}
|
|
4534
|
+
// ── Forward pass ──────────────────────────────────────────────────────────
|
|
4535
|
+
// input: [T][inputChannels]
|
|
4536
|
+
// Returns: [T][outputChannels] (same length — causal padding preserves T)
|
|
4537
|
+
forward(input) {
|
|
4538
|
+
const T = input.length;
|
|
4539
|
+
const pad = (this.kernelSize - 1) * this.dilation;
|
|
4540
|
+
const zeroCh = new Array(this.inputChannels).fill(0);
|
|
4541
|
+
const padded = [
|
|
4542
|
+
...Array.from({ length: pad }, () => [...zeroCh]),
|
|
4543
|
+
...input.map((row) => [...row])
|
|
4544
|
+
];
|
|
4545
|
+
this._paddedInput = padded;
|
|
4546
|
+
this._inputLen = T;
|
|
4547
|
+
const output = Array.from(
|
|
4548
|
+
{ length: T },
|
|
4549
|
+
() => new Array(this.outputChannels).fill(0)
|
|
4550
|
+
);
|
|
4551
|
+
for (let t = 0; t < T; t++) {
|
|
4552
|
+
for (let f = 0; f < this.outputChannels; f++) {
|
|
4553
|
+
let sum = this.biases[f];
|
|
4554
|
+
for (let k = 0; k < this.kernelSize; k++) {
|
|
4555
|
+
const srcPos = t + k * this.dilation;
|
|
4556
|
+
for (let c = 0; c < this.inputChannels; c++) {
|
|
4557
|
+
sum += this.kernels[f][k][c] * padded[srcPos][c];
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
output[t][f] = sum;
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
return output;
|
|
4564
|
+
}
|
|
4565
|
+
// ── Backward pass ─────────────────────────────────────────────────────────
|
|
4566
|
+
// dOutput: [T][outputChannels]
|
|
4567
|
+
// Returns dInput: [T][inputChannels]
|
|
4568
|
+
backward(dOutput, lr) {
|
|
4569
|
+
if (!this._paddedInput) {
|
|
4570
|
+
throw new Error("CausalConv1D.backward: call forward() first");
|
|
4571
|
+
}
|
|
4572
|
+
const T = this._inputLen;
|
|
4573
|
+
const pad = (this.kernelSize - 1) * this.dilation;
|
|
4574
|
+
const padded = this._paddedInput;
|
|
4575
|
+
const dKernels = Array.from(
|
|
4576
|
+
{ length: this.outputChannels },
|
|
4577
|
+
() => Array.from(
|
|
4578
|
+
{ length: this.kernelSize },
|
|
4579
|
+
() => new Array(this.inputChannels).fill(0)
|
|
4580
|
+
)
|
|
4581
|
+
);
|
|
4582
|
+
const dBiases = new Array(this.outputChannels).fill(0);
|
|
4583
|
+
const dPadded = Array.from(
|
|
4584
|
+
{ length: padded.length },
|
|
4585
|
+
() => new Array(this.inputChannels).fill(0)
|
|
4586
|
+
);
|
|
4587
|
+
for (let t = 0; t < T; t++) {
|
|
4588
|
+
for (let f = 0; f < this.outputChannels; f++) {
|
|
4589
|
+
const dv = dOutput[t][f];
|
|
4590
|
+
dBiases[f] += dv;
|
|
4591
|
+
for (let k = 0; k < this.kernelSize; k++) {
|
|
4592
|
+
const srcPos = t + k * this.dilation;
|
|
4593
|
+
for (let c = 0; c < this.inputChannels; c++) {
|
|
4594
|
+
dKernels[f][k][c] += dv * padded[srcPos][c];
|
|
4595
|
+
dPadded[srcPos][c] += dv * this.kernels[f][k][c];
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
for (let f = 0; f < this.outputChannels; f++) {
|
|
4601
|
+
for (let k = 0; k < this.kernelSize; k++) {
|
|
4602
|
+
for (let c = 0; c < this.inputChannels; c++) {
|
|
4603
|
+
this.kernels[f][k][c] = this._kOpts[f][k][c].step(
|
|
4604
|
+
this.kernels[f][k][c],
|
|
4605
|
+
dKernels[f][k][c],
|
|
4606
|
+
lr
|
|
4607
|
+
);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
this.biases[f] = this._bOpts[f].step(this.biases[f], dBiases[f], lr);
|
|
4611
|
+
}
|
|
4612
|
+
return dPadded.slice(pad);
|
|
4613
|
+
}
|
|
4614
|
+
};
|
|
4615
|
+
var TCN = class {
|
|
4616
|
+
// linear projection outputs
|
|
4617
|
+
constructor(inputChannels, channels, kernelSize, levels, outputSize, optimizerFactory = () => new SGD()) {
|
|
4618
|
+
// Cache for backward pass
|
|
4619
|
+
this._layerInputs = [];
|
|
4620
|
+
// inputs to each conv layer
|
|
4621
|
+
this._layerOutputs = [];
|
|
4622
|
+
// outputs from each conv layer (pre-relu)
|
|
4623
|
+
this._lastHidden = [];
|
|
4624
|
+
// post-relu output of last conv layer
|
|
4625
|
+
this._finalOutputs = [];
|
|
4626
|
+
if (levels <= 0) throw new Error("TCN: levels must be positive");
|
|
4627
|
+
if (outputSize <= 0) throw new Error("TCN: outputSize must be positive");
|
|
4628
|
+
this.inputChannels = inputChannels;
|
|
4629
|
+
this.channels = channels;
|
|
4630
|
+
this.kernelSize = kernelSize;
|
|
4631
|
+
this.levels = levels;
|
|
4632
|
+
this.outputSize = outputSize;
|
|
4633
|
+
this.layers = [];
|
|
4634
|
+
for (let l = 0; l < levels; l++) {
|
|
4635
|
+
const dilation = Math.pow(2, l);
|
|
4636
|
+
const inCh = l === 0 ? inputChannels : channels;
|
|
4637
|
+
this.layers.push(new CausalConv1D(inCh, channels, kernelSize, dilation, optimizerFactory));
|
|
4638
|
+
}
|
|
4639
|
+
const outLimit = Math.sqrt(2 / channels);
|
|
4640
|
+
this._outputW = Array.from(
|
|
4641
|
+
{ length: outputSize },
|
|
4642
|
+
() => Array.from({ length: channels }, () => (Math.random() * 2 - 1) * outLimit)
|
|
4643
|
+
);
|
|
4644
|
+
this._outputB = new Array(outputSize).fill(0);
|
|
4645
|
+
this._outOpts = Array.from(
|
|
4646
|
+
{ length: outputSize },
|
|
4647
|
+
() => Array.from({ length: channels }, () => optimizerFactory())
|
|
4648
|
+
);
|
|
4649
|
+
this._bOutOpts = Array.from({ length: outputSize }, () => optimizerFactory());
|
|
4650
|
+
}
|
|
4651
|
+
// ── Receptive field (informational) ──────────────────────────────────────
|
|
4652
|
+
// RF = (kernelSize - 1) · (2^levels - 1) + 1
|
|
4653
|
+
get receptiveField() {
|
|
4654
|
+
return (this.kernelSize - 1) * (Math.pow(2, this.levels) - 1) + 1;
|
|
4655
|
+
}
|
|
4656
|
+
// ── Forward pass ──────────────────────────────────────────────────────────
|
|
4657
|
+
// sequence: [T][inputChannels]
|
|
4658
|
+
// Returns: [T][outputSize]
|
|
4659
|
+
forward(sequence) {
|
|
4660
|
+
this._layerInputs = [];
|
|
4661
|
+
this._layerOutputs = [];
|
|
4662
|
+
let current = sequence;
|
|
4663
|
+
for (let l = 0; l < this.levels; l++) {
|
|
4664
|
+
this._layerInputs.push(current.map((row) => [...row]));
|
|
4665
|
+
const convOut = this.layers[l].forward(current);
|
|
4666
|
+
this._layerOutputs.push(convOut);
|
|
4667
|
+
const afterRelu = convOut.map((row) => row.map((v) => relu.fn(v)));
|
|
4668
|
+
if (current[0].length === afterRelu[0].length) {
|
|
4669
|
+
current = afterRelu.map((row, t) => row.map((v, c) => v + current[t][c]));
|
|
4670
|
+
} else {
|
|
4671
|
+
current = afterRelu;
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
this._lastHidden = current;
|
|
4675
|
+
const T = current.length;
|
|
4676
|
+
this._finalOutputs = Array.from(
|
|
4677
|
+
{ length: T },
|
|
4678
|
+
(_, t) => this._outputB.map(
|
|
4679
|
+
(b, i) => b + this._outputW[i].reduce((s, w, j) => s + w * current[t][j], 0)
|
|
4680
|
+
)
|
|
4681
|
+
);
|
|
4682
|
+
return this._finalOutputs.map((row) => [...row]);
|
|
4683
|
+
}
|
|
4684
|
+
// ── Train one step ────────────────────────────────────────────────────────
|
|
4685
|
+
// sequence: [T][inputChannels]
|
|
4686
|
+
// targets: [T][outputSize]
|
|
4687
|
+
// Returns MSE loss.
|
|
4688
|
+
train(sequence, targets, lr) {
|
|
4689
|
+
const outputs = this.forward(sequence);
|
|
4690
|
+
const T = outputs.length;
|
|
4691
|
+
let loss = 0;
|
|
4692
|
+
const dOut = outputs.map(
|
|
4693
|
+
(o, t) => o.map((v, k) => {
|
|
4694
|
+
const diff = v - targets[t][k];
|
|
4695
|
+
loss += diff * diff;
|
|
4696
|
+
return 2 * diff / this.outputSize;
|
|
4697
|
+
})
|
|
4698
|
+
);
|
|
4699
|
+
loss /= T * this.outputSize;
|
|
4700
|
+
const dWout = Array.from({ length: this.outputSize }, () => new Array(this.channels).fill(0));
|
|
4701
|
+
const dBout = new Array(this.outputSize).fill(0);
|
|
4702
|
+
const dHidden = Array.from({ length: T }, () => new Array(this.channels).fill(0));
|
|
4703
|
+
for (let t = 0; t < T; t++) {
|
|
4704
|
+
for (let i = 0; i < this.outputSize; i++) {
|
|
4705
|
+
const dv = dOut[t][i];
|
|
4706
|
+
dBout[i] += dv;
|
|
4707
|
+
for (let j = 0; j < this.channels; j++) {
|
|
4708
|
+
dWout[i][j] += dv * this._lastHidden[t][j];
|
|
4709
|
+
dHidden[t][j] += dv * this._outputW[i][j];
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
const scale = lr / T;
|
|
4714
|
+
for (let i = 0; i < this.outputSize; i++) {
|
|
4715
|
+
for (let j = 0; j < this.channels; j++) {
|
|
4716
|
+
this._outputW[i][j] = this._outOpts[i][j].step(this._outputW[i][j], dWout[i][j], scale);
|
|
4717
|
+
}
|
|
4718
|
+
this._outputB[i] = this._bOutOpts[i].step(this._outputB[i], dBout[i], scale);
|
|
4719
|
+
}
|
|
4720
|
+
let dCurrent = dHidden;
|
|
4721
|
+
for (let l = this.levels - 1; l >= 0; l--) {
|
|
4722
|
+
const convOut = this._layerOutputs[l];
|
|
4723
|
+
const layerIn = this._layerInputs[l];
|
|
4724
|
+
const dConvOut = dCurrent.map(
|
|
4725
|
+
(row, t) => row.map((d, c) => d * (convOut[t][c] > 0 ? 1 : 0))
|
|
4726
|
+
);
|
|
4727
|
+
let dPrevLayer = this.layers[l].backward(dConvOut, lr);
|
|
4728
|
+
if (layerIn[0].length === dCurrent[0].length) {
|
|
4729
|
+
dPrevLayer = dPrevLayer.map(
|
|
4730
|
+
(row, t) => row.map((d, c) => d + dCurrent[t][c])
|
|
4731
|
+
);
|
|
4732
|
+
}
|
|
4733
|
+
dCurrent = dPrevLayer;
|
|
4734
|
+
}
|
|
4735
|
+
return loss;
|
|
4736
|
+
}
|
|
4737
|
+
};
|
|
4738
|
+
|
|
4739
|
+
// src/GAN.ts
|
|
4740
|
+
var GAN = class {
|
|
4741
|
+
constructor(latentDim, generatorHidden, outputDim, discriminatorHidden, options) {
|
|
4742
|
+
this.latentDim = latentDim;
|
|
4743
|
+
const gStructure = [latentDim, ...generatorHidden, outputDim];
|
|
4744
|
+
this.generator = new NetworkN(gStructure, options?.generatorOptions ?? {});
|
|
4745
|
+
const dStructure = [outputDim, ...discriminatorHidden, 1];
|
|
4746
|
+
this.discriminator = new NetworkN(dStructure, options?.discriminatorOptions ?? {});
|
|
4747
|
+
}
|
|
4748
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
4749
|
+
// Generate a synthetic sample. If z is not provided, samples from N(0, 1).
|
|
4750
|
+
generate(z) {
|
|
4751
|
+
const latent = z ?? this.sampleLatent();
|
|
4752
|
+
return this.generator.predict(latent);
|
|
4753
|
+
}
|
|
4754
|
+
// Returns the discriminator's estimate that x is real, in [0, 1].
|
|
4755
|
+
discriminate(x) {
|
|
4756
|
+
return this.discriminator.predict(x)[0];
|
|
4757
|
+
}
|
|
4758
|
+
// ── Training Step ────────────────────────────────────────────────────────
|
|
4759
|
+
//
|
|
4760
|
+
// Runs one discriminator update and one generator update over the provided
|
|
4761
|
+
// real batch. Returns per-step losses for monitoring.
|
|
4762
|
+
//
|
|
4763
|
+
// Discriminator loss (binary cross-entropy, minimised via SGD):
|
|
4764
|
+
// L_D = -[ log D(x_real) + log(1 - D(G(z))) ]
|
|
4765
|
+
//
|
|
4766
|
+
// Generator loss:
|
|
4767
|
+
// L_G = -log D(G(z)) (non-saturating variant — avoids vanishing gradients
|
|
4768
|
+
// in early training when D is confident)
|
|
4769
|
+
//
|
|
4770
|
+
trainStep(realBatch, lr) {
|
|
4771
|
+
const eps = 1e-15;
|
|
4772
|
+
let dLossSum = 0;
|
|
4773
|
+
let gLossSum = 0;
|
|
4774
|
+
for (const xReal of realBatch) {
|
|
4775
|
+
const dReal = Math.max(eps, Math.min(1 - eps, this.discriminate(xReal)));
|
|
4776
|
+
const dRealDelta = [1 - dReal];
|
|
4777
|
+
this.discriminator.trainWithDeltas(xReal, dRealDelta, lr);
|
|
4778
|
+
dLossSum += -Math.log(dReal);
|
|
4779
|
+
const z = this.sampleLatent();
|
|
4780
|
+
const xFake = this.generate(z);
|
|
4781
|
+
const dFake = Math.max(eps, Math.min(1 - eps, this.discriminate(xFake)));
|
|
4782
|
+
const dFakeDelta = [0 - dFake];
|
|
4783
|
+
this.discriminator.trainWithDeltas(xFake, dFakeDelta, lr);
|
|
4784
|
+
dLossSum += -Math.log(1 - dFake);
|
|
4785
|
+
const z2 = this.sampleLatent();
|
|
4786
|
+
const xFake2 = this.generate(z2);
|
|
4787
|
+
const dScore = Math.max(eps, Math.min(1 - eps, this.discriminate(xFake2)));
|
|
4788
|
+
const gError = 1 - dScore;
|
|
4789
|
+
const gDelta = xFake2.map(() => gError / xFake2.length);
|
|
4790
|
+
this.generator.trainWithDeltas(z2, gDelta, lr);
|
|
4791
|
+
gLossSum += -Math.log(dScore);
|
|
4792
|
+
}
|
|
4793
|
+
const n = realBatch.length;
|
|
4794
|
+
return {
|
|
4795
|
+
dLoss: dLossSum / n,
|
|
4796
|
+
gLoss: gLossSum / n
|
|
4797
|
+
};
|
|
4798
|
+
}
|
|
4799
|
+
// Samples a latent vector z ~ N(0, 1)^latentDim using Box-Muller transform.
|
|
4800
|
+
sampleLatent() {
|
|
4801
|
+
const z = [];
|
|
4802
|
+
for (let i = 0; i < this.latentDim; i += 2) {
|
|
4803
|
+
const u1 = Math.random();
|
|
4804
|
+
const u2 = Math.random();
|
|
4805
|
+
const r = Math.sqrt(-2 * Math.log(u1 + 1e-15));
|
|
4806
|
+
const theta = 2 * Math.PI * u2;
|
|
4807
|
+
z.push(r * Math.cos(theta));
|
|
4808
|
+
if (i + 1 < this.latentDim) z.push(r * Math.sin(theta));
|
|
4809
|
+
}
|
|
4810
|
+
return z;
|
|
4811
|
+
}
|
|
4812
|
+
};
|
|
4813
|
+
|
|
4814
|
+
// src/VAE.ts
|
|
4815
|
+
var VAE = class {
|
|
4816
|
+
constructor(inputSize, encoderHidden, latentDim, decoderHidden, options) {
|
|
4817
|
+
this.latentDim = latentDim;
|
|
4818
|
+
const encoderStructure = [inputSize, ...encoderHidden, latentDim * 2];
|
|
4819
|
+
this.encoder = new NetworkN(encoderStructure, options ?? {});
|
|
4820
|
+
const decoderStructure = [latentDim, ...decoderHidden, inputSize];
|
|
4821
|
+
this.decoder = new NetworkN(decoderStructure, options ?? {});
|
|
4822
|
+
}
|
|
4823
|
+
// ── Encode ───────────────────────────────────────────────────────────────
|
|
4824
|
+
// Splits the encoder output into μ and logVar vectors.
|
|
4825
|
+
encode(x) {
|
|
4826
|
+
const out = this.encoder.predict(x);
|
|
4827
|
+
const mu = out.slice(0, this.latentDim);
|
|
4828
|
+
const logVar = out.slice(this.latentDim);
|
|
4829
|
+
return { mu, logVar };
|
|
4830
|
+
}
|
|
4831
|
+
// ── Reparametrisation Trick ──────────────────────────────────────────────
|
|
4832
|
+
// z = μ + σ·ε, ε ~ N(0,1)
|
|
4833
|
+
// σ = exp(0.5 · logVar) (ensures σ > 0 without constraining the network)
|
|
4834
|
+
reparametrize(mu, logVar) {
|
|
4835
|
+
return mu.map((m, i) => {
|
|
4836
|
+
const sigma = Math.exp(0.5 * logVar[i]);
|
|
4837
|
+
const eps = this._sampleNormal();
|
|
4838
|
+
return m + sigma * eps;
|
|
4839
|
+
});
|
|
4840
|
+
}
|
|
4841
|
+
// ── Decode ───────────────────────────────────────────────────────────────
|
|
4842
|
+
decode(z) {
|
|
4843
|
+
return this.decoder.predict(z);
|
|
4844
|
+
}
|
|
4845
|
+
// ── Forward Pass ─────────────────────────────────────────────────────────
|
|
4846
|
+
// Encodes, samples z, and decodes.
|
|
4847
|
+
forward(x) {
|
|
4848
|
+
const { mu, logVar } = this.encode(x);
|
|
4849
|
+
const z = this.reparametrize(mu, logVar);
|
|
4850
|
+
const reconstruction = this.decode(z);
|
|
4851
|
+
return { reconstruction, mu, logVar, z };
|
|
4852
|
+
}
|
|
4853
|
+
// ── Training Step ────────────────────────────────────────────────────────
|
|
4854
|
+
//
|
|
4855
|
+
// Performs one forward pass, computes the ELBO loss, and updates both
|
|
4856
|
+
// encoder and decoder weights via their built-in SGD.
|
|
4857
|
+
//
|
|
4858
|
+
// Reconstruction loss: L_recon = MSE(x, x̂)
|
|
4859
|
+
// KL divergence: L_kl = -½ Σ(1 + logVarᵢ - μᵢ² - exp(logVarᵢ))
|
|
4860
|
+
// Total: L = L_recon + L_kl
|
|
4861
|
+
//
|
|
4862
|
+
train(x, lr) {
|
|
4863
|
+
const { reconstruction, mu, logVar, z } = this.forward(x);
|
|
4864
|
+
const reconLoss = x.reduce((s, xi, i) => s + (xi - reconstruction[i]) ** 2, 0) / x.length;
|
|
4865
|
+
const klLoss = mu.reduce((s, m, i) => {
|
|
4866
|
+
return s - 0.5 * (1 + logVar[i] - m * m - Math.exp(logVar[i]));
|
|
4867
|
+
}, 0);
|
|
4868
|
+
const totalLoss = reconLoss + klLoss;
|
|
4869
|
+
const decoderDeltas = reconstruction.map((r, i) => (x[i] - r) / x.length);
|
|
4870
|
+
this.decoder.trainWithDeltas(z, decoderDeltas, lr);
|
|
4871
|
+
const encoderDeltas = [
|
|
4872
|
+
...mu.map((m) => -m),
|
|
4873
|
+
...logVar.map((lv) => -0.5 * (Math.exp(lv) - 1))
|
|
4874
|
+
];
|
|
4875
|
+
this.encoder.trainWithDeltas(x, encoderDeltas, lr);
|
|
4876
|
+
return { totalLoss, reconLoss, klLoss };
|
|
4877
|
+
}
|
|
4878
|
+
// ── Generate ─────────────────────────────────────────────────────────────
|
|
4879
|
+
// Samples z ~ N(0, I) and decodes it (pure generation, no input required).
|
|
4880
|
+
generate(z) {
|
|
4881
|
+
const latent = z ?? Array.from({ length: this.latentDim }, () => this._sampleNormal());
|
|
4882
|
+
return this.decode(latent);
|
|
4883
|
+
}
|
|
4884
|
+
// ── Private ──────────────────────────────────────────────────────────────
|
|
4885
|
+
// Box-Muller transform: samples one value from N(0, 1).
|
|
4886
|
+
_sampleNormal() {
|
|
4887
|
+
const u1 = Math.random();
|
|
4888
|
+
const u2 = Math.random();
|
|
4889
|
+
return Math.sqrt(-2 * Math.log(u1 + 1e-15)) * Math.cos(2 * Math.PI * u2);
|
|
4890
|
+
}
|
|
4891
|
+
};
|
|
4892
|
+
|
|
4893
|
+
// src/Tape.ts
|
|
4894
|
+
var Value = class _Value {
|
|
4895
|
+
constructor(data, children = [], op = "") {
|
|
4896
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
4897
|
+
this._backward = () => {
|
|
4898
|
+
};
|
|
4899
|
+
this.data = data;
|
|
4900
|
+
this.grad = 0;
|
|
4901
|
+
this._prev = new Set(children);
|
|
4902
|
+
this._op = op;
|
|
4903
|
+
}
|
|
4904
|
+
// ── Arithmetic Operations ────────────────────────────────────────────────
|
|
4905
|
+
// z = a + b → ∂z/∂a = 1, ∂z/∂b = 1
|
|
4906
|
+
add(other) {
|
|
4907
|
+
const o = other instanceof _Value ? other : new _Value(other);
|
|
4908
|
+
const out = new _Value(this.data + o.data, [this, o], "+");
|
|
4909
|
+
out._backward = () => {
|
|
4910
|
+
this.grad += out.grad;
|
|
4911
|
+
o.grad += out.grad;
|
|
4912
|
+
};
|
|
4913
|
+
return out;
|
|
4914
|
+
}
|
|
4915
|
+
// z = a * b → ∂z/∂a = b, ∂z/∂b = a
|
|
4916
|
+
mul(other) {
|
|
4917
|
+
const o = other instanceof _Value ? other : new _Value(other);
|
|
4918
|
+
const out = new _Value(this.data * o.data, [this, o], "*");
|
|
4919
|
+
out._backward = () => {
|
|
4920
|
+
this.grad += o.data * out.grad;
|
|
4921
|
+
o.grad += this.data * out.grad;
|
|
4922
|
+
};
|
|
4923
|
+
return out;
|
|
4924
|
+
}
|
|
4925
|
+
// z = aⁿ → ∂z/∂a = n·aⁿ⁻¹
|
|
4926
|
+
pow(exp) {
|
|
4927
|
+
const out = new _Value(Math.pow(this.data, exp), [this], `**${exp}`);
|
|
4928
|
+
out._backward = () => {
|
|
4929
|
+
this.grad += exp * Math.pow(this.data, exp - 1) * out.grad;
|
|
4930
|
+
};
|
|
4931
|
+
return out;
|
|
4932
|
+
}
|
|
4933
|
+
// z = max(0, a) → ∂z/∂a = a > 0 ? 1 : 0
|
|
4934
|
+
relu() {
|
|
4935
|
+
const out = new _Value(Math.max(0, this.data), [this], "ReLU");
|
|
4936
|
+
out._backward = () => {
|
|
4937
|
+
this.grad += (out.data > 0 ? 1 : 0) * out.grad;
|
|
4938
|
+
};
|
|
4939
|
+
return out;
|
|
4940
|
+
}
|
|
4941
|
+
// z = tanh(a) → ∂z/∂a = 1 - tanh(a)² = 1 - z²
|
|
4942
|
+
tanh() {
|
|
4943
|
+
const t = Math.tanh(this.data);
|
|
4944
|
+
const out = new _Value(t, [this], "tanh");
|
|
4945
|
+
out._backward = () => {
|
|
4946
|
+
this.grad += (1 - t * t) * out.grad;
|
|
4947
|
+
};
|
|
4948
|
+
return out;
|
|
4949
|
+
}
|
|
4950
|
+
// z = σ(a) = 1/(1+e⁻ᵃ) → ∂z/∂a = z·(1-z)
|
|
4951
|
+
sigmoid() {
|
|
4952
|
+
const s = 1 / (1 + Math.exp(-this.data));
|
|
4953
|
+
const out = new _Value(s, [this], "sigmoid");
|
|
4954
|
+
out._backward = () => {
|
|
4955
|
+
this.grad += s * (1 - s) * out.grad;
|
|
4956
|
+
};
|
|
4957
|
+
return out;
|
|
4958
|
+
}
|
|
4959
|
+
// z = eᵃ → ∂z/∂a = eᵃ = z
|
|
4960
|
+
exp() {
|
|
4961
|
+
const e = Math.exp(this.data);
|
|
4962
|
+
const out = new _Value(e, [this], "exp");
|
|
4963
|
+
out._backward = () => {
|
|
4964
|
+
this.grad += e * out.grad;
|
|
4965
|
+
};
|
|
4966
|
+
return out;
|
|
4967
|
+
}
|
|
4968
|
+
// ── Derived Operations (built from primitives) ───────────────────────────
|
|
4969
|
+
// a / b = a * b⁻¹
|
|
4970
|
+
div(other) {
|
|
4971
|
+
const o = other instanceof _Value ? other : new _Value(other);
|
|
4972
|
+
return this.mul(o.pow(-1));
|
|
4973
|
+
}
|
|
4974
|
+
// a - b = a + (b * -1)
|
|
4975
|
+
sub(other) {
|
|
4976
|
+
const o = other instanceof _Value ? other : new _Value(other);
|
|
4977
|
+
return this.add(o.mul(-1));
|
|
4978
|
+
}
|
|
4979
|
+
// -a = a * -1
|
|
4980
|
+
neg() {
|
|
4981
|
+
return this.mul(-1);
|
|
4982
|
+
}
|
|
4983
|
+
// ── Backward Pass ────────────────────────────────────────────────────────
|
|
4984
|
+
//
|
|
4985
|
+
// Propagates gradients from this node (treated as the loss L) back through
|
|
4986
|
+
// the entire computational graph.
|
|
4987
|
+
//
|
|
4988
|
+
// Steps:
|
|
4989
|
+
// 1. Build a topological ordering of all ancestor nodes.
|
|
4990
|
+
// 2. Set this.grad = 1 (∂L/∂L = 1).
|
|
4991
|
+
// 3. Visit nodes in reverse topological order, calling each _backward.
|
|
4992
|
+
//
|
|
4993
|
+
backward() {
|
|
4994
|
+
const topo = [];
|
|
4995
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4996
|
+
const buildTopo = (v) => {
|
|
4997
|
+
if (!visited.has(v)) {
|
|
4998
|
+
visited.add(v);
|
|
4999
|
+
for (const child of v._prev) buildTopo(child);
|
|
5000
|
+
topo.push(v);
|
|
5001
|
+
}
|
|
5002
|
+
};
|
|
5003
|
+
buildTopo(this);
|
|
5004
|
+
this.grad = 1;
|
|
5005
|
+
for (let i = topo.length - 1; i >= 0; i--) {
|
|
5006
|
+
topo[i]._backward();
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
5009
|
+
toString() {
|
|
5010
|
+
return `Value(data=${this.data.toFixed(4)}, grad=${this.grad.toFixed(4)}, op='${this._op}')`;
|
|
5011
|
+
}
|
|
5012
|
+
};
|
|
5013
|
+
|
|
5014
|
+
// src/WeightInspector.ts
|
|
5015
|
+
var WeightInspector = class _WeightInspector {
|
|
5016
|
+
// ── Per-layer statistics ─────────────────────────────────────────────────
|
|
5017
|
+
// Returns one WeightStats per layer in network.layers order.
|
|
5018
|
+
static inspect(network, deadThreshold = 1e-3) {
|
|
5019
|
+
return network.layers.map((layer) => {
|
|
5020
|
+
const weights = [];
|
|
5021
|
+
for (const neuron of layer.neurons) {
|
|
5022
|
+
weights.push(...neuron.weights, neuron.bias);
|
|
5023
|
+
}
|
|
5024
|
+
return _computeStats(weights, deadThreshold);
|
|
5025
|
+
});
|
|
5026
|
+
}
|
|
5027
|
+
// ── Global statistics ────────────────────────────────────────────────────
|
|
5028
|
+
// Aggregates all weights across the entire network.
|
|
5029
|
+
static inspectAll(network, deadThreshold = 1e-3) {
|
|
5030
|
+
const allWeights = [];
|
|
5031
|
+
for (const layer of network.layers) {
|
|
5032
|
+
for (const neuron of layer.neurons) {
|
|
5033
|
+
allWeights.push(...neuron.weights, neuron.bias);
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
return _computeStats(allWeights, deadThreshold);
|
|
5037
|
+
}
|
|
5038
|
+
// ── Formatted table ──────────────────────────────────────────────────────
|
|
5039
|
+
// Prints a compact diagnostic table to the console.
|
|
5040
|
+
static print(network, deadThreshold = 1e-3) {
|
|
5041
|
+
const perLayer = _WeightInspector.inspect(network, deadThreshold);
|
|
5042
|
+
const global = _WeightInspector.inspectAll(network, deadThreshold);
|
|
5043
|
+
const header = [
|
|
5044
|
+
"Layer".padEnd(8),
|
|
5045
|
+
"mean".padStart(9),
|
|
5046
|
+
"std".padStart(9),
|
|
5047
|
+
"min".padStart(9),
|
|
5048
|
+
"max".padStart(9),
|
|
5049
|
+
"dead".padStart(11),
|
|
5050
|
+
"params".padStart(8)
|
|
5051
|
+
].join(" ");
|
|
5052
|
+
console.log("");
|
|
5053
|
+
console.log("Weight Inspector:");
|
|
5054
|
+
console.log("\u2500".repeat(header.length));
|
|
5055
|
+
console.log(header);
|
|
5056
|
+
console.log("\u2500".repeat(header.length));
|
|
5057
|
+
perLayer.forEach((s, i) => {
|
|
5058
|
+
console.log(_formatRow(`Layer ${i}`, s));
|
|
5059
|
+
});
|
|
5060
|
+
console.log("\u2500".repeat(header.length));
|
|
5061
|
+
console.log(_formatRow("Global", global));
|
|
5062
|
+
console.log("");
|
|
5063
|
+
}
|
|
5064
|
+
// ── Dead ReLU detection ──────────────────────────────────────────────────
|
|
5065
|
+
//
|
|
5066
|
+
// Given a matrix of activations collected over a forward pass (rows = samples,
|
|
5067
|
+
// cols = neurons), counts neurons that output exactly 0 for every sample.
|
|
5068
|
+
//
|
|
5069
|
+
// How to collect activations:
|
|
5070
|
+
// Run net.predict() for each validation sample and record the output of
|
|
5071
|
+
// each hidden layer. Pass those as `activations` here.
|
|
5072
|
+
//
|
|
5073
|
+
// threshold: activations below this are counted as "dead" (default: 1e-6).
|
|
5074
|
+
//
|
|
5075
|
+
static countDeadReLUs(activations, threshold = 1e-6) {
|
|
5076
|
+
if (activations.length === 0) return 0;
|
|
5077
|
+
const numNeurons = activations[0].length;
|
|
5078
|
+
let dead = 0;
|
|
5079
|
+
for (let j = 0; j < numNeurons; j++) {
|
|
5080
|
+
const allDead = activations.every((row) => Math.abs(row[j]) < threshold);
|
|
5081
|
+
if (allDead) dead++;
|
|
5082
|
+
}
|
|
5083
|
+
return dead;
|
|
5084
|
+
}
|
|
5085
|
+
};
|
|
5086
|
+
function _computeStats(weights, deadThreshold) {
|
|
5087
|
+
const n = weights.length;
|
|
5088
|
+
if (n === 0) {
|
|
5089
|
+
return { mean: 0, std: 0, min: 0, max: 0, deadCount: 0, totalParams: 0 };
|
|
5090
|
+
}
|
|
5091
|
+
let sum = 0, sumSq = 0, min = Infinity, max = -Infinity, deadCount = 0;
|
|
5092
|
+
for (const w of weights) {
|
|
5093
|
+
sum += w;
|
|
5094
|
+
sumSq += w * w;
|
|
5095
|
+
if (w < min) min = w;
|
|
5096
|
+
if (w > max) max = w;
|
|
5097
|
+
if (Math.abs(w) < deadThreshold) deadCount++;
|
|
5098
|
+
}
|
|
5099
|
+
const mean = sum / n;
|
|
5100
|
+
const variance = sumSq / n - mean * mean;
|
|
5101
|
+
const std = Math.sqrt(Math.max(0, variance));
|
|
5102
|
+
return { mean, std, min, max, deadCount, totalParams: n };
|
|
5103
|
+
}
|
|
5104
|
+
function _fmt(n) {
|
|
5105
|
+
return (n >= 0 ? " " : "") + n.toFixed(4);
|
|
5106
|
+
}
|
|
5107
|
+
function _formatRow(label, s) {
|
|
5108
|
+
const deadStr = `${s.deadCount}/${s.totalParams}`;
|
|
5109
|
+
return [
|
|
5110
|
+
label.padEnd(8),
|
|
5111
|
+
_fmt(s.mean).padStart(9),
|
|
5112
|
+
_fmt(s.std).padStart(9),
|
|
5113
|
+
_fmt(s.min).padStart(9),
|
|
5114
|
+
_fmt(s.max).padStart(9),
|
|
5115
|
+
deadStr.padStart(11),
|
|
5116
|
+
String(s.totalParams).padStart(8)
|
|
5117
|
+
].join(" ");
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
// src/Metrics.ts
|
|
5121
|
+
function confusionMatrix(yTrue, yPred, numClasses) {
|
|
5122
|
+
const K = numClasses ?? Math.max(...yTrue, ...yPred) + 1;
|
|
5123
|
+
const matrix = Array.from({ length: K }, () => new Array(K).fill(0));
|
|
5124
|
+
for (let i = 0; i < yTrue.length; i++) {
|
|
5125
|
+
matrix[yTrue[i]][yPred[i]]++;
|
|
5126
|
+
}
|
|
5127
|
+
return matrix;
|
|
5128
|
+
}
|
|
5129
|
+
function precision(yTrue, yPred, positiveClass) {
|
|
5130
|
+
if (positiveClass !== void 0) {
|
|
5131
|
+
return _binaryPrecision(yTrue, yPred, positiveClass);
|
|
5132
|
+
}
|
|
5133
|
+
const K = Math.max(...yTrue, ...yPred) + 1;
|
|
5134
|
+
let sum = 0;
|
|
5135
|
+
for (let c = 0; c < K; c++) sum += _binaryPrecision(yTrue, yPred, c);
|
|
5136
|
+
return sum / K;
|
|
5137
|
+
}
|
|
5138
|
+
function recall(yTrue, yPred, positiveClass) {
|
|
5139
|
+
if (positiveClass !== void 0) {
|
|
5140
|
+
return _binaryRecall(yTrue, yPred, positiveClass);
|
|
5141
|
+
}
|
|
5142
|
+
const K = Math.max(...yTrue, ...yPred) + 1;
|
|
5143
|
+
let sum = 0;
|
|
5144
|
+
for (let c = 0; c < K; c++) sum += _binaryRecall(yTrue, yPred, c);
|
|
5145
|
+
return sum / K;
|
|
5146
|
+
}
|
|
5147
|
+
function f1Score(yTrue, yPred, positiveClass) {
|
|
5148
|
+
const p = precision(yTrue, yPred, positiveClass);
|
|
5149
|
+
const r = recall(yTrue, yPred, positiveClass);
|
|
5150
|
+
if (p + r === 0) return 0;
|
|
5151
|
+
return 2 * p * r / (p + r);
|
|
5152
|
+
}
|
|
5153
|
+
function accuracy(yTrue, yPred) {
|
|
5154
|
+
if (yTrue.length === 0) return 0;
|
|
5155
|
+
const correct = yTrue.filter((y, i) => y === yPred[i]).length;
|
|
5156
|
+
return correct / yTrue.length;
|
|
5157
|
+
}
|
|
5158
|
+
function rocCurve(yTrue, yScores) {
|
|
5159
|
+
const thresholds = [...new Set(yScores)].sort((a, b) => b - a);
|
|
5160
|
+
thresholds.unshift(thresholds[0] + 1);
|
|
5161
|
+
const P = yTrue.filter((y) => y === 1).length;
|
|
5162
|
+
const N = yTrue.length - P;
|
|
5163
|
+
const points = [];
|
|
5164
|
+
for (const t of thresholds) {
|
|
5165
|
+
let tp = 0, fp = 0;
|
|
5166
|
+
for (let i = 0; i < yTrue.length; i++) {
|
|
5167
|
+
const pred = yScores[i] >= t ? 1 : 0;
|
|
5168
|
+
if (pred === 1 && yTrue[i] === 1) tp++;
|
|
5169
|
+
if (pred === 1 && yTrue[i] === 0) fp++;
|
|
5170
|
+
}
|
|
5171
|
+
points.push({
|
|
5172
|
+
threshold: t,
|
|
5173
|
+
fpr: N > 0 ? fp / N : 0,
|
|
5174
|
+
tpr: P > 0 ? tp / P : 0
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
return points.sort((a, b) => a.fpr - b.fpr);
|
|
5178
|
+
}
|
|
5179
|
+
function auc(yTrue, yScores) {
|
|
5180
|
+
const curve = rocCurve(yTrue, yScores);
|
|
5181
|
+
let area = 0;
|
|
5182
|
+
for (let i = 1; i < curve.length; i++) {
|
|
5183
|
+
const dx = curve[i].fpr - curve[i - 1].fpr;
|
|
5184
|
+
const avgY = (curve[i].tpr + curve[i - 1].tpr) / 2;
|
|
5185
|
+
area += dx * avgY;
|
|
5186
|
+
}
|
|
5187
|
+
return Math.abs(area);
|
|
5188
|
+
}
|
|
5189
|
+
function mae(yTrue, yPred) {
|
|
5190
|
+
return yTrue.reduce((s, y, i) => s + Math.abs(y - yPred[i]), 0) / yTrue.length;
|
|
5191
|
+
}
|
|
5192
|
+
function rmse(yTrue, yPred) {
|
|
5193
|
+
const mseVal = yTrue.reduce((s, y, i) => s + (y - yPred[i]) ** 2, 0) / yTrue.length;
|
|
5194
|
+
return Math.sqrt(mseVal);
|
|
5195
|
+
}
|
|
5196
|
+
function r2Score(yTrue, yPred) {
|
|
5197
|
+
const mean = yTrue.reduce((s, y) => s + y, 0) / yTrue.length;
|
|
5198
|
+
const ssTot = yTrue.reduce((s, y) => s + (y - mean) ** 2, 0);
|
|
5199
|
+
const ssRes = yTrue.reduce((s, y, i) => s + (y - yPred[i]) ** 2, 0);
|
|
5200
|
+
if (ssTot === 0) return 1;
|
|
5201
|
+
return 1 - ssRes / ssTot;
|
|
5202
|
+
}
|
|
5203
|
+
function perplexity(yTrue, probabilities) {
|
|
5204
|
+
const eps = 1e-15;
|
|
5205
|
+
const T = yTrue.length;
|
|
5206
|
+
let logSum = 0;
|
|
5207
|
+
for (let t = 0; t < T; t++) {
|
|
5208
|
+
const p = Math.max(eps, probabilities[t][yTrue[t]]);
|
|
5209
|
+
logSum += Math.log(p);
|
|
5210
|
+
}
|
|
5211
|
+
return Math.exp(-logSum / T);
|
|
5212
|
+
}
|
|
5213
|
+
function printConfusionMatrix(matrix, labels) {
|
|
5214
|
+
const K = matrix.length;
|
|
5215
|
+
const lbs = labels ?? Array.from({ length: K }, (_, i) => String(i));
|
|
5216
|
+
const colW = Math.max(6, ...lbs.map((l) => l.length));
|
|
5217
|
+
const pad = (s, w) => s.padStart(w);
|
|
5218
|
+
const header = pad("", colW) + " " + lbs.map((l) => pad(l, colW)).join(" ");
|
|
5219
|
+
console.log("");
|
|
5220
|
+
console.log("Confusion Matrix (rows = actual, cols = predicted):");
|
|
5221
|
+
console.log(header);
|
|
5222
|
+
console.log("\u2500".repeat(header.length));
|
|
5223
|
+
for (let i = 0; i < K; i++) {
|
|
5224
|
+
const row = pad(lbs[i], colW) + " " + matrix[i].map((v) => pad(String(v), colW)).join(" ");
|
|
5225
|
+
console.log(row);
|
|
5226
|
+
}
|
|
5227
|
+
console.log("");
|
|
5228
|
+
}
|
|
5229
|
+
function classificationReport(yTrue, yPred, labels) {
|
|
5230
|
+
const K = Math.max(...yTrue, ...yPred) + 1;
|
|
5231
|
+
const lbs = labels ?? Array.from({ length: K }, (_, i) => `class_${i}`);
|
|
5232
|
+
const rows = [];
|
|
5233
|
+
const colW = Math.max(10, ...lbs.map((l) => l.length));
|
|
5234
|
+
const fmt = (n) => n.toFixed(4).padStart(10);
|
|
5235
|
+
const fmtI = (n) => String(n).padStart(10);
|
|
5236
|
+
rows.push(
|
|
5237
|
+
"label".padEnd(colW) + fmt(0).replace(/\d/g, " ").replace("0.0000", "precision") + fmt(0).replace(/\d/g, " ").replace("0.0000", " recall") + fmt(0).replace(/\d/g, " ").replace("0.0000", " f1-score") + fmtI(0).replace(/\d/g, " ").replace("0", " support")
|
|
5238
|
+
);
|
|
5239
|
+
rows.push("\u2500".repeat(colW + 44));
|
|
5240
|
+
let pSum = 0, rSum = 0, f1Sum = 0;
|
|
5241
|
+
for (let c = 0; c < K; c++) {
|
|
5242
|
+
const p = _binaryPrecision(yTrue, yPred, c);
|
|
5243
|
+
const r = _binaryRecall(yTrue, yPred, c);
|
|
5244
|
+
const f1 = p + r > 0 ? 2 * p * r / (p + r) : 0;
|
|
5245
|
+
const support = yTrue.filter((y) => y === c).length;
|
|
5246
|
+
pSum += p;
|
|
5247
|
+
rSum += r;
|
|
5248
|
+
f1Sum += f1;
|
|
5249
|
+
rows.push(lbs[c].padEnd(colW) + fmt(p) + fmt(r) + fmt(f1) + fmtI(support));
|
|
5250
|
+
}
|
|
5251
|
+
rows.push("\u2500".repeat(colW + 44));
|
|
5252
|
+
rows.push("macro avg".padEnd(colW) + fmt(pSum / K) + fmt(rSum / K) + fmt(f1Sum / K) + fmtI(yTrue.length));
|
|
5253
|
+
console.log("");
|
|
5254
|
+
console.log("Classification Report:");
|
|
5255
|
+
rows.forEach((r) => console.log(r));
|
|
5256
|
+
console.log("");
|
|
5257
|
+
}
|
|
5258
|
+
function _binaryPrecision(yTrue, yPred, pos) {
|
|
5259
|
+
let tp = 0, fp = 0;
|
|
5260
|
+
for (let i = 0; i < yTrue.length; i++) {
|
|
5261
|
+
if (yPred[i] === pos && yTrue[i] === pos) tp++;
|
|
5262
|
+
if (yPred[i] === pos && yTrue[i] !== pos) fp++;
|
|
5263
|
+
}
|
|
5264
|
+
return tp + fp > 0 ? tp / (tp + fp) : 0;
|
|
5265
|
+
}
|
|
5266
|
+
function _binaryRecall(yTrue, yPred, pos) {
|
|
5267
|
+
let tp = 0, fn = 0;
|
|
5268
|
+
for (let i = 0; i < yTrue.length; i++) {
|
|
5269
|
+
if (yTrue[i] === pos && yPred[i] === pos) tp++;
|
|
5270
|
+
if (yTrue[i] === pos && yPred[i] !== pos) fn++;
|
|
5271
|
+
}
|
|
5272
|
+
return tp + fn > 0 ? tp / (tp + fn) : 0;
|
|
5273
|
+
}
|
|
5274
|
+
|
|
5275
|
+
// src/EarlyStopping.ts
|
|
5276
|
+
var EarlyStopping = class {
|
|
5277
|
+
constructor(options) {
|
|
5278
|
+
this.patience = options?.patience ?? 10;
|
|
5279
|
+
this.minDelta = options?.minDelta ?? 1e-4;
|
|
5280
|
+
this.mode = options?.mode ?? "min";
|
|
5281
|
+
this.restoreBest = options?.restoreBest ?? false;
|
|
5282
|
+
this.counter = 0;
|
|
5283
|
+
this.stopped = false;
|
|
5284
|
+
this.bestEpoch = 0;
|
|
5285
|
+
this.bestWeights = null;
|
|
5286
|
+
this.bestValue = this.mode === "min" ? Infinity : -Infinity;
|
|
5287
|
+
}
|
|
5288
|
+
// ── update ───────────────────────────────────────────────────────────────
|
|
5289
|
+
//
|
|
5290
|
+
// Call once per epoch with the current metric value.
|
|
5291
|
+
// Returns true when training should stop (patience exhausted).
|
|
5292
|
+
//
|
|
5293
|
+
// Optionally pass `weights` (from net.getWeights()) to enable weight
|
|
5294
|
+
// snapshotting when restoreBest = true.
|
|
5295
|
+
//
|
|
5296
|
+
update(value, epoch, weights) {
|
|
5297
|
+
if (this.stopped) return true;
|
|
5298
|
+
const improved = this.mode === "min" ? value < this.bestValue - this.minDelta : value > this.bestValue + this.minDelta;
|
|
5299
|
+
if (improved) {
|
|
5300
|
+
this.bestValue = value;
|
|
5301
|
+
this.bestEpoch = epoch;
|
|
5302
|
+
this.counter = 0;
|
|
5303
|
+
if (this.restoreBest && weights !== void 0) {
|
|
5304
|
+
this.bestWeights = [...weights];
|
|
5305
|
+
}
|
|
5306
|
+
} else {
|
|
5307
|
+
this.counter++;
|
|
5308
|
+
if (this.counter >= this.patience) {
|
|
5309
|
+
this.stopped = true;
|
|
5310
|
+
return true;
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
return false;
|
|
5314
|
+
}
|
|
5315
|
+
// Resets all state — use to re-run training with a fresh early-stop monitor.
|
|
5316
|
+
reset() {
|
|
5317
|
+
this.counter = 0;
|
|
5318
|
+
this.stopped = false;
|
|
5319
|
+
this.bestEpoch = 0;
|
|
5320
|
+
this.bestWeights = null;
|
|
5321
|
+
this.bestValue = this.mode === "min" ? Infinity : -Infinity;
|
|
5322
|
+
}
|
|
5323
|
+
};
|
|
5324
|
+
|
|
5325
|
+
// src/LossPlotter.ts
|
|
5326
|
+
var LossPlotter = class {
|
|
5327
|
+
constructor(options) {
|
|
5328
|
+
this.width = options?.width ?? 60;
|
|
5329
|
+
this.height = options?.height ?? 15;
|
|
5330
|
+
this.title = options?.title ?? "Loss Curve";
|
|
5331
|
+
this.losses = [];
|
|
5332
|
+
this.epochs = [];
|
|
5333
|
+
}
|
|
5334
|
+
// Add a single (loss, epoch) pair.
|
|
5335
|
+
add(loss, epoch) {
|
|
5336
|
+
this.losses.push(loss);
|
|
5337
|
+
this.epochs.push(epoch ?? this.losses.length - 1);
|
|
5338
|
+
}
|
|
5339
|
+
// Add multiple loss values (epochs are auto-numbered from 0).
|
|
5340
|
+
addMultiple(losses) {
|
|
5341
|
+
for (const l of losses) this.add(l);
|
|
5342
|
+
}
|
|
5343
|
+
// Returns the ASCII plot as a multi-line string.
|
|
5344
|
+
render() {
|
|
5345
|
+
if (this.losses.length === 0) return `[${this.title}] \u2014 no data yet`;
|
|
5346
|
+
const losses = this.losses;
|
|
5347
|
+
const minL = Math.min(...losses);
|
|
5348
|
+
const maxL = Math.max(...losses);
|
|
5349
|
+
const range = maxL - minL || 1;
|
|
5350
|
+
const yLabelW = 8;
|
|
5351
|
+
const plotW = Math.max(4, this.width - yLabelW - 1);
|
|
5352
|
+
const plotH = Math.max(3, this.height);
|
|
5353
|
+
const grid = Array.from(
|
|
5354
|
+
{ length: plotH },
|
|
5355
|
+
() => new Array(plotW).fill(" ")
|
|
5356
|
+
);
|
|
5357
|
+
const n = losses.length;
|
|
5358
|
+
for (let idx = 0; idx < n; idx++) {
|
|
5359
|
+
const col = Math.round(idx / Math.max(1, n - 1) * (plotW - 1));
|
|
5360
|
+
const norm = (losses[idx] - minL) / range;
|
|
5361
|
+
const row = Math.round((1 - norm) * (plotH - 1));
|
|
5362
|
+
grid[row][col] = "*";
|
|
5363
|
+
}
|
|
5364
|
+
const lines = [];
|
|
5365
|
+
const titlePadded = ` ${this.title} `;
|
|
5366
|
+
const dashCount = Math.max(0, plotW + yLabelW - titlePadded.length);
|
|
5367
|
+
lines.push("\u250C" + titlePadded + "\u2500".repeat(dashCount) + "\u2510");
|
|
5368
|
+
for (let row = 0; row < plotH; row++) {
|
|
5369
|
+
let label;
|
|
5370
|
+
if (row === 0) {
|
|
5371
|
+
label = _fmtNum(maxL).padStart(yLabelW - 2) + " \u2524";
|
|
5372
|
+
} else if (row === plotH - 1) {
|
|
5373
|
+
label = _fmtNum(minL).padStart(yLabelW - 2) + " \u2524";
|
|
5374
|
+
} else if (row === Math.floor(plotH / 2)) {
|
|
5375
|
+
const mid = minL + range / 2;
|
|
5376
|
+
label = _fmtNum(mid).padStart(yLabelW - 2) + " \u2524";
|
|
5377
|
+
} else {
|
|
5378
|
+
label = " ".repeat(yLabelW - 1) + "\u2502";
|
|
5379
|
+
}
|
|
5380
|
+
lines.push(label + grid[row].join("") + "\u2502");
|
|
5381
|
+
}
|
|
5382
|
+
const xAxis = " ".repeat(yLabelW - 1) + "\u2514" + "\u2500".repeat(plotW) + "\u2518";
|
|
5383
|
+
lines.push(xAxis);
|
|
5384
|
+
const firstEpoch = String(this.epochs[0]);
|
|
5385
|
+
const lastEpoch = String(this.epochs[this.epochs.length - 1]);
|
|
5386
|
+
const xLabel = " ".repeat(yLabelW) + firstEpoch + " ".repeat(Math.max(1, plotW - firstEpoch.length - lastEpoch.length)) + lastEpoch + " epoch";
|
|
5387
|
+
lines.push(xLabel);
|
|
5388
|
+
lines.push(
|
|
5389
|
+
` min=${_fmtNum(minL)} max=${_fmtNum(maxL)} last=${_fmtNum(losses[losses.length - 1])} n=${n}`
|
|
5390
|
+
);
|
|
5391
|
+
return lines.join("\n");
|
|
5392
|
+
}
|
|
5393
|
+
// Prints the chart to stdout.
|
|
5394
|
+
print() {
|
|
5395
|
+
console.log(this.render());
|
|
5396
|
+
}
|
|
5397
|
+
// Clears all accumulated data.
|
|
5398
|
+
reset() {
|
|
5399
|
+
this.losses = [];
|
|
5400
|
+
this.epochs = [];
|
|
5401
|
+
}
|
|
5402
|
+
};
|
|
5403
|
+
function _fmtNum(n) {
|
|
5404
|
+
if (Math.abs(n) >= 1e4 || Math.abs(n) < 1e-3 && n !== 0) {
|
|
5405
|
+
return n.toExponential(1);
|
|
5406
|
+
}
|
|
5407
|
+
return n.toPrecision(4);
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
// src/DataAugmentation.ts
|
|
5411
|
+
var DataAugmentation = class _DataAugmentation {
|
|
5412
|
+
// ── Noise / Perturbation ─────────────────────────────────────────────────
|
|
5413
|
+
// Adds independent Gaussian noise to each feature: x'ᵢ = xᵢ + N(0, σ²).
|
|
5414
|
+
// sigma: standard deviation of the noise (default: 0.01).
|
|
5415
|
+
static addNoise(x, sigma = 0.01) {
|
|
5416
|
+
return x.map((v) => v + _sampleNormal() * sigma);
|
|
5417
|
+
}
|
|
5418
|
+
// Uniform jitter: x'ᵢ = xᵢ + U(-delta, delta).
|
|
5419
|
+
// delta: half-width of the uniform perturbation (default: 0.01).
|
|
5420
|
+
static jitter(x, delta = 0.01) {
|
|
5421
|
+
return x.map((v) => v + (Math.random() * 2 - 1) * delta);
|
|
5422
|
+
}
|
|
5423
|
+
// Reverses the order of the feature vector.
|
|
5424
|
+
// Useful when features are symmetric or represent a temporal window.
|
|
5425
|
+
static flipHorizontal(x) {
|
|
5426
|
+
return [...x].reverse();
|
|
5427
|
+
}
|
|
5428
|
+
// ── Normalisation ────────────────────────────────────────────────────────
|
|
5429
|
+
// Min-Max normalisation fitted on a dataset X.
|
|
5430
|
+
// Returns the normalised data and the per-feature min/max arrays.
|
|
5431
|
+
// Use normalizePoint() to apply the same transform to new samples.
|
|
5432
|
+
static normalize(X) {
|
|
5433
|
+
if (X.length === 0) return { normalized: [], min: [], max: [] };
|
|
5434
|
+
const d = X[0].length;
|
|
5435
|
+
const min = new Array(d).fill(Infinity);
|
|
5436
|
+
const max = new Array(d).fill(-Infinity);
|
|
5437
|
+
for (const row of X) {
|
|
5438
|
+
for (let j = 0; j < d; j++) {
|
|
5439
|
+
if (row[j] < min[j]) min[j] = row[j];
|
|
5440
|
+
if (row[j] > max[j]) max[j] = row[j];
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
const normalized = X.map((row) => _DataAugmentation.normalizePoint(row, min, max));
|
|
5444
|
+
return { normalized, min, max };
|
|
5445
|
+
}
|
|
5446
|
+
// Applies pre-computed min/max normalisation to a single sample.
|
|
5447
|
+
// Handles constant features (min === max) by mapping to 0.
|
|
5448
|
+
static normalizePoint(x, min, max) {
|
|
5449
|
+
return x.map((v, j) => {
|
|
5450
|
+
const range = max[j] - min[j];
|
|
5451
|
+
return range === 0 ? 0 : (v - min[j]) / range;
|
|
5452
|
+
});
|
|
5453
|
+
}
|
|
5454
|
+
// Z-Score standardisation fitted on a dataset X.
|
|
5455
|
+
// Returns the standardised data and per-feature mean/std arrays.
|
|
5456
|
+
// Use standardizePoint() to apply the same transform to new samples.
|
|
5457
|
+
static standardize(X) {
|
|
5458
|
+
if (X.length === 0) return { standardized: [], mean: [], std: [] };
|
|
5459
|
+
const n = X.length;
|
|
5460
|
+
const d = X[0].length;
|
|
5461
|
+
const mean = new Array(d).fill(0);
|
|
5462
|
+
for (const row of X) {
|
|
5463
|
+
for (let j = 0; j < d; j++) mean[j] += row[j];
|
|
5464
|
+
}
|
|
5465
|
+
for (let j = 0; j < d; j++) mean[j] /= n;
|
|
5466
|
+
const variance = new Array(d).fill(0);
|
|
5467
|
+
for (const row of X) {
|
|
5468
|
+
for (let j = 0; j < d; j++) variance[j] += (row[j] - mean[j]) ** 2;
|
|
5469
|
+
}
|
|
5470
|
+
const std = variance.map((v) => Math.sqrt(v / n));
|
|
5471
|
+
const standardized = X.map(
|
|
5472
|
+
(row) => _DataAugmentation.standardizePoint(row, mean, std)
|
|
5473
|
+
);
|
|
5474
|
+
return { standardized, mean, std };
|
|
5475
|
+
}
|
|
5476
|
+
// Applies pre-computed z-score standardisation to a single sample.
|
|
5477
|
+
// Constant features (std === 0) are mapped to 0.
|
|
5478
|
+
static standardizePoint(x, mean, std) {
|
|
5479
|
+
return x.map((v, j) => std[j] === 0 ? 0 : (v - mean[j]) / std[j]);
|
|
5480
|
+
}
|
|
5481
|
+
// ── Batch Augmentation ───────────────────────────────────────────────────
|
|
5482
|
+
// Generates `factor` noisy copies of each sample in X.
|
|
5483
|
+
// The original samples are included in the output (at factor = 1 the output
|
|
5484
|
+
// equals the input; at factor = 3 the dataset triples in size).
|
|
5485
|
+
// sigma: noise std dev (default: 0.01).
|
|
5486
|
+
static augmentBatch(X, y, factor = 2, sigma = 0.01) {
|
|
5487
|
+
const augX = [];
|
|
5488
|
+
const augY = [];
|
|
5489
|
+
for (let i = 0; i < X.length; i++) {
|
|
5490
|
+
augX.push([...X[i]]);
|
|
5491
|
+
augY.push(y[i]);
|
|
5492
|
+
for (let k = 1; k < factor; k++) {
|
|
5493
|
+
augX.push(_DataAugmentation.addNoise(X[i], sigma));
|
|
5494
|
+
augY.push(y[i]);
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
return { X: augX, y: augY };
|
|
5498
|
+
}
|
|
5499
|
+
// ── Shuffle ──────────────────────────────────────────────────────────────
|
|
5500
|
+
// Fisher-Yates shuffle — in-place permutation of indices.
|
|
5501
|
+
// Returns new arrays (does not mutate the inputs).
|
|
5502
|
+
static shuffle(X, y) {
|
|
5503
|
+
const indices = Array.from({ length: X.length }, (_, i) => i);
|
|
5504
|
+
for (let i = indices.length - 1; i > 0; i--) {
|
|
5505
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5506
|
+
[indices[i], indices[j]] = [indices[j], indices[i]];
|
|
5507
|
+
}
|
|
5508
|
+
return {
|
|
5509
|
+
X: indices.map((i) => X[i]),
|
|
5510
|
+
y: indices.map((i) => y[i])
|
|
5511
|
+
};
|
|
5512
|
+
}
|
|
5513
|
+
// ── Train / Val / Test Split ─────────────────────────────────────────────
|
|
5514
|
+
//
|
|
5515
|
+
// Splits the dataset into three non-overlapping partitions.
|
|
5516
|
+
// trainRatio + valRatio must be < 1.0; the remainder goes to test.
|
|
5517
|
+
// Shuffles automatically before splitting.
|
|
5518
|
+
//
|
|
5519
|
+
// Default split: 70% / 15% / 15%.
|
|
5520
|
+
//
|
|
5521
|
+
static split(X, y, trainRatio = 0.7, valRatio = 0.15) {
|
|
5522
|
+
if (trainRatio + valRatio >= 1) {
|
|
5523
|
+
throw new Error(
|
|
5524
|
+
`trainRatio (${trainRatio}) + valRatio (${valRatio}) must be < 1`
|
|
5525
|
+
);
|
|
5526
|
+
}
|
|
5527
|
+
const { X: sX, y: sY } = _DataAugmentation.shuffle(X, y);
|
|
5528
|
+
const n = sX.length;
|
|
5529
|
+
const trainEnd = Math.floor(n * trainRatio);
|
|
5530
|
+
const valEnd = trainEnd + Math.floor(n * valRatio);
|
|
5531
|
+
return {
|
|
5532
|
+
trainX: sX.slice(0, trainEnd),
|
|
5533
|
+
trainY: sY.slice(0, trainEnd),
|
|
5534
|
+
valX: sX.slice(trainEnd, valEnd),
|
|
5535
|
+
valY: sY.slice(trainEnd, valEnd),
|
|
5536
|
+
testX: sX.slice(valEnd),
|
|
5537
|
+
testY: sY.slice(valEnd)
|
|
5538
|
+
};
|
|
5539
|
+
}
|
|
5540
|
+
};
|
|
5541
|
+
function _sampleNormal() {
|
|
5542
|
+
const u1 = Math.random();
|
|
5543
|
+
const u2 = Math.random();
|
|
5544
|
+
return Math.sqrt(-2 * Math.log(u1 + 1e-15)) * Math.cos(2 * Math.PI * u2);
|
|
5545
|
+
}
|
|
2601
5546
|
export {
|
|
2602
5547
|
Adam,
|
|
2603
5548
|
AttentionHead,
|
|
5549
|
+
Autoencoder,
|
|
2604
5550
|
BatchNorm,
|
|
2605
5551
|
BiasVector,
|
|
5552
|
+
CausalConv1D,
|
|
2606
5553
|
ClipOptimizer,
|
|
2607
5554
|
ClippedOptimizerFactory,
|
|
2608
5555
|
Conv1D,
|
|
5556
|
+
Conv2D,
|
|
5557
|
+
DataAugmentation,
|
|
2609
5558
|
DataLoader,
|
|
5559
|
+
DecisionTree,
|
|
2610
5560
|
Dropout,
|
|
5561
|
+
EarlyStopping,
|
|
2611
5562
|
EmbeddingMatrix,
|
|
5563
|
+
Flatten,
|
|
5564
|
+
GAN,
|
|
2612
5565
|
GRULayer,
|
|
5566
|
+
GaussianNaiveBayes,
|
|
5567
|
+
HopfieldNetwork,
|
|
5568
|
+
KMeans,
|
|
2613
5569
|
LRScheduler,
|
|
2614
5570
|
LSTMLayer,
|
|
2615
5571
|
Layer,
|
|
2616
5572
|
LayerNorm,
|
|
5573
|
+
LinearRegression,
|
|
5574
|
+
LogisticRegression,
|
|
5575
|
+
LossPlotter,
|
|
5576
|
+
MaxPool2D,
|
|
2617
5577
|
ModelSaver,
|
|
2618
5578
|
Momentum,
|
|
2619
5579
|
MultiHeadAttention,
|
|
@@ -2624,23 +5584,46 @@ export {
|
|
|
2624
5584
|
NetworkTransformerRL,
|
|
2625
5585
|
Neuron,
|
|
2626
5586
|
NeuronN,
|
|
5587
|
+
PCA,
|
|
5588
|
+
Perceptron,
|
|
5589
|
+
RNN,
|
|
2627
5590
|
SGD,
|
|
5591
|
+
SOM,
|
|
5592
|
+
Seq2Seq,
|
|
5593
|
+
SoftmaxRegression,
|
|
5594
|
+
TCN,
|
|
2628
5595
|
Trainer,
|
|
2629
5596
|
TransformerBlock,
|
|
5597
|
+
VAE,
|
|
5598
|
+
Value,
|
|
5599
|
+
WeightInspector,
|
|
2630
5600
|
WeightMatrix,
|
|
5601
|
+
accuracy,
|
|
5602
|
+
auc,
|
|
5603
|
+
classificationReport,
|
|
5604
|
+
confusionMatrix,
|
|
2631
5605
|
crossEntropy,
|
|
2632
5606
|
crossEntropyDelta,
|
|
2633
5607
|
crossEntropyDeltaRaw,
|
|
2634
5608
|
defaultOptimizer,
|
|
2635
5609
|
elu,
|
|
5610
|
+
f1Score,
|
|
2636
5611
|
leakyRelu,
|
|
2637
5612
|
linear,
|
|
5613
|
+
mae,
|
|
2638
5614
|
makeElu,
|
|
2639
5615
|
makeLeakyRelu,
|
|
2640
5616
|
matMul,
|
|
2641
5617
|
mse,
|
|
2642
5618
|
mseDelta,
|
|
5619
|
+
perplexity,
|
|
5620
|
+
precision,
|
|
5621
|
+
printConfusionMatrix,
|
|
5622
|
+
r2Score,
|
|
5623
|
+
recall,
|
|
2643
5624
|
relu,
|
|
5625
|
+
rmse,
|
|
5626
|
+
rocCurve,
|
|
2644
5627
|
sigmoid2 as sigmoid,
|
|
2645
5628
|
softmax,
|
|
2646
5629
|
softmaxBackward,
|