Abbas, Tirumala, Simig, Ganguli, Morcos — arXiv:2303.09540 · Gadre et al. — arXiv:2304.14108 · 2023

Curation by Embedding

The highest-leverage thing an embedding model does is not search. It is deciding, before a single gradient step, which rows of your dataset deserve to exist.

Prerequisites: what a dot product is + what cosine similarity means. k-means, deduplication, filter benchmarking, and retrieval curation are built from zero.
10
Chapters
4
Interactive Sims
50%
LAION Removed, Accuracy Held
79.2%
DataComp-1B ImageNet

Chapter 0: The Quiet Superpower

Two teams get the same job. Train a CLIP model on web image-text pairs. They are handed identical GPUs, identical training code, identical architecture, identical learning-rate schedule, identical number of steps, identical random seed. Nothing about the model is theirs to choose.

The only decision either team makes is which subset of a shared pool of 128 million pairs they train on.

Team A trains on all of it. Their model reaches 17.6% zero-shot accuracy on ImageNet. Team B keeps about a third of the pool — chosen by a rule that takes twenty lines of code and no gradient steps — and reaches 29.7%.

Twelve points of accuracy, for free. Not from a new loss, not from a bigger model, not from more compute. From choosing rows. Those two numbers are real: they are the no-filtering and best-baseline results at the medium scale of the DataComp benchmark, which exists precisely so that this comparison can be made honestly. And the gap gets larger at larger scale, not smaller.

Here is the thing that should bother you. Every course teaches you how embeddings power search. Many teach retrieval-augmented generation. A few teach recommendation. Almost none teach the application that decides how good every model you will ever train is allowed to be: using embeddings to choose the training set itself.

Where embeddings actually get used

Take an honest inventory. An embedding model maps an object — an image, a sentence, an audio clip, a user — to a vector, such that similar objects land near each other. Here is what people do with that, ordered by how much attention each use gets versus how much it matters.

ApplicationHow much it is taughtWhat it changes
Semantic searchConstantlyYour query latency and recall
RAG retrievalConstantlyWhich three paragraphs your LLM sees
RecommendationOftenWhich item ranks fourth on a page
Clustering / topic discoverySometimesA dashboard someone looks at monthly
Zero-shot classificationSometimesWhether you need an annotation budget
Training-set curationAlmost neverThe ceiling on every model built from that data, forever

The last row is the meta-application. It sits upstream of all the others, because the embedder that powers your search was itself trained on a corpus that somebody curated — increasingly, with an embedder.

The claim this lesson defends. Between roughly 2022 and 2024, the frontier of computer vision and language modelling moved from "design a better architecture" to "assemble a better dataset," and the tool that made dataset assembly a measurable engineering discipline rather than folklore was the embedding. Three papers did most of the work of establishing that, and they attack three different failure modes with the same instrument.

The three papers, and the three things they subtract or add

SemDeDup — subtract redundancy
Embed every example, cluster, and delete the ones that are semantically the same as something you already have. On a 440-million-pair slice of LAION, this removed half the dataset while matching the baseline model — and improved it out of distribution.
DataComp — measure the subtraction
Freeze the model, the code, the compute, and the evaluation. Let researchers change only which subset of a 12.8-billion-pair pool they train on. Suddenly a filter is a comparable scientific artifact with a leaderboard.
DINOv2 — add by retrieval
Do not filter a pool down. Grow a dataset up: take a small curated seed, use it as a query set, and retrieve its nearest neighbours out of 1.2 billion uncurated web images. No labels, no taxonomy, no human in the loop after the seed.

Subtract redundancy. Subtract misalignment. Add by similarity. Three verbs, one instrument — a cosine between two vectors — and between them they define the state of the practice.

Why this is invisible in the literature you have read

Papers report architectures because architectures are describable in a figure. Data pipelines are described in an appendix, in a paragraph, often in the past tense and the passive voice: "the dataset was filtered for quality." That sentence hides more engineering than the model diagram above it.

There is also a structural reason. Until DataComp, there was no way to compare two curation strategies. If Lab A's model beats Lab B's, and the labs differ in data, architecture, compute, tokenizer, and schedule, the data contribution is unrecoverable. So curation knowledge stayed tacit, passed around as lore, and never accumulated.

What a benchmark does for a field. ImageNet did not make anyone smarter about convolutions. It made convolution ideas comparable, which let the field climb a gradient. DataComp does the same thing for filters. That is why a benchmark paper appears in a lesson mostly about an algorithm: the algorithm is only half the story, and the measuring instrument is the other half.

What "the data is the model" actually means, quantitatively

Slogans are cheap. Here is the arithmetic version, and it is the frame for everything that follows.

You have a fixed compute budget. Call it B samples seen — the total number of (image, caption) pairs your training run will process, counting repeats. This is the real currency: a GPU-hour buys you a number of forward-and-backward passes, and nothing you do to your dataset changes that number.

Now suppose a fraction q of your pool is useful — the caption actually describes the image, the example is not a near-copy of one you have already seen a hundred times, the concept is one the model needs. Train on the raw pool and you spend:

useful compute = q · B     wasted compute = (1 − q) · B

Put numbers on it. At DataComp's medium scale, B = 128 million samples seen and the pool is 128 million pairs. Suppose q = 0.25 — a quarter of raw Common Crawl alt-text is a genuine description. Then 96 million of your 128 million gradient contributions are teaching the model to associate images with text that does not describe them.

Now filter. Keep the top 30% by some quality score, and suppose that raises the useful fraction to q′ = 0.55. You now have 38.4 million pairs and a budget of 128 million samples seen, so each surviving pair is visited:

epochs = B / K = 128,000,000 / 38,400,000 = 3.33

Useful compute is now 0.55 × 128 million = 70.4 million, up from 32 million. You did not buy a single extra GPU-hour. You more than doubled the fraction of it that was spent on the actual learning signal.

And here is the tension that makes this an engineering problem rather than an obvious win. Filtering harder raises q′ but shrinks K, which raises the epoch count. At some point you are showing the model the same 5 million pristine pairs twenty-five times, and it starts memorising them instead of generalising. There is an optimum, it depends on your compute budget, and finding it is what Chapters 4 and 5 are about. "Clean your data" is not the lesson. "Clean your data to the point where the marginal removed example costs more in diversity than it saves in noise" is the lesson, and that point is measurable.

The four numbers to carry through the lesson

NumberWhere it comes fromWhat it proves
50%SemDeDup on a 440M-pair LAION subset: half the data removed, model quality held, out-of-distribution results slightly improvedHalf of a web-scale corpus can be semantic redundancy. Not exact copies — near-copies a hash cannot see
17.6% → 29.7%DataComp medium filtering track: no filtering versus the best baseline filter, identical trainingFilter choice is worth more than most architecture choices, at fixed compute
79.2%A CLIP ViT-L/14 trained on DataComp-1B, versus 75.3% for OpenAI's ViT-L/14 at the same compute and training procedureA better dataset, same everything else, beat the reference model by 3.7 points
1.2B → 142MDINOv2's curation: an uncurated web pool retrieved down to LVD-142M using a curated seed as the query setYou can build a training set with a nearest-neighbour search and no labels at all

Where we are going

Chapters 1–3 — the subtraction machinery
Why web-scale corpora rot (duplication, misalignment, skew) → SemDeDup derived line by line, with a three-vector example you compute by hand → what an embedding sees that a perceptual hash cannot
Chapters 4–6 — measuring and growing
DataComp's inversion: fix the model, vary the data → what actually won, including the results that contradict the obvious story → DINOv2's retrieval curation, which builds a dataset instead of trimming one
Chapters 7–9 — the loop and your Tuesday
Better embedder → better data → better embedder, and the four ways that spiral eats itself → recipes you can run this week on a fine-tuning set or a RAG corpus → what came next and what is still broken
Inline concept check — answer before reading on. Team B kept 30% of the pool and got 12 points more accuracy. Someone proposes keeping 3%, reasoning that if a bit of filtering is good, more is better. What is your prediction, and why?  …  Accuracy falls. The budget is fixed at 128 million samples seen, so 3% of a 128-million pool is 3.84 million unique pairs visited 33 times each. The model has plenty of compute and almost nothing new to spend it on: it memorises the survivors and stops generalising. Filtering trades noise for repetition, and repetition has its own cost. Chapter 5 shows the measured curve, and it is unimodal.

How the field arrived here

The move from architecture to data was not a fashion. It was the field running out of road on one axis and discovering an unmeasured one. The sequence is worth knowing, because it makes the shift feel earned rather than arbitrary.

EraWhere the effort wentWhat was fixedWhat nobody measured
Feature engineering, to about 2011Designing descriptors by hand — edges, gradients, keypointsThe dataset was small and hand-collectedWhether a different dataset would change the ranking of descriptors
The benchmark era, 2012–2018Architectures. Deeper, wider, residual, attentionThe dataset was frozen by the benchmark — that was the whole pointData, by construction. It was the control variable
The scale era, 2019–2022Parameters and tokens, guided by scaling lawsData was assumed interchangeable: more is moreWhether two corpora of the same size are equally valuable. They are not
The curation era, 2023–Which rows existArchitecture and recipe, deliberately, so the data can varyStill open: consent, provenance, and what the filters quietly delete

Read the third column down the table. Each era froze something in order to study something else, and each freeze created a blind spot the next era inherited. The benchmark era froze the dataset so thoroughly that a generation of researchers grew up treating data as a constant of nature. The scale era unfroze the size but not the composition.

The scaling laws were never wrong, they were just incomplete. They relate loss to parameters, tokens, and compute. They say nothing about which tokens. Fit a scaling law on a corpus and you get a curve; fit it on a better corpus and you get a different curve, usually shifted and sometimes steeper. The law is conditional on the data distribution, and for years that condition was invisible because everyone was drawing from roughly the same web.

What "a row" is, in five different systems

This lesson uses image-text pairs as its running example because that is where the measurements exist. But the abstraction is completely general, and recognising it in your own system is the point.

SystemA "row"Redundancy looks likeMisalignment looks like
Vision-language pretrainingAn image and its captionThe same stock photo across a hundred sitesAlt-text that is a filename
Language-model pretrainingA documentBoilerplate, syndicated news, license textNothing to align to — only quality
Instruction tuningA prompt and a responseTemplate instantiations from one skeletonA response that does not answer the prompt
Retrieval corporaA chunk of a documentThe same policy paragraph in five documentsA chunk split mid-sentence, meaningless alone
RecommendationA user-item interactionBot traffic and repeated impressionsAn accidental click

In every row of that table the operations are identical: embed, measure similarity, threshold, and look at the occupancy of the resulting clusters. Only the encoder changes.

Why the embedding, specifically

There are other ways to judge data. Heuristics (drop captions under three words). Classifiers (train a quality model). Human review (does not scale past a few thousand). What makes the embedding the instrument that actually won?

1. It is total
Every item gets a vector. No abstentions, no out-of-vocabulary, no "this document type is unsupported." A heuristic has coverage gaps; an encoder does not.
2. It induces a metric
Once every item is a point, "similar" becomes arithmetic. Redundancy is a distance, misalignment is a cross-modal distance, and skew is an occupancy histogram. Three complaints, one operation.
3. It amortises
One forward pass per item, cached forever, answers every question you will later think to ask of that corpus. Compare with a classifier, which answers exactly the one question it was trained for.

Property three is the economic argument, and it deserves numbers. Embedding a 128-million-image pool with a small vision transformer costs roughly 8.8 billion floating-point operations per image:

1.28 × 108 images × 8.8 × 109 FLOPs = 1.13 × 1018 FLOPs

Training on that pool costs a forward and backward pass through two towers, roughly four times more per sample:

1.28 × 108 samples × ≈ 3.8 × 1010 FLOPs = 4.9 × 1018 FLOPs

So the embedding pass is about 23% of one training run — and it is paid once, while the decisions it enables apply to every training run you will ever do on that corpus. Deduplication, alignment filtering, distribution auditing, decontamination, retrieval curation: all of them read the same cached vectors. That ratio is why this became standard practice within about a year.

Inline concept check. If embeddings are so useful, why not just train the model you actually want and use its internal representations to curate the data?  …  Because that is circular in the expensive direction: you would need the training run you were trying to make cheaper, and you would inherit every blind spot of a model trained on the uncurated data. The whole value of a pretrained, external embedder is that it arrives before you spend anything and it was shaped by a different corpus. Chapter 7 shows what happens as that independence erodes across generations.

When curation is the wrong lever

A lesson that only argues one direction is propaganda. Here is the honest boundary: three situations where the twelve-point result above does not transfer, and reaching for a filter would waste your week.

SituationWhy curation does not helpWhat to do instead
A small, hand-collected, already-clean datasetThere is no redundancy to remove and no misalignment to filter. Every removal is pure lossAugmentation, better regularisation, or collecting more — the classical answers, which are correct here
You are compute-bound with data to spareFiltering helps by improving the return per gradient step, but if you are only seeing 5% of your corpus anyway, the noise is already being skipped by samplingStill worth deduplicating (it is close to free), but expect small gains. Spend the effort on the model
The bottleneck is the objective, not the dataIf your loss cannot express what you need — ordering, counting, calibration — then no subset of the data teaches itChange the objective or the architecture. Chapter 3's invariance table is the diagnostic: if your embedder cannot see the property, neither can your model
The diagnostic question, and it takes one experiment. Train on a random half of your data at equal optimiser steps. If performance is nearly unchanged, your dataset has redundancy or noise to spare and curation will pay. If it drops sharply, you are data-limited and every row is earning its place — go collect more rather than filtering what you have. That single control run costs one training cycle and tells you which of the two regimes you are in, which is the most important thing to know before spending a month on a pipeline.

A working glossary

Six terms that get used loosely and mean specific things in this lesson. Pin them now and the later chapters read faster.

TermMeans exactlyDoes not mean
PoolThe full set of candidate rows before any selectionThe training set. Keeping that distinction is most of Chapter 4
FilterA function from a row to keep-or-drop. The artifact you are actually comparingA model, a loss, or anything that runs at inference time
Semantic duplicateTwo rows whose embeddings are within a chosen cosine thresholdTwo rows with the same bytes, or the same pixels. Those are cheaper problems
AlignmentCross-modal similarity: does this caption describe this image?Truth. A terse but accurate caption scores low; a florid wrong one can score high
AnchorThe reference set a filter measures similarity againstAn implementation detail. It is the filter's statement of intent
Samples seenTotal rows processed by training, counting repeats. The real compute currencyDataset size, or epochs. Conflating these confounds every comparison
Inline concept check. Your colleague says their new filter improved their model by 4 points, and that they kept 40% of the data and trained for the same number of epochs. What is the first question you ask?  …  "Same number of epochs over what?" If they ran the same epoch count over 40% of the rows, they trained on 60% less compute and still gained 4 points — which is a much bigger and more interesting claim than the one they made, and also a broken comparison. Matched steps, not matched epochs. This is the single most common error in data-ablation reporting, and it goes both directions depending on which side the confound lands.

The two-by-two that settles the argument

People argue about whether "data or model matters more" as though it were a philosophical question. It is an experiment, and the experiment has been run in both directions.

Same model, same dataSame model, better data
ReferenceThe baseline numberAt the benchmark's medium scale, the gap between no filtering and the best baseline filter is about 12 accuracy points on ImageNet zero-shot
At the frontierA well-known reference model reaches 75.3% ImageNet zero-shotThe same architecture and the same training procedure and the same compute, on a curated 1.4-billion-pair dataset, reaches 79.2%

That bottom-right cell is the cleanest single result in this lesson. Identical architecture. Identical training recipe. Identical compute. The only difference is which 1.4 billion pairs went in, and the difference is 3.7 points at a level of the curve where 3.7 points is expensive.

Now run the comparison the other way. Hold the data fixed and vary the model within a reasonable range — a somewhat different vision transformer, a somewhat different learning-rate schedule, a somewhat better initialisation. At fixed compute those changes move results by a fraction of a point in most published ablations. The two axes are not close to symmetric, and for several years the field was spending nearly all of its attention on the flatter one.

The reason the asymmetry is so large, stated once. An architecture change alters how efficiently the model extracts the information in the data. A data change alters what information is there to extract. The first is bounded by the second. You can be arbitrarily clever about learning from a corpus that does not contain what you need, and it will not help, which is why the ceiling always comes from the dataset and the ceiling is what everybody is actually fighting for.

Three things had to be true at once

If curation is this valuable, why did the methods appear in 2023 and not in 2016? Because the technique has three prerequisites and the last one arrived late.

Prerequisite 1: an encoder that places meaning consistently
You need a model that maps semantically similar things to nearby vectors across domains it was not trained on. Before large-scale contrastive pretraining, features transferred poorly enough that cross-corpus similarity was unreliable.
Prerequisite 2: an index that survives a billion vectors
Quantisation, inverted files, and GPU-accelerated search. Without them the memory arithmetic in Chapters 2 and 6 does not close and the method stays a paper.
Prerequisite 3: an open pool anyone could experiment on
The last to arrive and the one that turned a technique into a field. Without a shared pool, curation results are unverifiable claims about private data.

Two of those three came from adjacent problems — representation learning and vector search — and were not built with curation in mind. That is a common shape: a technique becomes obvious the moment its prerequisites accumulate somewhere else, and the people holding the prerequisites are usually not the people with the problem.

What you will be able to do by the end

ChapterThe capability it leaves you with
1Audit any corpus with three numbers, and say which of the three failure modes dominates it
2Implement semantic deduplication, choose k from a memory target, and defend the survivor rule
3Pick the right encoder for the duplicates you are hunting, and know which ones a hash will never find
4Design a controlled experiment that attributes a gain to a data decision instead of confounding it
5Set a threshold from a compute budget, and predict which filters compose and which just correlate
6Grow a targeted dataset by retrieval from a pool, using a folder of examples as the specification
7Instrument a curation loop so that diversity collapse is visible before it is irreversible
8Run all of it, this week, on a corpus you already own
Inline concept check. If data matters so much more than architecture, why does anyone still work on architectures?  …  Because the comparison above holds at fixed compute and fixed scale, and architecture research is largely about changing what fixed compute buys you. An architecture that is twice as efficient effectively doubles your budget, which then lets you train on more of your curated data — the two axes multiply rather than compete. The correct reading is not "architecture does not matter." It is "the data axis was unmeasured, so its returns were unclaimed, and unclaimed returns are where the cheap wins are."

A note on the numbers in this lesson

Data-curation writing is full of confidently-quoted figures whose provenance is unclear, so this lesson labels its own. Three categories appear, and they are used differently.

CategoryExampleHow to treat it
Reported — measured in one of the papersRoughly half of a LAION subset removable; 79.2% ImageNet zero-shot for a curated 1.4-billion-pair dataset; the medium-scale baseline tableCite it, and cite the conditions with it. Reported numbers are conditional on scale and recipe
Derived — arithmetic done here from stated quantitiesThe 193-petabyte matrix; 8,800 items per cluster; the entropy calculations; the 6.79σ cosine argumentCheck the arithmetic yourself. That is the point of showing it. These follow from the inputs and nothing else
Illustrative — constructed to make a shape visibleThe threshold-sweep table in Chapter 8; the precision figures in the epoch model; the keep-fraction curve in the simulationRead the shape, never the value. These are labelled where they appear, and they are there because the shape is the lesson
Why the distinction is worth making explicitly. A great deal of curation folklore consists of illustrative numbers that acquired the authority of reported ones by being repeated. "Keep the top 30%" is a measurement at one scale with one scorer on one pool; it is quoted as a constant. When you write up your own curation work, label your numbers the same way. It costs a column in a table and it prevents your estimate from being someone else's fact next year.

The whole lesson in one picture

A pool
Every row you could possibly train on. Web-shaped: duplicated, misaligned, skewed.
↓ one embedding pass, cached forever
Three distributions
Nearest-neighbour similarity · alignment similarity · cluster occupancy. Every subsequent decision reads one of these.
↓ subtract
Redundancy and misalignment removed
Cluster and collapse the knots (Chapter 2); rank and cut at a budget-derived percentile (Chapter 5).
↓ add
Coverage restored
Retrieve toward a seed you wrote down (Chapter 6). The only operation that can raise diversity.
↓ protect
Decontaminated, instrumented, logged
Copy-detection against every evaluation set; effective cluster count logged per round (Chapter 7).

Nine chapters expand that diagram. If you ever lose the thread, come back here: every technique in the lesson is one of those arrows, and every failure mode is what happens when one of them is missing.

Two teams train identical models with identical compute on subsets of the same pool and get 17.6% and 29.7% ImageNet accuracy. What does this experiment establish that a normal paper comparison cannot?

Chapter 1: Why Scale Alone Fails

The scaling-law era taught a simple reflex: if the model is not good enough, add data. That reflex is correct in a specific, narrow setting — when the added data is independently drawn from the distribution you care about. Web crawls violate that condition in three separate ways, and each violation has a mechanism you can compute.

Let us take them one at a time, because the fixes are different and people constantly conflate them.

Failure 1: duplication is silent importance weighting

Start with what a training run actually does. In one epoch of stochastic gradient descent over a corpus of N examples, the total parameter update is the sum of per-example gradients:

Δθ ∝ ∑i=1Nθ ℓ(xi, θ)

Now suppose one particular example — a stock photo of a golden retriever on a lawn, say — appears m times in the corpus, because it was scraped from m different sites that all licensed it. Its gradient enters the sum m times. The optimiser cannot tell the difference between "this example appears 50 times" and "this example appears once and I weighted it 50×."

Make it concrete. A corpus of 1,000 examples where one image appears 50 times:

weight of that one image = 50 / 1000 = 5% of every epoch's gradient

Five percent of your model's learning, forever, devoted to one picture. Ask yourself whether you would ever choose that weighting. Nobody would. But nobody chose it — it arrived through a scraper, and it is invisible unless you go looking.

Duplication is not "wasted compute." It is an unrequested change to your loss function. The distinction matters. Wasted compute would mean you learn the same thing more slowly. Silent importance weighting means you learn a different objective: one where popular-on-the-web is a proxy for important, and where the head of the distribution gets a multiplier the tail does not.

The measurable downstream harms are well documented. Duplicated training text is memorised and regurgitated far more readily than unique text — the deduplication work on language corpora found that removing duplicates cut the rate at which models emit memorised training strings by an order of magnitude, and improved held-out perplexity at the same time. Duplicates also smuggle test-set contamination past your splits: if the same image appears in the crawl under two URLs and one lands in your evaluation set, your evaluation is now partly a training-set lookup.

Three kinds of duplicate, and why only one is easy

KindExampleDetectable byRoughly how common on the web
ExactByte-identical file served from two URLsA hash of the bytes. Trivial, cheap, and everyone already does itCommon, and already removed by most pipelines
PerceptualSame photo, re-saved at 80% JPEG quality, resized, watermarked, or cropped by ten pixelsA perceptual hash — sometimes. Chapter 3 shows exactly where it breaksVery common. Every CDN, thumbnail, and social repost makes more
SemanticTwelve different photographs of the same white sneaker on the same white background, from a product catalogueOnly an embedding. No pixel-level method sees these as relatedThe dominant kind, and the one nobody was removing before 2023

That third row is the whole of SemDeDup's contribution. Its central empirical claim — that roughly half of a large LAION subset is semantically redundant — is a claim about a category of duplicate that byte-level and pixel-level tools are structurally blind to.

Failure 2: a mismatched pair is not neutral, it is an instruction

The second failure mode is noise, and contrastive training has an unusually vicious relationship with it. Consider what a web caption actually is. Alt-text was invented for screen readers and is, in practice, filled with whatever the site's CMS put there:

"IMG_20180714_154302.jpg"

"Click here to see more from this collection"

"cheap running shoes buy online free shipping best price 2023 discount"

"A golden retriever puppy sitting in tall grass at sunset"

Only the fourth is a description. The first is a filename. The second is navigation. The third is search-engine keyword soup. In a supervised classifier, a bad label corrupts exactly one example: the model gets one wrong instruction and moves on.

In contrastive training, the pairing is the label, and the loss compares every item in the batch against every other. So one bad pair does not damage one relationship. Work it out. With a batch of N pairs, the contrastive objective asserts:

damaged relationships per bad pair = 1 + 2(N − 1) = 2N − 1

At a batch size of 1,024 — unremarkable for CLIP training — a single mismatched pair issues 2,047 wrong instructions. It pulls an image toward a caption that does not describe it, pushes that caption away from 1,023 images including ones it might genuinely describe, and pushes that image away from 1,023 captions including its true one if it happens to be in the batch.

And cross-entropy pushes hardest where the model is most confident. The gradient of a softmax cross-entropy with respect to a logit is (p − y). If the model has correctly learned that "cheap running shoes buy online free shipping" is unrelated to a photo of a sneaker on grass, it will assign that pair a low probability — and the loss, believing the pair is correct, will produce a large gradient to fix the model's "mistake." Noise is most damaging precisely where the model has learned the most. This is why quality filtering pays off superlinearly rather than gently.

Failure 3: the web is Zipf-shaped, and scale multiplies the head

The third failure is the least discussed and, at the frontier, probably the most limiting. Concept frequencies on the internet follow a heavy-tailed law. Approximate it with Zipf: the r-th most common concept has frequency proportional to 1/r.

What fraction of the data do the top 100 concepts get, out of a million concepts? The total mass is the harmonic number, which for large n is well approximated by

Hn = 1 + 1/2 + 1/3 + … + 1/n  ≈  ln(n) + γ,   γ ≈ 0.5772

So the top 100 out of a million:

