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