UNPKG

clustering-tfjs

Version:

High-performance TypeScript clustering algorithms (K-Means, Spectral, Agglomerative) with TensorFlow.js acceleration and scikit-learn compatibility

236 lines (235 loc) 8.88 kB
"use strict"; 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.silhouetteScore = silhouetteScore; exports.silhouetteScoreSubset = silhouetteScoreSubset; const tf = __importStar(require("../tf-adapter")); const tensor_utils_1 = require("../utils/tensor-utils"); /** * Computes the Silhouette score. * * The silhouette coefficient for a sample is (b - a) / max(a, b) where: * - a is the mean distance between a sample and all other points in the same cluster * - b is the mean distance between a sample and all points in the nearest cluster * * The score ranges from -1 to +1: * - +1: Sample is far from neighboring clusters (well clustered) * - 0: Sample is on or very close to the decision boundary * - -1: Sample might have been assigned to the wrong cluster * * @param X - Data matrix of shape [n_samples, n_features] * @param labels - Cluster labels for each sample * @returns The mean silhouette score across all samples * @throws Error if k <= 1 */ function silhouetteScore(X, labels) { return tf.tidy(() => { // Convert inputs to tensors const data = (0, tensor_utils_1.isTensor)(X) ? X : tf.tensor2d(X); const labelArray = (0, tensor_utils_1.isTensor)(labels) ? Array.from(labels.dataSync()).map((l) => Math.round(l)) : labels; const n = data.shape[0]; // Get unique labels const uniqueLabels = Array.from(new Set(labelArray)); const k = uniqueLabels.length; // Validate inputs if (k <= 1) { throw new Error('Silhouette score requires at least 2 clusters'); } // Compute pairwise distances // D[i,j] = ||x_i - x_j||^2 const xNorm = data.square().sum(1).reshape([n, 1]); const xNormT = xNorm.reshape([1, n]); const cross = tf.matMul(data, data.transpose()); const distances = tf.sqrt(tf.maximum(tf.scalar(0), xNorm.add(xNormT).sub(cross.mul(2)))); // Compute silhouette for each sample const silhouetteValues = []; const distancesArray = distances.arraySync(); for (let i = 0; i < n; i++) { const sampleLabel = labelArray[i]; // Find indices of samples in same cluster and other clusters const sameClusterIndices = []; const otherClusterIndices = new Map(); for (let j = 0; j < n; j++) { if (i === j) continue; if (labelArray[j] === sampleLabel) { sameClusterIndices.push(j); } else { const label = labelArray[j]; if (!otherClusterIndices.has(label)) { otherClusterIndices.set(label, []); } otherClusterIndices.get(label).push(j); } } // Compute a(i): mean intra-cluster distance let a = 0; if (sameClusterIndices.length > 0) { for (const j of sameClusterIndices) { a += distancesArray[i][j]; } a /= sameClusterIndices.length; } // Compute b(i): mean distance to nearest cluster let b = Infinity; for (const [_label, indices] of otherClusterIndices) { let meanDist = 0; for (const j of indices) { meanDist += distancesArray[i][j]; } meanDist /= indices.length; if (meanDist < b) { b = meanDist; } } // Compute silhouette coefficient if (sameClusterIndices.length === 0) { // Single point in cluster silhouetteValues.push(0); } else { const s = (b - a) / Math.max(a, b); silhouetteValues.push(s); } } // Clean up xNorm.dispose(); xNormT.dispose(); cross.dispose(); distances.dispose(); // Return mean silhouette score return silhouetteValues.reduce((sum, val) => sum + val, 0) / n; }); } /** * Computes the Silhouette score for specific samples (subset). * Useful for large datasets where computing all pairwise distances is prohibitive. * * @param X - Data matrix of shape [n_samples, n_features] * @param labels - Cluster labels for each sample * @param sampleIndices - Indices of samples to compute silhouette for * @returns The mean silhouette score for the specified samples */ function silhouetteScoreSubset(X, labels, sampleIndices) { // Convert inputs const data = (0, tensor_utils_1.isTensor)(X) ? X : tf.tensor2d(X); const labelArray = (0, tensor_utils_1.isTensor)(labels) ? Array.from(labels.dataSync()).map((l) => Math.round(l)) : labels; const n = data.shape[0]; // Get unique labels const uniqueLabels = Array.from(new Set(labelArray)); const k = uniqueLabels.length; // Validate if (k <= 1) { if (!(0, tensor_utils_1.isTensor)(X)) { data.dispose(); } throw new Error('Silhouette score requires at least 2 clusters'); } const silhouetteValues = []; // Process each sample in the subset for (const i of sampleIndices) { const sampleLabel = labelArray[i]; // Get the sample point const samplePoint = tf.tidy(() => data.gather([i])); // Compute distances to all other points const distances = tf.tidy(() => { const diff = data.sub(samplePoint); return tf.sqrt(diff.square().sum(1)); }); const distArray = distances.dataSync(); // Compute a(i) and b(i) let a = 0; let aCount = 0; const clusterDistances = new Map(); for (let j = 0; j < n; j++) { if (i === j) continue; const dist = distArray[j]; const label = labelArray[j]; if (label === sampleLabel) { // Same cluster a += dist; aCount++; } else { // Other cluster if (!clusterDistances.has(label)) { clusterDistances.set(label, { sum: 0, count: 0 }); } const cluster = clusterDistances.get(label); cluster.sum += dist; cluster.count++; } } // Mean intra-cluster distance if (aCount > 0) { a /= aCount; } // Find nearest cluster let b = Infinity; for (const [_label, { sum, count }] of clusterDistances) { const meanDist = sum / count; if (meanDist < b) { b = meanDist; } } // Compute silhouette coefficient if (aCount === 0) { // Single point in cluster silhouetteValues.push(0); } else { const s = (b - a) / Math.max(a, b); silhouetteValues.push(s); } // Clean up samplePoint.dispose(); distances.dispose(); } // Clean up data tensor if we created it if (!(0, tensor_utils_1.isTensor)(X)) { data.dispose(); } // Return mean silhouette score return (silhouetteValues.reduce((sum, val) => sum + val, 0) / silhouetteValues.length); }