H100 ≈ ln(100) + 0.5772 = 4.6052 + 0.5772 = 5.1824
H1,000,000 ≈ ln(106) + 0.5772 = 13.8155 + 0.5772 = 14.3927
fraction = 5.1824 / 14.3927 = 0.360

One hundredth of one percent of the concepts take 36% of the data. Now do the thought experiment that matters: you double your crawl. Every concept's count doubles. The proportions do not move. You have twice as many pictures of dogs and cars, and you have gone from four photos of a Namaqua chameleon to eight.

This is why "just scrape more" stops working, and it is a different reason from the other two. Duplication wastes compute; noise corrupts gradients; skew is neither. Skew means the marginal terabyte is correctly labelled, unique, and useless, because it is more of what you already had. Fixing skew requires an operation the other two do not: not removal but reweighting or targeted retrieval. That is exactly the operation DINOv2 performs in Chapter 6, and it is why that paper belongs in this lesson alongside two subtraction methods.

The three failures, side by side

FailureWhat it looks like in raw bytesWhat it looks like in embedding spaceThe fix
DuplicationDifferent bytes, different URLs, different filenames. UndetectableA tight knot of points at cosine 0.98–1.00 of each otherSemDeDup: cluster, then collapse each knot to one survivor
MisalignmentA caption string and an image blob. No relationship is computableAn image vector and a text vector with a low cosine between themCLIP-score filtering: drop pairs below a threshold
SkewNothing at all — every example is individually fineWildly uneven occupancy across clusters; a few dense regions, a vast sparse tailDINOv2: retrieve toward a seed distribution, amplifying the tail

Read the second column and the third column together. In every row, the raw-bytes view offers no purchase and the embedding view offers a number you can threshold. That is the entire reason this lesson exists: the embedding turns three qualitative complaints about data into three quantities.

A worked audit you could run on any corpus tonight

Before any algorithm, here is the diagnostic. Given a corpus, embed a random sample of 100,000 items and compute three statistics.

python — the three-number data auditimport numpy as np

E = embed(sample)                        # (100_000, d), then L2-normalise
E = E / np.linalg.norm(E, axis=1, keepdims=True)

# 1. REDUNDANCY: what fraction has a very close neighbour?
#    (exact for 100k; use FAISS for more)
S = E @ E.T
np.fill_diagonal(S, -1)
nn_sim = S.max(axis=1)                    # nearest-neighbour cosine per item
print("frac with NN cosine > 0.95:", (nn_sim > 0.95).mean())

# 2. ALIGNMENT (paired data only): image-text cosine distribution
align = (E_img * E_txt).sum(axis=1)
print("median align:", np.median(align), " frac < 0.20:", (align < 0.20).mean())

# 3. SKEW: cluster occupancy entropy, in bits
labels = kmeans(E, k=1024).labels_
p = np.bincount(labels, minlength=1024) / len(labels)
p = p[p > 0]
H = -(p * np.log2(p)).sum()
print("entropy:", H, "of max", np.log2(1024), " effective clusters:", 2**H)

Three numbers: near-duplicate fraction, alignment fraction, and effective cluster count. Every method in this lesson is an intervention on one of them. If you take nothing else from this chapter, take the habit of computing them before you train.

Why "effective clusters" and not just entropy. Entropy is in bits and hard to feel. Two to the power of the entropy has units of clusters, and it answers a concrete question: if your occupancy were perfectly uniform over some number of clusters, how many would produce this much spread? A corpus split across 1,024 k-means clusters with entropy 6.2 bits has 26.2 ≈ 73 effective clusters. You paid for 1,024 and you are getting 73. That single number is the most useful diversity diagnostic in this lesson, and it comes back in Chapter 7 as the thing that collapses.

What this chapter did not claim

Be careful about the boundary. None of the above says scale is bad or that small clean datasets beat large ones. The scaling laws are real and adding data does help. The claim is narrower and stranger:

The naive model
Performance is a function of dataset size. More rows, better model, with diminishing but positive returns forever.
↓ what the pruning literature found
The corrected model
Performance is a function of the information in the dataset relative to compute. With a good pruning metric you can beat the power law: careful removal makes the curve steeper, not just shifted. Beyond a point, adding rows adds bytes and no information.

That result — that data pruning can bend the scaling curve rather than merely translate it — came out of the same research group that then built SemDeDup, and it is the theoretical licence for the whole enterprise. If pruning could only ever cost you performance, the best you could hope for from curation would be efficiency. Because pruning can improve performance, curation is a source of capability.

Failure 4: your test set is in your training set

There is a fourth failure that is really a consequence of the first, and it deserves its own name because it corrupts your ability to detect the other three.

Benchmark images and benchmark text are, without exception, republished. ImageNet photographs appear in blog posts about ImageNet. Evaluation questions appear in tutorials, in forum answers, in scraped repositories. A web crawl large enough to be useful is large enough to contain your evaluation.

Estimate the exposure. Suppose an evaluation set has 50,000 items, and suppose each has an independent 2% chance of appearing somewhere in a multi-billion-item crawl in a near-identical form. The expected number of leaked items is:

50,000 × 0.02 = 1,000 items, or 2% of the evaluation

Two percent sounds tolerable until you remember what it does to a comparison. If two models differ by 1.5 accuracy points and one of them was trained on a corpus with slightly more leakage, the entire measured difference can be leakage. And crucially, leakage does not distribute uniformly: it concentrates on the famous examples, which are also the ones every model finds easiest, so it inflates exactly the region where the metric is most saturated.

Why this belongs in a curation lesson rather than an evaluation one. Because deduplication is the fix, and because every technique in this lesson makes the problem worse before it makes it better. A retrieval-based curation pipeline is a machine for finding items similar to a reference set. Point it at a pool containing near-copies of your test data and it will find them with above-chance efficiency. The tool that cleans your corpus is the same tool that contaminates it, pointed in a different direction. Chapter 6 comes back to this with the concrete fix.

The gradient budget ledger

The three failures compose, and it is worth doing the arithmetic once, because the compounded number is larger than people expect.

Take a 100-million-pair pool and three independent-ish estimates: 30% of pairs are semantic near-duplicates of something else in the pool, 25% of pairs are misaligned, and the useful remainder is Zipf-skewed so that a large share of it is redundant information even when the rows are distinct.

StagePairsShare of the original
Raw pool100,000,000100%
After removing misaligned pairs (25%)75,000,00075%
After removing near-duplicates among survivors (30%)52,500,00052.5%
Of which the top 100 concepts occupy (36%, from the Zipf calculation)18,900,00018.9%
Distinct, aligned, tail-of-the-distribution examples33,600,00033.6%

Two-thirds of the corpus is misaligned, redundant, or piled onto concepts you already had in abundance. That number is a rough estimate built from rough inputs, and it is close enough to what the measured results in later chapters imply that it should recalibrate your intuition about what "a hundred million pairs" means.

Memorisation, and the mechanism behind it

Duplication has one more consequence that is not about efficiency at all.

A model trained with a next-token or contrastive objective reduces loss on an example either by generalising — learning a rule that covers it and many others — or by memorising it. Generalisation is cheaper in parameters and pays off across examples; memorisation is expensive per example but always available.

Now consider what duplication does to that tradeoff. If an example appears once, memorising it buys a loss reduction on one item. If it appears fifty times, memorising it buys fifty times the loss reduction for the same parameter cost. Duplication makes memorisation a better deal, in exactly the arithmetic sense.

value of memorising an example ∝ (number of copies) × (loss reduction per copy)

This is why deduplication reduces verbatim regurgitation so sharply, and it is why the effect is nonlinear: an example seen twice is not twice as likely to be memorised as one seen once, because the threshold at which memorising beats generalising is crossed somewhere in between.

Copies of an exampleMemorisation pressurePractical consequence
1NegligibleThe example contributes to a general rule and is otherwise forgotten
2–10RisingFragments become retrievable under the right prompt
Tens to hundredsHighVerbatim reproduction becomes reliable. Privacy and licensing exposure
ThousandsCertainThe model treats it as a fact about the world rather than an example

So deduplication is simultaneously a compute optimisation, a distributional correction, and a privacy and licensing mitigation. Three arguments, one operation, and the first is usually the only one anyone mentions.

Why nobody fixed this sooner

A fair question, given that none of the above is technically hard. Three structural reasons, and each one is a lesson about how fields allocate attention.

Nobody owned it. Data collection was infrastructure work, done once, credited to nobody, and then treated as settled. Model work was research, published, and cited. The incentive gradient pointed away from the higher-leverage axis for a decade.

It was unmeasurable. Chapter 4's whole argument. Without a controlled comparison, "our filter is better" was an unfalsifiable claim, so filters could not accumulate improvements the way architectures did.

The tool did not exist at the right price. Semantic deduplication requires an embedder good enough to place meaning consistently, cheap enough to run over a billion items. Before large pretrained encoders, the first condition failed. Before efficient inference and vector indices, the second did. The method is obvious in hindsight precisely because its two prerequisites arrived quietly, from elsewhere.

The general pattern, worth transferring. When a field is stuck, look for the variable that was frozen so long ago that people forgot it was a variable. It is usually frozen for a good historical reason — the freeze enabled progress on something else — and it is usually the place with the most unclaimed leverage. In machine learning that variable was the dataset. In your own system it might be the schema, the sampling policy, or the definition of the label.

Which failure dominates in your corpus?

The three failures are not equally present everywhere, and the fix for one does nothing for the others. Diagnose before you treat.

If you observe…The dominant failure is…Go to
A distinct spike of nearest-neighbour cosines above 0.97DuplicationChapter 2. Expect a large, cheap win
A long left tail in the alignment distribution, and reading samples from it makes you winceMisalignmentChapter 5. Expect the largest single win, if you have paired data
Effective cluster count far below the cluster count you asked for, with a few enormous clustersSkewChapter 6. Expect the hardest work and the most durable gain
All three, mildlyA normal web corpusAll three, in the order of Chapter 8's pipeline
None of the above, and the corpus is smallNothing. You are data-limitedCollect more. Curation is not your bottleneck

Two cautions on reading these signals. First, the nearest-neighbour spike is only visible after exact and perceptual duplicates are removed — otherwise it is swamped by trivial copies and tells you nothing new. Second, the alignment tail is scorer-dependent: a low-scoring pair can be a genuinely bad pair or a perfectly good pair phrased in a way your scorer was never trained on. Read samples from the tail before you trust the shape.

How sensitive is the ledger?

The two-thirds estimate above came from three guessed inputs, so it is worth asking how much the conclusion depends on the guesses. Vary them:

MisalignedDuplicated (of survivors)Distinct, aligned, tail examples
15%20%68% × 0.64 = 43.5%
25%30%52.5% × 0.64 = 33.6%
35%40%39% × 0.64 = 25.0%

Across a wide, plausible range of inputs the answer moves between roughly a quarter and roughly a half. The conclusion — that most of a raw web corpus is not carrying its weight — is robust to being badly wrong about any single input, which is the mark of an estimate worth making even when you cannot measure the pieces precisely.

This is the value of a rough calculation done explicitly. "Web data is noisy" is a vibe and produces no decisions. "Between a quarter and a half of my pool is doing real work, and the sensitivity analysis says that range holds across any input I find plausible" is a number, and it justifies spending a week on a filter. Do the crude arithmetic. Then do it again with the pessimistic inputs. If the conclusion survives, act on it.

Track the three numbers over time, not once

A one-time audit tells you the state of a corpus. Running it on every snapshot tells you about your pipeline, which is more useful.

What movesWhat it means
Near-duplicate fraction rising snapshot over snapshotYour crawler is re-fetching, or a new source is a mirror of an old one. Fix upstream, not with a filter
Alignment distribution shifting leftEither a new low-quality source entered the mix, or your scorer changed version. Check the second before believing the first
Effective cluster count fallingYour acquisition is narrowing — often because a successful source is being scaled up while others stay flat
All three stable, corpus growingHealthy. You are adding volume without adding rot, which is rarer than it sounds

The general point: these are not one-off research metrics, they are production telemetry. A corpus is a system with inputs and drift, and the same instincts that make you graph latency should make you graph diversity.

The same three failures in text, at different proportions

Everything above was argued with images because that is where the controlled measurements exist. Text has the identical three failures with different weights, and knowing the differences prevents a lot of wasted effort.

FailureIn imagesIn textWhy they differ
DuplicationVery high. Roughly half of a large web image corpus is semantically redundantLower for near-copies, but exact and near-exact copying is rampantImages are republished byte-for-byte or with light edits. Text is more often rewritten, which lands at cosine 0.7–0.85 — related but genuinely not redundant
MisalignmentThe dominant failure. Alt-text frequently is not a descriptionNot applicable in the same form — there is nothing to align text toText pretraining is unimodal, so "quality" replaces "alignment": is this document worth learning from at all?
SkewZipf over visual conceptsZipf over topics, and additionally over registers: forum posts, boilerplate, machine translation, generated fillerText carries a second axis images do not: the same topic appears at wildly different levels of care

The practical consequence is that text pipelines look different in a specific way: exact and near-exact deduplication is the workhorse and does most of the value, quality classification replaces cross-modal scoring, and semantic deduplication is a smaller final pass rather than the headline act.

Worked example: how syndication inflates a text corpus

Take one wire-service article. Estimate its footprint in a crawl:

Copy typeRough countCaught by
The original publication1
Syndicated republications, byte-identical body40Exact hashing of the body, if the boilerplate is stripped first
Republications with a different headline and trimmed paragraphs25Shingle-based near-duplicate detection
Aggregator pages quoting the first two paragraphs60Partially — a short quote shares few shingles with a long article
Rewrites and summaries by other outlets15Only a semantic pass. Shingle overlap is near zero
Total footprint141 documents from one articleThree different instruments, in that order

Now apply the importance-weight argument. Without deduplication, that one story contributes 141 times the gradient of a story published once. The events that get syndicated are, by definition, the ones already over-represented, so the effect compounds skew rather than distributing randomly. And notice how the instruments partition: exact hashing catches 40, shingles add 25 and some of the aggregators, and only a semantic pass touches the last 15. The three stages are not redundant with one another.

The boilerplate trap, which eats a lot of engineering time. Most of those 141 documents are byte-identical in the body and completely different in the page — different navigation, different advertising, different cookie notices, different comment sections. Hash the raw page and you find almost no duplicates. Extract the main content first and the same corpus is riddled with them. This is why boilerplate removal is upstream of deduplication in every serious text pipeline, and why "we hashed the documents and found few duplicates" is usually evidence about the extraction step rather than about the corpus.

Where generated text changes the arithmetic

One newer wrinkle worth naming, because it changes the failure modes rather than merely their magnitudes.

A growing share of web text is machine-generated. For curation this creates a category that fits none of the three failures cleanly: it is not duplicated (each output is distinct), it is not obviously misaligned (it is fluent and on-topic), and it does not create skew in the usual sense (it can be produced about anything). It is fluent, plausible, and low in information — and every quality signal built to detect the old failure modes rates it highly.

SignalVerdict on fluent generated fillerWhy the signal fails
Perplexity under a language modelExcellent — low perplexityIt was generated by a similar model. Low perplexity is the definition, not evidence of quality
Near-duplicate detectionPasses — every output is distinctSampling ensures surface variety even when content is repetitive
A learned quality classifierOften passesTrained to recognise well-formed prose, which this is
Cluster occupancySometimes catches itGenerated filler about a topic clusters unusually tightly. An anomalously dense cluster of fluent, distinct documents is a real tell

The honest state of this problem is that it is open. It is included here not because there is a fix to teach but because it is the clearest current example of the general principle: a curation signal detects the failure modes it was designed against, and a corpus will drift toward whatever the signal cannot see. That principle is the entire subject of Chapter 7.

In contrastive training with batch size 1,024, why is one mismatched image-caption pair much worse than one mislabelled example in supervised classification?

Chapter 2: SemDeDup, Derived

We want an algorithm that finds, among N examples, the groups that are "the same thing," and keeps one member of each group. Nothing about that sentence mentions clustering, thresholds, or centroids. Those all fall out of two constraints. Let us derive them rather than recite them.

Step 1: define "the same thing" with a number

Take a pretrained encoder f that maps an example to a vector. For LAION images, SemDeDup used the image encoder of a pretrained CLIP model; for text, the hidden states of a small pretrained language model. The choice matters and we will return to it, but the shape of the method does not depend on it.

Normalise every embedding to unit length:

ei = f(xi) / ‖f(xi)‖2,    so ‖ei‖ = 1

Why normalise? Because otherwise vector length becomes a confound. The raw dot product e·e′ = ‖e‖‖e′‖cosθ rewards long vectors for being long. Encoders systematically give longer vectors to some kinds of input — high-contrast images, long captions — and you do not want "this photo is vivid" to read as "this photo is a duplicate of everything." Normalising strips length and keeps only direction, so the only surviving quantity is the angle:

sim(xi, xj) = ei · ej = cosθij ∈ [−1, 1]

Two examples are semantic duplicates if that cosine exceeds a threshold ε. The whole method is now specified: compute all pairwise cosines, group anything above ε, keep one per group. And it is completely impossible to run.

Step 2: the quadratic wall, with real numbers

SemDeDup's LAION experiments used a subset of about 440 million pairs. The number of distinct pairs is:

N(N − 1)/2 ≈ (4.4 × 108)2 / 2 = 1.936 × 1017 / 2 = 9.68 × 1016

Ninety-seven quadrillion pairs. Two ways to feel how bad that is.

Memory. Storing that similarity matrix at 2 bytes per entry (half precision) needs 9.68 × 1016 × 2 = 1.94 × 1017 bytes, which is 193 petabytes. There is no machine.

Compute. Each cosine of two 512-dimensional unit vectors is 512 multiplies and 511 adds, call it 1,024 floating-point operations. So:

9.68 × 1016 pairs × 1,024 FLOPs = 9.9 × 1019 FLOPs

A GPU sustaining 1014 FLOP/s on this kind of dense arithmetic would need 9.9 × 105 seconds — about 11.5 GPU-days of nothing but dot products, before you have looked at a single duplicate. And you would still have nowhere to put the answers.

This is the constraint that shapes the algorithm. Everything distinctive about SemDeDup — the clustering step, the within-cluster restriction, the centroid-based tie-break — exists to get around the quadratic. If N were 50,000 you would just compute the full matrix and be done. The method is what "find all near-duplicates" becomes when N has nine digits.

Step 3: cluster first, and count what that saves

The insight is almost embarrassingly simple, and it is the same insight behind every approximate nearest-neighbour index: duplicates are, by definition, close together, so you never need to compare things that are far apart.

Run k-means on the embeddings with k clusters. Then only compare items that landed in the same cluster. If the clusters are roughly balanced, each holds N/k items, and the pair count becomes:

k · (N/k)2 / 2 = N2 / (2k)

Compare to N2/2 for the full matrix: the saving is exactly a factor of k. Not approximately — exactly, under the balanced-cluster assumption. That is a clean result worth holding on to: the number of clusters is the speedup.

SemDeDup used k = 50,000 on the LAION subset. Run the arithmetic:

QuantityGlobal (no clustering)Within-cluster, k = 50,000
Items per comparison group440,000,000440,000,000 / 50,000 = 8,800
Pairs per group9.68 × 10168,800 × 8,799 / 2 ≈ 38.7 million
Total pairs9.68 × 101638.7M × 50,000 ≈ 1.94 × 1012
Peak similarity-matrix memory193 petabytes8,8002 × 2 bytes = 155 megabytes
Dot-product compute≈ 11.5 GPU-days20 GPU-seconds

From 193 petabytes to 155 megabytes, and from a week and a half of GPU time to twenty seconds. And the 155 MB figure is per cluster, processed one at a time, so the job is trivially parallel and embarrassingly memory-friendly. That table is the whole reason SemDeDup can be run by a normal team on a normal cluster.

Watch which cost moved and which did not. Clustering itself is not free: k-means over 440M vectors with k = 50,000 is a serious job. But k-means is linear in N per iteration (each point compares to k centroids), so it costs N · k · d per pass — large, but not quadratic. The method trades an intractable quadratic for a large linear plus a tractable quadratic. That trade — pay a linear pass to shrink the quadratic's scope — is the most reusable idea in this chapter, and it is the same trade an inverted-file index makes at query time.

Step 4: what the approximation costs, honestly

Restricting comparisons to within-cluster means SemDeDup will miss any duplicate pair that k-means split across two clusters. Is that a lot?

Think about the geometry. Two semantic duplicates have cosine near 1, which means they are nearly the same point on the unit sphere. For k-means to separate them, the Voronoi boundary between two centroids would have to pass almost exactly between them. The set of points within cosine 0.99 of each other that straddle a boundary is small — it is essentially the surface area of the boundaries times the duplicate radius.

And the error is one-sided, which is what makes it acceptable. Clustering can cause SemDeDup to miss a duplicate. It can never cause SemDeDup to remove something that is not a duplicate, because every removal decision is still made by an explicit cosine comparison against the threshold. A method whose only failure mode is "occasionally leaves a duplicate in" is a method you can ship. A method that occasionally deletes a unique example would need far more care. Always ask of an approximation which direction its errors point.

The simulation later in this chapter lets you toggle between cluster-scoped and global comparison on a small set so you can see exactly which pairs the clustering misses, and how few they are.

Step 5: which duplicate survives?

Now the decision that people skip and that turns out to matter. You have found a group of, say, three items that are all above threshold with each other. You keep one. Which?

Three defensible rules:

RuleArgument for itWhat it does at scale
Keep a random memberUnbiased; no assumptionsFine at mild dedup rates; leaves the group's average character intact
Keep the member closest to the cluster centroidThe most prototypical, least weird example. Feels like "keep the best one"Collapses the dataset toward k canonical prototypes. Diversity dies
Keep the member farthest from the cluster centroidPreserves the spread of the cluster; the survivors are the ones least like everything elseSemDeDup's choice. Best at aggressive removal rates, roughly tied with the others when you are only removing a little

The counterintuitive rule wins, and the reason is the same distribution argument from Chapter 1. If you always keep the prototype, then after aggressive dedup your dataset is a list of cluster centres: exactly the head of the distribution, exactly the part you already had too much of. Keeping the outlier of each duplicate group preserves the corners of the space.

The three-vector worked example

Do it by hand. A cluster contains three near-identical product photographs of the same white sneaker. Their embeddings, in a toy three-dimensional space whose axes you can read as "sneaker-ness", "white-background-ness", and "studio-lighting-ness":

A = (0.60, 0.48, 0.64)    B = (0.64, 0.48, 0.60)    C = (0.48, 0.60, 0.64)

First check they are unit length, because every step below assumes it. For A: 0.602 + 0.482 + 0.642 = 0.3600 + 0.2304 + 0.4096 = 1.0000. Square root is 1. B and C are permutations of the same three components, so they are unit too. Good — cosine is now just the dot product.

Pairwise cosines. Multiply component by component and add:

cos(A,B) = (0.60)(0.64) + (0.48)(0.48) + (0.64)(0.60) = 0.3840 + 0.2304 + 0.3840 = 0.9984
cos(A,C) = (0.60)(0.48) + (0.48)(0.60) + (0.64)(0.64) = 0.2880 + 0.2880 + 0.4096 = 0.9856
cos(B,C) = (0.64)(0.48) + (0.48)(0.60) + (0.60)(0.64) = 0.3072 + 0.2880 + 0.3840 = 0.9792

With ε = 0.95, all three pairs are above threshold. They form one connected group: A–B, A–C, and B–C. Two survive-or-die decisions to make, and we keep exactly one of the three.

The cluster centroid. SemDeDup uses the k-means centroid of the whole cluster, but for a three-member cluster that is just their mean. Average each component:

cx = (0.60 + 0.64 + 0.48)/3 = 1.72/3 = 0.573333
cy = (0.48 + 0.48 + 0.60)/3 = 1.56/3 = 0.520000
cz = (0.64 + 0.60 + 0.64)/3 = 1.88/3 = 0.626667

The centroid of unit vectors is not unit, so we need its length before taking cosines:

‖c‖2 = 0.5733332 + 0.5200002 + 0.6266672 = 0.328711 + 0.270400 + 0.392711 = 0.991822
‖c‖ = √0.991822 = 0.995903

Cosine to the centroid, for each candidate. Dot with c, then divide by ‖c‖:

A · c = (0.60)(0.573333) + (0.48)(0.520000) + (0.64)(0.626667) = 0.344000 + 0.249600 + 0.401067 = 0.994667
cos(A, c) = 0.994667 / 0.995903 = 0.998759
B · c = (0.64)(0.573333) + (0.48)(0.520000) + (0.60)(0.626667) = 0.366933 + 0.249600 + 0.376000 = 0.992533
cos(B, c) = 0.992533 / 0.995903 = 0.996617
C · c = (0.48)(0.573333) + (0.60)(0.520000) + (0.64)(0.626667) = 0.275200 + 0.312000 + 0.401067 = 0.988267
cos(C, c) = 0.988267 / 0.995903 = 0.992333

The decision. Rank by cosine to the centroid: A (0.998759) is the most central, then B (0.996617), then C (0.992333). So:

RuleSurvivorWhat you lost
Keep closest to centroidAThe two examples that were least like the cluster's average — exactly the informative ones
Keep randomA, B, or C with probability 1/3 eachUnbiased, but you cannot reason about the survivor
Keep farthest from centroid (SemDeDup)CThe two most prototypical ones. The survivor pushes the cluster's boundary outward

Notice how narrow the differences are: 0.9988 versus 0.9923, a gap of 0.0065. On any single group the choice is nearly arbitrary. Across a hundred million groups it is the difference between a dataset that keeps its shape and one that shrinks toward its own average.

