Clustering and Topic Modeling with Embeddings
~40 min read
K-Means, HDBSCAN, and BERTopic for unsupervised discovery of topics and document clusters.
Clustering finds structure in unlabeled text corpora. Unlike classification, the label set is discovered from the data.
K-Means Clustering
from sklearn.cluster import KMeans import numpy as np # Reduce dimensions first (crucial for high-dim embeddings) import umap reducer = umap.UMAP(n_components=50, random_state=42) reduced = reducer.fit_transform(embeddings) # (N, 50) # Choose K using silhouette score from sklearn.metrics import silhouette_score scores = {} for k in range(5, 30, 5): km = KMeans(n_clusters=k, random_state=42).fit(reduced) scores[k] = silhouette_score(reduced, km.labels_) best_k = max(scores, key=scores.get)
- Strengths: fast, predictable (exactly K clusters), interpretable centroids
- Weaknesses: requires K, assumes spherical clusters, sensitive to outliers
HDBSCAN
import hdbscan clusterer = hdbscan.HDBSCAN(min_cluster_size=10, metric='euclidean') labels = clusterer.fit_predict(reduced_5d) # -1 = noise n_topics = len(set(labels)) - (1 if -1 in labels else 0) noise_pct = (labels == -1).mean() * 100
- Strengths: no K needed, handles irregular shapes, labels outliers as noise
- Weaknesses: slower, noise fraction can be high, sensitive to min_cluster_size
BERTopic Combines embedding → UMAP → HDBSCAN → c-TF-IDF:
- UMAP to 5 dims (preserves local structure, dramatically speeds HDBSCAN)
- HDBSCAN on 5-dim representation
- c-TF-IDF: TF-IDF where 'document' = all text in a cluster Produces top-N discriminative words per cluster as topic representation
Optional LLM topic naming: Pass top-10 c-TF-IDF words to an LLM: 'These words describe a topic: refund, payment, charge, billing, invoice, dispute. Name this topic in 3-5 words.' → 'Payment and Billing Disputes'
Hyperparameter guide:
- UMAP n_components: 5 for clustering, 2 for visualization
- UMAP n_neighbors: 10-50 (higher = more global structure)
- HDBSCAN min_cluster_size: ~0.5-1% of corpus size (start here)
- If noise > 40%: lower min_cluster_size
- If too many micro-clusters: raise min_cluster_size
💬 Deep Dive with AI
Key points
- •Always reduce embedding dimensionality with UMAP before clustering — raw 768-1536 dim embeddings produce poor clusters due to the curse of dimensionality
- •HDBSCAN's noise label (-1) is a feature, not a bug — real-world corpora have documents that don't cleanly belong to any topic
- •BERTopic's c-TF-IDF names topics by finding words that appear frequently in a cluster but rarely in other clusters — like TF-IDF where the 'document' is the whole cluster