UNPKG

clustering-tfjs

Version:

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

183 lines (182 loc) 7.25 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.calinskiHarabasz = calinskiHarabasz; exports.calinskiHarabaszEfficient = calinskiHarabaszEfficient; const tf = __importStar(require("../tf-adapter")); const tensor_utils_1 = require("../utils/tensor-utils"); /** * Computes the Calinski-Harabasz score (also known as Variance Ratio Criterion). * * The score is defined as the ratio of the between-cluster dispersion to the * within-cluster dispersion. Higher values indicate better-defined clusters. * * Formula: CH = (BSS / (k - 1)) / (WSS / (n - k)) * where: * - BSS = between-cluster sum of squares * - WSS = within-cluster sum of squares * - k = number of clusters * - n = number of samples * * @param X - Data matrix of shape [n_samples, n_features] * @param labels - Cluster labels for each sample * @returns The Calinski-Harabasz score (higher is better) * @throws Error if k <= 1 or k >= n_samples */ function calinskiHarabasz(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 and count const uniqueLabels = Array.from(new Set(labelArray)); const k = uniqueLabels.length; // Validate inputs if (k <= 1) { throw new Error('Calinski-Harabasz score requires at least 2 clusters'); } if (k >= n) { throw new Error('Number of clusters must be less than number of samples'); } // Compute global centroid const globalCentroid = data.mean(0); // Initialize accumulators let withinClusterSS = 0; let betweenClusterSS = 0; // Process each cluster for (const label of uniqueLabels) { // Get indices for this cluster const clusterIndices = []; for (let i = 0; i < labelArray.length; i++) { if (labelArray[i] === label) { clusterIndices.push(i); } } const clusterSize = clusterIndices.length; // Extract cluster points const clusterData = tf.gather(data, clusterIndices); // Compute cluster centroid const clusterCentroid = clusterData.mean(0); // Within-cluster sum of squares const diff = clusterData.sub(clusterCentroid.reshape([1, -1])); const squaredDiff = diff.square(); withinClusterSS += squaredDiff.sum().dataSync()[0]; // Between-cluster sum of squares const centroidDiff = clusterCentroid.sub(globalCentroid); const centroidDiffSquared = centroidDiff.square().sum(); betweenClusterSS += clusterSize * centroidDiffSquared.dataSync()[0]; // Clean up tensors clusterData.dispose(); clusterCentroid.dispose(); diff.dispose(); squaredDiff.dispose(); centroidDiff.dispose(); centroidDiffSquared.dispose(); } // Compute Calinski-Harabasz score const score = betweenClusterSS / (k - 1) / (withinClusterSS / (n - k)); // Clean up globalCentroid.dispose(); return score; }); } /** * Computes the Calinski-Harabasz score in a memory-efficient manner for large datasets. * This version processes clusters sequentially to minimize memory usage. * * @param X - Data matrix of shape [n_samples, n_features] * @param labels - Cluster labels for each sample * @returns The Calinski-Harabasz score */ function calinskiHarabaszEfficient(X, labels) { // 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('Calinski-Harabasz score requires at least 2 clusters'); } if (k >= n) { if (!(0, tensor_utils_1.isTensor)(X)) { data.dispose(); } throw new Error('Number of clusters must be less than number of samples'); } // Compute global centroid const globalCentroid = tf.tidy(() => data.mean(0)); let withinClusterSS = 0; let betweenClusterSS = 0; // Process each cluster for (const label of uniqueLabels) { tf.tidy(() => { // Get cluster indices const clusterIndices = labelArray .map((l, i) => (l === label ? i : -1)) .filter((i) => i >= 0); const clusterSize = clusterIndices.length; // Extract cluster data const clusterData = tf.gather(data, clusterIndices); const clusterCentroid = clusterData.mean(0); // Within-cluster SS const diff = clusterData.sub(clusterCentroid.reshape([1, -1])); withinClusterSS += diff.square().sum().dataSync()[0]; // Between-cluster SS const centroidDiff = clusterCentroid.sub(globalCentroid); betweenClusterSS += clusterSize * centroidDiff.square().sum().dataSync()[0]; }); } // Clean up globalCentroid.dispose(); if (!(0, tensor_utils_1.isTensor)(X)) { data.dispose(); } // Compute score return betweenClusterSS / (k - 1) / (withinClusterSS / (n - k)); }