Inline concept check. Why compute the cosine to the cluster centroid rather than to the centroid of the duplicate group itself?  …  Because the duplicate group's own centroid tells you which member is most typical of the duplicates, which is not the quantity you care about. You want to know which member sits furthest out in the cluster, because that is the one whose removal would shrink the region the dataset covers. The cluster is the unit of coverage; the group is just the unit of redundancy.

The algorithm, complete

python — SemDeDup, end to endimport numpy as np

def semdedup(embeddings, k, eps, keep="farthest"):
    """embeddings: (N, d) float32.  Returns a boolean keep-mask of length N."""
    E = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)

    # --- one linear pass: assign every point to a centroid ---
    km       = kmeans(E, k)                 # faiss.Kmeans in practice
    labels   = km.labels_                    # (N,)
    centroid = km.cluster_centers_           # (k, d), NOT unit length

    keep_mask = np.ones(len(E), dtype=bool)

    for c in range(k):                      # each cluster is independent -> parallel
        idx = np.where(labels == c)[0]
        if len(idx) < 2: continue
        Ec  = E[idx]                         # (m, d), m ~ N/k

        # rank members by how central they are in THIS cluster
        cen  = centroid[c] / np.linalg.norm(centroid[c])
        csim = Ec @ cen                      # (m,) cosine to centroid
        # the FIRST item in the order is the one that survives its group,
        # so "keep farthest" means process LEAST-central first. Easy to write backwards.
        order = np.argsort(csim) if keep == "farthest" else np.argsort(-csim)
        Ec, idx = Ec[order], idx[order]

        S = Ec @ Ec.T                        # (m, m) -- 155 MB at m = 8800, fp16
        # upper triangle: "does an EARLIER (more central) survivor already cover me?"
        S = np.triu(S, k=1)
        covered = (S > eps).any(axis=0)     # (m,) True -> drop
        keep_mask[idx[covered]] = False

    return keep_mask

Read the triu line carefully, because it is where the survivor rule is actually implemented and it is easy to get backwards. Items are sorted most-central first. S > eps in the strict upper triangle asks, for each item, "is there an item earlier in the order that I duplicate?" If yes, this item is dropped. So the item that survives a group is the one with no earlier duplicate — the first in the ordering. Sorting most-central first therefore keeps the most central. To keep the farthest, you sort least-central first, which is what the argsort(-csim) versus argsort(csim) switch does.

Concept meets realisation: the shapes. N = 440,000,000 embeddings at d = 512 in float16 is 440e6 × 512 × 2 = 450 GB — you stream them from disk, you do not hold them. labels is (N,) int32 = 1.76 GB, which does fit in RAM. centroid is (50,000, 512) float32 = 102 MB. Per cluster you materialise Ec at (8,800, 512) = 9 MB and S at (8,800, 8,800) = 155 MB in fp16. The peak working set is under a gigabyte. That is the difference between an algorithm and a paper.

The simulation: watch the survivors

Now play with it. Below is a two-dimensional stand-in for embedding space. The points are unit vectors on a high-dimensional sphere, drawn here by a projection with one honest property: for L2-normalised vectors, squared Euclidean distance and cosine are exactly related by

‖a − b‖2 = 2 − 2 cosθ   ⇒    cosθ = 1 − ‖a − b‖2 / 2

The plot is scaled so that a distance D across the box corresponds to cosθ = 1 − D2. So the threshold is literally a radius you can see — which is why the slider below is calibrated in radius, with the corresponding ε displayed beside it. A radius of 0.03 is ε = 0.9991; a radius of 0.15 is ε = 0.9775.

Cluster-scoped semantic deduplication

The slider is the duplicate radius itself — the threshold ε = 1 − r2 is shown beside it. Drag it and watch survivors shrink. Removed points become hollow rings tethered to the survivor that absorbed them. Switch the survivor rule to see the dataset collapse toward cluster centres (closest) or hold its shape (farthest). Switch the scope to global to see the handful of cross-cluster duplicates that clustering misses — and what that costs in comparisons.

Radius r r = 0.030 → ε = 0.9991
Survivor:
Scope:

Three things worth doing before moving on. Pull the radius down to 0.01 and notice that almost nothing is removed: at that threshold only the tight planted knots are caught. Push it to 0.12 and watch entire clusters collapse to two or three survivors — that is over-deduplication, and the removed points are no longer duplicates in any meaningful sense. And flip the survivor rule at a moderate threshold and watch the survivors move: with "closest," they migrate inward toward the cluster cores; with "farthest," they stay on the rim.

What the paper actually measured

Now the results, and they deserve to be stated with their caveats attached rather than as a slogan.

On the LAION image-text subset, SemDeDup with an appropriate threshold removed roughly half the pairs. A CLIP model trained on the deduplicated half, with the same number of training iterations, matched the model trained on the full set on in-distribution evaluation — and did slightly better on out-of-distribution evaluations. Reaching a target accuracy took roughly half the training iterations, because half the iterations were no longer being spent on repeats.

On text, the same procedure applied to a large web corpus with embeddings from a small pretrained language model let them remove a much smaller fraction — on the order of 15% — with no loss in perplexity and a proportional reduction in training time, plus improved perplexity on out-of-distribution text.

Why the removable fraction is so different between images and text, and it is not a flaw. Images on the web are republished: the same JPEG travels through stock libraries, marketplaces, news wires, and social reposts, and each hop makes a variant. Text is more often rewritten. Two articles about the same event share facts but not sentences, so their embeddings sit at cosine 0.7–0.85, not 0.98 — genuinely related, genuinely not redundant. The removable fraction is a property of how a medium propagates, not of the algorithm. Measure it on your own corpus before you assume 50%.

Why out-of-distribution performance goes up

The most surprising result deserves its own explanation, because "remove data, get better" trips people's alarms and it should.

Return to Chapter 1's arithmetic. A duplicate is an unrequested importance weight. Which examples get duplicated most on the web? The popular ones — the head of the distribution. So the duplication pattern is not uniform noise; it is a systematic upweighting of exactly the region your model already handles well.

Before dedup
The effective training distribution is the web's distribution squared in the head: popular concepts are both more frequent and more duplicated. In-distribution evaluation, which is drawn from a similar web-shaped distribution, looks fine. Out-of-distribution evaluation suffers.
↓ remove near-duplicates
After dedup
Each distinct concept contributes closer to once. The training distribution flattens toward "distinct things I have seen" rather than "things the web republishes." In-distribution holds; out-of-distribution improves, because the tail is no longer drowned.

Stated as one sentence: deduplication is distribution flattening disguised as compression. The compute saving is the advertised benefit. The distributional correction is the real one.

What SemDeDup is not

Two boundaries, because both get crossed constantly in practice.

It is not a quality filter. SemDeDup removes redundancy, not junk. A corpus of ten million distinct pieces of keyword-soup alt-text will pass through SemDeDup almost untouched, because junk is often highly diverse — that is what makes it junk. Conversely a corpus of pristine, expert-written captions about one narrow topic will be heavily deduplicated. The two axes are close to orthogonal.

It is not a difficulty or influence metric. There is a separate literature on pruning by how much an example contributes to learning — forgetting scores, gradient-norm ranking, influence functions, and the memorisation-based metrics. Those ask "does the model need this?" SemDeDup asks the cheaper, more robust question: "do I already have this?" The second question requires no training run to answer, which is precisely why it scales.

MethodQuestion it asksCostNeeds a trained model on your data?
Exact / perceptual dedupAre these the same bytes or pixels?Nearly freeNo
SemDeDupDo I already have something that means this?One embedding pass + k-means + within-cluster pairsNo — any pretrained embedder works
CLIP-score filteringDoes this caption describe this image?One embedding pass on each modalityNo — needs a pretrained aligner
Forgetting / influence pruningDoes the model need this example to learn?At least one full training run, often severalYes

The third column explains the adoption pattern. Influence-based pruning is more principled and it is barely used at web scale, because it requires the training run you were trying to make cheaper. Embedding-based curation is a strictly worse approximation that costs a single forward pass, and that ratio is why it won.

Choosing k, and what going wrong looks like

The number of clusters is the only free parameter besides the threshold, and both failure directions are instructive.

k too smallk about rightk too large
Clusters are huge, so the within-cluster quadratic is still painful. With k = 100 on 440M items you get 4.4M per cluster and 9.7 × 1012 pairs per cluster — you have saved a factor of 100 on an intractable number and it is still intractableClusters hold a few thousand to a few tens of thousands. The similarity matrix fits comfortably in memory and duplicates reliably co-assignClusters hold a handful of items each. Duplicate pairs increasingly land on opposite sides of a boundary, and recall falls. At k = N you have k-means with one item per cluster and find nothing

The rule of thumb that balances these is k ≈ √N, which makes cluster size also ≈ √N and equalises the two costs (the linear assignment pass and the quadratic within-cluster pass). For 440M that would suggest about 21,000 clusters; the paper used 50,000, which pushes cluster size down to 8,800 and buys a smaller memory peak at slightly higher clustering cost. Both are in the sensible band, and the result is not sensitive within it.

The real constraint on k is a memory target, not a theory. Pick the largest cluster size whose similarity matrix you are happy to hold: m2 × 2 bytes. At m = 8,800 that is 155 MB; at m = 20,000 it is 800 MB; at m = 50,000 it is 5 GB. Then set k = N / m. That is the actual decision procedure, and it is a systems decision wearing an algorithms hat.

Thresholds do not transfer, and here is the proof

One last warning before the results. The value of ε that removes 50% of a LAION subset with a particular CLIP image tower will remove some entirely different fraction of your corpus with a different encoder, because the two models produce different similarity distributions.

Chapter 3's random-cosine argument gives the intuition: the spread of similarities depends on the effective dimensionality of the representation, and the location of the "duplicate" mode depends on what the encoder was trained to consider identical. Two encoders can agree perfectly on the ordering of pairs and disagree completely on the numerical threshold that separates duplicates from neighbours.

always calibrate ε as a percentile of your own distribution, never as a number copied from a paper

Third worked example: a six-item cluster, step by step

The three-vector example showed the survivor rule. This one shows the greedy pass that implements it, because the mechanics have a subtlety worth catching.

A cluster of six items, with these cosines to the cluster centroid:

Itemcos to centroidRank, most central first
i₁0.99881
i₂0.99662
i₃0.99233
i₄0.98714
i₅0.97025
i₆0.93106

Suppose the pairwise cosines put i₁, i₂, i₃ all above ε = 0.95 with one another, i₄ and i₅ above threshold with each other, and i₆ above threshold with nothing.

Run it with the SemDeDup rule — farthest survives, so process least-central first: i₆, i₅, i₄, i₃, i₂, i₁.

StepItem consideredActionKept so far
1i₆Still kept. No later item is above threshold with iti₆
2i₅Still kept. Absorbs i₄i₆, i₅
3i₄Already removed. Skipi₆, i₅
4i₃Still kept. Absorbs i₂ and i₁i₆, i₅, i₃
5–6i₂, i₁Already removed. Skipi₆, i₅, i₃ — 3 of 6

Now run it with the opposite rule — closest survives, so process most-central first: i₁ keeps and absorbs i₂ and i₃; i₄ keeps and absorbs i₅; i₆ keeps. Survivors: i₁, i₄, i₆.

Same count — three — different members. The removal rate is identical, so a dashboard reporting only "50% removed" cannot distinguish the two runs. The difference is entirely in which three survive, and that is what shows up months later as a model that is good at prototypes and bad at edges.

The subtlety: duplicate groups are not always cliques

Everything above assumed the duplicate relation is transitive within a group. It is not, and the failure is instructive.

Consider three items with:

cos(a, b) = 0.96,   cos(b, c) = 0.96,   cos(a, c) = 0.91

At ε = 0.95, a and b are duplicates, b and c are duplicates, but a and c are not. This is a chain, not a clique, and it is extremely common: near-duplication is a similarity relation, and similarity relations are not transitive.

Now watch the greedy pass produce two different answers depending on order:

Processing orderWhat happensSurvivors
a, b, ca is kept; b is above threshold with a, so b is removed; c is compared only against the kept item a, and 0.91 < 0.95, so c survivesa, c
b, a, cb is kept; a is removed (0.96); c is removed (0.96)b

One order keeps two items, the other keeps one. Neither is wrong — there is no ground truth about how many distinct things a chain contains — but it means the ordering rule is doing two jobs, not one: it decides which member of a group survives, and it partly decides how the groups are drawn in the first place.

Why this is acceptable, and when it is not. At web scale, chains are rare relative to tight knots, and the choice is stable enough that the aggregate removal rate barely moves. But if your corpus is a smooth manifold rather than a set of knots — frames from a video, a time series, a document revision history — then everything is a chain, greedy dedup becomes strongly order-dependent, and you should not be using this method. Use explicit segmentation or a sampling rate instead. The diagnostic is the histogram from Chapter 3: no separated spike means no knots, which means no duplicates in the sense this algorithm assumes.
SemDeDup clusters first and then compares only within clusters. What does this approximation cost, and why is that cost acceptable?

Chapter 3: What a Hash Cannot See

Every data pipeline already deduplicates. Ask an engineer and they will tell you they hash the files and drop collisions. So a fair challenge to Chapter 2 is: what exactly does an embedding buy that a hash does not, and is it worth a GPU pass over the corpus?

To answer that properly we have to build a perceptual hash from zero, by hand, and find its edges.

The duplicate ladder

RungConcrete exampleCaught by
1. Byte-identicalThe same JPEG served from a CDN and its originSHA-256 of the file
2. Re-encodedThe same photo saved at JPEG quality 80 instead of 95Perceptual hash
3. Geometrically editedResized, ten pixels cropped, a watermark corner addedPerceptual hash — sometimes
4. Same scene, different frameTwo shots from the same photo session, half a second apartOnly an embedding
5. Same content, different renderingA product photo and a 3D render of the same productOnly an embedding
6. Same meaning, different mediumAn English caption and its German translationOnly a multilingual embedding

Rungs 1 to 3 are the pixel-level regime. Rungs 4 to 6 are the semantic regime, and the jump between them is the jump this chapter is about.

Building a perceptual hash by hand

The simplest useful perceptual hash is the difference hash, dHash. The recipe is four lines of prose:

1. Destroy resolution
Convert to greyscale and resize to a tiny grid — the standard is 9 columns by 8 rows. Everything below that scale is now unrepresentable, so compression artefacts and small edits vanish.
2. Compare neighbours, do not measure them
For each row, compare each pixel to the one on its right. Emit a 1 if it is brighter, 0 otherwise. 8 comparisons per row × 8 rows = 64 bits.
3. That is the hash
64 bits. Two images are near-duplicates if their bits differ in few places — the Hamming distance — typically a threshold of about 5 out of 64.

Step 2 is the clever one. By storing only relative brightness, the hash becomes invariant to anything that shifts or scales brightness uniformly. Let us prove that on real numbers.

Worked example: 16 bits by hand

Take a 5-wide, 4-tall greyscale patch (which gives 4 comparisons per row, 16 bits total — a miniature dHash you can compute in your head):

RowPixelsComparisons (is each pixel brighter than its right neighbour?)Bits
110, 40, 30, 90, 8010>40 no · 40>30 yes · 30>90 no · 90>80 yes0 1 0 1
220, 25, 60, 55, 5020>25 no · 25>60 no · 60>55 yes · 55>50 yes0 0 1 1
3200, 190, 100, 110, 120200>190 yes · 190>100 yes · 100>110 no · 110>120 no1 1 0 0
45, 15, 15, 40, 355>15 no · 15>15 no · 15>40 no · 40>35 yes0 0 0 1
hash = 0101   0011   1100   0001

Test 1: brighten the whole image by 20. Row 1 becomes 30, 60, 50, 110, 100. Comparisons: 30>60 no, 60>50 yes, 50>110 no, 110>100 yes → 0101. Identical. And it will be identical for every row, because adding a constant to both sides of an inequality never changes the inequality. Hamming distance 0.

That is a genuine, provable invariance, and it is why perceptual hashing works at all. Exposure changes, gamma shifts, and mild recompression all move pixel values without reordering neighbouring pixels.

Test 2: crop one column from the left. Now each row is shifted, with a new column arriving on the right. Row 1 becomes 40, 30, 90, 80, 75:

RowShifted pixelsNew bitsOld bitsBits differing
140, 30, 90, 80, 751 0 1 10 1 0 13
225, 60, 55, 50, 450 1 1 10 0 1 11
3190, 100, 110, 120, 1151 0 0 11 1 0 02
415, 15, 40, 35, 300 0 1 10 0 0 11
Total Hamming distance7 of 16 bits (44%)

Seven bits out of sixteen. Scaled to the standard 64-bit hash that is about 28 differing bits, against a duplicate threshold of 5. The crop is rejected. Same photograph, ten pixels narrower, and the hash calls it a different image.

The structural reason, and it generalises. A hash of this family encodes positions. Every bit is a statement about a specific pair of coordinates. Any transformation that moves content relative to the grid — crop, pad, rotate, change aspect ratio, add a border — renumbers the coordinates and scrambles the bits. Real perceptual-hash pipelines patch this with tricks (hash several crops, hash at several scales, use frequency-domain variants like pHash that are somewhat more robust), and every patch multiplies the index size and pushes the failure a little further out. None of them make the hash content-aware, because there is no content in a comparison of two adjacent brightness values.

Test 3: a second photograph of the same sneaker, taken from a slightly different angle. Every pixel is different. The dHash bits are effectively a fresh random 64-bit string. Expected Hamming distance to the original: 32 of 64. This is not a near-miss — it is indistinguishable from comparing two unrelated images. A perceptual hash is not merely bad at rung 4 of the ladder; it has no signal there at all.

The text side: MinHash, and the S-curve

Text pipelines use a different tool with the same shape, and it is worth deriving because the tradeoff it exposes is exactly the one an embedding threshold faces.

Represent a document as the set of its overlapping word n-grams (shingles). Similarity between documents is the Jaccard index:

J(A, B) = |A ∩ B| / |A ∪ B|

The MinHash trick: apply a random permutation to the universe of shingles and record the minimum element of each set. The probability that two sets have the same minimum is exactly J. That single fact turns a set-overlap computation into a coin flip you can repeat.

Now the indexing scheme. Take r · b hashes, arrange them in b bands of r rows, and declare two documents candidates if they match on all r hashes of any band. The probability of being flagged is:

P(flagged) = 1 − (1 − Jr)b

Work it with r = 5 and b = 20. For a truly near-duplicate pair with J = 0.8:

Jr = 0.85 = 0.32768
(1 − 0.32768)20 = 0.6723220 = e20 · ln(0.67232) = e−7.940 = 0.000355
P = 1 − 0.000355 = 0.99964

And for a merely related pair with J = 0.3:

Jr = 0.35 = 0.00243
(1 − 0.00243)20 = e20 · ln(0.99757) = e−0.0487 = 0.95251
P = 1 − 0.95251 = 0.0475

Eighty-percent-similar documents are caught 99.96% of the time; thirty-percent-similar documents are caught 4.75% of the time. That is a beautifully sharp threshold, produced entirely by choosing r and b, and it is the reason MinHash-LSH is the backbone of every large text-dedup pipeline.

And here is the limitation, in one sentence. Jaccard over word shingles is a measure of surface overlap. Two documents that say exactly the same thing in different words have J near zero. Translate an article and J is exactly zero. Paraphrase it and J might be 0.1. MinHash inherits this completely — it is an exact estimator of a similarity that does not measure meaning. Embedding cosine measures meaning and cannot be estimated by a coin flip, which is the whole trade: cheap-and-syntactic versus expensive-and-semantic.

The simulation: three verdicts, one image

Perceptual hash versus embedding cosine

Left: a reference 9×8 greyscale patch and its transformed twin. Middle: the two 64-bit dHashes, with differing bits lit in red. Right: the two verdicts — Hamming distance against a threshold of 5, and embedding cosine against a threshold of 0.95. Step through the transforms. The hash bits are computed live from the pixels; the cosine values are representative measured magnitudes, marked as such.

Walk the buttons in order and watch the two verdicts diverge. "Brighter" is the case the hash was designed for: zero differing bits. "Cropped" is the case that breaks it: the picture is obviously the same and the hash says no. "Re-shot" is the case the hash cannot even attempt. And the last two are the interesting ones, because they are where the embedding starts making decisions you have to think about.

The threshold is a policy, not a fact

Two photographs of the Eiffel Tower taken by different tourists on different days will sit at a high cosine — plausibly above 0.95 in a CLIP-style space. Are they duplicates?

It depends entirely on what you are training. For a general vision-language model, ten thousand near-identical Eiffel Tower shots are redundancy and you should keep a handful. For a landmark-recognition system, that variation across lighting, season, and viewpoint is the training signal and deleting it is self-harm. The cosine is the same number in both cases. Only the decision differs.

So calibrate, and calibrate against something real. Here is what that looks like on a small labelled sample: take 200 random pairs from your corpus that are above cosine 0.80, label 20 of them by hand as duplicate or not, and count.

εPairs flaggedTrue duplicates among themPrecisionRecall (of 10 true)F1
0.851499/14 = 0.6439/10 = 0.9000.750
0.901088/10 = 0.8008/10 = 0.8000.800
0.95666/6 = 1.0006/10 = 0.6000.750

Check the F1 at ε = 0.85 by hand: 2 · (0.643 · 0.900) / (0.643 + 0.900) = 2 · 0.5787 / 1.543 = 1.1574 / 1.543 = 0.750. At ε = 0.95: 2 · (1.000 · 0.600) / 1.600 = 1.200 / 1.600 = 0.750. The middle threshold wins on F1.

And now the honest caveat, which is more useful than the table. F1 on hand-labelled duplicate pairs is the wrong objective. Nobody is paying you to identify duplicates correctly; they are paying you for downstream model quality per unit compute. Those two objectives come apart in a specific way: false negatives (a duplicate you missed) cost you a little wasted compute, while false positives (a unique example you deleted) cost you coverage you can never recover. The costs are asymmetric, so the optimal threshold is higher than the F1-optimal one. Use the labelled sample to understand the shape of your cosine distribution, then pick the operating point from your budget — which is exactly the procedure in Chapter 8.

What the embedding actually encodes, and where it came from

One more thing to be precise about, because it determines which duplicates your pipeline can see. A CLIP-style image embedding is invariant to roughly what its training augmentations and its training data made it invariant to. Random-resized-crop during training teaches it to ignore framing. Colour jitter teaches it to ignore white balance. Contrastive training against captions teaches it to ignore anything a caption would not mention.

That last clause is the load-bearing one. The embedding's notion of "same" is inherited from the supervision that built it. If captions never distinguish two things, the embedder will not either, and your deduplicator will treat them as duplicates.

Property of an imageDo captions mention it?Does a CLIP embedding separate on it?Consequence for dedup
The main objectAlmost alwaysStronglyReliable
Scene and settingOftenYesReliable
Fine-grained species or modelSometimesWeaklyRisk of deleting genuinely distinct fine-grained examples
Exact count of objectsRarely and unreliablyPoorlyThree sheep and five sheep may be "duplicates"
Precise spatial arrangementRarelyPoorlyLayout variation gets flattened
Rendered text in the imageSometimes, and often verbatimVery stronglyText-heavy images cluster by their words, not their pictures

That last row became a small research finding of its own: a substantial share of high-scoring web image-text pairs score highly for a boring reason — the caption is a transcription of text visible in the image. Masking the rendered text before scoring changes which pairs a filter keeps, and improves the resulting model. Hold that thought; it returns in Chapter 5 as one of the ways a leaderboard teaches you something the metric alone would not.

Practical corollary you can act on today. Choose your dedup embedder to match what you are protecting. Deduplicating a general image corpus? A CLIP image tower is right, because it encodes the semantics captions care about. Deduplicating a fine-grained corpus of bird species or circuit boards? CLIP will cheerfully collapse classes you need, and a self-supervised feature extractor such as a DINO-family model — which is trained to separate instances rather than to match captions — is the safer choice. There is no universal "the embedding." There is only "the embedding whose invariances match your task's."

Choosing an embedder by its invariances

"Use an embedding" is not advice. Which embedding decides which duplicates you find and which distinctions you destroy. Here is the family tree, ordered from most semantic to most literal.

Encoder familyTrained toInvariant toRight for
Supervised classifier featuresPredict one of C classesEverything that does not change the class — aggressively soAlmost nothing here. It collapses within-class variation, which is usually what you are trying to preserve
CLIP-style cross-modalMatch an image to its captionAnything a caption would not mention: exact counts, precise layout, fine subcategoryGeneral web corpora, where "would a person describe these the same way?" is the right question
Self-supervised instance features (DINO family)Make augmented views of the same image agreeCrop, colour, scale — but it separates instances, not classesFine-grained corpora, retrieval curation, anywhere you must not merge two similar-looking-but-distinct things
Copy-detection descriptorsDetect that one image is a derivative of anotherCompression, resize, crop, overlay, colour shift — and nothing moreDecontamination against evaluation sets, and copyright or provenance work

Read the invariance column top to bottom and you are reading a dial from "same meaning" to "same origin." Those are different questions, and a pipeline usually needs answers to both, from different models.

The mistake this table is meant to prevent. Reaching for whichever encoder is closest to hand — usually a CLIP image tower, because it is everywhere — and running it against a fine-grained corpus. CLIP will happily report cosine 0.97 between two different bird species, two different circuit-board revisions, or two different people in the same uniform, because a caption would describe them identically. Deduplicate on that and you have deleted your task. The failure is silent: your removal rate looks reasonable and your model quietly loses a capability.

What each stage costs, and why the pipeline has two

Almost every production pipeline runs a cheap syntactic pass followed by an expensive semantic one. The arithmetic explains why.

