UNPKG

clusterkw

Version:

A package for clustering keywords using OpenAI embeddings

134 lines (112 loc) 3.55 kB
import { Cluster } from '../types'; /** * K-means clustering algorithm * @param keywords Array of keywords to cluster * @param embeddings Matrix of embeddings for each keyword * @param options Configuration options * @returns Array of clusters */ export function kmeansClustering( keywords: string[], embeddings: number[][], options: { k?: number; maxIterations?: number; minClusterSize?: number; } ): Cluster[] { const n = keywords.length; // Default options const k = options.k || 5; // Default to 5 clusters const maxIterations = options.maxIterations || 100; const minClusterSize = options.minClusterSize || 2; if (n <= k) { // If we have fewer keywords than clusters, just return one cluster with all keywords return [{ items: [...keywords] }]; } // Initialize centroids randomly const centroids: number[][] = []; const usedIndices = new Set<number>(); while (centroids.length < k) { const randomIndex = Math.floor(Math.random() * n); if (!usedIndices.has(randomIndex)) { centroids.push([...embeddings[randomIndex]]); usedIndices.add(randomIndex); } } // Initialize cluster assignments let clusterAssignments: number[] = Array(n).fill(-1); let iterations = 0; let changed = true; // Main K-means loop while (changed && iterations < maxIterations) { changed = false; iterations++; // Assign each point to the nearest centroid for (let i = 0; i < n; i++) { const embedding = embeddings[i]; let minDistance = Infinity; let closestCentroid = -1; for (let j = 0; j < k; j++) { const distance = euclideanDistance(embedding, centroids[j]); if (distance < minDistance) { minDistance = distance; closestCentroid = j; } } if (clusterAssignments[i] !== closestCentroid) { clusterAssignments[i] = closestCentroid; changed = true; } } // Update centroids const newCentroids: number[][] = Array(k).fill(0).map(() => Array(embeddings[0].length).fill(0)); const counts: number[] = Array(k).fill(0); for (let i = 0; i < n; i++) { const clusterIndex = clusterAssignments[i]; counts[clusterIndex]++; for (let j = 0; j < embeddings[i].length; j++) { newCentroids[clusterIndex][j] += embeddings[i][j]; } } for (let i = 0; i < k; i++) { if (counts[i] > 0) { for (let j = 0; j < newCentroids[i].length; j++) { newCentroids[i][j] /= counts[i]; } centroids[i] = newCentroids[i]; } } } // Create clusters from assignments const clusterMap: Map<number, string[]> = new Map(); for (let i = 0; i < n; i++) { const clusterIndex = clusterAssignments[i]; if (!clusterMap.has(clusterIndex)) { clusterMap.set(clusterIndex, []); } clusterMap.get(clusterIndex)!.push(keywords[i]); } // Convert to Cluster objects, filtering out clusters that are too small const clusters: Cluster[] = []; clusterMap.forEach((items) => { if (items.length >= minClusterSize) { clusters.push({ items }); } }); return clusters; } /** * Calculate Euclidean distance between two vectors */ function euclideanDistance(a: number[], b: number[]): number { if (a.length !== b.length) { throw new Error('Vectors must have the same length'); } let sum = 0; for (let i = 0; i < a.length; i++) { const diff = a[i] - b[i]; sum += diff * diff; } return Math.sqrt(sum); }