ClusterLens Algorithms: Clustering Image Embeddings From First Principles
On this page110
A beginner-first, experimental tutorial for PCA, KMeans, MiniBatchKMeans, FAISS, HDBSCAN, similarity graphs, outliers, reranking, and cluster quality.
Article details
- Status
- Building Publicly
- Subcategory
- ClusterLens
- Last reviewed
- 2 Sept 2026
- Prerequisites
- None; the article starts with points, distance, and grouping
Group similar images.
1. What is clustering trying to do?
red car blue car
red apple green apple
beach ocean
Subject grouping puts both cars together, both apples together, and both water scenes together.
Clustering compared with familiar tasks
| Task | What information is supplied? | Question |
|---|---|---|
| Classification | Examples with known names | “Which known class is this new object?” |
| Search | A query plus an existing collection | “Which existing objects are nearest to this query?” |
| Duplicate detection | A strict identity or perceptual rule | “Is this effectively the same file or image?” |
| Clustering | A collection and a relationship rule | “How can the whole collection be divided or connected?” |
image 0 → cluster 2
image 1 → cluster 2
image 2 → cluster 0
image 3 → noise (-1)
2. From an image to a point
image file
↓ decode and preprocess
pixel tensor
↓ learned image encoder
embedding vector
↓ interpret the vector as coordinates
point in an embedding space
car-01.jpg → [0.92, 0.18]
car-02.jpg → [0.88, 0.23]
beach-01.jpg → [0.12, 0.94]
invoice-01.jpg → [-0.71, 0.08]
Why not name every coordinate?
Image embedding contract
fθ is the learned encoder, i selects one image, and D is the embedding dimension.
Stack the points into a matrix
The matrix contract
Row i must continue to refer to the same image path throughout preparation, clustering, post-processing, and rendering.
Raw embedding memory
This counts only the dense matrix. Paths, thumbnails, model memory, indexes, labels, and UI objects require additional memory.
The row-order bug that mathematics will not catch
[.92, .18]C0[.88, .23]C0[.12, .94]C1[.08, .97]C1Correct: paths, embedding rows, and labels share one frozen index.
scan paths
→ freeze their order
→ build/load embeddings in that order
→ cluster rows
→ map labels back through the frozen order
→ apply display filters
3. Distance, direction, and normalization
Length and direction are different properties
Changing only length leaves cosine unchanged but changes the raw dot product. Normalization removes that magnitude variable.
Calculate vector length
L2 length
Squaring prevents positive and negative coordinates from cancelling. The square root returns to the coordinate scale.
Complete normalization dry run
The direction is preserved, while the new length is exactly one.
Cosine similarity
Cosine similarity
For already-normalized vectors, both lengths equal one, so cosine becomes the dot product.
Unit-sphere equivalence
Maximizing dot product and minimizing Euclidean distance produce the same pair ordering when both inputs are unit length.
A high cosine is not “90% the same”
Surprise 1: two opposites can be equally similar to one query
Symmetric candidates
Equal similarity to the same query does not imply that the two results are similar to each other.
At 60°, A and B both score 0.500 against the query, yet they score −0.500 against each other. A broad query can retrieve two very different branches.
Surprise 2: cosine ignores magnitude completely
x = [1, 0]
y = [100, 0]
Surprise 3: a model can preserve the “wrong” shared feature
query: small bird against a pale sky
candidate: airplane against a pale sky
| Rank | Human label | Cosine |
|---|
CLIP's closest gallery vector has a different CIFAR-10 class. Inspect the images for shared colour, texture, silhouette, background, or framing; the evidence records the score, but it cannot prove which visual cue caused it.
Common kinds of cosine false friends
| What appears similar to the model | Why it may feel wrong to a person | Example |
|---|---|---|
| background or scene | the user cared about the foreground object | bird in sky ↔ airplane in sky |
| colour palette | the user cared about category | red car ↔ red toy or red sign |
| outline or pose | the user cared about identity | standing dog ↔ standing deer |
| text layout | the user cared about document content | invoice ↔ unrelated form |
| broad concept | the user expected a duplicate | two entirely different beach photographs |
| generic composition | the image sits near many ordinary images | centred object on plain background |
similar subject?
similar colour?
similar composition?
same photograph?
same event?
same person?
4. The actual ClusterLens pipeline
paths
→ decoded/model-specific pixels
→ normalized embeddings
→ frozen N × D matrix
→ optional semantic PCA
→ L2-normalized prepared matrix
→ backend labels
→ outlier policy
→ tiny-cluster policy
→ centroid reranking
→ explanations and review UI
What “semantic” and “cosine” modes mean here
| Mode | Preparation |
|---|---|
semantic | normalize input → fit conditional PCA → project → normalize again |
cosine | normalize full vector without PCA |
semantic projection | input 768 -> PCA 50 | normalized
cosine full vector | input 768 -> full 768 | normalized
5. PCA: choosing a useful shadow
An everyday example: height and arm span repeat information
person height arm span
A 160 cm 162 cm
B 170 cm 171 cm
C 180 cm 179 cm
original coordinates: height, arm span
PCA coordinates: overall body-size direction,
arm-span-versus-height direction
Step 1: centre the data
Dataset mean
The mean gives the centre of the observed cloud, coordinate by coordinate.
Centred point
PCA studies variation around the collection’s centre rather than variation around the coordinate origin.
A complete two-dimensional PCA calculation
A = (2, 1)
B = (4, 3)
C = (6, 5)
1 · Mean
The centre is calculated separately for each original coordinate.
A − μ = (−2, −2)
B − μ = ( 0, 0)
C − μ = ( 2, 2)
2 · Covariance
Both diagonal entries are 4, so both coordinates vary. The off-diagonal value is also 4, so they move together perfectly in this toy dataset.
3 · Principal directions
The eigenvalue is the variance captured along that direction. Here every point lies exactly on the first direction, so the perpendicular direction captures zero variance.
4 · One-dimensional result
We replaced two coordinates with one without reconstruction loss for these three points. Real embeddings do not normally collapse this perfectly.
Step 2: project onto a direction
One-dimensional projection
The dot product asks how far the centred point travels along direction v.
The first principal component chooses the direction with maximum projected variance. In this teaching cloud, that direction also produces the shortest discarded orange segments.
From one direction to several components
512 original coordinates
→ choose component 1
→ choose perpendicular component 2
→ …
→ retain first K components
→ K projected coordinates
Explained variance
This measures variance, not semantic truth. A low-variance direction can still contain a distinction the user cares about.
The variance trap: PCA can discard the feature you care about
PC1 is the correct PCA choice because it preserves the widest numeric variation. It is the wrong one-dimensional choice if the task is to separate blue from red.
When ClusterLens applies PCA
Safe component count
ClusterLens also skips semantic PCA for fewer than four samples and whenever reduction would not reduce the input dimension.
Actual dimension experiment
The dimensions are ordered 256, 128, 64, 50, 32, and 16. A smaller vector can improve this particular clustering while still discarding exact neighbours. That is why “variance retained” and “task result retained” are separate measurements.
PCA variants and nearby tools are not interchangeable
| Method | Main idea | Useful when | Important warning |
|---|---|---|---|
| ordinary PCA | fit linear orthogonal directions on the full matrix | a moderate matrix fits in memory | current ClusterLens semantic path |
| randomized PCA solver | approximate leading directions efficiently | dimensions or sample counts are large | still linear PCA; approximation settings matter |
| IncrementalPCA | update the basis from batches | the full matrix cannot fit comfortably | batch ordering and batch size can affect approximation |
| Kernel PCA | perform PCA after a nonlinear kernel mapping | curved structure matters on smaller datasets | storage and computation scale poorly |
| whitening | divide retained coordinates by their standard deviations | a downstream method needs equal component variance | amplifies low-variance/noisy directions |
| t-SNE or UMAP | build a low-dimensional visualization | inspecting a 2D or 3D map | visual spacing is not a drop-in production metric space |
6. KMeans from the first assignment
The objective
KMeans inertia
xi is one point, cᵢ is its assigned cluster, and μcᵢ is that cluster’s centroid. KMeans tries to reduce this total squared error.
Perform the iterations
| point | distance C1 | distance C2 | owner |
|---|
Assign every point to its nearest centroid, then replace each centroid with the arithmetic mean of its assigned points.
Why the update is the mean
Centroid update
The arithmetic mean minimizes the sum of squared Euclidean distances to the members of that cluster.
Centroid dry run
Calculate each coordinate independently: `(1+1.2+.8)/3 = 1`; `(1+.8+1.3)/3 ≈ 1.033`.
What KMeans assumes
KMeans draws nearest-centroid territories
Two-centroid boundary
Expanding both sides produces a straight boundary. With many centroids, KMeans divides space into straight-edged convex territories.
Compact, similarly sized groups match the geometry that two centroids can summarize well.
Why a single outlier can move a centroid
[0, 1, 2]
mean = 1
Outlier pull
The centroid moved from 1 to 25.75 even though three of four observations remain between 0 and 2.
“KMeans” names a family of related choices
| Method | Representative | Assignment | What changes |
|---|---|---|---|
| ordinary KMeans | arithmetic mean | nearest Euclidean centroid | the baseline used for smaller ClusterLens runs |
| cosine-prepared KMeans | arithmetic mean after unit-normalizing inputs | nearest Euclidean centroid in prepared space | current ClusterLens cosine-kmeans meaning |
| spherical KMeans | centroid projected back to unit length | maximum cosine | direction is enforced during every update |
| MiniBatchKMeans | running approximate mean | nearest current centroid | cheaper updates from small batches |
| K-medoids | an actual member point | nearest medoid | more resistant to outliers, usually more expensive |
| Gaussian mixture model | mean plus covariance | soft probability of component membership | can model ellipses and uncertainty |
| balanced/constrained KMeans | mean with size rules | nearest feasible centroid | enforces workflow limits but changes the optimization problem |
7. Initialization and local answers
KMeans++ intuition
KMeans++ sampling pressure
D(x) is the distance from point x to its closest already-selected centroid. Far regions receive more probability, not a guarantee.
These runs intentionally use random initialization and only one start, making seed sensitivity visible. Production KMeans uses multiple starts to reduce this risk.
8. Choosing k
kThree different questions
The slider positions represent k = 2, 4, 6, 8, 10, 12, 16, 20, and 32. CIFAR-10 happens to have ten labels, so ARI can tell us how closely a run recovers that external partition. A normal photo folder does not provide this answer key.
Review-workload heuristic
For 10,000 images and about 100 desired thumbnails per review group, start near `k=100`, then inspect quality and group sizes.
9. MiniBatchKMeans: learn from spoonfuls
Incremental centroid update
η controls how much this observation can move the running centroid. Implementations derive it from accumulated counts rather than keeping one arbitrary constant.
Actual runtime comparison
The bars are relative within the selected vector count. Library startup, thread pools, convergence rules, and initialization all affect short timings, so quality and repeated runs belong beside speed.
10. FAISS from the first distance calculation
| Term | Beginner meaning |
|---|---|
| dense vector | a fixed-width row of numbers, such as a 512-number image embedding |
| similarity search | find the stored rows nearest to a query row |
| clustering | repeatedly use nearness to organize many rows into groups |
What FAISS is not
FAISS = vector database ✗
FAISS = approximate search ✗
FAISS = GPU-only ✗
product behavior
file paths, deletion, permissions, metadata, persistence
FAISS library
vector indexes, distance kernels, clustering, quantization
mathematical operation
L2 distance, inner product, nearest neighbour, KMeans objective
Start with one exact FAISS index
index = faiss.IndexFlatL2(dimension)
index.add(database_vectors)
distances, indices = index.search(query_vectors, k)
query q = [ 0.95, 0.05]
row 0 Ferrari = [ 1.00, 0.00]
row 1 Porsche = [ 0.80, 0.20]
row 2 beach = [ 0.00, 1.00]
row 3 invoice = [-0.80, 0.10]
Exact flat distance
FAISS reports squared L2 values for this index. Taking a square root would preserve the ranking, so the cheaper squared value is enough for nearest-neighbour selection.
| row ID | meaning | squared L2 | state |
|---|
No database row has been measured yet. With k = 1, the index will return one distance and one row ID for this query.
Ferrari: (0.95−1.00)² + (0.05−0.00)² = 0.005
Porsche: (0.95−0.80)² + (0.05−0.20)² = 0.045
beach: (0.95−0.00)² + (0.05−1.00)² = 1.805
invoice: (0.95+0.80)² + (0.05−0.10)² = 3.065
distances = [[0.005]]
indices = [[0]]
Batch shapes: why the result is a matrix
Search output contract
Every output row belongs to the query at the same row position. Every column advances from nearest to farther returned neighbours.
3 query vectors × top 2 neighbours
indices =
[[ 0, 1], ← query 0
[17, 4], ← query 1
[ 8, 11]] ← query 2
Exact and approximate indexes belong to the same library
| FAISS structure | Search meaning | Training? | Main tradeoff |
|---|---|---|---|
IndexFlatL2 | exact minimum squared L2 | no | scans every stored vector |
IndexFlatIP | exact maximum inner product | no | cosine requires unit-normalized vectors first |
| HNSW index | graph-guided approximate neighbours | no dataset training | extra graph memory for faster navigation |
IndexIVFFlat | search selected coarse buckets, then exact vectors inside them | yes | nprobe trades recall for work |
IndexIVFPQ | selected buckets plus compressed vector codes | yes | much lower memory with quantization error |
faiss.Kmeans | train centroids using repeated assignment and mean updates | yes | produces a partition, not a general-purpose database |
How FAISS KMeans uses search
assign every point to its nearest centroid
↓
replace each centroid with its assigned mean
↓
repeat
database inside the temporary index = K centroids
queries sent to that index = N image vectors
requested neighbours = 1
returned row ID = cluster label
points: A=(1,1) B=(1,2) C=(8,8) D=(9,8)
centroids: μ₀=(1,1) μ₁=(9,8)
nearest centroid search returns:
A → 0 B → 0 C → 1 D → 1
mean update:
μ₀ ← mean(A,B) = (1,1.5)
μ₁ ← mean(C,D) = (8.5,8)
KMeans assignment work
This is an intuition for scaling, not a stopwatch prediction. Vectorized kernels, cache behavior, convergence, threading, and hardware determine elapsed time.
FAISS can tile this work into cache- and SIMD-friendly blocks. It changes how efficiently the comparisons run; it does not make N × K relationships disappear.
Where CPU, SIMD, threads, and GPU enter
CPU-resident vectors
↓ host-to-device copy
GPU distance / selection work
↓ result copy
CPU-resident IDs and distances
Conceptual units only: for a tiny CPU-resident job, launch and transfer overhead can make the GPU path slower even when its distance kernel is faster.
What ClusterLens actually calls
d = embeddings.shape[1]
kmeans = faiss.Kmeans(d, num_clusters, niter=20, verbose=False)
kmeans.train(embeddings)
_, assignments = kmeans.index.search(embeddings, 1)
labels = assignments.reshape(-1)
Actual runtime: read the surprising result carefully
500 rows 0.2032 s
2,000 rows 0.2401 s
10,000 rows 0.0042 s
Same objective family, different actual partition
scikit-learn KMeans
- ARI
- —
- purity
- —
- size range
- —
- time
- —
FAISS KMeans
- ARI
- —
- purity
- —
- size range
- —
- time
- —
The same input, k, and broad objective family do not force identical memberships. Initialization and implementation defaults are part of the experiment.
Fallbacks must not borrow the requested backend’s name
The requested and actual backend agree because the optional dependency is available.
requested_backend = faiss
backend = cosine-kmeans
implementation = sklearn-kmeans
fallback_reason = FAISS is unavailable; used normalized scikit-learn KMeans instead.
From a FAISS index to a vector database
The mathematical core remains
The database adds a reliable definition of “eligible records,” durable identity, updates, filtering, and operational behavior around that ranking.
FAISS
a high-performance search engine on a workbench
vector database
the engine plus shelves, durable labels, receiving desk,
inventory rules, filters, concurrent access, and operations
One record, six vocabularies
ID = photo-184
vector = [0.14, -0.07, 0.81, ...]
metadata = {owner: "alice", folder: "cars", year: 2025}
| System | Container | One stored item | Non-vector data |
|---|---|---|---|
| FAISS | index | vector at an ID or row position | usually managed by application code |
| PostgreSQL + pgvector | table | SQL row | ordinary typed columns and JSON |
| Qdrant | collection | point | JSON payload |
| Milvus | collection | entity | scalar fields |
| Weaviate | collection | object | typed properties |
| Pinecone | index and namespace | record | flat metadata fields |
FAISS gives an application direct control over vector algorithms with little surrounding machinery. That is excellent for local tools, experiments, offline jobs, and custom infrastructure.
Why metadata filtering is not a decorative feature
| Global rank | Record | Cosine | Eligible? |
|---|---|---|---|
| 1 | another user’s Ferrari | 0.99 | no |
| 2 | Alice’s Porsche | 0.95 | yes |
| 3 | another user’s race car | 0.94 | no |
| 4 | Alice’s Mustang | 0.91 | yes |
| 5 | Alice’s red coupe | 0.89 | yes |
| 6 | Alice’s beach photo | 0.80 | no: wrong folder |
Post-filtering a fixed global top 3 leaves only Alice’s Porsche. The missing slots are not automatically refilled.
PostgreSQL with pgvector: add vectors to familiar SQL rows
CREATE TABLE photos (
id bigint PRIMARY KEY,
owner text,
folder text,
path text,
embedding vector(512)
);
SELECT id, path
FROM photos
WHERE owner = 'alice' AND folder = 'cars'
ORDER BY embedding <=> :query_vector
LIMIT 3;
FAISS: application calls an in-process vector index
pgvector: application sends SQL to PostgreSQL;
PostgreSQL plans filters, table access, and vector ordering
Qdrant: points, payloads, and filterable graph search
collection: photos
point:
id = photo-184
vector = [0.14, -0.07, ...]
payload = {owner: alice, folder: cars, year: 2025}
Milvus: a distributed vector-search data plane
incoming entities
↓
growing segments
↓ sealed and indexed
object storage keeps durable segment/index files
↓
query nodes load relevant sealed segments and search them
Weaviate: objects plus vector and inverted indexes
object store → retrieve the stored object
inverted indexes → property filters and BM25 keyword search
vector index → semantic nearest neighbours
Pinecone: managed records, namespaces, and search APIs
index: photo-search
namespace: alice
record:
id = photo-184
values = [0.14, -0.07, ...]
metadata = {folder: cars, year: 2025}
Same query, different ownership boundaries
| Question | FAISS | pgvector | Qdrant | Milvus | Weaviate | Pinecone |
|---|---|---|---|---|---|---|
| Who owns the service? | your application | PostgreSQL operator | self-host or vendor cloud | self-host or managed provider | self-host or vendor cloud | vendor-managed |
| Primary data model | vector index | relational rows | points + payload | entities + scalar fields | objects + properties | records + flat metadata |
| Familiar strength | algorithm control | SQL, joins, transactions | filter-aware vector retrieval | distributed vector scale | hybrid object search | low-operations managed retrieval |
| Typical vector choices | broad FAISS index family | exact, HNSW, IVFFlat | HNSW plus quantization/options | several configurable ANN families | HNSW, flat, dynamic, HFresh | service-managed search configuration |
| Metadata responsibility | mostly application | SQL engine and indexes | payload and payload indexes | scalar fields and indexes | properties and inverted indexes | record metadata filters |
| Important cost | build the surrounding system | share resources with OLTP/query planner | operate another service if self-hosted | greater distributed-system complexity | schema/index resource choices | network, service, and vendor dependency |
A complete choice dry run
local SQLite metadata
+ NumPy or FAISS vector index
+ explicit cache invalidation
= a reasonable small system
500 business customers
millions of document chunks
tenant isolation
continuous writes
metadata filters
hybrid keyword + semantic retrieval
availability requirements
| Situation | First system worth evaluating | Why |
|---|---|---|
| local experiment, offline batch, custom algorithm | FAISS | direct control and minimal surrounding service |
| vectors belong beside relational business rows | pgvector | one SQL and transactional model |
| dedicated retrieval with rich JSON filtering | Qdrant | points, payload indexes, and filter-aware HNSW |
| large distributed vector data plane | Milvus | segmented, disaggregated architecture |
| object search mixing BM25 and vectors | Weaviate | object, inverted, and vector indexes together |
| fully managed vector service is the priority | Pinecone | vendor operates the retrieval infrastructure |
What a vector database still does not decide
11. Density: how crowded is this point’s neighbourhood?
distances from A: 0.2, 0.4, 0.7, 3.8, 5.1
↑
core₃(A) = 0.7
With min_samples = 3 in this teaching convention, A needs radius 0.7 to reach its third other neighbour.
Density needs both a radius and a count
The two circles have the same radius. Only the tightly packed centre currently has enough local support to be a core point.
12. HDBSCAN from core distance to stable islands
Intuition: islands as the water level changes
At the loosest level, bridge points connect both hills. A flat density cut would call this one region.
Stage 1: core distances
core(A) = 0.7
core(B) = 0.5
core(C) = 2.4
Stage 2: mutual-reachability distance
Mutual-reachability distance
A connection cannot look denser than either endpoint’s local neighbourhood. This enlarges edges involving sparse points.
Mutual-reachability dry run
The raw pair is only 0.4 apart, but A needs a radius of 0.7 to establish its local density. The adjusted edge therefore costs 0.7.
Stage 3: a minimum spanning tree
one connected tree
→ two dense branches plus sparse points
→ smaller dense branches
→ individual points
Stage 4: condense and select stable branches
Persistence intuition
λ is inverse distance. A large cluster that survives across a long density interval accumulates more stability than a brief, fragile branch.
Put the four HDBSCAN stages together
First estimate how far every point must reach to establish a neighbourhood. The middle point needs a much larger core radius than points inside either compact group.
Tune the real model output
All 25 measured settings. Darker green means higher ARI for this labelled experiment; every cell also shows its noise rate. Select a cell to load its exact result above.
This grid changes only the two displayed parameters. Notice that a high silhouette can coexist with low coverage: rejecting difficult points makes the retained subset easier to separate.
Parameter meanings
| Parameter | Beginner interpretation | Typical consequence when increased |
|---|---|---|
min_cluster_size | smallest persistent group worth returning | fewer tiny groups; more points may become noise |
min_samples | evidence required for local density | more conservative membership; often more noise |
cluster_selection_epsilon | permit nearby branches to merge below a distance | fewer splits in very close dense regions |
allow_single_cluster | allow one global group | permits a one-group explanation |
Related density methods and HDBSCAN selection styles
| Method or style | What must be chosen? | What it returns | Main tradeoff |
|---|---|---|---|
| DBSCAN | one neighbourhood radius eps plus min_samples | flat dense components and noise | one global radius struggles when densities differ |
| OPTICS | neighbourhood and reachability settings | an ordering/reachability structure | exposes several density scales but needs extraction/interpretation |
| HDBSCAN EOM | minimum cluster size, density conservatism | persistent non-overlapping branches and noise | favours broader stable clusters |
| HDBSCAN leaf | the same hierarchy with leaf selection | finer terminal branches | can create more small, homogeneous groups |
| HDBSCAN with epsilon merge | hierarchy plus merge distance | nearby selected branches combined | useful when the hierarchy over-splits close regions |
Three outcomes that can all be valid
13. Similarity graphs: groups made from relationships
Construct the graph
ClusterLens graph edge
Candidate selection limits work; threshold τ decides whether a candidate relationship is strong enough; symmetrization makes connectivity independent of row order.
The bridge problem
A is similar to B
B is similar to C
therefore A, B, and C are connected
beach sunset — coast road — blue car — sports car — race track
At this threshold, three edges survive. The failed C–D edge breaks the chain into two components even though both sides contain strong local relationships.
Lower thresholds add edges and can create one giant component. Higher thresholds remove edges and can turn almost everything into noise. The useful region depends strongly on the embedding model.
Threshold graph, union kNN, and mutual kNN
| Construction | Edge requirement | Risk |
|---|---|---|
| all-pairs threshold | any pair above τ | quadratic comparisons and giant components |
| union kNN | either endpoint selects the other, then passes τ | preserves more links and more bridges |
| mutual kNN | both endpoints select each other, then pass τ | conservative; can fragment sparse regions |
14. Outliers are a policy, not bad images
| Backend | Natural behavior |
|---|---|
| KMeans | every point is assigned |
| MiniBatchKMeans | every point is assigned |
| HDBSCAN | can return noise label −1 |
| graph | isolated nodes and undersized components become −1 |
Assign an outlier
Because prepared points and product centroids are normalized, the dot product ranks centroid directions by cosine similarity.
car centroid 0.61
beach centroid 0.24
invoice centroid 0.18
The `keep` policy preserves the backend's uncertainty. The uncertain images remain visible in a separate review lane instead of being forced into the least-bad cluster.
Nearest does not automatically mean near enough
Assignment margin
A small margin means the winner barely beat an alternative. A product can require both an absolute score and a margin before hiding uncertainty.
best score ≥ 0.70
and
best score − second-best score ≥ 0.08
15. Tiny clusters: signal or clutter?
singleton point
→ compare with normalized centroids of large clusters
→ choose greatest dot product
→ append point to that cluster
→ recompute centroid for later explanation/reranking
S · μ₀ = 0.72
S · μ₁ = 0.69
Merge treats the receipt pair as fragmentation and moves both members into the nearest larger document cluster. This changes membership and should be recorded.
16. Ranking members inside a cluster
Representative-member score
Central members appear first. Peripheral members remain in the group but move later in the review order.
| Member | Similarity to centroid | Gallery position |
|---|---|---|
| beach-02 | 0.94 | 1 |
| beach-01 | 0.89 | 2 |
| coast-road | 0.72 | 3 |
| blue-pool | 0.51 | 4 |
“Representative” can mean several different things
Centroid-first ordering begins with the most average member. It quickly communicates the dominant pattern, but adjacent results may be visually redundant.
17. How do we know whether a grouping is good?
Calculate one silhouette score by hand
A = [0, 0] C = [4, 0]
B = [0, 2] D = [4, 2]
cluster blue = {A, B}
cluster red = {C, D}
Step 1 · within-cluster distance
Do not include A's zero distance to itself. That would make every point look artificially well packed.
Step 2 · nearest competing cluster
With several competing clusters, calculate one mean per cluster and keep the smallest. That is the easiest rival group for A to join.
Point silhouette
The denominator scales the result into the interval from −1 to 1.
Complete substitution
A is closer on average to its own cluster than to the nearest rival, so its score is positive.
Choose point A. Its own cluster contains one other point; the other cluster contains two points.
| Score near | Intuition | What it may indicate |
|---|---|---|
+1 | much closer to its own group | a compact, separated assignment |
0 | near a border | overlapping groups or a useful bridge |
−1 | closer to another group | a likely misassignment |
Why silhouette cannot be the only judge
| Metric | Needs ground-truth names? | What it asks | Important limitation |
|---|---|---|---|
| Silhouette | no | are points nearer their own cluster than another? | prefers geometric separation, not human intent |
| Cohesion | no | how similar are members to their centroid? | one tight giant cluster can hide missing distinctions |
| Separation | no | how far apart are cluster centroids? | ignores the spread inside each cluster |
| ARI | yes | do pairs of points agree with known class pairs? | the known classes may not match the product task |
| NMI | yes | how much class information and cluster information agree? | can respond differently to cluster count |
| Purity | yes | what fraction follows each cluster’s majority class? | singleton clusters can achieve perfect purity |
| Coverage | no | what fraction was not labelled noise? | says nothing about correctness of covered points |
| Stability | no | does the grouping survive seeds, order, or perturbations? | a consistently wrong result can still be stable |
One dataset, four ways to make one metric look good
animals: cat, dog, horse, deer
vehicles: car, truck, plane, ship
The desired output groups all four animals and all four vehicles. Coverage and purity are both complete, and same-versus-different pair decisions match the labels.
ARI ignores cluster-number names
run 1: [0, 0, 1, 1]
run 2: [7, 7, 3, 3]
truth {cat, dog} {car, truck}
prediction {cat, dog, car} {truck}
| Pair | Truth says | Prediction says | Agreement? |
|---|---|---|---|
| cat–dog | together | together | yes |
| cat–car | separate | together | no |
| cat–truck | separate | separate | yes |
| dog–car | separate | together | no |
| dog–truck | separate | separate | yes |
| car–truck | together | separate | no |
Purity can be gamed
18. Put the backends on the same actual embeddings
These numbers compare recorded outcomes, not universal algorithm rankings. KMeans is told to make ten groups; HDBSCAN and graph discover a count from their density or connectivity settings.
Read one result completely
10 clusters
0% noise
ARI 0.715
purity 85.8%
silhouette 0.100
Why HDBSCAN’s higher silhouette can coexist with lower ARI
Why graph purity can look absurdly good
Coverage
A selective method may be precise about what it keeps. Coverage reveals how much it refused to decide.
Inspect actual representatives, not only scalar scores
A representative gallery answers a question that ARI cannot: when a cluster is mixed, what visual pattern may have persuaded the model? Inspect full clusters before drawing a final conclusion; four representatives are only a summary.
This is not a speed tournament
19. A complete 12-image run, from files to displayed groups
3 beaches
3 bicycles
3 cars
3 fictional invoices
Start with twelve paths in a frozen order. Every later vector row, backend label, membership change, and thumbnail must still refer to these same identities.
Stage 1: freeze the file ledger
row 0 → beach-01.webp
row 1 → beach-02.webp
row 2 → beach-03.webp
row 3 → bicycle-01.webp
...
row 11 → invoice-03.webp
Stage 2: encode each image
12 image paths
→ preprocessing batches
→ CLIP image encoder
→ matrix shape [12, 512]
Stage 3: prepare the geometry
length(row 0) ≈ 1
length(row 1) ≈ 1
...
length(row 11) ≈ 1
Small-dataset PCA bound
Twelve centred observations cannot identify more than eleven independent directions of variation.
Stage 4: run KMeans with k = 4
k = 4cluster 0 → bicycle-01, bicycle-02, bicycle-03
cluster 1 → invoice-01, invoice-02, invoice-03
cluster 2 → beach-01, beach-02, beach-03
cluster 3 → car-01, car-02, car-03
ARI 1.000
NMI 1.000
purity 100%
coverage 100%
silhouette 0.274
Stage 5: compare different algorithm contracts
| Backend | Groups found | Noise | ARI | What happened? |
|---|---|---|---|---|
KMeans, k=4 | 4 | 0 | 1.000 | recovered the four requested partitions |
| HDBSCAN | 3 | 0 | 0.645 | combined bicycles and cars into one dense region |
| Graph | 1 | 9 | 0.267 | kept the invoice component and rejected the other nine |
| Model | KMeans ARI | HDBSCAN ARI | Graph coverage |
|---|---|---|---|
| CLIP | 1.000 | 0.645 | 25.0% |
| DINO | 1.000 | 1.000 | 16.7% |
| SigLIP | 1.000 | 0.275 | 25.0% |
Stage 6: post-process without hiding the change
requested backend
actual implementation
fallback reason
raw outlier count
raw-backend silhouette
final displayed silhouette
What this tiny run proves—and what it does not
20. Choosing an algorithm by the question you actually have
| If the product needs… | Start with… | Because… | Check carefully… |
|---|---|---|---|
every image in roughly k review piles | KMeans | it gives a complete partition and has a clear baseline | sensitivity to k, seeds, non-spherical groups |
| faster repeated updates on very large matrices | MiniBatchKMeans | it updates from small random batches | quality variance, batch size, convergence |
| the KMeans objective with optimized native routines | FAISS KMeans | it can execute that objective efficiently | packaging, CPU/GPU parity, initialization defaults |
| dense groups plus honest noise | HDBSCAN | it can discover cluster count and reject sparse points | coverage and density parameters |
| connected chains or near-duplicate families | similarity graph | connectivity can represent relationships a centroid misses | threshold bridges, giant components, isolated points |
Must every image receive a group?
├─ yes → begin with KMeans
│ ├─ too slow at scale? → test MiniBatchKMeans or FAISS
│ └─ unknown k? → sweep k and inspect stability + galleries
└─ no → should groups mean dense regions?
├─ yes → begin with HDBSCAN
└─ no, groups mean connected similarity chains
→ begin with a graph
You need complete coverage and can define or sweep k. A centroid baseline is the clearest first falsifiable experiment.
A minimum experimental protocol
Four implementation bugs the experiments exposed
1. A directed-neighbour traversal was order-sensitive
2. A missing optional backend could be reported as if it had run
requested_backend = hdbscan
backend = cosine-kmeans
implementation = sklearn-kmeans
fallback_reason = dependency unavailable
3. Quality could describe labels the user no longer saw
4. “Offline” model loading depended on import-time cached state
Production blueprint
IMAGE FILES
│ path order + invalidation metadata
▼
EMBEDDING MODEL
│ model ID + preprocessing + output dimension
▼
PREPARED MATRIX
│ optional fitted PCA + normalization + row map
▼
RAW BACKEND
│ requested backend + actual implementation + raw labels
▼
POST-PROCESSING
│ outlier policy + tiny-group policy + membership changes
▼
PRESENTATION
│ centroid ranking + explanations + review actions
▼
EVALUATION
geometry + coverage + labels + stability + human inspection
A file scan is already part of the mathematical contract. If row identity is lost, perfectly calculated labels can be attached to the wrong photographs.
21. The deeper lesson
embedding model → which visual relationships become geometric
normalization → whether vector magnitude matters
PCA → which observed directions are retained
backend → partitions, density islands, or connected regions
parameters → desired count, density scale, or edge strictness
outlier policy → whether uncertainty remains visible
tiny-group rule → whether rare groups survive
member ranking → which examples explain the result first
metrics → which properties we reward
Changing the embedding model invalidates every stored vector and every downstream geometric result. The source files remain valid, but PCA, labels, rankings, and recorded metrics must be recomputed.
Which clustering algorithm is best?
Which definition of a useful group does this workflow need, what evidence would show that the definition is being met, and which failures must remain visible to the user?
Primary references
- scikit-learn clustering documentation
- scikit-learn PCA documentation
- Arthur and Vassilvitskii: k-means++
- McInnes, Healy, and Astels: hdbscan
- HDBSCAN: how the algorithm works
- FAISS source and research references
- FAISS getting started: exact flat indexes
- FAISS implementation notes: KMeans assignment
- FAISS CPU and GPU interoperability
- pgvector: vector search for PostgreSQL
- Qdrant architecture and data model
- Milvus architecture overview
- Weaviate data and indexing concepts
- Pinecone indexing and record model