Exact hashing over a billion files is a checksum per file. It is disk-bound, not compute-bound, and it costs essentially nothing beyond the read you were doing anyway.

Perceptual hashing is a decode, a resize to 72 pixels, and 64 comparisons. Thousands of images per second per CPU core. A billion images is a few hundred core-hours — a rounding error.

Embedding is a neural forward pass. At a few thousand images per second on a single accelerator, a billion images is on the order of a hundred GPU-hours — a day on a small node, and real money at billion scale.

StageCost for 109 itemsCatches ladder rungs
Exact hashFree (part of the read)1
Perceptual hashA few hundred CPU-core-hours2, and some of 3
Embedding + clusteringOrder 100 GPU-hours, plus the index3, 4, 5, 6

So you run the cheap ones first, and not only to save money. The cheap pass also shrinks the expensive one. If perceptual hashing removes 12% of the corpus, the semantic stage's within-cluster comparison count — which is quadratic in cluster size — falls by:

(1 − 0.12)2 = 0.882 = 0.774  →   a 23% reduction in pairwise work

Twelve percent off the item count buys twenty-three percent off the quadratic. That leverage is the general reason cheap filters go first, and it applies to every stage ordering decision in Chapter 8.

Reading a cosine histogram

Before you pick any threshold, plot the distribution of nearest-neighbour cosines across a random sample. It is the single most informative picture in this whole lesson, and it usually has three distinguishable regions.

RegionTypical locationWhat lives thereWhat to do
The bulkA broad hump, often 0.4–0.8Ordinary unrelated and loosely-related items. This is your corpus being a corpusNothing. Any threshold here is deleting your dataset
The shoulderUsually 0.85–0.95Genuinely related items: same topic, same scene type, same template familyThis is where the judgement call lives. Sample and read before cutting
The spikeOften a distinct mass above 0.97Near-copies. Frequently a visible separate modeCut here first. It is almost always safe and often surprisingly large

If your histogram has a clean spike separated from the shoulder by a valley, you have been handed your threshold: put it in the valley. If it does not — if the density decreases smoothly all the way to 1.0 — then there is no natural notion of "duplicate" in your corpus under that embedder, and every threshold is a policy decision you are making rather than discovering. Both outcomes are useful to know, and you cannot know either without the plot.

A diagnostic that takes one line and catches a whole class of bug. Compute the fraction of items whose nearest-neighbour cosine exceeds 0.999. If that number is meaningfully above zero and you have already removed exact duplicates, you probably have a data-loading bug — the same item read twice under different keys — rather than a curation finding. Real near-duplicates cluster around 0.97–0.995. Exact ties at 0.9999 are usually your pipeline talking to itself.

Worked example: one word, and Jaccard falls off a cliff

The MinHash section gave the indexing mathematics. Here is the underlying similarity computed by hand, because the number is more shocking than the formula suggests.

Two sentences:

S₁ = "the cat sat on the mat"    S₂ = "the cat sat on a mat"

Shingle each into overlapping word triples. S₁ has six words, so four triples:

S₁ shinglesS₂ shinglesShared?
the cat satthe cat satyes
cat sat oncat sat onyes
sat on thesat on ano
on the maton a matno
|intersection| = 2,   |union| = 4 + 4 − 2 = 6
J = 2 / 6 = 0.3333

One word changed out of six, and the Jaccard index reads 0.33 — the same value you would get from two documents sharing a third of their content. Now push that through the banding formula with r = 5 and b = 20:

Jr = 0.33335 = 0.004115
(1 − 0.004115)20 = e20 · ln(0.995885) = e−0.08246 = 0.92085
P(flagged) = 1 − 0.92085 = 0.0792

Under 8%. The index will miss this pair more than nine times out of ten. Meanwhile a sentence embedder would place these two at a cosine somewhere around 0.97, and any semantic deduplicator would catch them immediately.

The lesson is not that MinHash is bad. It is doing exactly what it promises: estimating shingle overlap, fast, at any scale, with a tunable sharpness. The lesson is that shingle overlap is the wrong quantity whenever paraphrase counts as duplication. Web text pipelines use MinHash because it catches the dominant case — syndicated and copy-pasted text, where the overlap really is near-total — at a cost so low it is nearly free. They then run a semantic pass on the survivors, because the two methods fail in completely different places. Cheap and syntactic first, expensive and semantic second, exactly as the cost table above prescribes.

Reading the three thresholds side by side

One table to hold the whole chapter. Same pair of items, three instruments, three answers — and each answer is correct for a different question.

PairPerceptual hash / shinglesEmbedding cosineWhich is right?
Same file, re-encodedDuplicateDuplicateBoth. Remove without thinking
Same photo, croppedNot a matchDuplicateThe embedding. The hash is wrong here and this is its main failure
Same sentence, one word changedNot a matchDuplicateThe embedding, usually — unless the changed word was "not"
Two photos of the same landmarkNot a matchDuplicateDepends on your task. This is the policy case, not the accuracy case
Two questions about the same topicNot a matchOften "duplicate"The hash. These are genuinely different items and the embedding is over-merging
An evaluation item and its near-copy in the poolDuplicate, if it survived re-encodingDuplicateUse a copy-detection descriptor — a third instrument, for a third question

Rows four and five are the ones to sit with. In both cases the embedding says "duplicate" and the correct action differs, because the question being asked differs. No threshold can distinguish them, because the distinction is not in the data — it is in what you intend to build.

Two more instruments, and where they sit

The ladder in this chapter had two rungs of tooling — hashes and embeddings. Two more show up in real pipelines and it is worth knowing where they fall.

InstrumentWhat it measuresWhere it sits
SimHashCosine similarity of a sparse feature vector — usually term counts — compressed to a short bit string, compared by Hamming distanceBetween shingles and embeddings. Cheaper than an encoder, and it degrades gracefully with small edits where exact shingle matching does not
Learned binary hashingA trained map from an embedding to a short code, so that Hamming distance approximates cosineAn index for embeddings, not a separate notion of similarity. Use it to make a semantic pass cheap, never to replace one

The second row is a common source of confusion. Compressing an embedding to 64 bits does not turn a semantic comparison into a perceptual one; it is the same similarity, approximated. Whereas a perceptual hash computes a genuinely different quantity. Same output type — a short bit string with a Hamming distance — and completely different meaning.

The same ladder in other modalities

ModalityThe cheap instrumentThe semantic instrumentThe modality-specific trap
AudioSpectral fingerprinting — the technology behind song identificationAn audio encoder's embeddingThe same recording at a different pitch or tempo defeats naive fingerprints and not embeddings
VideoPer-frame hashes, aggregatedA clip-level embeddingEverything is a chain: adjacent frames are near-duplicates by construction. Segment first, then dedup across segments
CodeToken-level or AST-level exact matchingA code embeddingRenaming variables defeats token matching entirely, and forks share a heavy majority of their content legitimately
TabularExact row matching on a keyAn embedding over the row's fieldsTwo rows can be genuinely distinct records of the same event. Deduplicating them destroys count-based signals

Read the last column across all four. Every modality has a case where "these two items are similar" and "these two items are redundant" come apart for a reason specific to that medium. There is no general answer, and the video row — where near-duplication is the natural state of the data — is the clearest warning that the method in Chapter 2 assumes a corpus made of knots, not of continua.

A dHash is provably unchanged when every pixel of an image is brightened by the same amount, yet it changes drastically when the image is cropped by one column. What single property of the hash explains both facts?

Chapter 4: Benchmark the Filter

Chapter 2 gave us a curation method. It did not give us a way to know whether it was any good, and that gap is larger than it sounds.

Suppose you invent a filter. You apply it, train a model, and get 61% on your evaluation. Is that good? Compared to what? You could train an unfiltered baseline — but then you have used more data, so the comparison is confounded by dataset size. You could match the dataset size by randomly subsampling — better, but now your compute differs from every published number, so you cannot compare to anyone else's filter. And if a competing filter is evaluated by a different lab with a different learning-rate schedule, the comparison is meaningless.

This is why curation stayed folklore for a decade. There was no shared instrument. Every dataset paper reported "we filtered for quality" and a final number, and the number was entangled with a dozen other choices. Two labs could not tell whose filter was better, so nobody could climb a gradient. Compare with architecture research, where ImageNet made every convolution idea directly comparable and the field improved rapidly for eight years.

The inversion

Standard machine-learning benchmarks fix the data and let you vary the model. ImageNet hands you 1.28 million labelled images and asks for the best classifier. GLUE hands you nine tasks and asks for the best language model.

DataComp does the exact opposite. It fixes the model, the training code, the hyperparameters, the compute budget, and the evaluation suite. The only thing you are permitted to change is which subset of a fixed pool you train on.

Classical benchmark
Data fixed → you submit a model. Progress measured in architectures, losses, and optimisers.
↓ swap what varies
DataComp
Model fixed → you submit a list of example IDs. Progress measured in filters. Your entire contribution is a boolean mask.

Sit with how strange that is. A DataComp submission is not code that runs at inference. It is not weights. It is a set of universally unique identifiers — a list saying "train on these." Everything else in the pipeline is somebody else's frozen decision.

The pieces, and why each one is shaped the way it is

CommonPool. 12.8 billion image-text pairs extracted from Common Crawl, released as the shared substrate. Note the word pool, not dataset: it is deliberately unfiltered beyond safety processing, because a pre-cleaned pool would bake the organisers' filtering opinions into everyone's starting point.

Four scales. Filtering behaviour is not scale-invariant, so the benchmark runs at four sizes, each a factor of ten apart:

ScalePool sizeSamples seenModel
small12.8M12.8MViT-B/32
medium128M128MViT-B/32
large1.28B1.28BViT-B/16
xlarge12.8B12.8BViT-L/14

The small scale is deliberately runnable by a research group with a handful of GPUs, which is the design decision that made the benchmark actually get used. A benchmark only the largest labs can enter does not produce a field.

Samples seen equals pool size. This is the single most important design choice in the whole benchmark, and it is easy to skim past. The compute budget is measured in total samples processed, and it is set equal to the size of the unfiltered pool, regardless of how much you filter away.

Why "samples seen" and not "epochs"

Imagine the alternative. Suppose the budget were "one epoch over whatever you kept." Then a filter that removes 90% of the pool trains for 90% less compute. Its resulting model would probably be worse, and you would have learned nothing about the filter, because the comparison confounded data quality with training length.

Worse, the incentive would be inverted: the winning "filter" would be the identity function, since keeping everything buys the most compute.

Fixing samples-seen removes the confound and creates the real tradeoff. Work the arithmetic at the medium scale, where B = 128 million:

Keep fractionUnique pairs KEpochs = B / KWhat the model experiences
100%128,000,0001.00Maximum diversity, maximum noise, every pair seen once
50%64,000,0002.00Half the noise, each survivor twice
30%38,400,0003.33The empirical sweet spot for CLIP-score filtering at this scale
10%12,800,00010.00Very clean, but the same 12.8M pairs ten times — memorisation territory
3%3,840,00033.33Pristine and tiny. The model runs out of things to learn

Now the curve has to be unimodal, and it is: quality rises monotonically as you filter harder, diversity falls monotonically, and the product has an interior maximum. The benchmark's job is to let people find where it is.

Concept meets realisation — what a participant actually does. You download shard metadata containing, for each pair, a UID plus precomputed CLIP embeddings for the image and the text. You compute a mask in whatever way you like — a threshold on cosine, a clustering, a trained classifier, a nearest-neighbour lookup against some reference set. You write out the surviving UIDs. Then you run the organisers' training script unchanged, which resamples your subset until it has processed exactly B samples, and the organisers' evaluation script, which reports 38 numbers. Your creativity is confined entirely to the mask, which is precisely the point.

The evaluation suite, and why it is 38 tasks

The benchmark evaluates zero-shot on 38 downstream datasets: ImageNet and several of its distribution-shifted variants, a broad transfer collection covering natural, specialised, and structured domains, plus image-text retrieval benchmarks.

Why so many? Because a single evaluation number invites the field to overfit to it, and in a data benchmark that failure mode is unusually easy to hit. If ImageNet accuracy were the only score, the optimal strategy would be "retrieve the nearest neighbours of ImageNet training images from the pool and train on those" — which is a real and very effective strategy, and also not a general-purpose curation method. Reporting a 38-task average makes narrow benchmark-shaped filters visible as a gap between two columns.

And they report ImageNet separately anyway, which is the right call. Everyone will look at ImageNet whether or not it is the headline, so hiding it would just make the comparison happen informally and badly. Publishing both columns lets you see the divergence, and Chapter 5 shows a case where the ImageNet-best filter is not the average-best filter. A benchmark that surfaces its own gaming is more useful than one that pretends to be un-gameable.

Two tracks

TrackWhat you may doWhat it measures
FilteringChoose a subset of the provided CommonPool at your scale. Nothing else.Pure selection skill on a shared pool. Perfectly controlled, directly comparable
Bring Your Own DataAssemble any training set you like from any source, subject to the same compute budgetWhether curated external corpora beat clever filtering of a web pool. Less controlled, more realistic

The two tracks answer different questions and both matter. The filtering track isolates the algorithm. The bring-your-own-data track admits that in real life you also get to choose where the data comes from, and lets external corpora compete.

What the benchmark cannot tell you

A benchmark's limitations are part of its specification, and DataComp is unusually clear about its own. Four worth internalising before you cite any number from it.

Recipe dependence. Every result is "best filter for this training recipe at this compute budget." A filter that wins at 128 million samples seen with a ViT-B/32 need not win for a different architecture, a different loss, or a ten-times-longer run. The benchmark controls the confound by freezing it, which means the conclusion is conditional on the frozen value.

Filtering is only one intervention. The track's rules permit choosing rows and forbid changing them. But some of the largest gains in the following two years came from rewriting data — generating synthetic captions for images whose alt-text was useless, for instance. That whole family of methods is outside the filtering track by construction.

Anchor contamination. Several strong baselines use ImageNet as a reference set to decide what to keep. When you then evaluate on ImageNet, part of the measurement is circular. The paper is upfront about this; readers frequently are not. Chapter 7 makes this into a general principle about feedback loops.

A web pool is a web pool. CommonPool inherits everything that is in Common Crawl, including material that people did not consent to have used and content that safety processing catches imperfectly. Face blurring and NSFW filtering were applied; they are mitigations, not solutions. Any serious engagement with web-scale curation has to hold this alongside the technical results rather than after them.

The contribution, stated precisely. DataComp did not discover a filter. It built the instrument that lets filters be compared, and then established a set of strong, reproducible baselines with that instrument. Both halves matter, and the second half is what turned it from a proposal into infrastructure — a benchmark with no strong baseline is an empty leaderboard. Chapter 5 is those baselines, including the ones that are more interesting for failing than for winning.

A note on what "same" means here

One phrase does a lot of work in this chapter and deserves pinning: "identical training." It means the same source revision of the training script, the same hyperparameters, the same random seed, the same hardware precision, and the same evaluation code — not merely the same architecture name and learning rate.

Those extra clauses are not pedantry. Precision changes, dataloader shuffling, and evaluation-script versions each move results by amounts comparable to the effects being measured. A benchmark that specifies the model but not the script has not frozen anything; it has just moved the unmeasured variance somewhere less visible.

The version of this that bites in practice. Two of your own runs, three weeks apart, "identical except for the data." In between, someone bumped a library, the evaluation set gained forty examples, and the dataloader's shuffle seed changed. The measured difference is now the sum of four effects and you will attribute all of it to the data, because that is the change you were looking for. Pin the environment, snapshot the evaluation set, and record both hashes next to the result.

What a submission looks like on disk

Abstractions become concrete when you see the file. A filtering-track entry is, literally, this:

python — the whole of a DataComp filtering submissionimport numpy as np, pandas as pd

# The organisers ship, per shard: uid, plus PRECOMPUTED CLIP embeddings.
# You never touch a pixel unless you want to.
meta = pd.read_parquet("shard_00042.parquet")      # uid, url, text, ...
img  = np.load("shard_00042_img_emb.npy")          # (n, 768) float16
txt  = np.load("shard_00042_txt_emb.npy")          # (n, 768) float16

# --- your entire contribution starts here ---
img = img / np.linalg.norm(img, axis=1, keepdims=True)
txt = txt / np.linalg.norm(txt, axis=1, keepdims=True)
score = (img * txt).sum(axis=1)                  # (n,) the CLIP score
mask  = score > THRESHOLD                            # global 70th percentile
# --- and ends here ---

np.save("my_subset_uids.npy", meta.uid.values[mask])

# Then: `python train.py --scale medium --data_dir ... --uids my_subset_uids.npy`
# and `python evaluate.py --track filtering`. You changed nothing else.

Six lines of numpy between two comment markers. That is the artifact. Everything else — the model, the optimiser, the schedule, the resampling, the 38 evaluations — is somebody else's frozen decision, identical for every entrant.

The precomputed-embedding detail is not a convenience, it is a design choice. Shipping embeddings with the pool means a participant with two GPUs can iterate on filters in minutes rather than re-encoding a hundred million images. It lowers the entry cost of the search without lowering the cost of the verification, which is exactly the right asymmetry for a benchmark: easy to try an idea, expensive to claim a result.

How training reaches the budget from a smaller set

One mechanical detail that trips people up. If you keep 38.4 million pairs and the budget is 128 million samples seen, the training loop does not run 3.33 "epochs" in the tidy sense. It resamples with replacement from your shards until the counter hits 128 million.

Two consequences worth knowing:

What the 38 evaluations cover

FamilyWhat it probesWhy it is in the suite
ImageNetBroad object recognitionThe lingua franca. Reported separately because everyone will look
ImageNet distribution shiftsSketches, renditions, adversarially-filtered natural imagesCatches filters that overfit to photographic web imagery
Broad transfer collectionNatural, specialised (medical, satellite), and structured (counting, depth) tasksThe structured tasks are where CLIP-style models are weakest, so they have the most headroom to reveal differences
RetrievalImage-to-text and text-to-image rankingMeasures the joint space directly rather than through a classifier prompt

Notice that the suite is deliberately unbalanced toward things a naive filter will not improve. That is a feature. A benchmark whose tasks all reward the same intervention cannot distinguish between two interventions.

Three ways benchmarks fail, and how this one avoided them

FailureWhat it looks likeThe design decision that prevents it here
Too expensive to enterOnly three labs can run it; no community formsFour scales, the smallest deliberately runnable on a handful of GPUs
Trivially gameableOne metric, one obvious hack, the leaderboard saturates in a month38 evaluations, a separately-reported headline, and a pool large enough that memorising the eval is not a shortcut anyone can hide
Measures the wrong artifactParticipants tune something the benchmark did not intend to isolateEverything except the mask is frozen. There is nothing else to tune
Inline concept check. A participant proposes to spend their entire effort budget on a very expensive filter — running a large captioning model over every pool image to judge alignment more accurately than CLIP score can. Is that within the rules, and is it a good idea?  …  It is within the rules: the benchmark constrains training compute, not filtering compute, and that asymmetry is deliberate because a filter is computed once and reused. It is also a genuinely good idea, and the field went there — but notice what the rule permits. A sufficiently expensive filter can smuggle in an arbitrary amount of external knowledge, including knowledge derived from other datasets. The filtering track measures selection skill at unbounded selection cost, which is realistic and worth knowing when you read a leaderboard.

The cost of entry, and why that number matters

ScaleRoughly what one training run needsWho can iterate here
small (12.8M)A few GPU-hoursA student on a single machine, dozens of ideas per week
medium (128M)Tens of GPU-hoursA small lab, a few ideas per week
large (1.28B)Hundreds to thousands of GPU-hoursA well-resourced group, confirming a hypothesis
xlarge (12.8B)A serious cluster commitmentA handful of organisations, publishing a headline result

The four-scale structure encodes a research methodology: search cheaply at small, confirm at medium, verify the trend at large, and only then spend real money. And it exposes something the field needed to know — that filter rankings are not stable across scales, so a method validated only at small scale is a hypothesis, not a result.

The counterfactual: what a field looks like without the instrument

It is worth spelling out what was actually broken, because the same breakage exists in most engineering organisations right now and is rarely named.

Without a shared instrumentWith one
"We filtered for quality" appears in an appendix and cannot be checkedThe filter is a published artifact anyone can rerun
Two labs' filters cannot be compared, so neither builds on the otherImprovements stack, because each is measured against the same baselines
Negative results are unpublishable, so everyone rediscovers the same dead endsA leaderboard makes "this obvious idea does not work" a citable fact
Gains are attributed to whatever the paper is about, usually the architectureAttribution is forced by construction, because only one thing varied
Practitioners follow lore — thresholds copied between projects that share no encoderPractitioners follow measurements, and know which numbers transfer

Read the left column and ask which of those sentences describes a decision your own team makes. Sampling policies, retrieval corpora, label pipelines, evaluation sets — all of them are usually changed without a controlled comparison, for exactly the reason data was: building the control is boring and nobody gets credit for it.

What to demand of any data claim, including your own

1. What was held fixed?
If the answer is not "everything except the row selection," the comparison is confounded and the number means less than it appears to.
2. Was compute matched in steps?
Matched epochs over differently-sized sets is the most common broken comparison in the field, and it can point either direction.
3. What is the anchor, and does the evaluation avoid it?
A filter anchored on the thing you evaluate is a partly circular measurement. Not disqualifying — but it must be stated.
4. Does it hold at more than one scale?
Filter rankings reorder across scales. A single-scale result is a hypothesis with a number attached.

Where this benchmark sits among data-centric efforts

ApproachWhat variesStrengthWeakness
Classical benchmark (fixed dataset)The modelDecades of accumulated architecture knowledgeTreats data as a constant, which it is not
Dataset paper (release a corpus)Everything, implicitlyProvides the raw material everyone needsNo way to attribute gains to any specific curation decision
Filtering benchmarkThe subset onlyPerfect attribution. Cheap entry at small scaleConditional on the frozen recipe; forbids editing rows
Data-centric competitions with a fixed modelLabels, augmentation, and selectionCloser to applied practice, where you can editLarger surface, so attribution is weaker again

Each row trades attribution against realism. The filtering benchmark sits at the attribution end deliberately, and its limitations are the price of that position rather than oversights.

The transferable insight for your own work. When you want to learn something from an experiment, freeze aggressively — more aggressively than feels realistic. Realism and attribution trade off, and you can always add realism back once you know which variable matters. The instinct to change three things at once because "that is how it will actually be deployed" produces experiments that are faithful to production and teach you nothing.

The five design choices, ranked by how much they mattered

Benchmarks are mostly design decisions. Here they are in order of consequence, which is a useful exercise to run on any benchmark you are asked to trust.

RankChoiceWhat breaks without it
1Compute measured in samples seen, not epochsEverything. Filtering would also cut compute, the identity filter would win, and no result would mean anything
2Everything except the subset is frozenAttribution. A better filter with a worse schedule loses, and nobody can tell which caused what
3Multiple scales, the smallest one cheapThe community. A benchmark only three labs can enter produces three results, not a field
4A broad evaluation suite plus a separately-reported headlineHonesty. One metric invites a narrow hack; hiding the popular metric just moves the comparison somewhere worse
5The pool is released unfilteredNeutrality. Pre-cleaning would bake the organisers' opinions into every entrant's starting point

Notice that four of the five are about removing degrees of freedom rather than adding capability. That is what benchmark design is: deciding what participants are not allowed to change, so that what they do change becomes measurable.

What a bad version of this benchmark would look like

Concretising the failure modes makes the design choices feel earned.

Bad versionWhat would happen within a month
One scale, the largestThree entries, all from organisations with clusters. No iteration, no accumulated knowledge
One evaluation, ImageNet onlyEveryone retrieves ImageNet-like images from the pool. The leaderboard measures retrieval-toward-the-eval and calls it curation
Participants may change the learning rateHalf the reported gain is schedule tuning. Nobody can separate the two, and the filters stop being comparable
Budget measured in epochsThe winner keeps 100% of the pool, because that buys the most compute. The benchmark rewards doing nothing
The pool ships pre-filtered "for convenience"Every entrant inherits one filtering opinion, and the most important decision has already been made for them
The general test to apply to any benchmark, including the ones you build internally. Ask: what is the laziest strategy that scores well? If the answer is a real solution to the problem, the benchmark is well designed. If the answer is a shortcut — keep everything, retrieve the eval set, tune the schedule — then that shortcut is what the benchmark measures, regardless of intent, and it will be found within weeks of anyone caring about the score.

The evaluation harness, in shape

Concreteness again: the other end of the pipeline is as fixed as the training end, and seeing it makes the "38 numbers" concrete.

python — the frozen evaluation side, in outlinedef evaluate_all(model, tasks):
    results = {}
    for t in tasks:                              # 38 of them
        # Class names -> prompts -> text embeddings. The classifier is BUILT
        # from language at evaluation time; no head is ever trained.
        W = build_zeroshot_classifier(model, t.classnames, t.templates)
        correct = total = 0
        for images, labels in t.loader:
            feats = model.encode_image(images)
            feats = feats / feats.norm(dim=-1, keepdim=True)
            preds = (feats @ W).argmax(dim=-1)
            correct += (preds == labels).sum().item(); total += len(labels)
        results[t.name] = correct / total
    results["average"] = sum(results.values()) / len(results)
    return results                              # 38 numbers + the mean

Two details worth catching. First, the classifier is constructed from class names at evaluation time — no head is trained on any downstream task, so nothing about the evaluation can leak into the model. Second, the average is an unweighted mean over tasks with different metrics and different chance levels, which makes its absolute value close to meaningless while leaving comparisons between entries perfectly valid, since every entry is aggregated identically.

