UNPKG

clustering-tfjs

Version:

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

221 lines (220 loc) 8.59 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.daviesBouldin = daviesBouldin; exports.daviesBouldinEfficient = daviesBouldinEfficient; const tf = __importStar(require("../tf-adapter")); const tensor_utils_1 = require("../utils/tensor-utils"); /** * Computes the Davies-Bouldin score. * * The Davies-Bouldin index is defined as the average similarity measure * of each cluster with its most similar cluster. Lower values indicate * better clustering (clusters are more separated). * * Formula: DB = (1/k) * sum(max_{i≠j}(R_{ij})) * where R_{ij} = (s_i + s_j) / d_{ij} * - s_i = average distance from points in cluster i to its centroid * - d_{ij} = distance between centroids of clusters i and j * * @param X - Data matrix of shape [n_samples, n_features] * @param labels - Cluster labels for each sample * @returns The Davies-Bouldin score (lower is better) * @throws Error if k <= 1 */ function daviesBouldin(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; // Get unique labels const uniqueLabels = Array.from(new Set(labelArray)); const k = uniqueLabels.length; // Validate inputs if (k <= 1) { throw new Error('Davies-Bouldin score requires at least 2 clusters'); } // Compute centroids and intra-cluster dispersions const centroids = []; const dispersions = []; 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; if (clusterSize === 0) continue; // Extract cluster points const clusterData = tf.gather(data, clusterIndices); // Compute centroid const centroid = clusterData.mean(0); centroids.push(centroid); // Compute intra-cluster dispersion (average distance to centroid) if (clusterSize > 1) { const diff = clusterData.sub(centroid.reshape([1, -1])); const distances = tf.sqrt(diff.square().sum(1)); const avgDistance = distances.mean().dataSync()[0]; dispersions.push(avgDistance); distances.dispose(); diff.dispose(); } else { // Single point cluster has zero dispersion dispersions.push(0); } // Clean up clusterData.dispose(); } // Compute inter-cluster distances and similarity ratios const maxSimilarities = []; for (let i = 0; i < k; i++) { let maxSimilarity = 0; for (let j = 0; j < k; j++) { if (i === j) continue; // Compute distance between centroids const diff = centroids[i].sub(centroids[j]); const distance = tf.sqrt(diff.square().sum()).dataSync()[0]; diff.dispose(); // Avoid division by zero if (distance === 0) { // If centroids are identical, set similarity to infinity maxSimilarity = Infinity; break; } // Compute similarity ratio R_ij = (s_i + s_j) / d_ij const similarity = (dispersions[i] + dispersions[j]) / distance; if (similarity > maxSimilarity) { maxSimilarity = similarity; } } maxSimilarities.push(maxSimilarity); } // Clean up centroids for (const centroid of centroids) { centroid.dispose(); } // Compute Davies-Bouldin index as average of maximum similarities const dbScore = maxSimilarities.reduce((sum, val) => sum + val, 0) / k; return dbScore; }); } /** * Computes the Davies-Bouldin score with optimized memory usage. * This version minimizes tensor allocations and disposals. * * @param X - Data matrix of shape [n_samples, n_features] * @param labels - Cluster labels for each sample * @returns The Davies-Bouldin score (lower is better) */ function daviesBouldinEfficient(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; // 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('Davies-Bouldin score requires at least 2 clusters'); } // Store centroids and dispersions const centroidArrays = []; const dispersions = []; // Compute centroids and dispersions for (const label of uniqueLabels) { const clusterIndices = labelArray .map((l, i) => (l === label ? i : -1)) .filter((i) => i >= 0); if (clusterIndices.length === 0) continue; tf.tidy(() => { const clusterData = tf.gather(data, clusterIndices); const centroid = clusterData.mean(0); centroidArrays.push(Array.from(centroid.dataSync())); if (clusterIndices.length > 1) { const diff = clusterData.sub(centroid.reshape([1, -1])); const distances = tf.sqrt(diff.square().sum(1)); dispersions.push(distances.mean().dataSync()[0]); } else { dispersions.push(0); } }); } // Clean up data tensor if we created it if (!(0, tensor_utils_1.isTensor)(X)) { data.dispose(); } // Compute Davies-Bouldin index let dbSum = 0; for (let i = 0; i < k; i++) { let maxSimilarity = 0; for (let j = 0; j < k; j++) { if (i === j) continue; // Compute Euclidean distance between centroids let distance = 0; for (let d = 0; d < centroidArrays[i].length; d++) { const diff = centroidArrays[i][d] - centroidArrays[j][d]; distance += diff * diff; } distance = Math.sqrt(distance); if (distance === 0) { maxSimilarity = Infinity; break; } const similarity = (dispersions[i] + dispersions[j]) / distance; if (similarity > maxSimilarity) { maxSimilarity = similarity; } } dbSum += maxSimilarity; } return dbSum / k; }