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
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.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));
}