A habit worth borrowing for internal metrics. An aggregate that is meaningless in absolute terms but valid for comparison is a perfectly good instrument — as long as everyone knows which of the two it is. Most internal "health scores" get this backwards: they are quoted absolutely ("we are at 0.72") when they are only meaningful as a delta. Write down, next to the definition, whether the number is a level or a comparison. It prevents an entire genre of argument.

One more thing the freeze bought

A subtle benefit of freezing the recipe: it made negative results publishable. If your clever filter loses to a one-line cosine threshold, that is now a citable fact rather than a private disappointment, because the comparison is unambiguous and anyone can reproduce it.

Fields without a shared instrument accumulate positive results only, which means every practitioner independently rediscovers the same dead ends. A leaderboard where obvious-but-worse ideas are visibly obvious-but-worse is doing as much work as the winning entry, and it is the part that never gets credited.

The BYOD track, and why it exists

A benchmark that only permits filtering a fixed pool answers one question well and quietly forbids another: is filtering a web pool even the right strategy? The bring-your-own-data track exists to keep that question open.

Entry typeWhat it demonstrates if it wins
A filtered subset of the shared poolSelection skill. Directly comparable to every other entry
An externally curated corpusThat where data comes from beats how you filter it — a result the filtering track structurally cannot produce
A mixture of bothThe realistic answer, and the one most practitioners need. Also the hardest to attribute

The design tension is honest and unresolved: the filtering track is rigorous and narrow, the open track is realistic and confounded, and a field needs both. Running them side by side under one compute budget is the compromise, and it lets a reader see how far pure selection gets before external data has to enter.

The question to carry away from this chapter. Not "which filter won," which changes yearly, but: what would I have to freeze to make my own data decisions comparable? For most teams the answer is uncomfortable — the training script drifts, the evaluation set grows, the mixture changes between runs — and writing the answer down is usually the highest-value hour available.
Why does DataComp fix the compute budget at "samples seen equal to the unfiltered pool size" rather than "one epoch over whatever you keep"?

Chapter 5: What Actually Wins

With the instrument built, the question becomes empirical. Somebody ran the baselines. Here is what they found, in the order that a person discovering it would find it, including the parts that contradict the tidy story.

The score everything is built on

The workhorse filter is CLIP score, and it is one line. Take a pretrained CLIP model. Embed the image with its image tower, embed the caption with its text tower, normalise both, take the dot product:

s(image, text) = fimg(image) · ftxt(text) / (‖fimg‖ ‖ftxt‖)

That number is a direct measurement of the quantity that matters: does this caption describe this image? Chapter 1 argued that mismatched pairs are the expensive failure. CLIP score is a cheap estimator of mismatch, and it needs no training, no labels, and one forward pass per modality.

Rank the pool by s and keep the top fraction. Worked example on a pool of ten pairs, sorted:

RankCaptionCLIP scoreTop 30% cut
1"a golden retriever puppy sitting in tall grass at sunset"0.336keep
2"red ceramic mug on a wooden table"0.312keep
3"snow-covered pine forest, aerial view"0.291keep
4"Nike Air Max 90 white size 10 mens"0.244drop
5"summer collection 2023"0.191drop
6"DSC_0148"0.142drop
7"click here for more"0.113drop
8"IMG_20180714_154302.jpg"0.094drop
9" "0.061drop
10"cheap shoes buy online free shipping discount best price"0.058drop

Look at rank 4. "Nike Air Max 90 white size 10 mens" is true, and useful, and it gets dropped by a top-30% cut. That is the cost of the filter made visible: it is not separating true from false, it is ranking by descriptiveness in CLIP's own idiom, and terse product metadata reads as low-descriptiveness even when accurate.

The baseline table

These are the reported filtering-track baselines at the medium scale — a 128-million pool with a 128-million-sample budget. Two columns, because they disagree.

FilterKeepsImageNetAvg. over 38
No filtering100%0.1760.258
Basic (English, caption length, image size)≈ 30%0.2260.285
LAION-2B-style filter≈ 10%0.2300.292
CLIP score, B/32 scorer, top 30%30%0.2560.328
CLIP score, L/14 scorer, top 30%30%0.2730.338
Image-based (cluster and keep ImageNet-like)≈ 25%0.2680.312
Image-based ∩ CLIP score (L/14 top 30%)≈ 8%0.2970.328

Treat the ordering and the size of the gaps as the lesson, not the third decimal place; exact values shift with scale and with which evaluation subset is averaged.

Finding 1: a one-line filter nearly doubles accuracy

No filtering gives 0.176 on ImageNet. CLIP score with an L/14 scorer at top 30% gives 0.273. That is a relative improvement of 55% from a rule you can write in an afternoon, with no training.

It is worth pausing on how much of the field's effort would be needed to find 10 accuracy points from architecture at this scale. Years. The data axis had been sitting there untouched because nobody could measure it.

Finding 2: aggressiveness has an optimum, and it is not at the extremes

Sweep the keep fraction with the CLIP-score filter and the curve rises, peaks, and falls. At the medium scale, top-30% is around the peak. Both directions from there are worse, for the two reasons Chapter 4 set up: keep more and the noise comes back; keep less and the epoch count climbs into memorisation.

And the optimum moves with scale. At larger pools you can afford to be more aggressive, because 10% of 1.28 billion is still 128 million unique pairs — plenty of diversity — whereas 10% of 12.8 million is 1.28 million, which is not. The keep fraction is not a constant of nature; it is a function of how many unique examples you end up with relative to your budget.

The rule of thumb worth memorising. Do not think in keep-percentages. Think in epochs over survivors. Empirically, the sweet spot across scales lands somewhere in the neighbourhood of two to five passes over the filtered set. Compute the keep fraction that lands you there for your budget, and start the search from that point rather than from a percentage someone quoted at a different scale.

Finding 3: combining an alignment filter with a distribution filter beats either

The two strongest individual baselines measure different things. CLIP score asks is this pair aligned? The image-based filter asks a completely different question: does this image look like the kind of image we care about? It works by clustering image embeddings and keeping clusters whose centres are near ImageNet training images.

Their intersection — keep pairs that are both well-aligned and in a desirable cluster — gives the best ImageNet result, 0.297. Two filters attacking orthogonal failure modes compose, exactly as Chapter 1's taxonomy predicts: one attacks misalignment, the other attacks distribution skew.

Finding 4: the winner depends on the yardstick, and this is the important one

Read the last two rows of the table again, carefully.

FilterImageNetAvg. over 38Verdict
CLIP score L/14, top 30%0.2730.338Wins on the broad average
Image-based ∩ CLIP score0.2970.328Wins on ImageNet

The intersection filter is better on ImageNet by 2.4 points and worse on the 38-task average by 1.0 point. There is no single winner, and the reason is not subtle: the image-based half of the intersection is anchored on ImageNet. It keeps images that look like ImageNet images. So it optimises ImageNet accuracy, and it does so partly by narrowing the dataset in a direction that costs you on the other 37 tasks.

This is the most transferable lesson in the chapter, and it has nothing to do with images. A curation filter is a statement about what you want your model to be good at. It is not a neutral cleaning operation. Anchoring your filter on a reference set makes your model better at things resembling that reference set and, at fixed compute, worse at everything else. There is no free lunch in the shape of a dataset — only a choice about where to spend it, made explicit. If your filter has an anchor, name it, and make sure your evaluation includes something the anchor does not cover.

Finding 5: a better scorer makes a better filter — but "better" is its own axis

Compare the two CLIP-score rows: swapping a B/32 scorer for an L/14 scorer moves ImageNet from 0.256 to 0.273 with the identical filtering rule. The filter is only as good as the embedding underneath it, which is the opening of Chapter 7's feedback loop.

But then a follow-up result complicated this in a genuinely surprising way. If you stop treating the filter model as a hand-me-down and instead train a network whose only job is to filter, you get much better datasets — and the crucial finding is that filtering ability and classification ability come apart. A network that is a worse zero-shot classifier can be a better data filter. The two capabilities are correlated but distinct, and optimising the wrong one wastes effort.

Why they come apart, mechanically. Zero-shot classification asks a model to place an image nearest the correct one of a thousand class prompts — a hard, fine-grained decision. Filtering asks it to rank a pair's alignment on a coarse scale so a threshold can cut. A model can be excellent at the coarse judgement and mediocre at the fine one, especially if it was trained on data chosen for breadth rather than for label precision. If you are building a filter, evaluate it as a filter: train a small model on its output and measure that. Do not assume the best available encoder is the best available scorer.

The bias CLIP score carries, and what to do about it

CLIP score is not a neutral measurement of "is this a good pair." It measures agreement in a particular learned space, and that space has a shape.

CLIP score systematically prefersWhyWhat you lose
Short, noun-heavy, alt-text-shaped captionsThat is the distribution CLIP was trained onLong, compositional, precise descriptions — often the most informative text you have
EnglishOverwhelmingly the training languageMost of the world's data
Images with legible text matching the captionCLIP reads rendered text extremely well, so caption-matches-sign is an easy high scoreCompute spent on optical character recognition dressed up as visual learning
Common, centrally-typical objectsBetter represented in the scorer's own training setThe rare tail — which is precisely what you were hoping to buy with scale

The rendered-text bias got measured directly. If you mask the text regions of an image before computing the score, a substantial share of high-scoring pairs collapse — their alignment was carried entirely by the words in the picture. Filtering on the masked score keeps a different, better set. That result is a good template for interrogating any scorer: ablate the shortcut and see how much of the score was the shortcut.

The simulation: compare the filters yourself

Filter leaderboard comparator

Left: the medium-scale baselines as reported. Switch the metric between ImageNet and the 38-task average and watch the ranking change — that reordering is Finding 4. Right: the keep-fraction sweep for CLIP-score filtering, with the epoch count over survivors annotated at each point; the curve shape is calibrated to the reported optimum near 30%, so read the shape and the position of the peak, not the individual heights.

Metric:
Keep fraction 30%

Drag the keep-fraction slider to 100% and to 3% and read the annotation: at one extreme you are seeing 128 million noisy pairs once each, at the other you are seeing 3.8 million clean pairs thirty-three times each. Neither is where the peak is. The peak is where the marginal removed example stops being noise and starts being coverage.

The whole chapter as a decision procedure

1. Measure alignment, not vibes
Compute a cross-modal (or query-document) similarity for every row. Look at the histogram before choosing a threshold. The left tail is your junk and it will be obvious.
2. Set the cut from the epoch count
Pick the keep fraction that lands you at roughly two to five passes over survivors for your compute budget. Do not copy a percentage from a different scale.
3. Compose orthogonal filters
Alignment, redundancy, and distribution are three different failure modes. Intersecting filters that attack different ones compounds; stacking two alignment filters mostly does not.
4. Name your anchor and evaluate off it
If a filter is anchored on a reference set, your evaluation must include tasks that reference set does not resemble — or you are measuring the anchor, not the filter.

Why CLIP scores are small numbers, and what a threshold of 0.28 means

The first time you compute CLIP scores you notice something odd: matched pairs score around 0.30, mismatched pairs around 0.10, and almost nothing reaches 0.5. If cosine runs to 1.0, why is everything crowded into the bottom third?

Two reasons, and both are worth understanding because they determine how to set thresholds.

Reason 1: random vectors in high dimensions are nearly orthogonal. Take two independent random unit vectors in d dimensions. Their cosine has mean zero and standard deviation:

σ = 1 / √d

For d = 512 that is 1/22.63 = 0.0442. So a cosine of 0.30 sits about 6.8 standard deviations above chance. It is an enormous signal that merely looks like a small number, because our intuition for cosine comes from two and three dimensions where random vectors are often quite aligned.

0.30 / 0.0442 = 6.79σ  versus a mismatched pair at 0.10 / 0.0442 = 2.26σ

Reason 2: the modality gap. In a contrastively trained two-tower model, image embeddings and text embeddings do not interleave. They occupy two separate cones on the sphere, with a gap between them that training never closes — because the loss only cares about relative ranking within a batch, and a uniform offset between the two clouds ranks identically. So the achievable cross-modal cosine has a ceiling well below 1.0 that has nothing to do with how well the pair matches.

The practical consequence, and it saves people a lot of confusion. Cross-modal cosines are only interpretable relative to the distribution produced by the same model. A published threshold like 0.28 is meaningless without naming the scorer, because a different model has a different gap and a different spread. Never copy a threshold across models. Always set thresholds as percentiles of your own score distribution, which is scale-free and transfers. "Keep the top 30%" survives a model swap; "keep above 0.28" does not.

Setting a percentile threshold from a sample

You cannot sort a 12.8-billion-item pool to find its 70th percentile without a very annoying amount of I/O. You do not have to. Sample.

Draw a uniform random sample of n items, sort those, and take the sample's 70th percentile as your threshold. The standard error of a sample quantile at probability p is approximately:

SE ≈ √( p(1−p) / n ) / f(q)

where f(q) is the density of scores at the quantile. The first factor is the one you control. With p = 0.7:

Sample size n√(p(1−p)/n)Practical accuracy of the cut
1,0000.0145The kept fraction lands within roughly ±1.5 points of 30%
100,0000.00145Within roughly ±0.15 points
1,000,0000.00046Within roughly ±0.05 points. Far past the point of caring

Check the first row: √(0.7 × 0.3 / 1000) = √(0.21/1000) = √0.00021 = 0.0145. A sample of a hundred thousand is already overkill for a decision whose optimum is a broad plateau. Do not build infrastructure to compute an exact quantile over a pool; sample a hundred thousand rows and move on.

Which filters compose, and which just fight

Finding 3 said that intersecting an alignment filter with a distribution filter beats either. That is not a general law about intersections, and knowing when composition helps is worth more than the specific result.

CombinationComposes?Why
Alignment ∩ distributionYesDifferent failure modes. A well-aligned pair can still be the ten-thousandth photo of a sunset
Alignment ∩ redundancyYesAlso different. Duplicates are usually well-aligned — stock photos have excellent captions
Two alignment scorersBarelyHighly correlated. The second one mostly re-ranks the same ordering and costs another pass
Alignment ∩ caption lengthNegativelyLength is already baked into the score. Stacking them double-penalises short-but-accurate text
Distribution ∩ redundancyCarefullyBoth shrink the tail if you are not watching. Instrument occupancy entropy when you run them together

The rule underneath: filters compose when they are attacking different failure modes and fight when they are correlated estimators of the same one. Before adding a filter, ask which of Chapter 1's three failures it addresses. If the answer is one you already cover, you are buying correlation, not coverage.

The other track, and the finding it produced

The bring-your-own-data track let entrants assemble a training set from anywhere, subject to the same compute budget. Two results are worth carrying.

First, well-curated external corpora are strong — unsurprising, and it establishes a useful upper reference for how far pool filtering can go.

Second, and more interesting, the thing that eventually beat careful filtering was not better selection at all. It was rewriting: generating synthetic captions for images whose alt-text was useless, so that a pair which any alignment filter would have discarded becomes a good pair instead. A discarded image is a wasted image; a recaptioned one is recovered inventory.

Why this reframes the whole chapter. Filtering treats the dataset as fixed and the subset as the decision variable. Rewriting treats the rows themselves as editable. Once you see that, "we keep 30% and throw away 70%" starts to look like an admission rather than an achievement — you paid to crawl a hundred million images and are training on thirty. The frontier moved toward repair, and the honest reading of a filtering leaderboard is that it measures how well you can select given that you refuse to edit.

Running this leaderboard on your own problem

The benchmark's structure ports to any system where you control a dataset. Five steps:

1. Freeze everything but the mask
Same model, same seed, same steps, same evaluation. If you change two things you have learned nothing. This is the hard part, and it is organisational rather than technical.
2. Fix the budget in steps, not epochs
Otherwise a filter that removes half your data also halves your compute, and you cannot attribute the difference.
3. Build a small scale you can iterate at
A version of your training run that finishes in an hour. Ten cheap experiments beat one expensive one, and this is the step people skip.
4. Evaluate on more than one thing
At minimum: your headline metric, plus one task your filter's anchor does not resemble. The gap between them is the measurement that matters.
5. Confirm the trend holds when you scale up
Filter rankings are not stable across scales. A win at your small scale is a hypothesis until it survives the real one.
Inline concept check. You run a filtering experiment at your small scale and the filtered model beats the unfiltered one by 3 points. Your colleague points out that the filtered run also finished 40% faster. What has gone wrong, and what is the fix?  …  The runs are not comparable: the filtered set was smaller, so at a fixed epoch count it saw fewer samples — the filter is being credited for a difference that includes less training. Worse, the sign is ambiguous, because the filtered model won despite less compute. Fix it by matching total optimiser steps rather than epochs, which means resampling the smaller set. Then the two runs cost the same and the only difference left is which rows were in it.

Worked example: the cut, and what it bought

Take the ten-pair table above as a miniature pool and do the whole calculation, so the abstract "top 30%" becomes arithmetic.

Finding the cut. Ten items sorted descending. A top-30% cut keeps the highest 3, so the threshold sits between the 3rd and 4th scores:

threshold = (0.291 + 0.244) / 2 = 0.2675

What it kept and what it cost. Judge each of the ten by hand as genuinely useful or not: items 1, 2, 3 and 4 are real descriptions; 5 through 10 are metadata, navigation, or keyword soup. So the pool's useful fraction is q = 4/10 = 0.40, and the kept set's is q′ = 3/3 = 1.00. Precision went to 1 and recall to 3/4 = 0.75 — we lost the true-but-terse product listing at rank 4.

The compute ledger. Suppose the budget is 10 samples seen (scaled to match the pool, as the benchmark does).

No filterTop 30%
Unique items103
Epochs1.03.33
Useful fraction of each gradient step0.401.00
Useful samples seen10 × 0.40 = 4.010 × 1.00 = 10.0
Distinct useful items available43

Two and a half times the useful compute, at the cost of one distinct useful item. On this toy pool that is clearly a good trade. Push the cut to the top 10% and the ledger flips: useful compute stays at 10.0 while distinct useful items fall to 1, so you would be showing the model one caption ten times. That is the unimodal curve, visible in five rows of arithmetic.

The quantity to actually optimise, stated once. Not precision, not recall, not the removal rate. It is useful samples seen, subject to enough distinct items that the model does not memorise them. The first term rises as you filter harder and the second falls, and the peak is where their product turns over. Every leaderboard number in this chapter is an empirical estimate of where that turn happens for one recipe at one scale.

What the filter would have kept, if you asked it differently

One more thing the ten-item table can show. Rank 4 — the accurate but terse product listing — is dropped by an alignment filter and would be kept by several other criteria:

CriterionVerdict on "Nike Air Max 90 white size 10 mens"Why
Alignment score, top 30%DropTerse metadata reads as low-descriptiveness in CLIP's idiom
Caption is not boilerplateKeepIt carries real, specific information about the object
Contains a concrete noun from a concept listKeepMetadata-balancing filters would keep it and cap its category
A recaptioning model rewrites itKeep, improved"A white Nike Air Max 90 sneaker on a plain background" scores far higher

Three of four alternatives keep it. That is the concrete case for why the leaderboard eventually moved past pure score-thresholding: a large fraction of what an alignment filter discards is not junk, it is badly captioned inventory, and discarding is only one of the things you can do with it.

What the leaderboard could not tell you

Every number in this chapter carries conditions. Five worth attaching whenever you quote them.

ConditionWhy it limits the conclusion
One training recipeThe comparison is "best filter for this architecture, schedule, and budget." A longer run changes the optimal aggressiveness, because the epoch count changes
One poolFilters are tuned, implicitly, to the failure modes of a particular crawl. A pool with different rot has a different best filter
Evaluation is English-centric and largely photographicA filter that discards non-English pairs pays no measured cost. That is a property of the yardstick, not of the filter
Selection cost is unboundedTraining compute is fixed; filtering compute is not. A very expensive filter can import an arbitrary amount of external knowledge
Rows may be selected, not editedWhich rules out the intervention that later turned out to matter most
None of these are complaints. Each is a deliberate scoping choice, and every one of them buys attribution — the thing that made the results usable at all. The failure mode is not the benchmark having conditions; it is readers quoting the numbers without them, which is how "keep the top 30%" became a rule of thumb that people apply to corpora, encoders, and budgets that share nothing with the setting it was measured in.

The three numbers to quote, if you quote anything

NumberWhat it establishesWhat it does not
17.6% → 29.7% at medium scaleThat row selection is worth more than most architectural choices at fixed computeThat 30% is the right keep fraction for you. Different scale, different scorer, different answer
0.273 on ImageNet versus 0.338 on the 38-task average, for the same filterThat there is no scalar "best filter" — every filter encodes a choice about what to be good atThat either column is the right one. That depends on what you are building
79.2% versus 75.3% at equal compute and procedureThat the data axis is live at the frontier, not just in the small-scale regimeThat curation is the only thing that mattered in that result. It is one controlled comparison, cleanly done

Three numbers, three claims, three explicit limits. If you carry the limits with the numbers, you will be a more careful reader of this literature than most of the people citing it.

The intersection filter beats CLIP-score-alone on ImageNet (0.297 vs 0.273) but loses on the 38-task average (0.328 vs 0.338). What is the best explanation?

Chapter 6: Curation by Retrieval

Everything so far has been subtraction. Start with a pool, compute a score, throw rows away. That framing has a hidden assumption: that the pool already contains, somewhere inside it, the dataset you want.

For image-text pairs that assumption is roughly fine. For a different problem it fails completely, and the fix is one of the most elegant uses of an embedding in this whole lesson.

The problem with no captions

Self-supervised visual pretraining wants a large, diverse, balanced image set. No labels needed — that is the point of self-supervision. But this creates an awkward position:

OptionSizeBalanceProblem
ImageNet-22k≈ 14MCurated, roughly balanced by classSmall, and its taxonomy is a particular carving of the visual world
Raw web crawlBillionsZipf-shaped disasterChapter 1's failure 3, at maximum strength. Vastly more data, dominated by the head
Web crawl + CLIP scoreHundreds of millionsBetter, but…Requires captions. Many images have none, and the filter inherits language bias

You cannot filter by alignment because there is no text to align with. You cannot filter by class because you have no labels and refuse to buy any. What is left?

The move: stop asking "is this image good?" and start asking "is this image like the images I already trust?" The first question needs a quality model, which needs supervision. The second needs only a similarity function — and you already have one, because any pretrained encoder gives you cosines. Curation becomes a nearest-neighbour search, and the thing you search with is a small curated seed collection.

The pipeline, with the actual numbers

DINOv2's data pipeline builds LVD-142M — 142 million images — from two ingredients.

Ingredient A: the curated seed
ImageNet-22k, the ImageNet-1k training split, Google Landmarks, and a set of fine-grained collections. Clean, human-assembled, and far too small on its own. Used purely as a set of queries — the labels are never used.
↓ and ↓
Ingredient B: the uncurated pool
Roughly 1.2 billion unique images pulled from a web crawl, after URL filtering, safety filtering, face blurring, and removal of exact and near-duplicate copies.
↓ embed both with a self-supervised ViT-H/16, cosine similarity ↓
Retrieval
For each seed image, pull its nearest neighbours out of the 1.2 billion. Union the results, deduplicate, and you have LVD-142M — a dataset with the distribution of the seed and the size of the web.

Read that last box slowly, because it is the whole idea. The seed supplies distribution; the pool supplies volume. Neither supplies labels. There is no taxonomy anywhere in the pipeline — only a query set and a cosine.

Two retrieval modes, and when each applies

Sample-based retrieval. For each query image, take its N nearest neighbours from the pool, with N typically small — four is the usual setting. Precise, and the retrieved volume scales with the number of queries.

Arithmetic: a query set of 1,000,000 images at N = 4 yields at most 4,000,000 retrieved images before deduplication. In practice fewer are unique, because popular pool images are the nearest neighbour of many queries. Good when the seed is large.

Cluster-based retrieval. When the query set is small, N = 4 gives you almost nothing. So instead: k-means the uncurated pool into a large number of clusters — on the order of 100,000 — then, for each query, take M images from the cluster the query falls into.

Arithmetic: 1.2 billion images over 100,000 clusters gives an average cluster of 12,000 images. A seed of 1,000 images touching, say, 300 distinct clusters and taking M = 1,000 from each yields up to 300,000 images from a 1,000-image seed — an amplification factor of 300.

ModeYield per queryPrecisionUse when
Sample-based, N = 4≤ 4 imagesHigh — the neighbours really do look like the queryThe seed is already large (millions of queries)
Cluster-based, M = 1,000up to 1,000 imagesLower — a cluster of 12,000 contains a rangeThe seed is small and you need amplification (a rare fine-grained collection)
The tradeoff is exactly the precision-recall dial from Chapter 3, wearing different clothes. Tight retrieval gives you images that genuinely resemble your seed, and not many of them. Loose retrieval gives you volume and drift. And note what "drift" means here: it is not necessarily bad. A cluster around a rare bird species contains other birds, other perching poses, other forest backgrounds — variation you actively want. The right M is the one where the retrieved set is still recognisably about the seed's concept but no longer a set of copies of it.

Why retrieval instead of a classifier

An obvious alternative: train a classifier on the curated seed and use it to score the pool. Keep images it confidently assigns to a seed class. Why is retrieval better?

ApproachWhat it commits toCost of changing your mind
Classifier filterA fixed taxonomy. The classifier has one output per class and can only express membership in those classesNew concept means new labels, new head, retraining. And confidently-wrong scores on anything outside the taxonomy
Retrieval filterOnly a similarity function. The "taxonomy" is whatever images you put in the query setAdd images to the seed and re-run the search. No training, no labels, no new parameters

