001 Notes
ClusterLens Algorithms: PCA, KMeans, FAISS, HDBSCAN, And Graphs
1. The Real Pipeline
images -> clustering algorithm -> groups
flowchart TD
A[Image files] --> B[Decode and preprocess]
B --> C[Embedding model]
C --> D[Embedding cache]
D --> E[Matrix: N images x D dimensions]
E --> F[Normalize vectors]
F --> G{Semantic mode?}
G -- yes --> H[Optional PCA projection]
G -- no --> I[Full normalized vectors]
H --> J[Prepared matrix]
I --> J
J --> K{Backend choice}
K --> L[KMeans]
K --> M[MiniBatchKMeans]
K --> N[FAISS KMeans]
K --> O[HDBSCAN]
K --> P[Similarity graph]
L --> Q[Raw labels]
M --> Q
N --> Q
O --> Q
P --> Q
Q --> R[Outlier policy]
R --> S[Tiny cluster policy]
S --> T[Centroid/member reranking]
T --> U[Explanations and review UI]
Backend output:
row 0 -> cluster 3
row 1 -> cluster 3
row 2 -> cluster 8
row 3 -> cluster -1
Product output:
Cluster 3 looks like: beach, coast, blue sky
Representative images: row 1, row 0, row 12
Cohesion: high
Nearest competing cluster: cluster 4
Outliers: 18 images
2. The Matrix Contract
image_0001.jpg -> [ 0.12, -0.44, 0.08, ...]
image_0002.jpg -> [ 0.10, -0.41, 0.11, ...]
image_0003.jpg -> [-0.72, 0.03, 0.55, ...]
N = number of images
D = embedding dimension
matrix shape = N x D
20,000 images x 512 dimensions = 10,240,000 float values
10,240,000 values * 4 bytes = about 40 MB
| Row | Meaning |
|---|---|
0 | first image path in the run |
1 | second image path in the run |
127 | exactly one stable image path |
1. scan files
2. freeze ordered image list
3. build embeddings in that order
4. cluster matrix rows
5. map labels back to frozen image list
6. apply UI filters after labels are stable
3. Normalization And Cosine Geometry
cosine(a, b) = dot(a, b) / (length(a) * length(b))
normalized_v = v / length(v)
cosine(a, b) = dot(a, b)
Before normalization:
magnitude can dominate
long vectors may look more important
dot product is not the same as cosine
After normalization:
direction dominates
dot product matches cosine
vector scores are easier to explain
distance_squared(a, b) = 2 - 2 * cosine(a, b)
| Cosine similarity | Squared Euclidean distance |
|---|---|
1.00 | 0.00 |
0.90 | 0.20 |
0.70 | 0.60 |
0.00 | 2.00 |
-1.00 | 4.00 |
Normalize before cosine-style clustering.
Record that normalization happened.
Do not mix normalized and unnormalized scores in the same explanation.
4. Similarity Modes
Semantic clustering:
"Put conceptually similar images together."
Visual clustering:
"Put visually similar images together."
Duplicate review:
"Find images that are identical or near-identical."
| Mode | Better signal | Example |
|---|---|---|
| semantic | CLIP/OpenCLIP/SigLIP/DINO-style embeddings | dog, puppy, pet photos |
| visual | CNN/vision embeddings, color/layout features | screenshots, UI captures, similar scenes |
| duplicate | perceptual hashes, ORB, exact file hashes | resized copy, edited crop, repeated export |
5. PCA From First Principles
Which directions in this dataset explain the most variation?
.
. .
. .
. .
. .
input matrix: N x D
centered matrix: subtract column means
components: directions of high variance
projected matrix: N x K
512 dimensions -> PCA 50 dimensions
fewer dimensions -> faster distance calculations
less redundant signal -> sometimes cleaner clusters
smaller matrix -> lower memory pressure
too few samples -> PCA learns noise
too small K -> useful distinctions collapse
wrong mode -> projection removes details the user wanted
if semantic_mode:
if sample_count >= minimum_samples and input_dimension > target_dimension:
fit PCA
project vectors
normalize again
else:
use full normalized vectors
else:
use full normalized vectors
semantic projection | input 512 -> PCA 50 | normalized
6. KMeans Internals
Can these points be divided into k groups so each point is close to its group's center?
minimize sum(distance(point, assigned_center)^2)
choose k initial centers
repeat:
assign each point to the nearest center
update each center to the mean of assigned points
until convergence or iteration limit
| Point | Coordinates | Distance to C1 (1,1) | Distance to C2 (6,6) | Label |
|---|---|---|---|---|
| A | (1.0, 1.1) | 0.1 | 7.0 | C1 |
| B | (1.2, 0.9) | 0.2 | 7.0 | C1 |
| C | (6.1, 5.9) | 7.0 | 0.1 | C2 |
| D | (5.8, 6.2) | 7.1 | 0.3 | C2 |
C1 = mean(A, B)
C2 = mean(C, D)
"Make 12 review groups."
"Split this folder into 30 buckets."
"Give me manageable chunks to inspect."
This cluster is centered around these representative images.
Images are ordered by closeness to the center.
| Failure | Why it happens |
|---|---|
needs k | the algorithm cannot infer the right group count |
| forces assignment | every point belongs somewhere, even bad outliers |
| prefers round/compact groups | elongated or irregular groups split poorly |
| sensitive to initialization | different seeds can produce different clusters |
| not naturally hierarchical | no built-in “group within group” structure |
7. Choosing k
ktoo few clusters:
each group is huge
review is slow
mixed concepts hide inside clusters
too many clusters:
groups become tiny
UI becomes noisy
review becomes fragmented
k = sqrt(N)
k = N / target_images_per_cluster
k = user-selected review count
N = 10,000 images
target_images_per_cluster = 100
k = 100 clusters
8. MiniBatchKMeans
iteration 1: all 20,000 vectors
iteration 2: all 20,000 vectors
iteration 3: all 20,000 vectors
iteration 1: 512 vectors
iteration 2: 512 vectors
iteration 3: 512 vectors
new_center = old_center + learning_rate * (batch_mean - old_center)
flowchart LR
A[Prepared matrix] --> B[Sample batch]
B --> C[Assign batch to centers]
C --> D[Update centers]
D --> B
D --> E[Final labels]
Good wording:
Fast approximate grouping for large folders.
Bad wording:
More advanced clustering.
9. FAISS
prepared matrix
-> train FAISS centroids
-> assign vectors to nearest centroid
-> return labels
-> product post-processing
| Design | Consequence |
|---|---|
| FAISS required | harder install, harder packaging, more native dependency risk |
| FAISS optional | baseline remains portable |
| CPU path kept | correctness can be tested without acceleration |
| backend visible | users can understand why runs differ |
baseline:
NumPy + scikit-learn
optional acceleration:
FAISS CPU
separate GPU packaging:
Torch CUDA runtime for model inference when shipped
10. HDBSCAN From First Principles
Can I split all points into k compact groups?
Where are the stable dense regions, and which points are noise?
many vacation photos
some screenshots
some receipts
some one-off memes
some accidental duplicates
some unrelated downloaded images
cluster 0: 120 images
cluster 1: 88 images
cluster 2: 31 images
cluster -1: 247 outliers/noise images
dense:
o o o o
o o o o
sparse:
. . .
small core distance -> dense area
large core distance -> sparse area
mutual_reachability(a, b) =
max(core_distance(a), core_distance(b), distance(a, b))
| Parameter | Product meaning |
|---|---|
min_cluster_size | smallest group worth showing |
min_samples | how conservative noise detection should be |
cluster_selection_epsilon | how much nearby density can merge |
allow_single_cluster | whether one giant group is acceptable |
HDBSCAN is not better KMeans.
It answers a different question.
11. Similarity Graph Clustering
image = node
similarity above threshold = edge
connected region = cluster
A -- B -- C D -- E
F
cluster 0: A, B, C
cluster 1: D, E
outlier/singleton: F
threshold graph:
connect A and B if similarity >= 0.82
kNN graph:
connect A to its top k nearest neighbors
mutual kNN graph:
connect A and B only if each is in the other's neighbor list
These images are grouped because each image is linked to at least one similar neighbor.
N x N comparisons
12. Outlier Policy
| Backend | Natural outlier behavior |
|---|---|
| KMeans | no natural outliers; every point is assigned |
| MiniBatchKMeans | same as KMeans |
| HDBSCAN | returns noise label -1 |
| graph clustering | isolated nodes or tiny components |
for each point:
score = similarity(point, assigned_centroid)
if score < threshold:
move to outliers
Outliers: images not confidently assigned to a cluster.
Bad images.
13. Tiny Cluster Policy
cluster 0: 310 images
cluster 1: 244 images
cluster 2: 2 images
cluster 3: 1 image
cluster 4: 197 images
| Policy | When useful |
|---|---|
| keep tiny clusters | duplicate review, anomaly review |
| merge into nearest larger cluster | broad organization |
| move to outliers | simplify review |
| collapse in UI | keep data without visual clutter |
tiny_cluster_centroid -> nearest_large_cluster_centroid -> merge
14. Member Reranking
centroid = mean(member_vectors)
centroid = normalize(centroid)
for each member:
score = dot(member_vector, centroid)
sort by score descending
Before rerank:
random scan order
After rerank:
most central images first
edge cases later
15. Quality Scores
a = average distance to points in the same cluster
b = average distance to points in the nearest other cluster
silhouette = (b - a) / max(a, b)
semantic overlap is real
visual overlap is real
folders may contain mixed concepts
duplicate and semantic similarity can disagree
cluster size
mean cohesion
median cohesion
minimum cohesion
nearest competing cluster
separation margin
outlier count
backend used
PCA/full vector space used
16. CUDA And GPU Wording
embedding inference
large matrix multiplication
nearest-neighbor search
distance calculations
batched image preprocessing
GPU builds can accelerate embedding inference through Torch CUDA.
FAISS CPU can accelerate vector operations without requiring CUDA.
Custom CUDA clustering kernels are a future optimization unless shipped and measured.
grid:
one block handles a tile of images and centroids
threads:
each thread computes partial dot products over dimensions
shared memory:
cache centroid tiles
reduction:
sum partial dot products
output:
nearest centroid for each image
data transfer can erase the gain
PyTorch/FAISS already provide optimized kernels
packaging CUDA increases release complexity
debugging numerical differences is expensive
CPU path still needs to exist
1. Make the CPU implementation correct.
2. Cache embeddings and avoid repeated work.
3. Use vectorized NumPy/scikit paths.
4. Add FAISS where vector search dominates.
5. Use Torch CUDA for model inference in GPU builds.
6. Write custom CUDA only after profiling proves it is necessary.
17. Backend Selection Guide
| Need | Better backend | Why |
|---|---|---|
| fixed number of review groups | KMeans | simple objective and predictable count |
| fast preview on large folders | MiniBatchKMeans | approximate center updates |
| large vector workload | FAISS KMeans | optimized native vector routines |
| unknown number of groups | HDBSCAN | finds density and noise |
| many outliers | HDBSCAN or graph | does not force every point into a centroid |
| near-duplicate review | graph plus perceptual hashes | edges map well to candidate groups |
| explainable threshold grouping | graph | ”similar enough” edges are intuitive |
Does the user specify k?
yes -> KMeans family
no -> HDBSCAN or graph
Is the folder large and preview-oriented?
yes -> MiniBatchKMeans or FAISS
Are many outliers expected?
yes -> HDBSCAN or explicit outlier policy
Is this duplicate review?
yes -> perceptual hash prefilter, graph, ORB/embedding rerank
Does the runtime have optional acceleration?
yes -> choose faster backend, keep CPU fallback
18. Implementation Blueprint
scan image paths
freeze ordered paths
for each path:
load cached embedding if valid
otherwise compute embedding
store embedding with model signature and file fingerprint
matrix = stack embeddings in frozen order
matrix = float32(matrix)
if similarity mode uses cosine:
matrix = l2_normalize(matrix)
if semantic mode and PCA is safe:
matrix = PCA(matrix)
matrix = l2_normalize(matrix)
labels = run_backend(matrix, options)
clusters = group row indexes by label
clusters = apply_outlier_policy(matrix, clusters)
clusters = merge_or_keep_tiny_clusters(matrix, clusters)
clusters = rerank_members_by_centroid(matrix, clusters)
explanations = compute_cluster_explanations(matrix, clusters)
return clusters, explanations, metrics
backend
input dimension
prepared dimension
PCA components if used
normalization
outlier count
cluster quality score
runtime
model signature
19. What Was Done And Why
20. What Was Used, Removed, Or Deferred
L2 normalization
semantic PCA projection when safe
KMeans
MiniBatchKMeans
optional FAISS CPU backend
optional HDBSCAN backend
similarity graph clustering
outlier policy
tiny-cluster policy
centroid/member reranking
cluster explanations
perceptual hash index for duplicate-oriented lanes
treating one clustering algorithm as universal
forcing HDBSCAN into fixed-k workflows
hiding PCA and backend choices
using perceptual hashes as semantic labels
presenting approximate preview results as final truth
automatic duplicate deletion from clustering alone
custom CUDA clustering kernels
automatic parameter tuning that overrides user intent
guaranteed perfect clusters
GPU FAISS claims unless the shipped build actually includes them
destructive cleanup based only on embedding similarity
Comments