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.
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%.
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.
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.
| Application | How much it is taught | What it changes |
|---|---|---|
| Semantic search | Constantly | Your query latency and recall |
| RAG retrieval | Constantly | Which three paragraphs your LLM sees |
| Recommendation | Often | Which item ranks fourth on a page |
| Clustering / topic discovery | Sometimes | A dashboard someone looks at monthly |
| Zero-shot classification | Sometimes | Whether you need an annotation budget |
| Training-set curation | Almost never | The 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.
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.
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.
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:
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:
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.
| Number | Where it comes from | What it proves |
|---|---|---|
| 50% | SemDeDup on a 440M-pair LAION subset: half the data removed, model quality held, out-of-distribution results slightly improved | Half 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 training | Filter 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 procedure | A better dataset, same everything else, beat the reference model by 3.7 points |
| 1.2B → 142M | DINOv2's curation: an uncurated web pool retrieved down to LVD-142M using a curated seed as the query set | You can build a training set with a nearest-neighbour search and no labels at all |
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.
| Era | Where the effort went | What was fixed | What nobody measured |
|---|---|---|---|
| Feature engineering, to about 2011 | Designing descriptors by hand — edges, gradients, keypoints | The dataset was small and hand-collected | Whether a different dataset would change the ranking of descriptors |
| The benchmark era, 2012–2018 | Architectures. Deeper, wider, residual, attention | The dataset was frozen by the benchmark — that was the whole point | Data, by construction. It was the control variable |
| The scale era, 2019–2022 | Parameters and tokens, guided by scaling laws | Data was assumed interchangeable: more is more | Whether two corpora of the same size are equally valuable. They are not |
| The curation era, 2023– | Which rows exist | Architecture and recipe, deliberately, so the data can vary | Still 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.
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.
| System | A "row" | Redundancy looks like | Misalignment looks like |
|---|---|---|---|
| Vision-language pretraining | An image and its caption | The same stock photo across a hundred sites | Alt-text that is a filename |
| Language-model pretraining | A document | Boilerplate, syndicated news, license text | Nothing to align to — only quality |
| Instruction tuning | A prompt and a response | Template instantiations from one skeleton | A response that does not answer the prompt |
| Retrieval corpora | A chunk of a document | The same policy paragraph in five documents | A chunk split mid-sentence, meaningless alone |
| Recommendation | A user-item interaction | Bot traffic and repeated impressions | An 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.
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?
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:
Training on that pool costs a forward and backward pass through two towers, roughly four times more per sample:
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.
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.
| Situation | Why curation does not help | What to do instead |
|---|---|---|
| A small, hand-collected, already-clean dataset | There is no redundancy to remove and no misalignment to filter. Every removal is pure loss | Augmentation, better regularisation, or collecting more — the classical answers, which are correct here |
| You are compute-bound with data to spare | Filtering 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 sampling | Still worth deduplicating (it is close to free), but expect small gains. Spend the effort on the model |
| The bottleneck is the objective, not the data | If your loss cannot express what you need — ordering, counting, calibration — then no subset of the data teaches it | Change the objective or the architecture. Chapter 3's invariance table is the diagnostic: if your embedder cannot see the property, neither can your model |
Six terms that get used loosely and mean specific things in this lesson. Pin them now and the later chapters read faster.
| Term | Means exactly | Does not mean |
|---|---|---|
| Pool | The full set of candidate rows before any selection | The training set. Keeping that distinction is most of Chapter 4 |
| Filter | A function from a row to keep-or-drop. The artifact you are actually comparing | A model, a loss, or anything that runs at inference time |
| Semantic duplicate | Two rows whose embeddings are within a chosen cosine threshold | Two rows with the same bytes, or the same pixels. Those are cheaper problems |
| Alignment | Cross-modal similarity: does this caption describe this image? | Truth. A terse but accurate caption scores low; a florid wrong one can score high |
| Anchor | The reference set a filter measures similarity against | An implementation detail. It is the filter's statement of intent |
| Samples seen | Total rows processed by training, counting repeats. The real compute currency | Dataset size, or epochs. Conflating these confounds every comparison |
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 data | Same model, better data | |
|---|---|---|
| Reference | The baseline number | At 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 frontier | A well-known reference model reaches 75.3% ImageNet zero-shot | The 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.
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.
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.
| Chapter | The capability it leaves you with |
|---|---|
| 1 | Audit any corpus with three numbers, and say which of the three failure modes dominates it |
| 2 | Implement semantic deduplication, choose k from a memory target, and defend the survivor rule |
| 3 | Pick the right encoder for the duplicates you are hunting, and know which ones a hash will never find |
| 4 | Design a controlled experiment that attributes a gain to a data decision instead of confounding it |
| 5 | Set a threshold from a compute budget, and predict which filters compose and which just correlate |
| 6 | Grow a targeted dataset by retrieval from a pool, using a folder of examples as the specification |
| 7 | Instrument a curation loop so that diversity collapse is visible before it is irreversible |
| 8 | Run all of it, this week, on a corpus you already own |
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.
| Category | Example | How to treat it |
|---|---|---|
| Reported — measured in one of the papers | Roughly half of a LAION subset removable; 79.2% ImageNet zero-shot for a curated 1.4-billion-pair dataset; the medium-scale baseline table | Cite it, and cite the conditions with it. Reported numbers are conditional on scale and recipe |
| Derived — arithmetic done here from stated quantities | The 193-petabyte matrix; 8,800 items per cluster; the entropy calculations; the 6.79σ cosine argument | Check the arithmetic yourself. That is the point of showing it. These follow from the inputs and nothing else |
| Illustrative — constructed to make a shape visible | The threshold-sweep table in Chapter 8; the precision figures in the epoch model; the keep-fraction curve in the simulation | Read the shape, never the value. These are labelled where they appear, and they are there because the shape is the lesson |
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.
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.
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:
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:
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.
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.
| Kind | Example | Detectable by | Roughly how common on the web |
|---|---|---|---|
| Exact | Byte-identical file served from two URLs | A hash of the bytes. Trivial, cheap, and everyone already does it | Common, and already removed by most pipelines |
| Perceptual | Same photo, re-saved at 80% JPEG quality, resized, watermarked, or cropped by ten pixels | A perceptual hash — sometimes. Chapter 3 shows exactly where it breaks | Very common. Every CDN, thumbnail, and social repost makes more |
| Semantic | Twelve different photographs of the same white sneaker on the same white background, from a product catalogue | Only an embedding. No pixel-level method sees these as related | The 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.
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:
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:
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.
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
So the top 100 out of a million:
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.
| Failure | What it looks like in raw bytes | What it looks like in embedding space | The fix |
|---|---|---|---|
| Duplication | Different bytes, different URLs, different filenames. Undetectable | A tight knot of points at cosine 0.98–1.00 of each other | SemDeDup: cluster, then collapse each knot to one survivor |
| Misalignment | A caption string and an image blob. No relationship is computable | An image vector and a text vector with a low cosine between them | CLIP-score filtering: drop pairs below a threshold |
| Skew | Nothing at all — every example is individually fine | Wildly uneven occupancy across clusters; a few dense regions, a vast sparse tail | DINOv2: 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.
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.
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:
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.
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:
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.
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.
| Stage | Pairs | Share of the original |
|---|---|---|
| Raw pool | 100,000,000 | 100% |
| After removing misaligned pairs (25%) | 75,000,000 | 75% |
| After removing near-duplicates among survivors (30%) | 52,500,000 | 52.5% |
| Of which the top 100 concepts occupy (36%, from the Zipf calculation) | 18,900,000 | 18.9% |
| Distinct, aligned, tail-of-the-distribution examples | 33,600,000 | 33.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.
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.
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 example | Memorisation pressure | Practical consequence |
|---|---|---|
| 1 | Negligible | The example contributes to a general rule and is otherwise forgotten |
| 2–10 | Rising | Fragments become retrievable under the right prompt |
| Tens to hundreds | High | Verbatim reproduction becomes reliable. Privacy and licensing exposure |
| Thousands | Certain | The 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.
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 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.97 | Duplication | Chapter 2. Expect a large, cheap win |
| A long left tail in the alignment distribution, and reading samples from it makes you wince | Misalignment | Chapter 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 clusters | Skew | Chapter 6. Expect the hardest work and the most durable gain |
| All three, mildly | A normal web corpus | All three, in the order of Chapter 8's pipeline |
| None of the above, and the corpus is small | Nothing. You are data-limited | Collect 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.
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:
| Misaligned | Duplicated (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.
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 moves | What it means |
|---|---|
| Near-duplicate fraction rising snapshot over snapshot | Your crawler is re-fetching, or a new source is a mirror of an old one. Fix upstream, not with a filter |
| Alignment distribution shifting left | Either a new low-quality source entered the mix, or your scorer changed version. Check the second before believing the first |
| Effective cluster count falling | Your acquisition is narrowing — often because a successful source is being scaled up while others stay flat |
| All three stable, corpus growing | Healthy. 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.
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.
| Failure | In images | In text | Why they differ |
|---|---|---|---|
| Duplication | Very high. Roughly half of a large web image corpus is semantically redundant | Lower for near-copies, but exact and near-exact copying is rampant | Images 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 |
| Misalignment | The dominant failure. Alt-text frequently is not a description | Not applicable in the same form — there is nothing to align text to | Text pretraining is unimodal, so "quality" replaces "alignment": is this document worth learning from at all? |
| Skew | Zipf over visual concepts | Zipf over topics, and additionally over registers: forum posts, boilerplate, machine translation, generated filler | Text 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.
Take one wire-service article. Estimate its footprint in a crawl:
| Copy type | Rough count | Caught by |
|---|---|---|
| The original publication | 1 | — |
| Syndicated republications, byte-identical body | 40 | Exact hashing of the body, if the boilerplate is stripped first |
| Republications with a different headline and trimmed paragraphs | 25 | Shingle-based near-duplicate detection |
| Aggregator pages quoting the first two paragraphs | 60 | Partially — a short quote shares few shingles with a long article |
| Rewrites and summaries by other outlets | 15 | Only a semantic pass. Shingle overlap is near zero |
| Total footprint | 141 documents from one article | Three 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.
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.
| Signal | Verdict on fluent generated filler | Why the signal fails |
|---|---|---|
| Perplexity under a language model | Excellent — low perplexity | It was generated by a similar model. Low perplexity is the definition, not evidence of quality |
| Near-duplicate detection | Passes — every output is distinct | Sampling ensures surface variety even when content is repetitive |
| A learned quality classifier | Often passes | Trained to recognise well-formed prose, which this is |
| Cluster occupancy | Sometimes catches it | Generated 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.
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.
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:
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:
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.
SemDeDup's LAION experiments used a subset of about 440 million pairs. The number of distinct pairs is:
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:
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.
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:
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:
| Quantity | Global (no clustering) | Within-cluster, k = 50,000 |
|---|---|---|
| Items per comparison group | 440,000,000 | 440,000,000 / 50,000 = 8,800 |
| Pairs per group | 9.68 × 1016 | 8,800 × 8,799 / 2 ≈ 38.7 million |
| Total pairs | 9.68 × 1016 | 38.7M × 50,000 ≈ 1.94 × 1012 |
| Peak similarity-matrix memory | 193 petabytes | 8,8002 × 2 bytes = 155 megabytes |
| Dot-product compute | ≈ 11.5 GPU-days | ≈ 20 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.
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.
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.
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:
| Rule | Argument for it | What it does at scale |
|---|---|---|
| Keep a random member | Unbiased; no assumptions | Fine at mild dedup rates; leaves the group's average character intact |
| Keep the member closest to the cluster centroid | The 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 centroid | Preserves the spread of the cluster; the survivors are the ones least like everything else | SemDeDup'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.
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":
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:
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:
The centroid of unit vectors is not unit, so we need its length before taking cosines:
Cosine to the centroid, for each candidate. Dot with c, then divide by ‖c‖:
The decision. Rank by cosine to the centroid: A (0.998759) is the most central, then B (0.996617), then C (0.992333). So:
| Rule | Survivor | What you lost |
|---|---|---|
| Keep closest to centroid | A | The two examples that were least like the cluster's average — exactly the informative ones |
| Keep random | A, B, or C with probability 1/3 each | Unbiased, but you cannot reason about the survivor |
| Keep farthest from centroid (SemDeDup) | C | The 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.
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.
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.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
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.
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.
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.
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.
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.
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.
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.
| Method | Question it asks | Cost | Needs a trained model on your data? |
|---|---|---|---|
| Exact / perceptual dedup | Are these the same bytes or pixels? | Nearly free | No |
| SemDeDup | Do I already have something that means this? | One embedding pass + k-means + within-cluster pairs | No — any pretrained embedder works |
| CLIP-score filtering | Does this caption describe this image? | One embedding pass on each modality | No — needs a pretrained aligner |
| Forgetting / influence pruning | Does the model need this example to learn? | At least one full training run, often several | Yes |
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.
The number of clusters is the only free parameter besides the threshold, and both failure directions are instructive.
| k too small | k about right | k 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 intractable | Clusters hold a few thousand to a few tens of thousands. The similarity matrix fits comfortably in memory and duplicates reliably co-assign | Clusters 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.
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.
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:
| Item | cos to centroid | Rank, most central first |
|---|---|---|
| i₁ | 0.9988 | 1 |
| i₂ | 0.9966 | 2 |
| i₃ | 0.9923 | 3 |
| i₄ | 0.9871 | 4 |
| i₅ | 0.9702 | 5 |
| i₆ | 0.9310 | 6 |
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₁.
| Step | Item considered | Action | Kept so far |
|---|---|---|---|
| 1 | i₆ | Still kept. No later item is above threshold with it | i₆ |
| 2 | i₅ | Still kept. Absorbs i₄ | i₆, i₅ |
| 3 | i₄ | Already removed. Skip | i₆, i₅ |
| 4 | i₃ | Still kept. Absorbs i₂ and i₁ | i₆, i₅, i₃ |
| 5–6 | i₂, i₁ | Already removed. Skip | i₆, 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.
Everything above assumed the duplicate relation is transitive within a group. It is not, and the failure is instructive.
Consider three items with:
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 order | What happens | Survivors |
|---|---|---|
| a, b, c | a 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 survives | a, c |
| b, a, c | b 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.
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.
| Rung | Concrete example | Caught by |
|---|---|---|
| 1. Byte-identical | The same JPEG served from a CDN and its origin | SHA-256 of the file |
| 2. Re-encoded | The same photo saved at JPEG quality 80 instead of 95 | Perceptual hash |
| 3. Geometrically edited | Resized, ten pixels cropped, a watermark corner added | Perceptual hash — sometimes |
| 4. Same scene, different frame | Two shots from the same photo session, half a second apart | Only an embedding |
| 5. Same content, different rendering | A product photo and a 3D render of the same product | Only an embedding |
| 6. Same meaning, different medium | An English caption and its German translation | Only 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.
The simplest useful perceptual hash is the difference hash, dHash. The recipe is four lines of prose:
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.
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):
| Row | Pixels | Comparisons (is each pixel brighter than its right neighbour?) | Bits |
|---|---|---|---|
| 1 | 10, 40, 30, 90, 80 | 10>40 no · 40>30 yes · 30>90 no · 90>80 yes | 0 1 0 1 |
| 2 | 20, 25, 60, 55, 50 | 20>25 no · 25>60 no · 60>55 yes · 55>50 yes | 0 0 1 1 |
| 3 | 200, 190, 100, 110, 120 | 200>190 yes · 190>100 yes · 100>110 no · 110>120 no | 1 1 0 0 |
| 4 | 5, 15, 15, 40, 35 | 5>15 no · 15>15 no · 15>40 no · 40>35 yes | 0 0 0 1 |
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:
| Row | Shifted pixels | New bits | Old bits | Bits differing |
|---|---|---|---|---|
| 1 | 40, 30, 90, 80, 75 | 1 0 1 1 | 0 1 0 1 | 3 |
| 2 | 25, 60, 55, 50, 45 | 0 1 1 1 | 0 0 1 1 | 1 |
| 3 | 190, 100, 110, 120, 115 | 1 0 0 1 | 1 1 0 0 | 2 |
| 4 | 15, 15, 40, 35, 30 | 0 0 1 1 | 0 0 0 1 | 1 |
| Total Hamming distance | 7 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.
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.
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:
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:
Work it with r = 5 and b = 20. For a truly near-duplicate pair with J = 0.8:
And for a merely related pair with J = 0.3:
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.
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.
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 flagged | True duplicates among them | Precision | Recall (of 10 true) | F1 |
|---|---|---|---|---|---|
| 0.85 | 14 | 9 | 9/14 = 0.643 | 9/10 = 0.900 | 0.750 |
| 0.90 | 10 | 8 | 8/10 = 0.800 | 8/10 = 0.800 | 0.800 |
| 0.95 | 6 | 6 | 6/6 = 1.000 | 6/10 = 0.600 | 0.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.
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 image | Do captions mention it? | Does a CLIP embedding separate on it? | Consequence for dedup |
|---|---|---|---|
| The main object | Almost always | Strongly | Reliable |
| Scene and setting | Often | Yes | Reliable |
| Fine-grained species or model | Sometimes | Weakly | Risk of deleting genuinely distinct fine-grained examples |
| Exact count of objects | Rarely and unreliably | Poorly | Three sheep and five sheep may be "duplicates" |
| Precise spatial arrangement | Rarely | Poorly | Layout variation gets flattened |
| Rendered text in the image | Sometimes, and often verbatim | Very strongly | Text-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.
"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 family | Trained to | Invariant to | Right for |
|---|---|---|---|
| Supervised classifier features | Predict one of C classes | Everything that does not change the class — aggressively so | Almost nothing here. It collapses within-class variation, which is usually what you are trying to preserve |
| CLIP-style cross-modal | Match an image to its caption | Anything a caption would not mention: exact counts, precise layout, fine subcategory | General 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 agree | Crop, colour, scale — but it separates instances, not classes | Fine-grained corpora, retrieval curation, anywhere you must not merge two similar-looking-but-distinct things |
| Copy-detection descriptors | Detect that one image is a derivative of another | Compression, resize, crop, overlay, colour shift — and nothing more | Decontamination 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.
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.
| Stage | Cost for 109 items | Catches ladder rungs |
|---|---|---|
| Exact hash | Free (part of the read) | 1 |
| Perceptual hash | A few hundred CPU-core-hours | 2, and some of 3 |
| Embedding + clustering | Order 100 GPU-hours, plus the index | 3, 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:
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.
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.
| Region | Typical location | What lives there | What to do |
|---|---|---|---|
| The bulk | A broad hump, often 0.4–0.8 | Ordinary unrelated and loosely-related items. This is your corpus being a corpus | Nothing. Any threshold here is deleting your dataset |
| The shoulder | Usually 0.85–0.95 | Genuinely related items: same topic, same scene type, same template family | This is where the judgement call lives. Sample and read before cutting |
| The spike | Often a distinct mass above 0.97 | Near-copies. Frequently a visible separate mode | Cut 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.
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:
Shingle each into overlapping word triples. S₁ has six words, so four triples:
| S₁ shingles | S₂ shingles | Shared? |
|---|---|---|
| the cat sat | the cat sat | yes |
| cat sat on | cat sat on | yes |
| sat on the | sat on a | no |
| on the mat | on a mat | no |
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:
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.
One table to hold the whole chapter. Same pair of items, three instruments, three answers — and each answer is correct for a different question.
| Pair | Perceptual hash / shingles | Embedding cosine | Which is right? |
|---|---|---|---|
| Same file, re-encoded | Duplicate | Duplicate | Both. Remove without thinking |
| Same photo, cropped | Not a match | Duplicate | The embedding. The hash is wrong here and this is its main failure |
| Same sentence, one word changed | Not a match | Duplicate | The embedding, usually — unless the changed word was "not" |
| Two photos of the same landmark | Not a match | Duplicate | Depends on your task. This is the policy case, not the accuracy case |
| Two questions about the same topic | Not a match | Often "duplicate" | The hash. These are genuinely different items and the embedding is over-merging |
| An evaluation item and its near-copy in the pool | Duplicate, if it survived re-encoding | Duplicate | Use 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.
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.
| Instrument | What it measures | Where it sits |
|---|---|---|
| SimHash | Cosine similarity of a sparse feature vector — usually term counts — compressed to a short bit string, compared by Hamming distance | Between shingles and embeddings. Cheaper than an encoder, and it degrades gracefully with small edits where exact shingle matching does not |
| Learned binary hashing | A trained map from an embedding to a short code, so that Hamming distance approximates cosine | An 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.
| Modality | The cheap instrument | The semantic instrument | The modality-specific trap |
|---|---|---|---|
| Audio | Spectral fingerprinting — the technology behind song identification | An audio encoder's embedding | The same recording at a different pitch or tempo defeats naive fingerprints and not embeddings |
| Video | Per-frame hashes, aggregated | A clip-level embedding | Everything is a chain: adjacent frames are near-duplicates by construction. Segment first, then dedup across segments |
| Code | Token-level or AST-level exact matching | A code embedding | Renaming variables defeats token matching entirely, and forks share a heavy majority of their content legitimately |
| Tabular | Exact row matching on a key | An embedding over the row's fields | Two 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.
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.
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.
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.
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:
| Scale | Pool size | Samples seen | Model |
|---|---|---|---|
| small | 12.8M | 12.8M | ViT-B/32 |
| medium | 128M | 128M | ViT-B/32 |
| large | 1.28B | 1.28B | ViT-B/16 |
| xlarge | 12.8B | 12.8B | ViT-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.
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 fraction | Unique pairs K | Epochs = B / K | What the model experiences |
|---|---|---|---|
| 100% | 128,000,000 | 1.00 | Maximum diversity, maximum noise, every pair seen once |
| 50% | 64,000,000 | 2.00 | Half the noise, each survivor twice |
| 30% | 38,400,000 | 3.33 | The empirical sweet spot for CLIP-score filtering at this scale |
| 10% | 12,800,000 | 10.00 | Very clean, but the same 12.8M pairs ten times — memorisation territory |
| 3% | 3,840,000 | 33.33 | Pristine 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.
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.
| Track | What you may do | What it measures |
|---|---|---|
| Filtering | Choose 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 Data | Assemble any training set you like from any source, subject to the same compute budget | Whether 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.
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.
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.
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.
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:
| Family | What it probes | Why it is in the suite |
|---|---|---|
| ImageNet | Broad object recognition | The lingua franca. Reported separately because everyone will look |
| ImageNet distribution shifts | Sketches, renditions, adversarially-filtered natural images | Catches filters that overfit to photographic web imagery |
| Broad transfer collection | Natural, specialised (medical, satellite), and structured (counting, depth) tasks | The structured tasks are where CLIP-style models are weakest, so they have the most headroom to reveal differences |
| Retrieval | Image-to-text and text-to-image ranking | Measures 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.
| Failure | What it looks like | The design decision that prevents it here |
|---|---|---|
| Too expensive to enter | Only three labs can run it; no community forms | Four scales, the smallest deliberately runnable on a handful of GPUs |
| Trivially gameable | One metric, one obvious hack, the leaderboard saturates in a month | 38 evaluations, a separately-reported headline, and a pool large enough that memorising the eval is not a shortcut anyone can hide |
| Measures the wrong artifact | Participants tune something the benchmark did not intend to isolate | Everything except the mask is frozen. There is nothing else to tune |
| Scale | Roughly what one training run needs | Who can iterate here |
|---|---|---|
| small (12.8M) | A few GPU-hours | A student on a single machine, dozens of ideas per week |
| medium (128M) | Tens of GPU-hours | A small lab, a few ideas per week |
| large (1.28B) | Hundreds to thousands of GPU-hours | A well-resourced group, confirming a hypothesis |
| xlarge (12.8B) | A serious cluster commitment | A 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.
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 instrument | With one |
|---|---|
| "We filtered for quality" appears in an appendix and cannot be checked | The filter is a published artifact anyone can rerun |
| Two labs' filters cannot be compared, so neither builds on the other | Improvements stack, because each is measured against the same baselines |
| Negative results are unpublishable, so everyone rediscovers the same dead ends | A leaderboard makes "this obvious idea does not work" a citable fact |
| Gains are attributed to whatever the paper is about, usually the architecture | Attribution is forced by construction, because only one thing varied |
| Practitioners follow lore — thresholds copied between projects that share no encoder | Practitioners 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.
| Approach | What varies | Strength | Weakness |
|---|---|---|---|
| Classical benchmark (fixed dataset) | The model | Decades of accumulated architecture knowledge | Treats data as a constant, which it is not |
| Dataset paper (release a corpus) | Everything, implicitly | Provides the raw material everyone needs | No way to attribute gains to any specific curation decision |
| Filtering benchmark | The subset only | Perfect attribution. Cheap entry at small scale | Conditional on the frozen recipe; forbids editing rows |
| Data-centric competitions with a fixed model | Labels, augmentation, and selection | Closer to applied practice, where you can edit | Larger 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.
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.
| Rank | Choice | What breaks without it |
|---|---|---|
| 1 | Compute measured in samples seen, not epochs | Everything. Filtering would also cut compute, the identity filter would win, and no result would mean anything |
| 2 | Everything except the subset is frozen | Attribution. A better filter with a worse schedule loses, and nobody can tell which caused what |
| 3 | Multiple scales, the smallest one cheap | The community. A benchmark only three labs can enter produces three results, not a field |
| 4 | A broad evaluation suite plus a separately-reported headline | Honesty. One metric invites a narrow hack; hiding the popular metric just moves the comparison somewhere worse |
| 5 | The pool is released unfiltered | Neutrality. 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.
Concretising the failure modes makes the design choices feel earned.
| Bad version | What would happen within a month |
|---|---|
| One scale, the largest | Three entries, all from organisations with clusters. No iteration, no accumulated knowledge |
| One evaluation, ImageNet only | Everyone retrieves ImageNet-like images from the pool. The leaderboard measures retrieval-toward-the-eval and calls it curation |
| Participants may change the learning rate | Half the reported gain is schedule tuning. Nobody can separate the two, and the filters stop being comparable |
| Budget measured in epochs | The 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 |
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 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.
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 type | What it demonstrates if it wins |
|---|---|
| A filtered subset of the shared pool | Selection skill. Directly comparable to every other entry |
| An externally curated corpus | That where data comes from beats how you filter it — a result the filtering track structurally cannot produce |
| A mixture of both | The 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.
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 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:
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:
| Rank | Caption | CLIP score | Top 30% cut |
|---|---|---|---|
| 1 | "a golden retriever puppy sitting in tall grass at sunset" | 0.336 | keep |
| 2 | "red ceramic mug on a wooden table" | 0.312 | keep |
| 3 | "snow-covered pine forest, aerial view" | 0.291 | keep |
| 4 | "Nike Air Max 90 white size 10 mens" | 0.244 | drop |
| 5 | "summer collection 2023" | 0.191 | drop |
| 6 | "DSC_0148" | 0.142 | drop |
| 7 | "click here for more" | 0.113 | drop |
| 8 | "IMG_20180714_154302.jpg" | 0.094 | drop |
| 9 | " " | 0.061 | drop |
| 10 | "cheap shoes buy online free shipping discount best price" | 0.058 | drop |
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.
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.
| Filter | Keeps | ImageNet | Avg. over 38 |
|---|---|---|---|
| No filtering | 100% | 0.176 | 0.258 |
| Basic (English, caption length, image size) | ≈ 30% | 0.226 | 0.285 |
| LAION-2B-style filter | ≈ 10% | 0.230 | 0.292 |
| CLIP score, B/32 scorer, top 30% | 30% | 0.256 | 0.328 |
| CLIP score, L/14 scorer, top 30% | 30% | 0.273 | 0.338 |
| Image-based (cluster and keep ImageNet-like) | ≈ 25% | 0.268 | 0.312 |
| Image-based ∩ CLIP score (L/14 top 30%) | ≈ 8% | 0.297 | 0.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.
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.
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 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.
Read the last two rows of the table again, carefully.
| Filter | ImageNet | Avg. over 38 | Verdict |
|---|---|---|---|
| CLIP score L/14, top 30% | 0.273 | 0.338 | Wins on the broad average |
| Image-based ∩ CLIP score | 0.297 | 0.328 | Wins 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.
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.
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 prefers | Why | What you lose |
|---|---|---|
| Short, noun-heavy, alt-text-shaped captions | That is the distribution CLIP was trained on | Long, compositional, precise descriptions — often the most informative text you have |
| English | Overwhelmingly the training language | Most of the world's data |
| Images with legible text matching the caption | CLIP reads rendered text extremely well, so caption-matches-sign is an easy high score | Compute spent on optical character recognition dressed up as visual learning |
| Common, centrally-typical objects | Better represented in the scorer's own training set | The 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.
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.
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 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:
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.
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.
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:
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,000 | 0.0145 | The kept fraction lands within roughly ±1.5 points of 30% |
| 100,000 | 0.00145 | Within roughly ±0.15 points |
| 1,000,000 | 0.00046 | Within 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.
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.
| Combination | Composes? | Why |
|---|---|---|
| Alignment ∩ distribution | Yes | Different failure modes. A well-aligned pair can still be the ten-thousandth photo of a sunset |
| Alignment ∩ redundancy | Yes | Also different. Duplicates are usually well-aligned — stock photos have excellent captions |
| Two alignment scorers | Barely | Highly correlated. The second one mostly re-ranks the same ordering and costs another pass |
| Alignment ∩ caption length | Negatively | Length is already baked into the score. Stacking them double-penalises short-but-accurate text |
| Distribution ∩ redundancy | Carefully | Both 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 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.
The benchmark's structure ports to any system where you control a dataset. Five steps:
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:
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 filter | Top 30% | |
|---|---|---|
| Unique items | 10 | 3 |
| Epochs | 1.0 | 3.33 |
| Useful fraction of each gradient step | 0.40 | 1.00 |
| Useful samples seen | 10 × 0.40 = 4.0 | 10 × 1.00 = 10.0 |
| Distinct useful items available | 4 | 3 |
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.
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:
| Criterion | Verdict on "Nike Air Max 90 white size 10 mens" | Why |
|---|---|---|
| Alignment score, top 30% | Drop | Terse metadata reads as low-descriptiveness in CLIP's idiom |
| Caption is not boilerplate | Keep | It carries real, specific information about the object |
| Contains a concrete noun from a concept list | Keep | Metadata-balancing filters would keep it and cap its category |
| A recaptioning model rewrites it | Keep, 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.
Every number in this chapter carries conditions. Five worth attaching whenever you quote them.
| Condition | Why it limits the conclusion |
|---|---|
| One training recipe | The comparison is "best filter for this architecture, schedule, and budget." A longer run changes the optimal aggressiveness, because the epoch count changes |
| One pool | Filters 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 photographic | A filter that discards non-English pairs pays no measured cost. That is a property of the yardstick, not of the filter |
| Selection cost is unbounded | Training compute is fixed; filtering compute is not. A very expensive filter can import an arbitrary amount of external knowledge |
| Rows may be selected, not edited | Which rules out the intervention that later turned out to matter most |
| Number | What it establishes | What it does not |
|---|---|---|
| 17.6% → 29.7% at medium scale | That row selection is worth more than most architectural choices at fixed compute | That 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 filter | That there is no scalar "best filter" — every filter encodes a choice about what to be good at | That either column is the right one. That depends on what you are building |
| 79.2% versus 75.3% at equal compute and procedure | That the data axis is live at the frontier, not just in the small-scale regime | That 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.
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.
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:
| Option | Size | Balance | Problem |
|---|---|---|---|
| ImageNet-22k | ≈ 14M | Curated, roughly balanced by class | Small, and its taxonomy is a particular carving of the visual world |
| Raw web crawl | Billions | Zipf-shaped disaster | Chapter 1's failure 3, at maximum strength. Vastly more data, dominated by the head |
| Web crawl + CLIP score | Hundreds of millions | Better, 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?
DINOv2's data pipeline builds LVD-142M — 142 million images — from two ingredients.
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.
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.
| Mode | Yield per query | Precision | Use when |
|---|---|---|---|
| Sample-based, N = 4 | ≤ 4 images | High — the neighbours really do look like the query | The seed is already large (millions of queries) |
| Cluster-based, M = 1,000 | up to 1,000 images | Lower — a cluster of 12,000 contains a range | The seed is small and you need amplification (a rare fine-grained collection) |
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?
| Approach | What it commits to | Cost of changing your mind |
|---|---|---|
| Classifier filter | A fixed taxonomy. The classifier has one output per class and can only express membership in those classes | New concept means new labels, new head, retraining. And confidently-wrong scores on anything outside the taxonomy |
| Retrieval filter | Only a similarity function. The "taxonomy" is whatever images you put in the query set | Add 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.
"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:
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:
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.
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 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 set | Size | Distribution | Result |
|---|---|---|---|
| Curated seed only | Small | Good | Limited by size — a self-supervised model wants volume |
| Raw uncurated pool | Huge | Web-shaped, head-dominated | Limited by distribution — more data, more of the same |
| Retrieved set | Large | Seed-shaped | Best 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.
Now the three papers can be laid side by side, and the taxonomy is cleaner than it looked in Chapter 0.
| Paper | Operation | Failure attacked | What the embedding is used for |
|---|---|---|---|
| SemDeDup | Subtract redundancy | Duplication | Similarity within one modality, to find copies |
| DataComp baselines | Subtract misalignment | Noise | Similarity across two modalities, to score pairs |
| DINOv2 curation | Add by similarity | Skew | Similarity 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.
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 component | What it contributes to the retrieved distribution |
|---|---|
| A broad object taxonomy | Coverage of everyday categories, and a rough balance across them — the counterweight to web skew |
| A landmark collection | Places, architecture, outdoor scenes at many scales and lighting conditions |
| Fine-grained collections | Within-category discrimination: the reason the resulting features work on tasks that need to tell two similar things apart |
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:
with P = 6,000,000 retrievable and D = 4,000,000 draws:
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 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:
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.
| Knob | Raise it and… | Typical starting point |
|---|---|---|
| nlist (partitions) | Each partition shrinks, so scanning is cheaper — but the coarse assignment gets noisier | Roughly the square root of the item count |
| nprobe (partitions scanned) | Recall rises, latency rises linearly. The single most useful dial | Start at 1% of nlist and tune against measured recall |
| PQ bytes per vector | Accuracy rises, memory rises linearly | 32 bytes is a common balance for 768–1024 dimensions |
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.
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.
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.
| Stage | Operation | Items after | What changed about the distribution |
|---|---|---|---|
| 0. Raw | — | 1,000M | Web-shaped. Heavy head, noisy captions, everything republished |
| 1. Syntactic | Language, caption length, image size, valid encoding | 640M | Barely changed in shape — you removed things that were not data |
| 2. Alignment | Keep the top 30% by cross-modal cosine | 192M | Useful fraction rises sharply. Distribution narrows toward CLIP's idiom |
| 3. Semantic dedup | Cluster, compare within cluster, ε at the 0.97 knee | 134M | The head flattens. Effective cluster count rises, because you removed piles, not variety |
| 4. Retrieval top-up | Query the discarded 500M with a seed of under-represented concepts | 158M | The tail comes back. This is the only stage that adds |
| 5. Decontamination | Copy-detection against every evaluation set | 157.8M | Nothing 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.
| Stage | Dominant cost | Reusable across projects? |
|---|---|---|
| Syntactic | A pass over metadata. Negligible | Yes — it is just rules |
| Embedding | One forward pass per item per modality. The big one-time bill | Yes, and this is the point. Cache the vectors and every later stage is nearly free |
| Alignment filtering | One dot product per pair. Free once embedded | Yes |
| Clustering | Linear per iteration in items × clusters. Real but tractable | Yes — the assignment is reusable for dedup, auditing, and rebalancing |
| Within-cluster dedup | Quadratic in cluster size. Bounded by your choice of k | Partly — the threshold is a policy you may revisit |
| Retrieval index | Building and holding a quantised index. Serious systems work | Yes — 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.
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.
| Setting | The pool | The seed | What you get |
|---|---|---|---|
| Domain-adapting a language model | A large general web corpus you already have | A few thousand documents from your domain — internal wikis, filings, manuals | Millions of general-web documents that look like your domain, without buying domain data |
| Building a RAG corpus | Everything your crawler can reach | The questions your users actually ask, embedded as queries | A corpus shaped by demand rather than by what was easy to crawl |
| Fixing a model's weak slice | Your unlabelled backlog | Two hundred examples of the failure | The 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.
Retrieval curation needs a seed. What if you do not have one?
| Situation | Where the seed comes from |
|---|---|
| No curated data at all | Cluster 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 examples | Use 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 items | Cluster-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 skewed | Balance 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.
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.
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.
One comparison holds this chapter together, and it is worth stating in its most general form because it recurs everywhere.
| Specify by rule | Specify by example | |
|---|---|---|
| Form | A taxonomy, a schema, a regular expression, a classifier | A folder of instances and a similarity function |
| Coverage | Exactly what the rule says, and nothing outside it | Whatever is near the examples — fuzzy edges, by design |
| Cost of change | Rewrite the rule, relabel, retrain, redeploy | Add examples, re-run the search |
| Auditability | High — the rule is a written artifact people argue about | Low — the specification is a folder, and nobody reviews folders |
| Failure mode | Confidently wrong outside its vocabulary | Silently 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.
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.
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.
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.
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.
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.
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:
Entropy, in bits. Each term is p · log2(1/p):
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.
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:
| Round | Cumulative factor | Effective clusters |
|---|---|---|
| 0 | 1.000 | 3.78 |
| 1 | 0.913 | 3.45 |
| 2 | 0.834 | 3.15 |
| 3 | 0.761 | 2.88 |
| 4 | 0.695 | 2.63 |
| 5 | 0.634 | 2.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.
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:
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.
| Mitigation | Mechanism | Cost |
|---|---|---|
| Diversity floor | Keep a fixed fraction of the pool sampled uniformly at random, exempt from all scoring. Bounds how far any round can distort occupancy | A slice of your budget spent on unfiltered data. Cheap insurance |
| Independent filter | Build the filter model on data chosen independently of the downstream evaluation, and evaluate the filter as a filter rather than as a classifier | A separate training run, and the discipline not to reuse your best encoder |
| Metadata balancing | Balance 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 path | Requires a concept list, and coarser selection than a learned score |
| Per-round instrumentation | Log cluster-occupancy entropy, effective cluster count, and per-cluster removal rate every round. Collapse is invisible in accuracy and obvious in these | Almost nothing. This is the highest-value-per-line item in the lesson |
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.
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.
You may never train a foundation model. You will almost certainly build something with this shape, because the pattern generalises well beyond pretraining.
| System | The loop | What collapses |
|---|---|---|
| RAG corpus maintained by relevance feedback | Retriever scores documents → low scorers get pruned → retriever retrained on what remains | Documents about topics your retriever was always weak on. Your coverage gap becomes permanent |
| Recommender trained on logged impressions | Model ranks items → only ranked items get shown → only shown items get logged → model retrains on the log | The 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 judge | Judge scores examples → low scorers removed → next model trained on survivors, next judge distilled from it | Anything the judge's own training under-represented, including styles it finds unfamiliar rather than wrong |
| Alert triage tuned on analyst-confirmed incidents | Model surfaces alerts → analysts label what they see → model retrains on those labels | Incident 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.
It helps to see that this is not a hypothetical. Trace one lineage:
| Generation | The embedder | What it selected | What that trained |
|---|---|---|---|
| 0 | A CLIP model trained on a proprietary web corpus | Alt-text pairs above a cosine threshold | Open web-scale image-text datasets |
| 1 | Open CLIP models trained on those datasets | Higher-quality subsets, by score and by clustering | Curated pools with measurably better downstream results |
| 2 | Models trained on the curated pools | Better subsets still, plus purpose-trained filtering networks | The current generation |
| 3 | …and so on, with each generation's selector inheriting the previous generation's blind spots | This 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.
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.
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.
| Symptom | What it usually means |
|---|---|
| Benchmark improves every round, but user-reported failures do not | You are optimising the benchmark's distribution. The tail your users live in is being curated away |
| The kept-score distribution gets tighter each round | Goodhart. The corpus is converging on the scorer's fixed point |
| Effective cluster count falls monotonically | Straightforward narrowing. Turn on a diversity floor before the next round, not after |
| A few clusters have keep rates near zero | Whole regions of the space are being deleted. Sample them and look before accepting |
| The filter and the evaluation share an ancestor | Your measurement is partly circular. Add an evaluation that ancestor never saw |
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:
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:
The round is worth running only if V goes up:
Set that equal to 1 and solve for the useful fraction at which rounds stop paying:
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:
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.740 | q* = 0.826 | q* = 0.884 |
| 0.913 (the toy round) | q* = 0.611 | q* = 0.724 | q* = 0.808 |
| 0.85 (aggressive filter) | q* = 0.460 | q* = 0.586 | q* = 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.
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.
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:
| Round | Share without a floor | Share with a 10% uniform floor |
|---|---|---|
| 0 | 2.00% | 2.00% |
| 1 | 1.20% | 0.9 × 1.20 + 0.1 × 2.00 = 1.28% |
| 2 | 0.72% | 0.9 × 0.77 + 0.20 = 0.89% |
| 3 | 0.43% | 0.68% |
| 4 | 0.26% | 0.57% |
| 5 | 0.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.
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.
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:
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.
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 | % removed | Character of what is being removed |
|---|---|---|---|
| 0.99 | 480 | 1.2% | Literal copies and whitespace variants |
| 0.95 | 3,360 | 8.4% | Template instantiations: same prompt, different entity |
| 0.90 | 7,840 | 19.6% | Paraphrases and same-task-different-wording |
| 0.88 | 9,920 | 24.8% | Target reached — still recognisably redundant |
| 0.85 | 13,240 | 33.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.
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.
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:
where S is what you have already selected. Work it by hand with λ = 0.7. Four candidates with similarity to the query:
and pairwise similarities:
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:
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:
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 dedup | Runtime MMR | |
|---|---|---|
| Cost | One-time, at indexing | O(k2) similarities per query, on every query |
| Risk | Deleting a chunk that mattered. Irreversible | None — nothing is deleted, only reordered |
| Handles query-specific redundancy | No — the threshold is global | Yes — two chunks can be redundant for one query and not another |
| Recommendation | Conservative (ε ≈ 0.97), for true copies only | Always on, λ between 0.6 and 0.8 |
Generalise what Recipe A did informally into a procedure.
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.
A pipeline usually has several stages. The order changes both cost and outcome.
| Order | Effect |
|---|---|
| 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 scoring | One embedding pass per modality. Do this before dedup, because dedup's cost is quadratic in what survives |
| 3. Semantic dedup | Now operating on a smaller, cleaner set — cheaper, and it finds more duplicates, because a quality filter concentrates the distribution |
| 4. Decontamination against evaluation sets | Last, and with a copy-detection embedder rather than a semantic one. Everything upstream can pull test near-copies in |
| 5. Diversity floor / rebalancing | Add 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.
| Corpus | Embedder | ε starting point | Watch out for |
|---|---|---|---|
| Web images for pretraining | CLIP image tower | 0.95–0.97 | Fine-grained classes collapsing into one another |
| Fine-grained image corpus | A self-supervised feature extractor, not CLIP | 0.97–0.99 | CLIP will merge species and product variants you need |
| Web text for pretraining | MinHash-LSH first, then a small sentence embedder | 0.92–0.95 | Paraphrase is not redundancy in text as often as in images |
| Instruction / fine-tuning data | A sentence embedder over prompt + response | 0.88–0.93 | Templates. Check per-cluster removal rates |
| RAG chunks | The same embedder you retrieve with | 0.97+ only | Version numbers and dates hiding in the 3% difference |
| Evaluation sets | A copy-detection embedder | As strict as you can afford | This is decontamination, not curation. Different job, different tool |
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.
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.
| Step | What to do | Threshold posture |
|---|---|---|
| 1 | Index every evaluation item with a copy-detection embedder (or n-gram shingles for text) | — |
| 2 | Query the index with every training candidate | — |
| 3 | Remove training items above threshold | Err heavily toward removal. A false positive costs one training example; a false negative costs the validity of your headline number |
| 4 | Report the contamination rate you found | Publish 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.
| Failure | How it presents | The fix |
|---|---|---|
| Wrong embedder for the corpus | Removal rate looks fine; a downstream capability quietly disappears | Match 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 paper | Removes 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 filtering | Redundancy metrics look healthy, model still memorises | Quality first. Filtering concentrates the distribution, which reveals duplicates the earlier pass could not see |
| Metric | Why |
|---|---|
| Removal rate, overall and per cluster | Per-cluster outliers are data-generation bugs, not curation results |
| Effective cluster count, before and after | The only cheap early warning for diversity collapse |
| The threshold, and the percentile it corresponds to | The percentile is the portable number. The threshold is an artifact of your encoder |
| Encoder name and version | Change the encoder and every number above becomes incomparable |
| Fifty sampled removals, saved to a file | So that the next person — probably you, in six months — can audit the decision instead of re-deriving it |
| Contamination rate found against each evaluation set | A property of the experiment. Report it alongside the accuracy it qualifies |
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.
| Minute | Do this | You are looking for |
|---|---|---|
| 0–5 | Count rows. Count distinct rows by exact hash of the content | If those differ by more than a few percent, the pipeline has a bug. Stop and fix it before anything else |
| 5–10 | Read twenty rows sampled uniformly at random. Not the first twenty — files are sorted | Format surprises, truncation, encoding damage, a whole source you did not know was in there |
| 10–20 | Embed a sample of 20,000 rows. Plot the nearest-neighbour cosine histogram | A 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–25 | Cluster the sample into 200 groups. Print the sizes and one example from the five largest | Skew, and its identity. The largest cluster is usually a template or a single dominant source |
| 25–30 | Compute effective cluster count. Compare against 200 | A 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.
Numbers without reasons do not transfer, so here is why each recommendation in the corpus cheat sheet is where it is.
| Setting | The 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 encoder | Both 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 first | It 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 recommendation | Because the cost asymmetry is extreme. Remove anything plausible. You are trading a handful of training rows against the validity of your headline number |
Curation produces a set. Training consumes a stream, and the mapping between them is a policy you are choosing whether or not you notice.
| Policy | What it means | When it bites |
|---|---|---|
| Uniform over the curated set | Every surviving row equally likely. The implicit default | Your filter's removal rates varied by cluster, so uniform sampling still reflects whatever skew survived |
| Balanced by cluster | Sample a cluster, then a row within it | Small clusters get heavily repeated. Effective only with a floor on cluster size |
| Weighted by source | Explicit mixture weights per origin | Requires provenance, which most pipelines lose at the first join. Worth preserving for exactly this reason |
| Curriculum by score | Cleaner data first, or last | Fashionable, 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.
Three papers, one instrument, and a change in what the field considers a contribution. Here is what happened next and what is still broken.
| Chapter | The question | The instrument | The number |
|---|---|---|---|
| 1 | What is wrong with a web corpus? | Three distributions from one embedding pass | Top 100 concepts of a million take 36% of the data |
| 2 | How do I remove what I already have? | Cluster, compare within cluster, keep the outlier | Clustering divides the pairwise work by exactly k |
| 3 | Why not just hash it? | dHash, MinHash, and their blind spots | A one-column crop flips 7 of 16 bits; one changed word takes Jaccard to 0.33 |
| 4 | How do I know a filter is better? | Freeze everything but the mask | Samples seen, not epochs. That single choice makes the rest work |
| 5 | What actually wins? | Alignment scoring, and composition with a distribution filter | 0.176 → 0.297 on ImageNet; and the average-best filter is a different one |
| 6 | How do I get what I do not have? | Retrieval from a curated seed | 1.2B pool → 142M curated, with no labels anywhere |
| 7 | What does this cost over time? | Effective cluster count, per round | A factor of 0.913 per round is 37% of your diversity in five rounds |
| 8 | What do I do on Monday? | Embed, histogram, bisect, read fifty, log | k = √N, two to five epochs over survivors, ε from budget |
| Quantity | Value | Where it came from |
|---|---|---|
| LAION subset SemDeDup operated on | ≈ 440M pairs | SemDeDup, image experiments |
| Clusters used for that subset | k = 50,000 | SemDeDup |
| Items per cluster | 440M / 50,000 = 8,800 | Derived, Chapter 2 |
| Global pair count vs within-cluster | 9.68 × 1016 vs 1.94 × 1012 — a factor of k | Derived, Chapter 2 |
| Similarity-matrix memory, global vs per cluster | 193 PB vs 155 MB | Derived, Chapter 2 |
| Fraction of LAION removable | ≈ 50%, performance held, out-of-distribution improved | SemDeDup |
| Fraction of web text removable | ≈ 15%, perplexity held or improved | SemDeDup, text experiments |
| SemDeDup survivor rule | Keep the member farthest from the cluster centroid | SemDeDup |
| CommonPool size | 12.8B image-text pairs | DataComp |
| DataComp scales | 12.8M / 128M / 1.28B / 12.8B, samples seen = pool size | DataComp |
| Medium scale, no filtering | 0.176 ImageNet, 0.258 avg-38 | DataComp baselines |
| Medium scale, CLIP score L/14 top 30% | 0.273 ImageNet, 0.338 avg-38 | DataComp baselines |
| Medium scale, image-based ∩ CLIP score | 0.297 ImageNet, 0.328 avg-38 | DataComp baselines |
| DataComp-1B result | ViT-L/14 to 79.2% ImageNet zero-shot, +3.7 points over OpenAI's ViT-L/14 at equal compute | DataComp |
| DINOv2 uncurated pool | ≈ 1.2B unique images | DINOv2, data section |
| DINOv2 curated result | LVD-142M | DINOv2 |
| Retrieval modes | Sample-based (N ≈ 4 neighbours per query); cluster-based (k ≈ 100,000, M per query) | DINOv2 |
| Index memory, float32 vs product-quantised | 4.9 TB vs 38.4 GB at 32 bytes per vector | Derived, Chapter 6 |
| MinHash-LSH sharpness | r = 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.45 | Derived, Chapter 7 |
| Paper | The technique | The thing that mattered more |
|---|---|---|
| SemDeDup | Cluster, then compare within cluster, keep the outlier | Establishing that half of a web-scale image corpus is semantic redundancy — a number nobody had, about a category of duplicate nobody was removing |
| DataComp | A pool, four scales, a frozen recipe, 38 evaluations | Making a filter into a comparable scientific artifact, so curation knowledge could accumulate instead of staying tacit |
| DINOv2's data pipeline | Retrieval-augmented curation from a curated seed | Showing that a dataset can be grown by nearest-neighbour search with no labels and no taxonomy — steering by example rather than by rule |
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.
| Limitation | Where it comes from | What would fix it |
|---|---|---|
| Every filter inherits its embedder's blind spots | The embedding is a lossy map, and there is no way to recover a distinction it discarded | Ensembles 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 measurements | Cosine is a number; "duplicate" is a decision about your task | Nothing 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 run | Cheap proxies for influence. An open problem — the principled methods still cost the run you were trying to avoid |
| Recipe-conditional results | Benchmarks freeze the training recipe to isolate the data | Reporting filter results across several recipes and budgets, which almost nobody does |
| Consent, licensing, and provenance | Web pools contain material whose creators never agreed to this | Not 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 have | Every selection rule encodes a target distribution | Evaluations that are deliberately off-anchor, and the discipline to report them alongside the flattering ones |
| If you want… | Go to |
|---|---|
| The machinery that makes any of this searchable | Vector databases and embedding ops |
| The clustering step, derived properly | k-means |
| What a cosine actually measures and its alternatives | Similarity metrics and vector embeddings |
| The contrastive objective these embedders are trained with | Contrastive learning and CLIP |
| The language-model side of filtering and deduplication | CS336: data filtering and dedup and data sources and pipelines |
| The retrieval system this all feeds | RAG and multimodal RAG |
| The models being curated for | DINOv2 and OpenCLIP |
Strip everything away and one claim remains, and it is falsifiable, which is what makes it worth holding.
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.
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.
| Question | Where 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.
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.
| Objection | The part that is right | The part that is not |
|---|---|---|
| "This is just data cleaning with extra steps" | The operations really are simple: a dot product and a threshold | Cleaning 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 pairs | Redundancy 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 them | Generated 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 discoveries | A 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 money | Every 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 |
| Exercise | What it teaches | Roughly |
|---|---|---|
| 1. Run the thirty-minute triage on a corpus you already have | That your data is not what you think it is. Everyone is surprised by their largest cluster | Half an hour |
| 2. Implement the greedy dedup pass and reproduce the six-item table by hand | That the ordering rule does two jobs, and that chains are not cliques | An hour |
| 3. Compare a perceptual hash and an embedding on fifty pairs you construct | Exactly where the hash stops working, in your data rather than in a diagram | An afternoon |
| 4. Build a two-point filtering benchmark: filtered and unfiltered, matched steps | The discipline of freezing everything else, which is harder than it sounds | A day plus a training run |
| 5. Seed a retrieval curation from a folder of failure cases and mix the result in | That you can steer a dataset with examples, and how easily it overshoots | A 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 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.
| Moment | What became possible |
|---|---|
| Web-scale image-text corpora released openly, filtered by a cross-modal score | Anyone 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 it | Removal stopped being a purely economic act. Careful pruning could improve a model, which made curation a source of capability |
| Semantic deduplication at web scale | The 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 recipe | Filters became comparable artifacts. Curation knowledge started to accumulate |
| Retrieval-based curation from a curated seed | Datasets could be grown rather than trimmed, with no labels and no taxonomy |
| Purpose-trained filtering networks; metadata balancing; recaptioning | The 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 data | The 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.
| Instinct | Replace 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.
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 ask | Why it matters | What "unstated" usually means |
|---|---|---|
| What was the pool, and what was actually trained on? | The ratio is the filter's aggressiveness, which sets the epoch count | The 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 evaluation | Usually 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 bias | Not 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 confounded | Matched epochs rather than matched steps. Check |
| What is the anchor of any reference-set filter? | Tells you which evaluations are partly circular | An anchor exists and is described as "a high-quality reference set" |
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.
| Problem | Why it is hard | What a solution would look like |
|---|---|---|
| Cheap learnability signals | "Does the model need this?" currently requires the training run you were trying to avoid | A 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 removed | You cannot measure the absence of a capability you never tested for | Automated 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 loop | Every learned selector inherits its training distribution | Selection signals grounded outside the model family — metadata, provenance, external structure — combined with, not replaced by, learned scores |
| Recipe-robust filter evaluation | Results are conditional on the frozen recipe, and freezing is what makes them comparable | Reporting across several recipes and budgets, and a norm that a single-recipe result is a hypothesis |
| Provenance and consent at scale | The pool predates the norms; the tooling does not exist | Per-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.
| Step | What to do | The decision that matters |
|---|---|---|
| 1. Pick a corpus you own | A fine-tuning set, a document collection, an image folder. Ten thousand items is plenty | Choose one where you can judge quality by eye. You will need to |
| 2. Embed once, cache forever | Any small pretrained embedder. Write the vectors to disk keyed by ID | Match the embedder's invariances to what you are protecting (Chapter 3) |
| 3. Compute the three audit numbers | Nearest-neighbour similarity distribution, alignment distribution if paired, cluster-occupancy entropy | Do this before you decide anything. The histogram tells you where your threshold lives |
| 4. Bisect to a removal target | Set the target from your compute budget, not from a paper's percentage | Two to five epochs over survivors (Chapter 5) |
| 5. Read fifty removed items | With your own eyes, before committing | Non-negotiable. Every aggregate can hide a systematic error a sample reveals |
| 6. Check per-cluster removal rates | Sort clusters by removal rate and look at the top ten | An outlier cluster is a data-generation bug, not a dedup result |
| 7. Train and compare at equal steps | Same optimiser steps, filtered versus unfiltered | Equal steps, not equal epochs — otherwise you have confounded quality with compute |
| 8. Log entropy every time you re-curate | Effective cluster count, per round | The only cheap early warning for Chapter 7's ratchet |
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.