This is the same argument as open-vocabulary classification, applied one level up. A classifier's vocabulary is welded into a weight matrix; a retriever's vocabulary is whatever you hand it at query time. Applied to models, that observation gives you zero-shot classification. Applied to datasets, it gives you a curation pipeline you can steer with a folder of example images.

The practical superpower this unlocks. Suppose your deployed model is weak on a specific thing — say, industrial equipment photographed in low light. With a classifier pipeline you would define a class, label thousands of images, and retrain the filter. With a retrieval pipeline you collect two hundred example images, add them to the seed, re-run the nearest-neighbour search, and get back a hundred thousand pool images that look like them. The whole intervention is a folder and a search. This is the single most directly useful technique in this lesson for anyone who owns a model with a known blind spot.

The engineering, which is where the real difficulty lives

"Find the nearest neighbours of a million queries in a billion-item pool" is a sentence that hides a hard systems problem. Do the memory arithmetic first, because it explains every design decision that follows.

Storing 1.2 billion embeddings at 1,024 dimensions in float32:

1.2 × 109 × 1,024 × 4 bytes = 4.9 × 1012 bytes = 4.9 terabytes

That does not fit in the memory of any reasonable GPU cluster, and streaming it from disk for every query batch is hopeless. The standard fix is product quantisation: split each vector into subvectors, learn a small codebook for each, and store one byte per subvector instead of four bytes per dimension. At 32 bytes per vector:

1.2 × 109 × 32 bytes = 3.84 × 1010 bytes = 38.4 gigabytes

A 128× compression, and now the index fits across a handful of GPUs with room to spare. Combine that with an inverted-file structure — coarse-cluster the pool, and at query time only scan the few nearest coarse clusters — and the search becomes tractable. The reported infrastructure for this job was on the order of twenty compute nodes with eight GPUs each, running for under two days.

Note what just happened: to build a dataset by retrieval you need a production-grade vector index. The curation problem is a vector-database problem, and the two skill sets are the same skill set.

Deduplicating against your own evaluation

One step in the pipeline is easy to overlook and is genuinely important: near-duplicates of benchmark test images are removed from the pool, using a dedicated copy-detection embedding built for exactly that job.

Why does this matter more for a retrieval pipeline than for a random crawl? Because retrieval actively seeks similarity. If your seed contains ImageNet training images, and the pool contains near-copies of ImageNet test images — and it does, because the web republishes everything — then a nearest-neighbour search is a machine specifically optimised for pulling those copies in. The pipeline's core mechanism is also its contamination mechanism.

The general rule, which applies to every system in this lesson. Any similarity-based curation step must be paired with a similarity-based decontamination step against your evaluation sets, using an embedder tuned for copy detection rather than for semantics. And run the decontamination after retrieval, not before: retrieval is what pulls the near-copies in. Skipping this does not produce a slightly optimistic number; it produces a benchmark result that is partly a memory test, and you will not be able to tell from the number alone.

What this bought, and the honest framing

The models trained on the retrieved set outperform the same architecture and recipe trained on the raw uncurated pool, and outperform training on the curated seed alone. The gains are largest on fine-grained and dense-prediction tasks — precisely the tasks that need the tail of the distribution, which is precisely what retrieval amplified.

The honest framing is a comparison of three quantities that people conflate:

Training setSizeDistributionResult
Curated seed onlySmallGoodLimited by size — a self-supervised model wants volume
Raw uncurated poolHugeWeb-shaped, head-dominatedLimited by distribution — more data, more of the same
Retrieved setLargeSeed-shapedBest of both, and the ablation is the paper's evidence that curation rather than scale is doing the work

That middle row is the one that should update your priors. The uncurated pool is roughly eight times larger than the retrieved set and it trains a worse model. More data, worse outcome, same everything else.

Three philosophies, one instrument

Now the three papers can be laid side by side, and the taxonomy is cleaner than it looked in Chapter 0.

PaperOperationFailure attackedWhat the embedding is used for
SemDeDupSubtract redundancyDuplicationSimilarity within one modality, to find copies
DataComp baselinesSubtract misalignmentNoiseSimilarity across two modalities, to score pairs
DINOv2 curationAdd by similaritySkewSimilarity to a reference set, to steer the distribution

Same instrument, three verbs, three failure modes. If you internalise nothing else from this lesson, internalise this table: it is a complete diagnostic checklist for any corpus you are ever handed.

The seed is a prior, and choosing it is the real decision

It is tempting to read "no labels" as "no human judgement." That is wrong, and getting it right changes how you use the method.

The seed collection is the judgement. Someone decided that a general object taxonomy, a landmark collection, and several fine-grained sets together represent "the visual world worth learning." That decision is every bit as opinionated as writing a taxonomy — it is just expressed in examples rather than in category names, and therefore never written down anywhere as a list of commitments.

Seed componentWhat it contributes to the retrieved distribution
A broad object taxonomyCoverage of everyday categories, and a rough balance across them — the counterweight to web skew
A landmark collectionPlaces, architecture, outdoor scenes at many scales and lighting conditions
Fine-grained collectionsWithin-category discrimination: the reason the resulting features work on tasks that need to tell two similar things apart
Curation without labels is not curation without values. Swap the seed and you get a different dataset, a different model, and different downstream strengths, with no line of code changed. That is the method's greatest strength — steering costs almost nothing — and its quietest risk, because a taxonomy invites scrutiny (people argue about category lists) while a folder of example images does not. If you build one of these, write down what is in the seed and why, and treat that document as part of the model card.

Retrieval overlap, and why the union is much smaller than the sum

Naively, a million queries retrieving four neighbours each gives four million images. In practice it gives far fewer, and the reason is worth computing because it determines how many queries you need.

Popular pool images are the nearest neighbour of many queries. Model this crudely: suppose the four million retrievals are spread over an effective set of six million "retrievable" images, drawn roughly independently. The expected number of distinct images pulled is:

E[unique] = P · ( 1 − (1 − 1/P)D )  ≈  P · ( 1 − e−D/P )

with P = 6,000,000 retrievable and D = 4,000,000 draws:

D / P = 4/6 = 0.6667
e−0.6667 = 0.5134
E[unique] = 6,000,000 × (1 − 0.5134) = 6,000,000 × 0.4866 = 2,920,000

Four million retrievals, 2.92 million unique images — a 27% collapse. And the effect worsens as you retrieve more: doubling to eight million draws gives 6M × (1 − e−1.333) = 6M × 0.7364 = 4.42 million unique, so the second four million draws bought only 1.5 million new images. Retrieval has sharply diminishing returns per query, which is exactly why the pipeline needs a large or well-spread seed rather than a small one queried harder.

The index, in the terms you would actually build it

The memory arithmetic above explains product quantisation. Here is the other half: even compressed, you cannot compare every query to every vector.

An inverted-file index coarse-clusters the pool into nlist partitions and, at query time, scans only the nprobe partitions whose centroids are nearest the query. With a pool of 1.2 billion, nlist = 32,768 partitions hold about 36,600 vectors each. Probing 32 of them scans:

32 / 32,768 = 0.098% of the pool = about 1.17 million candidate vectors per query

Down from 1.2 billion — a thousandfold reduction — at the cost of missing any true neighbour that fell into an unprobed partition. That is the same one-sided approximation as SemDeDup's clustering step, for the same reason, with the same acceptable failure direction: you miss a few neighbours, you never invent one.

KnobRaise it and…Typical starting point
nlist (partitions)Each partition shrinks, so scanning is cheaper — but the coarse assignment gets noisierRoughly the square root of the item count
nprobe (partitions scanned)Recall rises, latency rises linearly. The single most useful dialStart at 1% of nlist and tune against measured recall
PQ bytes per vectorAccuracy rises, memory rises linearly32 bytes is a common balance for 768–1024 dimensions
The point of dwelling on the index. Every paper in this lesson has a systems half that the abstract does not mention, and the systems half is where the method either becomes practical or does not. SemDeDup's contribution is as much "the within-cluster restriction makes it fit in 155 MB" as it is "semantic duplicates exist." Retrieval curation's contribution is as much "product quantisation makes 4.9 TB into 38 GB" as it is "use the seed as a query set." Read the engineering sections. That is where the idea becomes a thing you can do.

Steering by example: the recipe for a known blind spot

This is the most directly reusable procedure in the lesson. You have a deployed model. It fails on a specific slice. You have a large unlabelled pool.

1. Collect two hundred examples of the failure
Not labels — just examples. Screenshots from the incident, a folder of photographs, a handful of documents. Two hundred is enough; a thousand is comfortable.
2. Embed them and query the pool index
Cluster-based retrieval if the seed is this small: find which pool clusters your examples land in and pull a slice from each.
3. Deduplicate and decontaminate the retrieved set
Retrieval concentrates near-copies by construction, and it will also happily pull in near-copies of your evaluation slice. Both passes, in that order.
4. Mix, do not replace
Blend the retrieved slice into your existing training mixture at a controlled ratio. Replacing the mixture with the retrieved slice fixes the blind spot and creates three new ones.
5. Evaluate on the slice AND on everything else
The slice will improve; that was never in doubt. The question is what it cost elsewhere, and you will only know if you measured elsewhere first.

Step 4 is where this goes wrong in practice. Targeted retrieval is powerful enough that a naive application will happily rebuild your entire training distribution around one incident. Treat the retrieved slice as an ingredient with a mixing weight, not as the new dataset.

Inline concept check. You retrieve a hundred thousand images similar to your two hundred failure cases, and your model improves dramatically on that slice while its overall score barely moves. Is that a good outcome?  …  Almost certainly yes, and better than it looks. "Overall barely moved" means the hundred thousand new images did not displace enough of the mixture to hurt — you bought a capability at close to zero cost, which is the whole promise of steering by example. The outcome to fear is the opposite: a large overall gain, which usually means your retrieved slice resembles your evaluation set more than it resembles your failure mode, and you have measured contamination rather than improvement.

All three methods on one corpus, with the numbers flowing through

The three papers are usually read separately. In practice you run them in sequence, and watching a corpus pass through all three is the best summary of the lesson.

Start with a raw crawl of 1,000 million image-text pairs.

StageOperationItems afterWhat changed about the distribution
0. Raw1,000MWeb-shaped. Heavy head, noisy captions, everything republished
1. SyntacticLanguage, caption length, image size, valid encoding640MBarely changed in shape — you removed things that were not data
2. AlignmentKeep the top 30% by cross-modal cosine192MUseful fraction rises sharply. Distribution narrows toward CLIP's idiom
3. Semantic dedupCluster, compare within cluster, ε at the 0.97 knee134MThe head flattens. Effective cluster count rises, because you removed piles, not variety
4. Retrieval top-upQuery the discarded 500M with a seed of under-represented concepts158MThe tail comes back. This is the only stage that adds
5. DecontaminationCopy-detection against every evaluation set157.8MNothing about quality. Everything about whether you can believe the result

Three things to notice in that table, and each is a chapter's thesis compressed to a row.

Stage 3 raises effective diversity while removing items. That is Chapter 2's out-of-distribution result: deduplication is distribution flattening disguised as compression. Removal and narrowing are not the same thing, and conflating them is the most common misreading of curation work.

Stage 4 is the only one that can undo stage 2's narrowing. Every subtractive filter has an anchor and every anchor narrows. Retrieval is the sole operation in the toolkit that can put coverage back, which is why a pipeline of subtractions alone will always drift toward its filters' fixed point.

Stage 5 removes almost nothing and is not optional. 0.2 million items out of 158 million — a rounding error in the dataset and the difference between a measurement and a memory test.

The ordering is not arbitrary and each swap has a cost. Dedup before alignment and you under-remove, because the junk you have not yet dropped is diverse and dilutes the duplicate signal. Retrieve before dedup and you spend the quadratic on items you are about to delete. Decontaminate before retrieval and you decontaminate the wrong set, since retrieval is what pulls evaluation near-copies in. The pipeline in that table is the order the constraints force, not a convention.

What each stage costs, in one place

StageDominant costReusable across projects?
SyntacticA pass over metadata. NegligibleYes — it is just rules
EmbeddingOne forward pass per item per modality. The big one-time billYes, and this is the point. Cache the vectors and every later stage is nearly free
Alignment filteringOne dot product per pair. Free once embeddedYes
ClusteringLinear per iteration in items × clusters. Real but tractableYes — the assignment is reusable for dedup, auditing, and rebalancing
Within-cluster dedupQuadratic in cluster size. Bounded by your choice of kPartly — the threshold is a policy you may revisit
Retrieval indexBuilding and holding a quantised index. Serious systems workYes — and it is the same index your product search would use

The second row is the economic heart of everything in this lesson. One embedding pass, cached, and then five different curation questions are answered with dot products. That is why the practice consolidated around embeddings within about a year of anyone trying it seriously.

Porting retrieval curation to text and to your own domain

Nothing in the method is visual. Strip the images out and the recipe reads: embed a pool, embed a seed, retrieve neighbours, deduplicate, mix. Here is what that becomes in three other settings.

SettingThe poolThe seedWhat you get
Domain-adapting a language modelA large general web corpus you already haveA few thousand documents from your domain — internal wikis, filings, manualsMillions of general-web documents that look like your domain, without buying domain data
Building a RAG corpusEverything your crawler can reachThe questions your users actually ask, embedded as queriesA corpus shaped by demand rather than by what was easy to crawl
Fixing a model's weak sliceYour unlabelled backlogTwo hundred examples of the failureThe steering recipe above, applied to whatever you have

The middle row deserves a sentence of its own, because it inverts how most retrieval corpora get built. The usual approach is to index everything you can and hope coverage follows. Seeding retrieval with real queries makes the corpus a function of demand, and it makes the coverage gap measurable: queries whose nearest pool neighbours are all far away are exactly the topics you have nothing about.

The diagnostic that falls out for free. Embed a thousand real user queries. For each, record the cosine to its nearest corpus chunk. Sort ascending and read the bottom fifty. That list is your coverage gap, ranked, in the users' own words — and you got it without labels, without a survey, and without waiting for someone to file a complaint. It is the single highest-value hour available to anyone running a retrieval system, and it is the same nearest-neighbour query the curation pipeline runs, pointed at a different question.

The cold-start problem, and how to get around it

Retrieval curation needs a seed. What if you do not have one?

SituationWhere the seed comes from
No curated data at allCluster the pool itself and hand-pick a few examples from each of the largest clusters. Crude, but it produces a balanced seed from nothing in an afternoon
A taxonomy but no examplesUse a text-to-image or text-to-document retrieval step: embed the category names, retrieve, and let a person accept or reject. The taxonomy becomes a seed without anyone labelling
A tiny seed, tens of itemsCluster-based retrieval rather than nearest-neighbour, so each query amplifies. Then re-seed from the accepted results and repeat — a deliberate, supervised version of the Chapter 7 loop
A seed that is itself skewedBalance the seed before retrieving. The retrieved distribution inherits the seed's skew and multiplies it by the retrieval count

That last row is worth taking seriously. Retrieval amplifies whatever the seed emphasises, so a seed with three times as many photographs of one category produces a retrieved set with the same imbalance, at a hundred times the volume. Balance the seed, then retrieve. It is a cheap step that is easy to skip and expensive to undo.

Why this closes the loop back to Chapter 1

Recall the three failures and their fixes. Deduplication attacks duplication; alignment filtering attacks noise. Both are subtractive, and subtraction cannot fix skew — removing rows from a Zipf-shaped corpus leaves a Zipf-shaped corpus.

head-heavy pool − (any subset) = still head-heavy, unless the subset was chosen to rebalance

Retrieval is the operation that can put mass back where it is missing. It is the only tool in the lesson that increases the count of anything, and therefore the only one that can raise effective cluster count rather than lower it. Every complete pipeline needs at least one additive stage, and this is it.

The synthesis, in one sentence. Subtractive curation improves the ratio of signal to noise; additive curation improves the coverage of the signal — and only the second can give you a capability the pool's shape was denying you. Teams that only ever subtract end up with clean datasets that are quietly narrower every quarter, and the metric that would have shown it is the one nobody logs.

A closing contrast: taxonomy versus seed

One comparison holds this chapter together, and it is worth stating in its most general form because it recurs everywhere.

Specify by ruleSpecify by example
FormA taxonomy, a schema, a regular expression, a classifierA folder of instances and a similarity function
CoverageExactly what the rule says, and nothing outside itWhatever is near the examples — fuzzy edges, by design
Cost of changeRewrite the rule, relabel, retrain, redeployAdd examples, re-run the search
AuditabilityHigh — the rule is a written artifact people argue aboutLow — the specification is a folder, and nobody reviews folders
Failure modeConfidently wrong outside its vocabularySilently drifts wherever the seed leans

Retrieval curation trades the third row for the fourth. That trade is overwhelmingly worth making at scale — a taxonomy for the visual world is a project, a seed is an afternoon — but the cost is real and it is paid in accountability rather than in accuracy.

So write the seed down. Not the images: the reasoning. What is in it, why each part is there, what you expected each to contribute, and what you deliberately left out. That document is the taxonomy you skipped, and producing it takes an hour. Without it, the most consequential decision in your pipeline exists only as a directory listing.
DINOv2's curation uses a small curated collection as a query set to retrieve from 1.2 billion uncurated images. Why is this preferable to training a classifier on the curated collection and using it to score the pool?

Chapter 7: The Loop That Eats Itself

Something has been quietly true in every chapter and we have not named it.

SemDeDup deduplicates LAION using a CLIP embedder. CLIP was trained on a web dataset. LAION itself was assembled by filtering Common Crawl with a CLIP score. DataComp's best baselines score candidate pairs with a CLIP model trained on a different filtered web corpus. DINOv2 retrieves its training set using a self-supervised model, and the resulting DINOv2 is a better retriever for the next round.

embedder Et  →  filter F(Et)  →  dataset Dt+1  →  embedder Et+1  →  …

This is a closed loop, it has been spinning for several generations of models, and almost nobody analyses it explicitly. It is the reason curation works as well as it does, and it is the source of the field's least-discussed systemic risk.

Why the loop works at all

The first question should be why this is not obviously circular nonsense. If you filter data with a model trained on filtered data, where does new information enter?

Two answers, and both are load-bearing.

Ranking is easier than the task. The filter does not need to be as good as the model you are trying to train. It only needs to rank better than random. A mediocre CLIP model can reliably tell that "a golden retriever puppy sitting in tall grass" matches its photo better than "click here for more" does — a judgement far coarser than the fine-grained classification you actually want. So a weak model can select data that trains a strong one. The loop amplifies because each round asks less of the filter than it delivers in the student.

The pool is new information. Each round draws from a vast unfiltered reservoir the previous model never saw. The filter is not generating data; it is selecting from an external source. As long as the pool contains signal the current model lacks, and the filter is better than chance at finding it, the loop imports information rather than recycling it.

Compare with the collapse literature on generative self-training. When a model trains on its own generated outputs, there is no external source and errors compound — documented, repeatedly, as model collapse. Curation loops are structurally different: the data is real, and the model only chooses. That difference is why curation loops have been productive where naive self-generation has not. But "structurally different" does not mean "safe," and the next section is why.

Failure 1: the filter cannot keep what the embedder cannot see

An embedding is a lossy map. Two inputs the encoder maps to the same vector are, to any downstream filter, the same input. There is no threshold, no clustering, no clever aggregation that can recover a distinction the encoder discarded.

Chapter 3's table listed CLIP's weak axes: exact counts, precise spatial arrangement, fine-grained subcategory. Now push that through the loop. A dedup filter built on CLIP will treat "three sheep" and "five sheep" as near-duplicates and delete one. The next model, trained on the survivors, sees fewer counting examples and gets worse at counting. Its embeddings distinguish counts even less. The next round of filtering is more aggressive on the same axis.

Round t
The embedder is weak on some property P. Examples differing only in P look like duplicates, so they are removed.
Round t+1
The training set now has less variation in P. The new embedder learns even less about P. It is now weaker on P than its predecessor.
↓ and so on
Round t+n
P has been curated out of the corpus entirely. No number of rounds will recover it, because nothing in the loop can notice that P is missing.

This is a ratchet, not an oscillation. Blind spots are self-reinforcing, and the system has no error signal for a capability it never had.

Failure 2: distributional narrowing, quantified

The second failure is measurable, so let us measure it. Use the effective-cluster-count metric from Chapter 1.

Take a toy pool split across five clusters, with occupancies in millions of items:

counts = (40, 30, 20, 8, 2)  →   total 100, so p = (0.40, 0.30, 0.20, 0.08, 0.02)

Entropy, in bits. Each term is p · log2(1/p):

0.40 × 1.3219 = 0.5288
0.30 × 1.7370 = 0.5211
0.20 × 2.3219 = 0.4644
0.08 × 3.6439 = 0.2915
0.02 × 5.6439 = 0.1129
H = 1.9187 bits  (maximum for 5 clusters is log25 = 2.3219)
effective clusters = 2H = 21.9187 = 3.78

Now apply a filter whose scorer is better at judging the head than the tail — which is exactly what a filter trained on web data is. Suppose it keeps, in millions: 14, 12, 9, 1.5, 0.2. Total surviving: 36.7.

p′ = (0.3815, 0.3270, 0.2452, 0.0409, 0.0054)
0.3815 × 1.3904 = 0.5304
0.3270 × 1.6128 = 0.5274
0.2452 × 2.0278 = 0.4973
0.0409 × 4.6127 = 0.1885
0.0054 × 7.5191 = 0.0410
H′ = 1.7845 bits  →   effective clusters = 21.7845 = 3.45

One round of filtering cost 0.134 bits, taking effective clusters from 3.78 to 3.45 — a factor of 0.913. Small. Now compound it. If each round multiplies effective clusters by the same 0.913:

RoundCumulative factorEffective clusters
01.0003.78
10.9133.45
20.8343.15
30.7612.88
40.6952.63
50.6342.40

Five generations of "mild, sensible, individually defensible" filtering and you have lost 37% of your effective diversity. No single round looked wrong. Every round improved the immediate benchmark.

The reason this is invisible in practice. Each round is evaluated against a benchmark whose distribution also came from the head. So the rounds that shrink your tail improve your score, round after round, right up until a deployment surface that needed the tail fails in a way no offline metric predicted. Diversity collapse does not show up as a bad number. It shows up as an absent capability, which is the hardest kind of regression to detect because there is no test for it in your suite.

Failure 3: benchmark entanglement

Chapter 5's Finding 4 was a local instance of a general problem. If your filter is anchored on a reference set R, and your evaluation is drawn from something resembling R, then "this filter is better" and "this filter targets my evaluation" are the same statement and you cannot separate them.

This is not misconduct; it is arithmetic. The remedy is procedural rather than technical:

Failure 4: the retrieval-contamination coupling

Covered in Chapter 6 and worth restating as a loop property. Every round of a similarity-driven pipeline is a machine for finding things similar to your reference set. Your evaluation sets are, on the web, republished. So each round pulls evaluation near-copies into training with above-chance efficiency, and the pipeline gets better at doing so as its embedder improves. Decontamination is not a one-time hygiene step; it is a per-round requirement that gets more important as the system improves.

Four mitigations that actually work

MitigationMechanismCost
Diversity floorKeep a fixed fraction of the pool sampled uniformly at random, exempt from all scoring. Bounds how far any round can distort occupancyA slice of your budget spent on unfiltered data. Cheap insurance
Independent filterBuild the filter model on data chosen independently of the downstream evaluation, and evaluate the filter as a filter rather than as a classifierA separate training run, and the discipline not to reuse your best encoder
Metadata balancingBalance by counts against an external concept list rather than by model score — cap how many items any one concept contributes. Sidesteps the loop entirely, because no learned model is in the selection pathRequires a concept list, and coarser selection than a learned score
Per-round instrumentationLog cluster-occupancy entropy, effective cluster count, and per-cluster removal rate every round. Collapse is invisible in accuracy and obvious in theseAlmost nothing. This is the highest-value-per-line item in the lesson
Why the diversity floor works, in one line of algebra. If a fraction r of your kept set is sampled uniformly, then the surviving occupancy is a mixture q′i = (1−r)·qi + r·pi. No cluster's share can ever fall below r · pi, no matter how brutal the scorer is. A tail cluster with pool share 0.02 and a 10% floor retains at least 0.002 of the kept set every round, forever. The floor converts an unbounded multiplicative decay into a bounded one, which is the difference between "narrows a bit" and "disappears."

The simulation: run the loop and watch the tail

The curation feedback loop

Top: the loop — embedder, filter, dataset, and back. Bottom: five cluster occupancies with the entropy and effective-cluster readout. Each round applies a filter whose per-cluster keep-probability favours clusters the scorer already handles well, sharpened by the aggressiveness slider. Turn the diversity floor on and run ten rounds; then turn it off and run ten more. The head barely moves either way. Watch the fifth bar.

Aggressiveness 0.55

Two experiments to run. First: floor off, aggressiveness 0.8, ten rounds. The fifth cluster goes to essentially zero and the effective count falls by more than half. Second: floor on, same settings, ten rounds. The effective count drops and then stabilises — the floor puts a hard bottom under every cluster. The head clusters look nearly identical in both runs, which is exactly why nobody notices.

What the loop means for your own systems

You may never train a foundation model. You will almost certainly build something with this shape, because the pattern generalises well beyond pretraining.

SystemThe loopWhat collapses
RAG corpus maintained by relevance feedbackRetriever scores documents → low scorers get pruned → retriever retrained on what remainsDocuments about topics your retriever was always weak on. Your coverage gap becomes permanent
Recommender trained on logged impressionsModel ranks items → only ranked items get shown → only shown items get logged → model retrains on the logThe long tail of the catalogue. This one is well known and has a name: feedback loops in recommendation
Fine-tuning set curated by an LLM judgeJudge scores examples → low scorers removed → next model trained on survivors, next judge distilled from itAnything the judge's own training under-represented, including styles it finds unfamiliar rather than wrong
Alert triage tuned on analyst-confirmed incidentsModel surfaces alerts → analysts label what they see → model retrains on those labelsIncident classes the first model never surfaced. They will never appear in the labels

