clustering-tfjs
Version:
High-performance TypeScript clustering algorithms (K-Means, Spectral, Agglomerative) with TensorFlow.js acceleration and scikit-learn compatibility
233 lines (232 loc) • 8.88 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.qr_eigen_decomposition = qr_eigen_decomposition;
exports.tridiagonal_qr_eigen = tridiagonal_qr_eigen;
const tf = __importStar(require("../tf-adapter"));
/**
* QR Algorithm-based eigendecomposition for symmetric matrices.
*
* The QR algorithm is more numerically stable than Jacobi iteration
* and converges faster for most matrices. This implementation uses
* TensorFlow.js's built-in QR decomposition.
*
* Algorithm:
* 1. Start with A₀ = A
* 2. For each iteration:
* - Compute QR decomposition: Aᵢ = QᵢRᵢ
* - Form Aᵢ₊₁ = RᵢQᵢ
* 3. As i → ∞, Aᵢ converges to a diagonal matrix of eigenvalues
* 4. The product Q₀Q₁...Qᵢ gives the eigenvectors
*/
function qr_eigen_decomposition(matrix, { maxIterations = 1000, tolerance = 1e-10, } = {}) {
return tf.tidy(() => {
const n = matrix.shape[0];
// Initialize
let A = matrix.clone();
let V = tf.eye(n); // Accumulate eigenvector transformations
// Helper to compute off-diagonal norm
const offDiagonalNorm = (M) => {
const data = M.arraySync();
let sum = 0;
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (i !== j) {
sum += data[i][j] * data[i][j];
}
}
}
return Math.sqrt(sum);
};
let iter = 0;
let offDiag = Infinity;
// Apply Wilkinson shift for better convergence on small eigenvalues
const wilkinsonShift = (M) => {
if (n < 2)
return 0;
const data = M.arraySync();
const a = data[n - 2][n - 2];
const b = data[n - 2][n - 1];
const c = data[n - 1][n - 1];
// Compute shift as eigenvalue of 2x2 bottom-right submatrix closest to c
const delta = (a - c) / 2;
const sign = delta >= 0 ? 1 : -1;
return (c -
(sign * b * b) / (Math.abs(delta) + Math.sqrt(delta * delta + b * b)));
};
while (iter < maxIterations && offDiag > tolerance) {
// Apply shift for better convergence
const shift = wilkinsonShift(A);
const I = tf.eye(n);
const A_shifted = shift !== 0 ? A.sub(I.mul(shift)) : A;
// QR decomposition
const [Q, R] = tf.linalg.qr(A_shifted);
// Update A = RQ + shift*I
A.dispose();
A = R.matMul(Q);
if (shift !== 0) {
const temp = A;
A = A.add(I.mul(shift));
temp.dispose();
}
// Accumulate eigenvector transformations
const V_new = V.matMul(Q);
V.dispose();
V = V_new;
// Check convergence
offDiag = offDiagonalNorm(A);
// Cleanup
Q.dispose();
R.dispose();
I.dispose();
if (shift !== 0)
A_shifted.dispose();
iter++;
}
if (iter === maxIterations) {
console.warn(`QR algorithm did not converge after ${maxIterations} iterations. Final off-diagonal norm: ${offDiag}`);
}
// Extract eigenvalues from diagonal
const A_data = A.arraySync();
const eigenvalues = A_data.map((row, i) => row[i]);
// Extract eigenvectors
const V_data = V.arraySync();
// Sort by eigenvalue (ascending)
const indexed = eigenvalues.map((val, idx) => ({ val, idx }));
indexed.sort((a, b) => a.val - b.val);
const sortedValues = indexed.map((p) => p.val);
const sortedVectors = Array(n)
.fill(0)
.map(() => Array(n).fill(0));
// Rearrange eigenvector columns
for (let newIdx = 0; newIdx < n; newIdx++) {
const oldIdx = indexed[newIdx].idx;
for (let row = 0; row < n; row++) {
sortedVectors[row][newIdx] = V_data[row][oldIdx];
}
}
return { eigenvalues: sortedValues, eigenvectors: sortedVectors };
});
}
/**
* Specialized QR algorithm for tridiagonal matrices.
* Since normalized Laplacians are often nearly tridiagonal after
* similarity transformations, this can be more efficient.
*/
function tridiagonal_qr_eigen(diagonal, offDiagonal, computeVectors = true) {
const n = diagonal.length;
// Clone arrays to avoid mutation
const d = [...diagonal];
const e = [...offDiagonal, 0]; // Pad with 0 for convenience
let V;
if (computeVectors) {
V = Array(n)
.fill(0)
.map((_, i) => Array(n)
.fill(0)
.map((_, j) => (i === j ? 1 : 0)));
}
// QL algorithm (variant of QR for tridiagonal matrices)
for (let i = 0; i < n - 1; i++) {
let iter = 0;
let m;
do {
// Find small off-diagonal element
for (m = i; m < n - 1; m++) {
const dd = Math.abs(d[m]) + Math.abs(d[m + 1]);
if (Math.abs(e[m]) <= Number.EPSILON * dd)
break;
}
if (m !== i) {
if (iter++ === 30) {
console.warn('Tridiagonal QR: Too many iterations');
break;
}
// Compute shift
const g = (d[i + 1] - d[i]) / (2 * e[i]);
const r = Math.sqrt(g * g + 1);
const shift = d[m] - e[i] / (g + (g >= 0 ? r : -r));
// QR step
let s = 1, c = 1;
let p = 0;
for (let j = m - 1; j >= i; j--) {
const f = s * e[j];
const b = c * e[j];
const r = Math.sqrt(f * f + (d[j] - shift) * (d[j] - shift));
e[j + 1] = r;
if (r === 0) {
d[j + 1] -= p;
e[m] = 0;
break;
}
s = f / r;
c = (d[j] - shift) / r;
const g = d[j + 1] - p;
const r2 = (d[j] - g) * s + 2 * c * b;
p = s * r2;
d[j + 1] = g + p;
// Update eigenvectors
if (computeVectors && V) {
for (let k = 0; k < n; k++) {
const f = V[k][j + 1];
V[k][j + 1] = s * V[k][j] + c * f;
V[k][j] = c * V[k][j] - s * f;
}
}
}
d[i] -= p;
e[i] = g;
e[m] = 0;
}
} while (m !== i);
}
// Sort eigenvalues and eigenvectors
const indexed = d.map((val, idx) => ({ val, idx }));
indexed.sort((a, b) => a.val - b.val);
const eigenvalues = indexed.map((p) => p.val);
let eigenvectors;
if (computeVectors && V) {
eigenvectors = Array(n)
.fill(0)
.map(() => Array(n).fill(0));
for (let newIdx = 0; newIdx < n; newIdx++) {
const oldIdx = indexed[newIdx].idx;
for (let row = 0; row < n; row++) {
eigenvectors[row][newIdx] = V[row][oldIdx];
}
}
}
return { eigenvalues, eigenvectors };
}