Every row is the same structure, and every row has the same fix: reserve a random, unfiltered slice; instrument diversity, not just accuracy; and keep the selector's provenance independent of the evaluation.

The actual chain, named

It helps to see that this is not a hypothetical. Trace one lineage:

GenerationThe embedderWhat it selectedWhat that trained
0A CLIP model trained on a proprietary web corpusAlt-text pairs above a cosine thresholdOpen web-scale image-text datasets
1Open CLIP models trained on those datasetsHigher-quality subsets, by score and by clusteringCurated pools with measurably better downstream results
2Models trained on the curated poolsBetter subsets still, plus purpose-trained filtering networksThe current generation
3…and so on, with each generation's selector inheriting the previous generation's blind spotsThis is where we are. Nobody has published a systematic audit of what has been lost along the way

Every arrow in that table is a real, published step, and the whole chain took about three years. The improvement is genuine and large. The unmeasured quantity is what fell off the edge at each hop.

Failure 5: the selector becomes the target

One more failure, and it is the oldest one in statistics wearing new clothes.

When a measure becomes a target it ceases to be a good measure. In a curation loop, the score is a target for the data rather than for a person, which makes it feel safe. It is not, because the selection process optimises the score exactly as hard as a person would.

Concretely: CLIP score rewards captions in CLIP's own idiom. So a filtered corpus contains disproportionately CLIP-idiom captions. So the next model trained on it is even more confident about CLIP-idiom captions and even less calibrated about everything else. So its scores are even more idiom-selective. The corpus converges toward the fixed point of the scorer — a set of examples the scorer is maximally confident about, which is not the same as a set of examples a learner would benefit most from.

The tell, and it is easy to check. Compute the score distribution of your kept set under the next generation scorer. If the distribution is tightening round after round — if variance is falling — you are converging on the scorer's fixed point rather than on quality. A healthy filtered corpus should keep a broad score distribution, because "good enough to keep" is a threshold, not an objective to maximise. If your data looks like it is maximising the filter, the filter has become the target.

The instrumentation, in code

Chapter 7's mitigations are only worth anything if they are running. Here is the whole dashboard, and it is short:

python — per-round curation health metricsdef curation_health(E_kept, E_pool, labels_kept, labels_pool, k, scores_kept):
    """Log this EVERY time you re-curate. It is the only early warning there is."""
    out = {}

    # 1. Diversity: effective cluster count, kept vs pool
    def eff(lab):
        p = np.bincount(lab, minlength=k) / len(lab)
        p = p[p > 0]
        return 2 ** (-(p * np.log2(p)).sum())
    out["eff_clusters_kept"] = eff(labels_kept)
    out["eff_clusters_pool"] = eff(labels_pool)
    out["diversity_ratio"]  = out["eff_clusters_kept"] / out["eff_clusters_pool"]

    # 2. Where the removals landed. A cluster at 90% is a data-generation bug.
    kept_c = np.bincount(labels_kept, minlength=k)
    pool_c = np.bincount(labels_pool, minlength=k)
    keep_rate = np.divide(kept_c, np.maximum(pool_c, 1))
    out["keep_rate_p05"] = np.percentile(keep_rate[pool_c > 50], 5)   # the starved tail
    out["keep_rate_p95"] = np.percentile(keep_rate[pool_c > 50], 95)

    # 3. Goodhart check: is the kept score distribution TIGHTENING each round?
    out["score_mean"] = float(scores_kept.mean())
    out["score_std"]  = float(scores_kept.std())    # falling round over round = converging
                                                    # on the scorer, not on quality
    # 4. Redundancy left behind
    out["frac_nn_above_095"] = float((nn_cosines(E_kept) > 0.95).mean())
    return out

Four numbers, logged per round, plotted over rounds. Nothing here needs a training run, and the diversity ratio alone would have caught every collapse scenario in this chapter before it cost anything.

Five symptoms that you are in a bad loop

SymptomWhat it usually means
Benchmark improves every round, but user-reported failures do notYou are optimising the benchmark's distribution. The tail your users live in is being curated away
The kept-score distribution gets tighter each roundGoodhart. The corpus is converging on the scorer's fixed point
Effective cluster count falls monotonicallyStraightforward narrowing. Turn on a diversity floor before the next round, not after
A few clusters have keep rates near zeroWhole regions of the space are being deleted. Sample them and look before accepting
The filter and the evaluation share an ancestorYour measurement is partly circular. Add an evaluation that ancestor never saw
Inline concept check. Someone argues that the diversity floor is wasteful: 10% of the budget spent on unfiltered data that is, by hypothesis, mostly junk. What is the counter-argument, in one line?  …  The floor is not buying quality, it is buying an option. Its job is to keep every region of the space non-empty so that the next generation's filter can still see that those regions exist. A capability you have curated to zero cannot be recovered by any later round, because nothing in the loop can notice it is missing — so the floor is insurance against an irreversible loss, priced at 10% of one training run.

When does the loop stop paying? A stopping rule you can derive

Rounds of curation are not free and not infinitely profitable. Here is a crude model that gives an actual stopping condition, and its crudeness is the point — you can compute it from two numbers you are already logging.

Say the value of a corpus is roughly the useful fraction times the effective diversity:

V = q · D

Each round of filtering does two things. It raises the useful fraction toward 1 by removing some share δ of the remaining junk, and it multiplies diversity by some factor ρ < 1:

qt+1 = qt + δ(1 − qt)     Dt+1 = ρ · Dt

The round is worth running only if V goes up:

Vt+1 / Vt = [ qt + δ(1 − qt) ] / qt · ρ = [ 1 + δ(1/qt − 1) ] · ρ

Set that equal to 1 and solve for the useful fraction at which rounds stop paying:

1 + δ(1/q* − 1) = 1/ρ  ⇒   1/q* − 1 = (1/ρ − 1) / δ  ⇒   q* = 1 / (1 + (1/ρ − 1)/δ)

Now plug in the numbers this chapter already produced. The toy filtering round cost a diversity factor of ρ = 0.913, and suppose each round removes a quarter of the remaining junk, δ = 0.25:

1/ρ − 1 = 1/0.913 − 1 = 1.09529 − 1 = 0.09529
0.09529 / 0.25 = 0.38116
q* = 1 / 1.38116 = 0.724

Once your corpus is about 72% useful, another round of that filter destroys more value in diversity than it creates in quality. Keep going and you are actively making the dataset worse, while your benchmark — which is drawn from the head — continues to improve.

ρ (diversity kept per round)δ = 0.15δ = 0.25δ = 0.40
0.95 (gentle filter)q* = 0.740q* = 0.826q* = 0.884
0.913 (the toy round)q* = 0.611q* = 0.724q* = 0.808
0.85 (aggressive filter)q* = 0.460q* = 0.586q* = 0.694

Read the bottom-left cell: with an aggressive filter that only slowly improves quality, rounds stop paying once the corpus is barely half useful — which is to say almost immediately. Aggressive filters have to earn their diversity cost with a large quality gain, and most of them do not after the first round.

What this model gets right and what it does not. It is right that the two effects oppose each other multiplicatively and therefore have a crossover, and it is right that the crossover depends on the ratio of the two rates rather than on either alone. It is wrong in detail: value is not literally q · D, δ is not constant across rounds, and diversity is not a scalar. Treat it as a device for asking the right question — "what is my ρ per round, and is my δ large enough to pay for it?" — rather than as a formula to obey. You are already logging both quantities if you ran the instrumentation above.

The one-round special case, which is where most people actually are

Most teams run this loop exactly once: take a pretrained embedder, filter, train. The multi-round analysis then seems irrelevant. It is not, for two reasons.

You are not at round one, the field is. Your pretrained embedder was trained on data curated by a previous generation's filter. You inherit whatever narrowing happened upstream, and you cannot see it, because the only lens you have is the one that produced it.

Deployment is a round. If you retrain periodically on data selected with the previous model — logged interactions, flagged examples, retrieved documents, human labels on what the model surfaced — you are running the loop whether or not you call it that. The instrumentation costs a few lines and the failure it detects is irreversible.

Worked example: five rounds, with and without the floor

The simulation shows this interactively; here it is as arithmetic you can check, because the mechanism matters more than the animation.

Track only the smallest cluster, whose share of the pool starts at 2%. Suppose each round the filter keeps it at a relative rate of 0.60 compared with the average cluster — a plausible penalty for a region the scorer handles poorly. Without a floor, its share is multiplied by roughly 0.60 each round and renormalised, which to a first approximation means:

RoundShare without a floorShare with a 10% uniform floor
02.00%2.00%
11.20%0.9 × 1.20 + 0.1 × 2.00 = 1.28%
20.72%0.9 × 0.77 + 0.20 = 0.89%
30.43%0.68%
40.26%0.57%
50.16%0.51%

Without the floor the cluster is heading to zero geometrically and will be gone within a few more rounds. With the floor it converges to a positive limit — solve the fixed point of x = 0.9(0.6x) + 0.1(0.02) and you get x = 0.002/(1 − 0.54) = 0.00435, about 0.44%. The floor does not preserve the cluster's original share. It guarantees the cluster continues to exist, which is the only property that matters, because existence is what a later round can build back from and extinction is not.

Read the two columns at round 5 side by side. 0.16% versus 0.51% — a difference of a third of a percentage point in a table nobody is looking at, and it is the difference between a capability that can be recovered and one that cannot. This is the characteristic shape of curation failures: the number that matters is tiny, it lives in the tail, and every aggregate metric on your dashboard is dominated by the head, which looks fine in both columns.
Why does a curation loop (filter real data with a model, train the next model on the survivors) avoid the collapse that afflicts training on a model's own generated outputs — and what risk does it still carry?

Chapter 8: Recipes You Can Run

Everything so far has been about corpora with nine or ten digits. You probably do not have one. You almost certainly have a fine-tuning set, a document collection, or an evaluation suite, and every technique in this lesson applies at that scale with better economics, because at ten thousand items you can compute things web-scale teams have to approximate.

Four recipes, each with the arithmetic worked out.

Recipe A: deduplicate a fine-tuning set

The setting: 40,000 instruction-response pairs assembled from several sources, some overlapping. You suspect redundancy and you want to know how much.

Step 1: choose k. A good default is k ≈ √N, which balances cluster count against cluster size. For N = 40,000:

k = √40,000 = 200  →   average cluster size = 40,000 / 200 = 200

Step 2: count the work. Pairs per cluster: 200 × 199 / 2 = 19,900. Across 200 clusters: 3,980,000 pairs. At 384 dimensions that is about 4 × 106 × 768 = 3 × 109 floating-point operations. Your laptop does that in under a second. Compare with the global computation: 40,0002/2 = 800 million pairs — also feasible, at 200× the cost and 3.2 GB for the matrix in fp16.

At this scale, skip the clustering. Below roughly 100,000 items, just compute the full similarity matrix in blocks and be exact. Clustering is an approximation you adopt under duress. The habit of reaching for the web-scale version of an algorithm when you have a small problem is a common and expensive mistake — you inherit the approximation's failure modes and none of its necessity. Use k-means when the quadratic actually hurts, not before.

Step 3: pick the threshold from your budget, by bisection. You do not know what ε means for your data. You do know how much you want to remove. So sweep. Here is the shape of a real sweep on a mixed instruction set — your numbers will differ, the shape will not:

εItems removed% removedCharacter of what is being removed
0.994801.2%Literal copies and whitespace variants
0.953,3608.4%Template instantiations: same prompt, different entity
0.907,84019.6%Paraphrases and same-task-different-wording
0.889,92024.8%Target reached — still recognisably redundant
0.8513,24033.1%Now removing genuinely distinct examples on the same topic. Too far

The bisection is trivial: evaluate at 0.99 and 0.85, see that 25% lies between them, try the midpoint 0.92, and narrow. Four or five evaluations gets you within a percent of any target, and each evaluation is a threshold comparison over a matrix you already computed.

Step 4: read the per-cluster removal rates. This is the step people skip and it is the most informative output of the whole procedure.

python — the diagnostic that finds your sloprates = []
for c in range(k):
    idx = np.where(labels == c)[0]
    if len(idx) < 5: continue
    removed = (~keep_mask[idx]).mean()
    rates.append((removed, c, len(idx)))

rates.sort(reverse=True)
for r, c, n in rates[:10]:
    print(f"cluster {c}: {n} items, {r:.0%} removed")
    print("  sample:", texts[np.where(labels == c)[0][0]][:140])

If the overall removal rate is 25% but one cluster is at 87%, you have found a template. Someone generated four thousand near-identical examples from one prompt skeleton. That cluster is not a deduplication problem; it is a data-generation problem, and the right fix is upstream: regenerate with more variation, or cap that cluster's contribution deliberately rather than letting a cosine threshold decide.

Step 5: verify against something real. Fine-tune on the deduplicated set with the same number of optimiser steps as the original. If held-out quality holds or improves, you have found free compute. If it drops, your threshold was too aggressive — and note that a drop is informative in itself: it means the "duplicates" carried signal, which usually means your embedder's invariances are wrong for this data.

Recipe B: filter a RAG corpus, where duplicates hurt differently

Near-duplicate chunks in a retrieval corpus cause a failure that has nothing to do with training compute, and it is worse than the training case.

Your retriever returns the top k = 8 chunks. Suppose five of those eight are near-duplicates of one another — the same policy paragraph, copied into five documents. The model receives eight chunks and four distinct facts. The fifth-ranked distinct fact, which might have been the one that answers the question, was pushed to position 9 and never retrieved.

distinct evidence delivered = 8 slots − 4 redundant copies = 4

You paid for eight chunks of context, you spent the tokens, and half of it was the same sentence. And the failure is silent: the answer looks confidently sourced, because it is sourced, five times, to one document.

Two fixes, and you want both.

Offline: deduplicate the chunks. Same as Recipe A, but with a much stricter threshold. Use ε around 0.97, because in a knowledge corpus two documents that are 90% similar often differ in the 10% that matters — a version number, an effective date, an exception clause. Deleting one is a correctness bug, not a diversity loss.

At query time: maximal marginal relevance. Select results one at a time, each time trading relevance against novelty:

MMR(d) = λ · sim(q, d) − (1 − λ) · maxd′ ∈ S sim(d, d′)

where S is what you have already selected. Work it by hand with λ = 0.7. Four candidates with similarity to the query:

sim(q,A) = 0.82,  sim(q,B) = 0.80,  sim(q,C) = 0.78,  sim(q,D) = 0.61

and pairwise similarities:

sim(A,B) = 0.95,  sim(A,C) = 0.93,  sim(A,D) = 0.42
sim(B,C) = 0.96,  sim(B,D) = 0.40,  sim(C,D) = 0.38

Round 1. S is empty, so the penalty term is zero and MMR reduces to plain similarity. Pick A at 0.82. Now S = {A}.

Round 2. Compute for each remaining candidate:

MMR(B) = 0.7 × 0.80 − 0.3 × 0.95 = 0.560 − 0.285 = 0.275
MMR(C) = 0.7 × 0.78 − 0.3 × 0.93 = 0.546 − 0.279 = 0.267
MMR(D) = 0.7 × 0.61 − 0.3 × 0.42 = 0.427 − 0.126 = 0.301

D wins, despite being ranked fourth by raw similarity, because it is the only candidate saying something A did not. S = {A, D}.

Round 3. The penalty is now the max over both selected items:

MMR(B) = 0.560 − 0.3 × max(0.95, 0.40) = 0.560 − 0.285 = 0.275
MMR(C) = 0.546 − 0.3 × max(0.93, 0.38) = 0.546 − 0.279 = 0.267

B, then C. Final order: A, D, B, C instead of A, B, C, D. One reorder, and the context window now contains two genuinely different pieces of evidence in its first two slots instead of two paraphrases.

Offline dedupRuntime MMR
CostOne-time, at indexingO(k2) similarities per query, on every query
RiskDeleting a chunk that mattered. IrreversibleNone — nothing is deleted, only reordered
Handles query-specific redundancyNo — the threshold is globalYes — two chunks can be redundant for one query and not another
RecommendationConservative (ε ≈ 0.97), for true copies onlyAlways on, λ between 0.6 and 0.8
The asymmetry worth remembering. In training, a duplicate costs you a fraction of a gradient budget and the cost is diffuse. In retrieval, a duplicate costs you a whole slot out of a small k, and the cost is concentrated on the exact query where the missing evidence mattered. That is why retrieval corpora deserve stricter dedup at inference and gentler dedup at rest: reorder aggressively, delete timidly.

Recipe C: choose thresholds from budget, never from intuition

Generalise what Recipe A did informally into a procedure.

1. Start from compute, not from data
How many samples will you process? Call it B. That number is fixed by your GPU-hours and is not negotiable.
2. Choose your epoch count
Two to five passes over survivors is the empirically useful band. Pick a target E, then K = B / E is the number of unique examples you want to end up with.
3. Bisect the threshold to hit K
Evaluate the filter at two thresholds on a 5% subsample, interpolate, and refine. Four evaluations gets you close; the subsample makes each one seconds rather than hours.
4. Sanity-check what fell out
Read fifty removed items and fifty survivors. If the removed pile contains things you would obviously want, the score is measuring the wrong thing and no threshold will fix it.

Step 4 is not optional and it takes ten minutes. Every serious data-curation failure I have seen was visible in a fifty-item sample and invisible in every aggregate.

Recipe D: what order do the filters go in?

A pipeline usually has several stages. The order changes both cost and outcome.

OrderEffect
1. Cheap syntactic filters first (length, language, resolution, encoding validity)Removes a large fraction for nearly zero cost, shrinking everything downstream. Always first
2. Alignment / quality scoringOne embedding pass per modality. Do this before dedup, because dedup's cost is quadratic in what survives
3. Semantic dedupNow operating on a smaller, cleaner set — cheaper, and it finds more duplicates, because a quality filter concentrates the distribution
4. Decontamination against evaluation setsLast, and with a copy-detection embedder rather than a semantic one. Everything upstream can pull test near-copies in
5. Diversity floor / rebalancingAdd back a uniform random slice, or cap over-represented clusters. This is the step almost nobody does

Step 3's parenthetical is a real and slightly counterintuitive effect: filtering for quality first increases the measured duplicate rate, because you have removed the diverse junk and what remains is the well-formed, frequently-republished material. If you dedup first and quality-filter second, you will under-remove duplicates and then wonder why your redundancy metrics look fine while your model memorises.

The corpus cheat sheet

CorpusEmbedderε starting pointWatch out for
Web images for pretrainingCLIP image tower0.95–0.97Fine-grained classes collapsing into one another
Fine-grained image corpusA self-supervised feature extractor, not CLIP0.97–0.99CLIP will merge species and product variants you need
Web text for pretrainingMinHash-LSH first, then a small sentence embedder0.92–0.95Paraphrase is not redundancy in text as often as in images
Instruction / fine-tuning dataA sentence embedder over prompt + response0.88–0.93Templates. Check per-cluster removal rates
RAG chunksThe same embedder you retrieve with0.97+ onlyVersion numbers and dates hiding in the 3% difference
Evaluation setsA copy-detection embedderAs strict as you can affordThis is decontamination, not curation. Different job, different tool
The one-line version of all four recipes. Embed everything once. Look at three distributions — nearest-neighbour similarity, alignment score, and cluster occupancy. Choose thresholds from your compute budget rather than from a paper's percentage. Read fifty removed items with your own eyes before you commit. Instrument diversity, not just accuracy. Every technique in this lesson is an elaboration of those five sentences.
Cross-domain bridge
Curation is a vector-database workload wearing a research hat
Every operation in this lesson is something a vector database already does. SemDeDup's within-cluster comparison is an inverted-file index's probe list. DINOv2's retrieval is a k-nearest-neighbour query with a batch of a million queries. Product quantisation appears in both for the identical reason: 4.9 terabytes does not fit and 38 gigabytes does. If you can build a production retrieval system, you already have every skill needed to curate a training corpus — you are just pointing it upstream. See our vector databases and embedding ops lessons for the indexing machinery, k-means for the clustering step, and CS336: data filtering and deduplication for the language-model side of the same story.

Recipe A, as one runnable script

python — deduplicate a fine-tuning set, end to endimport numpy as np, json

texts = [ex["prompt"] + "\n" + ex["response"] for ex in data]   # 40,000
E = embed(texts)                                    # (40000, 384)
E = E / np.linalg.norm(E, axis=1, keepdims=True)

# 1. LOOK before deciding. Nearest-neighbour cosine histogram.
S = E @ E.T
np.fill_diagonal(S, -1)
nn = S.max(axis=1)
for q in [50, 75, 90, 95, 99]:
    print(f"p{q} nearest-neighbour cosine: {np.percentile(nn, q):.4f}")

# 2. Bisect eps to a REMOVAL TARGET derived from your compute budget.
def removed_at(eps):
    keep = np.ones(len(E), dtype=bool)
    for i in range(len(E)):                       # greedy: earlier survivors win
        if not keep[i]: continue
        dup = np.where((S[i] > eps) & keep)[0]
        keep[dup[dup > i]] = False
    return keep

lo, hi, target = 0.80, 0.999, 0.25
for _ in range(8):                                 # 8 steps is plenty
    mid  = (lo + hi) / 2
    frac = 1 - removed_at(mid).mean()
    if frac > target: lo = mid
    else:            hi = mid
eps  = (lo + hi) / 2
keep = removed_at(eps)
print(f"eps={eps:.4f}  removed={1-keep.mean():.1%}")

# 3. READ FIFTY REMOVED ITEMS. Non-negotiable.
for i in np.where(~keep)[0][:50]:
    print("---", texts[i][:160].replace("\n", " | "))

json.dump([d for d, k in zip(data, keep) if k], open("deduped.json", "w"))

Note the bisection direction: raising ε removes less, so when the removed fraction is above target you move the lower bound up. Getting that backwards produces a script that confidently converges on the wrong end, and it is the single most common bug in this procedure.

Recipe E: decontaminate an evaluation set

Different job, different tool, and it belongs in every pipeline that touches web data.

The question is not "do these mean the same thing?" but "is one of these derived from the other?" Semantic similarity is the wrong instrument — two different questions about the same topic are semantically close and are not contamination. Use a copy-detection descriptor, or for text, exact and near-exact substring matching.

StepWhat to doThreshold posture
1Index every evaluation item with a copy-detection embedder (or n-gram shingles for text)
2Query the index with every training candidate
3Remove training items above thresholdErr heavily toward removal. A false positive costs one training example; a false negative costs the validity of your headline number
4Report the contamination rate you foundPublish it. It is a property of the experiment, like the seed

Worked: an evaluation set of 50,000 items, a training candidate set of 40,000,000. You find 1,900 training items above threshold. Removing them costs 0.005% of your training data. Not removing them puts an unknown fraction of your evaluation inside your training set. There is no version of that trade where you keep them.

And do it last, after every other stage. Chapter 6's warning restated as an operational rule: quality filters and retrieval both actively pull evaluation near-copies in, because famous items are well-captioned, heavily republished, and close to your reference sets. Decontaminating before those stages measures the wrong set. Decontaminate the thing you are about to train on, not the thing you started with.

Three ways this goes wrong in practice

FailureHow it presentsThe fix
Wrong embedder for the corpusRemoval rate looks fine; a downstream capability quietly disappearsMatch the encoder's invariances to what you must preserve (Chapter 3). When in doubt, use an instance-level rather than semantic encoder
Threshold copied from a paperRemoves 3% when you expected 40%, or 70% when you expected 25%Calibrate as a percentile of your distribution. Thresholds never transfer across encoders
Dedup before quality filteringRedundancy metrics look healthy, model still memorisesQuality first. Filtering concentrates the distribution, which reveals duplicates the earlier pass could not see

What to log, every time

MetricWhy
Removal rate, overall and per clusterPer-cluster outliers are data-generation bugs, not curation results
Effective cluster count, before and afterThe only cheap early warning for diversity collapse
The threshold, and the percentile it corresponds toThe percentile is the portable number. The threshold is an artifact of your encoder
Encoder name and versionChange the encoder and every number above becomes incomparable
Fifty sampled removals, saved to a fileSo that the next person — probably you, in six months — can audit the decision instead of re-deriving it
Contamination rate found against each evaluation setA property of the experiment. Report it alongside the accuracy it qualifies

Recipe F: triage a corpus somebody handed you, in thirty minutes

The most common real situation: you inherit a dataset, nobody remembers how it was built, and you need to know whether to trust it before you spend a training run on it.

MinuteDo thisYou are looking for
0–5Count rows. Count distinct rows by exact hash of the contentIf those differ by more than a few percent, the pipeline has a bug. Stop and fix it before anything else
5–10Read twenty rows sampled uniformly at random. Not the first twenty — files are sortedFormat surprises, truncation, encoding damage, a whole source you did not know was in there
10–20Embed a sample of 20,000 rows. Plot the nearest-neighbour cosine histogramA spike above 0.97 means redundancy. No spike means either a clean corpus or a smooth manifold — check which by reading the top pairs
20–25Cluster the sample into 200 groups. Print the sizes and one example from the five largestSkew, and its identity. The largest cluster is usually a template or a single dominant source
25–30Compute effective cluster count. Compare against 200A number far below 200 means the corpus is much narrower than its row count suggests

Thirty minutes and one GPU produce a paragraph you can put in a document: how many rows, how many distinct, how much near-duplication, how many effective clusters, and what the five biggest clusters actually are. That paragraph is worth more than most dataset cards.

The finding to expect, and it is remarkably consistent. On a corpus nobody has audited, the five largest clusters usually account for a share of the data that surprises everyone involved, and at least one of them is something nobody intended — a scraping artifact, one prolific source, or a template that was supposed to generate variety and did not. That discovery is the return on the thirty minutes, and it is usually actionable upstream rather than by filtering.

The thresholds table, with the reasoning attached

Numbers without reasons do not transfer, so here is why each recommendation in the corpus cheat sheet is where it is.

SettingThe reasoning
Image corpora sit lower (0.95–0.97) than RAG chunks (0.97+)Deleting one of two similar photos costs a little coverage; deleting one of two similar policy paragraphs can delete the exception clause that answers a question
Fine-grained corpora sit higher and change encoderBoth knobs matter and the encoder matters more. A stricter threshold on the wrong encoder still merges two species
Instruction data sits lower (0.88–0.93)Templated generation produces genuine redundancy at cosines well below the image regime, because paraphrase is the mechanism rather than republication
Text pretraining runs shingle-based dedup firstIt is nearly free and catches the dominant case, which really is copy-paste. The semantic pass then works on a smaller, harder residue
Decontamination has no threshold recommendationBecause the cost asymmetry is extreme. Remove anything plausible. You are trading a handful of training rows against the validity of your headline number

The mixing question nobody asks until it is too late

Curation produces a set. Training consumes a stream, and the mapping between them is a policy you are choosing whether or not you notice.

PolicyWhat it meansWhen it bites
Uniform over the curated setEvery surviving row equally likely. The implicit defaultYour filter's removal rates varied by cluster, so uniform sampling still reflects whatever skew survived
Balanced by clusterSample a cluster, then a row within itSmall clusters get heavily repeated. Effective only with a floor on cluster size
Weighted by sourceExplicit mixture weights per originRequires provenance, which most pipelines lose at the first join. Worth preserving for exactly this reason
Curriculum by scoreCleaner data first, or lastFashionable, fragile, and rarely reproduces across recipes. Try it last, not first

The honest state of practice: uniform is the default, balanced-by-cluster is the cheapest improvement, and explicit source weights are what you actually want but require provenance you probably discarded. If you are building a pipeline from scratch, carry a source identifier on every row from the very first stage. It costs a column and it is the thing you will most wish you had.

Why should a RAG chunk corpus be deduplicated with a much stricter threshold (around 0.97) than a training corpus (around 0.90), even though both use the same cosine?

Chapter 9: Legacy & Cheat Sheet

Three papers, one instrument, and a change in what the field considers a contribution. Here is what happened next and what is still broken.

One table for the whole lesson

ChapterThe questionThe instrumentThe number
1What is wrong with a web corpus?Three distributions from one embedding passTop 100 concepts of a million take 36% of the data
2How do I remove what I already have?Cluster, compare within cluster, keep the outlierClustering divides the pairwise work by exactly k
3Why not just hash it?dHash, MinHash, and their blind spotsA one-column crop flips 7 of 16 bits; one changed word takes Jaccard to 0.33
4How do I know a filter is better?Freeze everything but the maskSamples seen, not epochs. That single choice makes the rest work
5What actually wins?Alignment scoring, and composition with a distribution filter0.176 → 0.297 on ImageNet; and the average-best filter is a different one
6How do I get what I do not have?Retrieval from a curated seed1.2B pool → 142M curated, with no labels anywhere
7What does this cost over time?Effective cluster count, per roundA factor of 0.913 per round is 37% of your diversity in five rounds
8What do I do on Monday?Embed, histogram, bisect, read fifty, logk = √N, two to five epochs over survivors, ε from budget

Every number in this lesson, in one table

QuantityValueWhere it came from
LAION subset SemDeDup operated on≈ 440M pairsSemDeDup, image experiments
Clusters used for that subsetk = 50,000SemDeDup
Items per cluster440M / 50,000 = 8,800Derived, Chapter 2
Global pair count vs within-cluster9.68 × 1016 vs 1.94 × 1012 — a factor of kDerived, Chapter 2
Similarity-matrix memory, global vs per cluster193 PB vs 155 MBDerived, Chapter 2
Fraction of LAION removable≈ 50%, performance held, out-of-distribution improvedSemDeDup
Fraction of web text removable≈ 15%, perplexity held or improvedSemDeDup, text experiments
SemDeDup survivor ruleKeep the member farthest from the cluster centroidSemDeDup
CommonPool size12.8B image-text pairsDataComp
DataComp scales12.8M / 128M / 1.28B / 12.8B, samples seen = pool sizeDataComp
Medium scale, no filtering0.176 ImageNet, 0.258 avg-38DataComp baselines
Medium scale, CLIP score L/14 top 30%0.273 ImageNet, 0.338 avg-38DataComp baselines
Medium scale, image-based ∩ CLIP score0.297 ImageNet, 0.328 avg-38DataComp baselines
DataComp-1B resultViT-L/14 to 79.2% ImageNet zero-shot, +3.7 points over OpenAI's ViT-L/14 at equal computeDataComp
DINOv2 uncurated pool≈ 1.2B unique imagesDINOv2, data section
DINOv2 curated resultLVD-142MDINOv2
Retrieval modesSample-based (N ≈ 4 neighbours per query); cluster-based (k ≈ 100,000, M per query)DINOv2
Index memory, float32 vs product-quantised4.9 TB vs 38.4 GB at 32 bytes per vectorDerived, Chapter 6
MinHash-LSH sharpnessr = 5, b = 20: J = 0.8 caught 99.96% of the time, J = 0.3 caught 4.75%Derived, Chapter 3
Entropy cost of one filtering round (toy)1.9187 → 1.7845 bits; effective clusters 3.78 → 3.45Derived, Chapter 7

What the three papers actually contributed

PaperThe techniqueThe thing that mattered more
SemDeDupCluster, then compare within cluster, keep the outlierEstablishing that half of a web-scale image corpus is semantic redundancy — a number nobody had, about a category of duplicate nobody was removing
DataCompA pool, four scales, a frozen recipe, 38 evaluationsMaking a filter into a comparable scientific artifact, so curation knowledge could accumulate instead of staying tacit
DINOv2's data pipelineRetrieval-augmented curation from a curated seedShowing that a dataset can be grown by nearest-neighbour search with no labels and no taxonomy — steering by example rather than by rule

What came next

Better filters, trained on purpose
Rather than borrowing a CLIP model as a scorer, train a network whose only job is filtering. This won DataComp by a wide margin and produced the finding that filtering ability is a distinct axis from classification ability — a worse classifier can be a better filter.
Fixing the data instead of dropping it
A caption that does not describe its image is not only removable — it is rewritable. Generating synthetic captions for otherwise-good images recovers pairs the filter would have discarded, and this whole family sits outside the filtering track by construction.
Escaping the loop
Balance by metadata counts against an external concept list instead of by a model score. No learned selector sits in the path, so Chapter 7's failure modes are structurally avoided — at the cost of coarser selection.
The same benchmark for language
The DataComp design ported to text: a shared pool, frozen training recipes at several scales, and a leaderboard for language-data filters. The result was the same as in vision — careful curation beat larger uncurated corpora at equal compute.

And the sibling line of work on the deduplication side: combining semantic deduplication with diversity-aware selection for language-model pretraining, which is the direct descendant of SemDeDup and confirms the same finding in a different modality.

What is still broken

LimitationWhere it comes fromWhat would fix it
Every filter inherits its embedder's blind spotsThe embedding is a lossy map, and there is no way to recover a distinction it discardedEnsembles of embedders with different training signals; explicit diversity floors; auditing what a filter removes along axes the embedder is known to be weak on
Thresholds are policies masquerading as measurementsCosine is a number; "duplicate" is a decision about your taskNothing technical. Calibrate against downstream outcomes and write down what you decided and why
No notion of learnability or difficulty"Do I already have this?" is cheap; "does the model need this?" requires a training runCheap proxies for influence. An open problem — the principled methods still cost the run you were trying to avoid
Recipe-conditional resultsBenchmarks freeze the training recipe to isolate the dataReporting filter results across several recipes and budgets, which almost nobody does
Consent, licensing, and provenanceWeb pools contain material whose creators never agreed to thisNot a filtering problem. Provenance tracking, opt-out infrastructure, and licensed corpora — and honesty about the gap in the meantime
Curation optimises for the evaluation you haveEvery selection rule encodes a target distributionEvaluations that are deliberately off-anchor, and the discipline to report them alongside the flattering ones

Connections across the site

If you want…Go to
The machinery that makes any of this searchableVector databases and embedding ops
The clustering step, derived properlyk-means
What a cosine actually measures and its alternativesSimilarity metrics and vector embeddings
The contrastive objective these embedders are trained withContrastive learning and CLIP
The language-model side of filtering and deduplicationCS336: data filtering and dedup and data sources and pipelines
The retrieval system this all feedsRAG and multimodal RAG
The models being curated forDINOv2 and OpenCLIP

The sentence to leave with

Strip everything away and one claim remains, and it is falsifiable, which is what makes it worth holding.

At fixed compute, the composition of your training set constrains your model more tightly than its architecture does — and composition is measurable with one embedding pass and three histograms.

The first half of that sentence is what the three papers established with controlled experiments. The second half is what makes it actionable rather than merely true: the measurement is cheap, it is available before you spend anything, and nothing about it requires a frontier lab.

The uncomfortable corollary is that most teams do not make the measurement, which means most teams are choosing their model's ceiling by accident. That is not a criticism of anyone — the instrument is two years old and the habit has not spread yet. It is an opportunity, and it expires as the habit becomes normal.

Questions to take back to the papers

Reading a paper with a question is a different activity from reading it to find out what it says. Here are the questions this lesson leaves open, matched to where the answers live.

QuestionWhere to look
How sensitive is the removable fraction to the choice of embedder?The SemDeDup ablations. Then run it yourself with two encoders on one corpus — the answer is corpus-specific and the experiment is cheap
Does the survivor rule matter as much as claimed at mild dedup rates?The same ablations. The interesting regime is aggressive removal, and the paper says so
How stable are filter rankings across the four scales?The benchmark's per-scale tables. This is the most under-read part of that paper and the most useful for deciding what to trust
What exactly is in the retrieval seed, and how was it balanced?The data-processing section of the self-supervised vision work. The composition is the specification of the dataset
How much of a filter's value survives when the evaluation is chosen to avoid its anchor?Nobody has published this cleanly. It is a good project
What did each generation of the loop delete?Also unpublished. Also a good project, and a more important one

The last two rows are not rhetorical. They are the two measurements this field most obviously needs and has not made, and both are within reach of a small group with a modest cluster and the patience to build the instrument first.

Where to actually start, tomorrow. Not with a paper. With your own corpus, the thirty-minute triage from Chapter 8, and a plot of the nearest-neighbour cosine histogram. Everything in this lesson is downstream of looking at that plot once. The papers will make far more sense after you have been surprised by your own data than before.

Objections, and what they are worth

A lesson should be able to state the strongest case against itself. Five objections, each with the part that is right and the part that is not.

ObjectionThe part that is rightThe part that is not
"This is just data cleaning with extra steps"The operations really are simple: a dot product and a thresholdCleaning removes what is broken. This removes what is redundant or off-distribution — categories that are invisible to any per-row validity check, because each row is individually fine
"The gains will vanish as models get better"Some will. Stronger models are more robust to noisy pairsRedundancy and skew are properties of the corpus, not of the model. A better model trained on a corpus that is half copies still spends half its compute on copies
"Synthetic data makes curation obsolete"Generation does dissolve scarcity in some domains, and rewriting rows beats discarding themGenerated corpora need curation more, not less: they are trivially redundant, trivially skewed toward the generator's modes, and their failure modes are invisible to the quality signals we have
"The thresholds are arbitrary, so this is not science"They are policies, and papers report them as though they were discoveriesA calibrated policy with a stated objective and a controlled measurement is exactly how applied science works. The alternative is not rigour, it is an uncalibrated policy nobody wrote down
"Only large labs can do any of this"The headline experiments cost real moneyEvery technique here works at ten thousand rows on one machine, and the economics are better at small scale because you can compute exactly what the large labs must approximate

Five exercises, in increasing order of ambition

ExerciseWhat it teachesRoughly
1. Run the thirty-minute triage on a corpus you already haveThat your data is not what you think it is. Everyone is surprised by their largest clusterHalf an hour
2. Implement the greedy dedup pass and reproduce the six-item table by handThat the ordering rule does two jobs, and that chains are not cliquesAn hour
3. Compare a perceptual hash and an embedding on fifty pairs you constructExactly where the hash stops working, in your data rather than in a diagramAn afternoon
4. Build a two-point filtering benchmark: filtered and unfiltered, matched stepsThe discipline of freezing everything else, which is harder than it soundsA day plus a training run
5. Seed a retrieval curation from a folder of failure cases and mix the result inThat you can steer a dataset with examples, and how easily it overshootsA week

Exercise 4 is the one that changes how you work, and it is the one people skip because it produces no artifact except a number. That number is the whole point: it is the first time you will have measured a data decision instead of believing one.

The last thing to say about all of this

The three papers in this lesson share an unusual property. None of them introduced a new architecture, a new loss, or a new optimiser. SemDeDup is k-means and a threshold. The benchmark is a pool and a frozen script. Retrieval curation is a nearest-neighbour search. Every component was decades old.

What they did was notice that a variable everyone had frozen was worth unfreezing, and then build the instruments to measure it. That is a different kind of contribution from the one the field was set up to reward, and it turned out to be worth more than most of the alternatives available at the time.

The transferable version, which has nothing to do with machine learning. Look at what your system treats as a given. Not the things people argue about — those are already being optimised — but the things nobody argues about, because they were decided once, by someone who has left, for reasons that made sense then. The sampling policy. The schema. The definition of the metric. The set of things you log. That is where the unclaimed leverage is, and it is unclaimed precisely because nobody has built the instrument that would make it visible.

The three-year arc, in order

MomentWhat became possible
Web-scale image-text corpora released openly, filtered by a cross-modal scoreAnyone could train a contrastive vision-language model. Alignment filtering became the default and duplicates were left in
Data pruning shown to bend the scaling curve rather than shift itRemoval stopped being a purely economic act. Careful pruning could improve a model, which made curation a source of capability
Semantic deduplication at web scaleThe category of duplicate that hashes cannot see got a method and a number. Half of a large image corpus turned out to be redundant
A filtering benchmark with a shared pool and a frozen recipeFilters became comparable artifacts. Curation knowledge started to accumulate
Retrieval-based curation from a curated seedDatasets could be grown rather than trimmed, with no labels and no taxonomy
Purpose-trained filtering networks; metadata balancing; recaptioningThe filter became a first-class model; the feedback loop got an escape hatch; and rows became editable rather than only keepable
The same benchmark design ported to language dataThe vision result replicated: curation beat scale at equal compute, in a second modality

Seven steps, each one enabled by the last, and the whole arc runs on the same primitive: comparing two vectors.

If you remember one paragraph

The compressed version. Embed your corpus once and cache the vectors. Three numbers then tell you what is wrong with it: the nearest-neighbour similarity distribution (redundancy), the cross-modal or query-document similarity distribution (misalignment), and the cluster-occupancy entropy (skew). Fix redundancy by clustering and collapsing knots, keeping the member farthest from each cluster centre. Fix misalignment by ranking and cutting at a percentile chosen so that survivors get two to five passes under your compute budget. Fix skew by retrieving toward a seed you wrote down and can defend. Then decontaminate against every evaluation set with a copy-detection model, log the effective cluster count, and read fifty removed items with your own eyes before you commit. Everything else in this lesson is the derivation of those seven sentences.

What this changes about how you spend a week

InstinctReplace it with
"The model is underperforming, let me try a bigger one""Let me train on a random half at equal steps and see whether I am data-limited or noise-limited"
"We need more data""We need more distinct, aligned, tail data — let me measure how much of what I have is any of those"
"Our retrieval quality is bad, let me fine-tune the embedder""Let me check how many of my top-8 chunks are near-copies of one another first"
"This filter is better, it scored higher""Higher on what, anchored on what, and at matched steps?"
"We deduplicated, so the corpus is clean""We deduplicated at which level — bytes, pixels, or meaning? Those are three different operations"

Every row on the right takes less than a day and costs no GPU-hours worth mentioning. That asymmetry — a day of measurement against a week of training — is the practical reason this material pays for itself immediately, whatever scale you work at.

The five ideas to keep

1. Three failures, one instrument
Duplication, misalignment, and skew are distinct problems with distinct fixes, and a cosine measures all three: within a modality, across modalities, and against a reference set.
2. Cluster to escape the quadratic
Restricting comparisons to within k clusters divides the work by exactly k, and the resulting errors are one-sided: you miss duplicates, you never delete something unique.
3. Thresholds come from budgets
Fix the compute, choose two to five passes over survivors, and bisect the threshold to hit that count. Never copy a number from a paper written against a different encoder.
4. Every filter has an anchor
A curation rule is a statement about what the model should be good at. Name the anchor, and evaluate on something it does not resemble — or you are measuring the anchor.
5. Instrument diversity, not just accuracy
Collapse does not look like a bad number. It looks like a capability that quietly stopped existing, and effective cluster count is the only cheap way to see it coming.

How to read the data section of a paper

A practical skill this lesson should leave you with. Data sections are short, passive, and load-bearing. Here is what to look for.

Question to askWhy it mattersWhat "unstated" usually means
What was the pool, and what was actually trained on?The ratio is the filter's aggressiveness, which sets the epoch countThe authors report the final size and not the pool. Assume heavy filtering
What model produced the filtering scores?Determines the inherited blind spots and whether the loop is closed with the evaluationUsually a widely-used pretrained encoder. Ask what it was trained on
Was there a deduplication step, and at what level?Exact, perceptual, and semantic are three different operations with different yields"We removed duplicates" almost always means exact or perceptual only
How was decontamination done, and what rate was found?Without this the headline numbers have an unquantified upward biasNot done, or done semantically when it should have been copy-detection
Is the compute matched across the ablation rows?Otherwise data quality and training length are confoundedMatched epochs rather than matched steps. Check
What is the anchor of any reference-set filter?Tells you which evaluations are partly circularAn anchor exists and is described as "a high-quality reference set"
The reframe worth carrying out of this lesson. When you read "we trained on 1.4 billion image-text pairs," you should now hear a compound claim: a pool was crawled, a scorer was chosen, a threshold was set, duplicates were removed at some level, a distribution was implicitly targeted, and an evaluation may or may not have been protected. Six decisions, usually reported as one number. Every one of them moves the result more than most architecture choices do.

What changed about what counts as a contribution

The deepest effect of these three papers is not any of their techniques. It is that they made "we built a better dataset" into a claim that can be checked.

Before, the sentence was unfalsifiable, so it was rarely made and never argued about. After, it comes with a protocol: freeze the recipe, publish the mask, report the suite. Data work moved from infrastructure to research, and the practical consequence is that curation knowledge now accumulates the way architecture knowledge did between 2012 and 2018 — each result standing on the last, on a shared instrument.

And the part that is not solved. Making a claim checkable is not the same as making it right. The current instrument measures downstream accuracy at fixed compute on a fixed recipe against a fixed suite. It does not measure what a filter deleted, who consented to the data being there, whether the resulting distribution is one anybody would choose, or what capability quietly failed to appear. Those are the open problems, and they are not going to be solved by a better cosine.

Open problems worth working on

ProblemWhy it is hardWhat a solution would look like
Cheap learnability signals"Does the model need this?" currently requires the training run you were trying to avoidA proxy computed from a small model or a partial run that ranks examples by marginal value and transfers to the large run
Auditing what a filter removedYou cannot measure the absence of a capability you never tested forAutomated discovery of clusters with anomalously low keep rates, surfaced with samples for human review, run as a gate rather than a report
Curation that does not close the loopEvery learned selector inherits its training distributionSelection signals grounded outside the model family — metadata, provenance, external structure — combined with, not replaced by, learned scores
Recipe-robust filter evaluationResults are conditional on the frozen recipe, and freezing is what makes them comparableReporting across several recipes and budgets, and a norm that a single-recipe result is a hypothesis
Provenance and consent at scaleThe pool predates the norms; the tooling does not existPer-item provenance carried through the pipeline, honoured opt-outs, and licensed corpora competing on the same instrument

Notice that four of the five are measurement problems rather than algorithm problems. That is the shape of a field right after it acquires an instrument: the next bottleneck is always what the instrument does not see.

Build it yourself — the weekend version

StepWhat to doThe decision that matters
1. Pick a corpus you ownA fine-tuning set, a document collection, an image folder. Ten thousand items is plentyChoose one where you can judge quality by eye. You will need to
2. Embed once, cache foreverAny small pretrained embedder. Write the vectors to disk keyed by IDMatch the embedder's invariances to what you are protecting (Chapter 3)
3. Compute the three audit numbersNearest-neighbour similarity distribution, alignment distribution if paired, cluster-occupancy entropyDo this before you decide anything. The histogram tells you where your threshold lives
4. Bisect to a removal targetSet the target from your compute budget, not from a paper's percentageTwo to five epochs over survivors (Chapter 5)
5. Read fifty removed itemsWith your own eyes, before committingNon-negotiable. Every aggregate can hide a systematic error a sample reveals
6. Check per-cluster removal ratesSort clusters by removal rate and look at the top tenAn outlier cluster is a data-generation bug, not a dedup result
7. Train and compare at equal stepsSame optimiser steps, filtered versus unfilteredEqual steps, not equal epochs — otherwise you have confounded quality with compute
8. Log entropy every time you re-curateEffective cluster count, per roundThe only cheap early warning for Chapter 7's ratchet

References worth reading next

  1. Abbas, A., Tirumala, K., Simig, D., Ganguli, S., Morcos, A. S. "SemDeDup: Data-efficient learning at web-scale through semantic deduplication," 2023 — arXiv:2303.09540. The core method of Chapter 2.
  2. Gadre, S. Y. et al. "DataComp: In search of the next generation of multimodal datasets," NeurIPS 2023 Datasets and Benchmarks — arXiv:2304.14108. The benchmark of Chapters 4 and 5.
  3. Oquab, M. et al. "DINOv2: Learning Robust Visual Features without Supervision," 2023 — arXiv:2304.07193. Read the data-processing section for Chapter 6.
  4. Sorscher, B., Geirhos, R., Shekhar, S., Ganguli, S., Morcos, A. S. "Beyond neural scaling laws: beating power law scaling via data pruning," NeurIPS 2022 — arXiv:2206.14486. The theoretical licence for the whole enterprise.
  5. Lee, K. et al. "Deduplicating Training Data Makes Language Models Better," ACL 2022 — arXiv:2107.06499. Exact and MinHash deduplication for text, and the memorisation result.
  6. Schuhmann, C. et al. "LAION-400M," 2021 — arXiv:2111.02114; "LAION-5B," 2022 — arXiv:2210.08402. Where CLIP-score filtering became standard practice.
  7. Fang, A. et al. "Data Filtering Networks," 2023 — arXiv:2309.17425. Training a network whose only job is filtering, and the decoupling of filter quality from model quality.
  8. Xu, H. et al. "Demystifying CLIP Data," 2023 — arXiv:2309.16671. Metadata balancing as an alternative to score-based filtering — the Chapter 7 escape hatch.
  9. Maini, P. et al. "T-MARS: Improving Visual Representations by Circumventing Text Feature Learning," 2023 — arXiv:2307.03132. The rendered-text shortcut in CLIP score.
  10. Nguyen, T. et al. "Improving Multimodal Datasets with Image Captioning," 2023 — arXiv:2307.10350. Rewriting data instead of discarding it.
  11. Tirumala, K. et al. "D4: Improving LLM Pretraining via Document De-Duplication and Diversification," 2023 — arXiv:2308.12284. SemDeDup's direct language-model descendant.
  12. Li, J. et al. "DataComp-LM: In search of the next generation of training sets for language models," 2024 — arXiv:2406.11794. The DataComp design ported to text.
  13. Pizzi, E. et al. "A Self-Supervised Descriptor for Image Copy Detection," CVPR 2022 — arXiv:2202.10261. The copy-detection embedder used for decontamination.
  14. Johnson, J., Douze, M., Jégou, H. "Billion-scale similarity search with GPUs," 2017 — arXiv:1702.08734. The index that makes all of this runnable.
  15. Radford, A. et al. "Learning Transferable Visual Models From Natural Language Supervision" (CLIP), 2021 — arXiv:2103.00020. The embedder underneath nearly every filter here.
"What I cannot create, I do not understand."
Embed ten thousand of your own rows tonight. Compute the nearest-neighbour histogram. You will find duplicates you did not know you had, and the 50% figure will stop being a number you read.
Exit gate — teach it back before you leave.

Without scrolling up: (1) derive why clustering into k groups reduces the pairwise comparison count by exactly a factor of k, and state which direction the resulting errors point; (2) given three unit vectors in a duplicate group, say which one SemDeDup keeps and why the counterintuitive rule wins at scale; (3) explain why DataComp fixes samples-seen rather than epochs, and compute the epoch count at a 30% keep fraction with a 128-million budget; (4) explain why the intersection filter wins on ImageNet and loses on the 38-task average; (5) describe how DINOv2 builds a dataset with no labels, and name the contamination risk its own mechanism creates; (6) compute the effective cluster count from an occupancy vector and say what a diversity floor does to it. If any of the six stalls, its chapter is one tap away.

Which single sentence best captures why these three papers belong in one lesson?