Contrastive learning needs a pair of views of the same thing. Text has no safe augmentation. So SimCSE stopped looking at the input and looked inside the model — where a random mask was already generating a second view, for free, on every forward pass.
You have forty thousand support tickets and a duplicate problem. Somebody writes "my card was declined at checkout" on Monday. Somebody else writes "payment failed when I tried to pay" on Tuesday. These are the same ticket. No word except "pay" is shared, and even that one appears in a different form.
Keyword matching will not find this. You need a function that eats a sentence and produces a fixed-length vector, such that two sentences meaning the same thing land near each other and two sentences meaning different things land far apart. That function is a sentence embedding — a map from variable-length text to a fixed point in Rd, where geometric closeness is supposed to mean semantic closeness.
Everything downstream is built on that one function. Retrieval, clustering, deduplication, semantic search, RAG, recommendation, spam grouping, near-duplicate detection. Get the embedding right and all of them are a nearest-neighbour query away. Get it wrong and every one of them fails in the same quiet, hard-to-debug way: the numbers all look plausible and none of them mean anything.
The field's shared yardstick is STS — Semantic Textual Similarity. A dataset of sentence pairs, each scored by human annotators from 0 (unrelated) to 5 (equivalent). The model produces a cosine similarity for each pair. You then ask: does the model's ranking of the pairs agree with the humans' ranking?
The agreement number is Spearman's rank correlation — the Pearson correlation computed on the ranks rather than the raw values. Rank-based is the right choice here, because nobody claims a cosine of 0.8 should mean a human score of 4.0. All we want is that pairs the humans put higher, the model also puts higher. Spearman runs from −1 (perfectly reversed) through 0 (no relationship) to 1 (perfect agreement), and the literature reports it ×100.
Here is the state of the world when SimCSE was written, and it is genuinely embarrassing for deep learning.
GloVe is a 2014 method: a static lookup table with one vector per word type, trained by factorising a word co-occurrence matrix. To embed a sentence you average the word vectors. It has no parameters that know about word order, negation, syntax, or context. It cannot tell "dog bites man" from "man bites dog".
BERT-base is a 110-million-parameter transformer pretrained on 3.3 billion words with masked language modelling. It is contextual, deep, and revolutionised almost every NLP task on contact. To embed a sentence you average its final-layer token vectors (or take the [CLS] token).
| Method | Avg. Spearman over 7 STS tasks |
|---|---|
| GloVe embeddings, averaged | 61.32 |
| BERT-base, averaged token embeddings (first + last layer) | 56.70 |
BERT-base, [CLS] token | worse still |
Read that again. A 2014 bag of static word vectors beats a 2018 transformer by four and a half points, on the task of deciding whether two sentences mean the same thing. The transformer knows vastly more about language. It is simply not storing that knowledge in a place where cosine similarity can read it.
Each dot is one sentence pair from a semantic-similarity benchmark: human score on the horizontal axis, model cosine on the vertical. A perfect model would produce a rising line. Switch encoders and watch the cloud change shape. Notice what is wrong with raw BERT: it is not that the correlation is weak, it is that the entire vertical range has collapsed into a band near the top. Unrelated sentences score 0.7.
This is a schematic reconstruction, not the paper's raw predictions: the dots are generated so that each panel's Spearman correlation matches the reported number and the cosine range matches the known behaviour of each method. It is here to make a number visible, not to substitute for the number.
Two failure shapes are worth separating, because they need different fixes and people constantly conflate them.
Failure shape one: no ranking signal. The cloud is a blob. High-similarity and low-similarity pairs get the same cosine. The model genuinely does not know.
Failure shape two: compressed range. The cloud rises but occupies a sliver — every pair, related or not, scores between 0.65 and 0.95. The model does know something, but the information is buried under a huge constant offset that swamps it. Raw BERT is mostly this second kind, which is why post-processing tricks (Chapter 6) can rescue a surprising amount of it without any training.
By 2019 the field had an answer: SBERT (Sentence-BERT). Take BERT, put two copies in a siamese arrangement, and fine-tune on labelled sentence pairs so that the geometry is forced to mean something. The labels come from NLI — Natural Language Inference — datasets, where humans wrote, for each premise, one sentence that follows from it (entailment), one that contradicts it, and one unrelated (neutral).
SBERT works. It takes the 7-task STS average from 56.70 to 74.89. And it costs exactly what supervised learning always costs.
| The bill for supervision | What it means concretely |
|---|---|
| 570,152 SNLI + 392,702 MNLI annotated triples | Roughly a million human judgements, collected over years by two large funded projects |
| English only | Swahili, Tamil, Basque: no NLI corpus of comparable size. Start from zero |
| General domain only | Your legal contracts, your radiology reports, your product SKUs — no NLI corpus exists |
| Static | New jargon, new product names, new failure modes: re-annotate or accept drift |
So the question that opens this paper is not "can we do better than SBERT". It is: can we get that geometry without paying that bill at all?
The obvious idea is contrastive learning: instead of labels, use pairs of views of the same object. Show the model two versions of the same thing and one version of a different thing, and train it to tell which is which. Vision had made this work spectacularly — SimCLR, MoCo — using image augmentations. Crop the photo, jitter the colours, flip it horizontally: the content is unchanged, the pixels are completely different. Two views, no labels.
Text has no such augmentation, and the reason is worth stating precisely rather than hand-waving. Images are continuous and redundant; text is discrete and brittle. A photograph's meaning survives moving every pixel by a few percent, because meaning lives in a heavily overdetermined signal. A sentence's meaning does not survive changing one token, because meaning lives in a code with almost no redundancy.
| Proposed text augmentation | Why it seems fine | Why it breaks |
|---|---|---|
| Delete a random word | Sentences are redundant, surely | "The film was not good" → "The film was good". One deletion, meaning inverted. And the model is being trained to call those two identical |
| Crop a span | Analogous to an image crop | A sentence's clauses are not interchangeable regions. Cropping "she denied that he left" yields "he left" |
| Synonym replacement | Synonyms mean the same thing | Rarely exactly. "Cheap" / "inexpensive" / "shoddy" all appear in a thesaurus. Context decides, and the augmenter does not read context |
| Mask-and-refill with an MLM | Use the language model to keep it fluent | Fluent is not faithful. The refiller happily writes a grammatical sentence with a different subject |
| Back-translation (EN→DE→EN) | Genuinely meaning-preserving, mostly | Needs two translation models, is orders of magnitude slower than a forward pass, and its paraphrases are systematically biased toward MT-flavoured phrasing |
SimCSE's answer is one of those ideas that sounds like a joke until you check the number.
Stop trying to perturb the input. The model is already perturbing its own internals, on every single forward pass, for free, and has been since 2017.
That perturbation is dropout: during training, a transformer randomly zeroes a fraction p of the activations in each layer (and rescales the survivors), a regularisation trick that has been switched on by default in every BERT-family model ever released. It means that feeding the same sentence twice through the same model in training mode produces two different vectors.
The whole unsupervised method is: take a million raw sentences from Wikipedia, feed each one through BERT twice, call the two outputs a positive pair, call every other sentence in the batch a negative, and minimise a contrastive loss. There is no augmentation module. There is no label. There is no second network. The implementation change relative to a standard fine-tuning loop is roughly four lines.
| Method | Human labels used | Avg. STS (7 tasks) |
|---|---|---|
| BERT-base, averaged | none | 56.70 |
| BERT-flow (post-hoc normalising flow) | none | 66.55 |
| BERT-whitening (post-hoc linear transform) | none | 66.28 |
| CT-BERT (previous best unsupervised) | none | 72.05 |
| SBERT-base (supervised on NLI) | ~1M triples | 74.89 |
| Unsupervised SimCSE-BERT-base | none | 76.25 |
| Supervised SimCSE-BERT-base | 314k NLI pairs | 81.57 |
| Supervised SimCSE-RoBERTa-large | 314k NLI pairs | 83.76 |
The row that stops people is the sixth. An unsupervised model, trained on unlabelled Wikipedia sentences, beats the supervised SBERT that consumed a million human annotations. Not by a lot — 1.4 points — but the direction is the shock. And when SimCSE is also given the NLI labels, it takes another 5.3 points on top, so the two contributions stack rather than compete.
SimCSE is not important because 76.25 is a large number. It is important because it is the point at which the modern embedding recipe crystallised. Open the training code of essentially any text embedder shipped since — sentence-transformers v2, E5, GTE, BGE, Instructor, the commercial embedding APIs — and you will find the same four ingredients, in the same arrangement:
Ingredients 2, 3 and 4 are Chapters 1, 2 and 4 of this lesson. Chapter 9 traces exactly which later systems changed which ingredient and why.
Before we can talk about where positive pairs come from, we need to know what we are going to do with them. This chapter builds the contrastive objective from nothing, one repair at a time, until we arrive at the exact equation SimCSE optimises. Every symbol will be named, the gradient will be derived, and we will run a small batch through by hand.
We want an encoder f such that a sentence x and its paraphrase x+ get nearby vectors. The most direct way to say that is to write it as a loss and minimise it:
Where N is the number of pairs in the batch, f is the encoder (BERT plus whatever pooling), and the double bars are the Euclidean norm. Lower means the pair is closer. Minimise it and the pairs get closer. Done?
No. There is a solution with loss exactly zero that requires no understanding of language at all:
Map everything to the origin. Every pair distance is zero. The loss is zero. It is the global minimum, it is trivially reachable by gradient descent, and it is completely useless. This is representational collapse — the failure mode where a model satisfies an objective by discarding all information rather than by organising it.
"Gradient descent finds the constant function" deserves a demonstration rather than an assertion, and for a linear encoder we can do it exactly.
Let the encoder be a matrix: f(x) = A x. Write δi = xi − xi+ for the difference between a pair. Then
Differentiate: ∂L/∂A = 2AS. So one gradient step with learning rate η gives
The update multiplies A by a fixed contraction. Suppose S = diag(0.5, 0.2) and η = 0.5, so I − 2ηS = diag(0.5, 0.8). After ten steps every column of A has been scaled by 0.510 = 0.00098 in the first direction and 0.810 = 0.107 in the second. After a hundred steps, 7.9 × 10−31 and 2.0 × 10−10.
The encoder is being annihilated, exponentially, in every direction where the data has any variation at all. And the loss is dropping the whole time — monotonically, smoothly, exactly as a healthy training curve looks. This is why collapse is so dangerous in practice: the loss curve gives you no warning whatsoever.
The repair is to make the objective relative. It should not be enough for the positive pair to be close; it must be closer than the alternatives. Now the constant function fails, because if everything is at the origin, the positive is not closer than anything — it ties with all of them.
There are two classical ways to write "closer than the alternatives". The first is a triplet margin loss:
Read it as: the positive must be closer than the negative by at least a margin m; if it already is, the loss is zero and nothing happens. This works — it is what SBERT and most of the 2015–2019 metric-learning literature used — and it has two properties that InfoNCE fixes.
| Triplet margin loss | InfoNCE | |
|---|---|---|
| Negatives per anchor | One, chosen explicitly | All N−1 others in the batch, free |
| Gradient once satisfied | Exactly zero — the hinge closes | Small but non-zero, and it re-focuses automatically |
| Hard-negative mining | Mandatory. Without it most triplets are already satisfied and training stalls | Implicit — the softmax weights each negative by how confusable it is |
| Extra hyperparameters | Margin m, plus a mining strategy | Temperature τ |
| Failure mode | Silence: the loss reads 0.00 while the model has learned little | Saturation at very low τ (Chapter 5) — but it is visible in the loss |
The "exactly zero" row is the one that decides it. With a margin of 0.2 and a batch where 90% of triplets are already separated by more than 0.2, ninety percent of your compute produces no gradient at all. Teams spend enormous effort on mining pipelines to fix this. InfoNCE makes the mining a property of the loss instead of a property of the data loader, which is why it took over.
The second way, which is what SimCSE uses, is to turn the whole thing into a multiple-choice test.
Here is the reframing, and it is the single most useful mental model in this lesson.
Take a batch of N pairs: (x1, x1+), (x2, x2+), …, (xN, xN+). Now scramble the second elements and hand the model a quiz:
That is an ordinary N-way classification problem, and we already know how to train those: produce a score for each candidate, softmax the scores into a distribution, take the cross-entropy against the correct index. The labels are free — they are just 0, 1, 2, ..., N-1, the identity permutation. No human wrote them.
This is the entire idea. The in-batch negatives trick means every other example in the batch serves as a negative for every example, so a batch of size N yields N positive constraints and N(N−1) negative constraints at the cost of N forward passes. Negatives are free.
We need a number saying how compatible two embeddings are. The obvious choice is the dot product hiThj. It is the wrong choice, for a reason worth seeing concretely.
Suppose the encoder produces h1 = (3, 4) for a sentence and h1+ = (6, 8) for its paraphrase. Those point in exactly the same direction — the second is the first doubled. Semantically the encoder has done a perfect job. The dot product is 3(6) + 4(8) = 18 + 32 = 50. Now take a genuinely unrelated sentence whose embedding happens to be large: h2+ = (20, 1). The dot product with h1 is 3(20) + 4(1) = 64. The unrelated sentence wins, purely because it is long.
Worse, the model can exploit this. Under a raw dot product, one way to reduce the loss is to grow ‖h‖ for confident examples, which inflates every score in that row. The optimiser will happily spend capacity on a magnitude channel that has nothing to do with meaning.
The fix is cosine similarity: divide out the lengths, keeping only direction.
Check the example: ‖(3,4)‖ = 5, ‖(6,8)‖ = 10, so the cosine is 50/(5×10) = 1.000, correctly reporting a perfect match. For (20,1): ‖(20,1)‖ = √401 = 20.025, so the cosine is 64/(5 × 20.025) = 64/100.12 = 0.639. Order restored.
Equivalently: L2-normalise every embedding onto the unit sphere first, then the plain dot product is the cosine. That is how it is implemented, and it is the reason you will see F.normalize in every contrastive codebase.
Put the pieces together. For example i in a batch of N:
This is the InfoNCE loss (Noise-Contrastive Estimation with an information-theoretic reading, from van den Oord et al. 2018), and it is exactly the loss SimCSE minimises. Every symbol:
| Symbol | Meaning | Shape / value |
|---|---|---|
| N | Batch size — and therefore the number of candidates in the multiple-choice test | 64 for unsupervised SimCSE, 512 for supervised |
| hi | Embedding of the i-th sentence (the "query" view) | R768 for BERT-base |
| hi+ | Embedding of its positive partner (the "key" view) | R768 |
| sim(·,·) | Cosine similarity | scalar in [−1, 1] |
| τ | Temperature — divides every score before the softmax | 0.05 in SimCSE, so the multiplier is 20 |
| the sum over j | Runs over all N keys in the batch, including the correct one | N terms |
| ℓi | Loss for row i | scalar, ≥ 0 |
| L | Batch loss = mean of ℓi | scalar |
Two details people get wrong. First, the correct pair appears in the denominator as well as the numerator — the sum is over all N, not the N−1 negatives. That is what makes the ratio a genuine probability and bounds the loss below by 0. Second, the loss is written per row: row i asks "which key belongs to query i". The unsupervised SimCSE objective as published is one-directional, exactly like this. (CLIP-style symmetric losses also add the transpose direction; Chapter 5 says when that matters.)
Two reference values make the loss readable at a glance.
The chance level is ln N. If the model has no idea — all N scores equal — then every probability is 1/N, and ℓi = −log(1/N) = log N. For N = 64 that is ln 64 = 4.159. If your contrastive loss is sitting at 4.16 with a batch of 64, your model is guessing, and the most common cause by far is a mis-set temperature.
The floor is 0, reached when pii = 1, i.e. the positive score is infinitely far above all others. With cosine scores bounded in [−1, 1] and a finite temperature, this is unreachable: the best possible row has sim = 1 on the diagonal and −1 everywhere else, giving
At τ = 0.05 and N = 64: e−40 = 4.25 × 10−18, times 63, gives 2.7 × 10−16. So the practical floor is zero to sixteen decimal places. At τ = 1 the same expression gives log(1 + 63 × 0.1353) = log(9.525) = 2.254 — the loss literally cannot go below 2.25 no matter how perfect the encoder is. That single calculation is the most compelling argument for a small temperature there is.
Numbers make this concrete. Suppose a batch of N = 3 produces the following cosine similarity matrix, where row i is query i and column j is key j:
| cos | key 1 | key 2 | key 3 |
|---|---|---|---|
| query 1 | 0.72 | 0.54 | 0.19 |
| query 2 | 0.54 | 0.72 | 0.15 |
| query 3 | 0.36 | −0.27 | 0.78 |
Take row 1 at τ = 1 first, so the arithmetic is transparent. Logits are just the cosines: 0.72, 0.54, 0.19.
Sum: 2.0544 + 1.7160 + 1.2092 = 4.9796. Probabilities:
Loss for row 1: ℓ1 = −ln(0.4126) = 0.8853.
Compare to chance, ln 3 = 1.0986. The model is only 0.21 nats better than guessing, even though the correct answer has the highest cosine by a comfortable 0.18 margin. That is the flatness problem: with τ = 1 a decisive geometric win translates into a feeble probabilistic one.
Now the same row at τ = 0.05, multiplying every cosine by 20. Logits: 14.4, 10.8, 3.8. Subtract the max (a standard numerical-stability step that leaves the softmax unchanged) to get 0, −3.6, −10.6:
Sum: 1.0273488. So p11 = 1/1.0273488 = 0.97338, and ℓ1 = ln(1.0273488) = 0.02698.
Same geometry. Same encoder. Thirty-three times less loss, purely from the divisor. Chapter 5 unpacks what that does to the gradients — because "less loss" is not automatically "better training", and the trade-off is sharp.
To understand any loss you must know what it does to the parameters. Write sij = sim(hi, hj+) and the logit zij = sij/τ. Then
Differentiate with respect to a single logit zik. The first term contributes −1 when k = i and 0 otherwise. The second term is the log-sum-exp, whose derivative is the softmax:
and therefore, chaining through the temperature,
This little expression is worth sitting with, because it tells you the entire training dynamics.
| Case | Gradient w.r.t. the similarity | What the update does | Magnitude |
|---|---|---|---|
| k = i (the positive) | (pii − 1)/τ, which is negative | Increases sii — pulls the pair together | Proportional to (1 − pii): vanishes once the model is already right |
| k ≠ i (a negative) | pik/τ, positive | Decreases sik — pushes them apart | Proportional to pik: concentrated on the negatives the model currently finds confusing |
Two consequences that people pay for when they forget them. First, the total force is balanced: ∑k ∂ℓi/∂zik = ∑k pik − 1 = 0. The attraction on the positive exactly equals the sum of repulsions on the negatives. Second, InfoNCE performs automatic hard-negative mining: you never have to select difficult negatives, because the softmax weights them by exactly how threatening they currently are.
Check that on the worked numbers. At τ = 1, row 1's probabilities were (0.4126, 0.3446, 0.2428). The attraction weight is 1 − 0.4126 = 0.5874, and the two repulsions are 0.3446 and 0.2428 — the harder negative gets 1.42× the push of the easier one. At τ = 0.05, the probabilities were (0.97338, 0.026596, 0.0000241); the ratio between the two repulsions is now 0.026596/0.0000241 = 1103. Sharpening the softmax does not just shrink the loss; it re-allocates virtually all of the repulsive force onto the single hardest negative. Hold that thought for Chapter 5.
Here is the whole objective as it actually executes, with every tensor shape named. This is the code SimCSE runs; everything else in the paper is about what you feed it.
python — the contrastive core, end to endimport torch, torch.nn.functional as F # z1, z2 are the two views of the SAME N sentences. # z1: (N, 768) the "query" view h_i # z2: (N, 768) the "key" view h_i^+ (row i is row i's partner) z1 = F.normalize(z1, dim=-1) # (N, 768), every row now has norm 1 z2 = F.normalize(z2, dim=-1) # (N, 768) sim = z1 @ z2.T # (N, N) sim[i,j] = cos(h_i, h_j^+) logits = sim / 0.05 # (N, N) temperature = 0.05 -> multiply by 20 labels = torch.arange(z1.shape[0], device=z1.device) # (N,) = [0,1,2,...,N-1] loss = F.cross_entropy(logits, labels) # scalar
Six lines. The one that carries the whole idea is the labels line: the supervision is arange(N). No human produced it; it is a consequence of how the batch was assembled. Everything creative in this paper happens upstream of these six lines, in the question of how z1 and z2 came to be two views of the same sentence.
Because the labels come from the batch construction rather than from the world, a falling contrastive loss proves only that the model can tell batch members apart. It is entirely possible to drive the loss to 0.001 while producing embeddings that are useless for STS — if the model discovers a shortcut feature (sentence length, first token, a punctuation quirk) that identifies each batch member without touching meaning.
And SimCSE is the method where that worry bites hardest, because its two views are the same token sequence. Sentence length, punctuation, rare tokens, casing — every surface feature is preserved exactly across the positive pair. A model that learned nothing but "match the sentence with the same number of tokens" would drive this loss down beautifully. This is the strongest objection to the whole method, and you should be holding it against the paper right now.
The answer is an ablation we will meet in Chapter 2, and it is clean. If surface identity were what the model exploits, then the variant with dropout switched off — where the two views are not merely surface-identical but bit-identical, making every shortcut maximally available — ought to do at least as well. It does not. It scores 71.1 on STS-B dev against 82.5 with dropout at p = 0.1. The shortcut is fully available in both settings; only one of them learns anything worth having. Whatever dropout supplies, it is not a shortcut, and Chapter 3 will name it precisely.
Chapter 1 built the machine and left a hole in it: hi+. The loss needs a second view of every sentence, and Chapter 0 established that editing tokens to produce one is a losing game. This chapter fills the hole.
Dropout is a regularisation technique from 2014: during training, each activation in a layer is independently zeroed with probability p. Nothing more exotic than that. Let u be a layer's output vector and m a random binary mask with mk ~ Bernoulli(1 − p). Then the naive version is
This has a problem you can see with one line of arithmetic. The expected value of the output has shrunk: E[hk] = E[mk] · uk = (1 − p) uk. At p = 0.1, every activation is 10% smaller in expectation than it would be without dropout. So a network trained with dropout and evaluated without it sees systematically inflated inputs at every layer, and the inflation compounds through depth.
The universal fix is inverted dropout: scale the survivors up during training so the expectation is preserved, and do nothing at all at test time.
Check it at p = 0.1: survivors are multiplied by 1/0.9 = 1.111. Nine tenths of the units survive and are 11.1% larger; one tenth is zero. The mean is unchanged; the variance is not. That injected variance is the regularisation — and, for our purposes, it is the augmentation.
"BERT uses dropout" is too vague to reason about. Here is the real inventory for BERT-base, all at p = 0.1 by default in the released config:
| Site | Tensor it masks | Shape (batch 1, T = 32 tokens) | Count per layer |
|---|---|---|---|
| Embedding output | Token + position + segment sum, after LayerNorm | (32, 768) | once, before layer 1 |
| Attention probabilities | The softmax'd attention matrix, per head | (12, 32, 32) | 1 |
| Attention output | The projected context vector, before the residual add | (32, 768) | 1 |
| Feed-forward output | The second FFN projection, before the residual add | (32, 768) | 1 |
Count the Bernoulli draws in one forward pass of a 32-token sentence through the 12 layers:
Three quarters of a million random bits are sampled every time a sentence goes through the model. The probability that two forward passes draw the same mask is, for practical purposes, zero. So the following statement — which sounds like a bug report — is simply true:
Write hiz = fθ(xi, z) for the encoder's output on sentence xi under dropout mask z. Sample two independent masks, zi and zi′. Then the objective is Chapter 1's InfoNCE with the two views defined this way:
That is the entire unsupervised method. The only difference from a loss you could have written in 2018 is the superscripts: the positive pair is one sentence under two masks, and the negatives are the other sentences in the batch under their own masks.
Here is the part that is genuinely charming. SimCSE does not run the model twice. It duplicates each sentence inside the batch and runs one forward pass, because dropout masks are sampled per element — two copies of the same row get independent masks automatically.
python — the four lines that make the positive pair# batch: N sentences, tokenised to length T input_ids = batch["input_ids"] # (N, T) # 1. duplicate: each sentence appears twice, adjacent input_ids = input_ids.unsqueeze(1).repeat(1, 2, 1) # (N, 2, T) input_ids = input_ids.view(-1, input_ids.size(-1)) # (2N, T) <- flatten # 2. ONE forward pass in train mode. Dropout is sampled per row, # so rows 2i and 2i+1 (the same tokens!) get DIFFERENT masks. out = encoder(input_ids).last_hidden_state[:, 0] # (2N, 768) [CLS] token out = mlp(out) # (2N, 768) pooler: Linear + Tanh # 3. split back into the two views out = out.view(-1, 2, out.size(-1)) # (N, 2, 768) z1, z2 = out[:, 0], out[:, 1] # (N, 768) each # 4. Chapter 1's six lines, unchanged. loss = F.cross_entropy(F.normalize(z1,dim=-1) @ F.normalize(z2,dim=-1).T / 0.05, torch.arange(z1.size(0), device=z1.device))
Notice what is absent: there is no augmentation function, no synonym dictionary, no translation model, no data preprocessing step. The augmentation is a side effect of model.train(). The compute cost is one extra forward pass per sentence — the batch is 2N rows instead of N — and nothing else.
repeat must interleave (N,2,T then flatten) so that each pair is adjacent and the later view(-1,2,d) recovers the right partner. If you instead concatenate two copies of the batch (cat([x,x]), giving 2N rows in blocks), you must split as out[:N], out[N:] — mixing the two conventions silently pairs sentence i with sentence i+N/2 and your loss will sit stubbornly at ln N. (2) The model must be in train() mode. Call eval() anywhere in the loop and dropout switches off, the two views become bit-identical, and you have silently reproduced the paper's worst ablation.One sentence, one encoder, two forward passes. The grid is a slice of hidden units; a dark cell was dropped, a bright cell survived and was scaled up by 1/(1−p). Below, the two resulting embeddings and the cosine between them. Resample to see how much the twin pair moves, change p, and switch on fixed mask to see the pathological case where both passes draw the same mask — the twins become identical and the positive term stops existing.
Play with it until three things are obvious. At p = 0 the two views are identical and the cosine is exactly 1.000, forever, no matter how many times you resample. At p = 0.5 the views are wildly different and the cosine wanders down toward 0.5 — the "positive pair" is now barely a pair at all. At p = 0.1 the cosine sits high but never at 1: the twins are close, and reliably not identical. That narrow band is where the method lives.
The sim shows the twin cosine hovering near 0.9 at p = 0.1 and near 0.5 at p = 0.5. Those numbers are not accidents. For our simplified single-site dropout there is an exact answer, and it is startlingly clean.
Write the two views component-wise, with mA and mB independent Bernoulli(1−p) masks:
The numerator. A unit contributes to the dot product only if it survived both masks, which happens with probability (1−p)2 by independence. The 1/(1−p) factors contribute 1/(1−p)2. They cancel exactly:
The denominator. Each norm involves only one mask, so only one factor of (1−p) survives against the squared rescaling:
For large d these quantities concentrate tightly around their means, so ‖hA‖ ‖hB‖ ≈ ‖u‖2/(1−p), and
The expected cosine between a sentence and its dropout twin is one minus the dropout rate. Check it against a Monte-Carlo simulation over 4,000 random vectors:
| p | Simulated E[cos], d = 32 | Simulated E[cos], d = 768 | Prediction 1 − p |
|---|---|---|---|
| 0.05 | 0.9495 | 0.9499 | 0.9500 |
| 0.10 | 0.8990 | 0.8997 | 0.9000 |
| 0.15 | 0.8495 | 0.8509 | 0.8500 |
| 0.20 | 0.7955 | 0.8007 | 0.8000 |
| 0.50 | 0.4924 | 0.4997 | 0.5000 |
One honest correction before you over-apply it. Real BERT applies dropout at 37 sites inside the network, and each perturbation is partially absorbed by the LayerNorms and residual connections that follow it — the model was trained to be robust to exactly this noise. So the observed twin cosine in a real encoder is higher than 1−p, not equal to it. What survives is the monotone relationship and the shape of the curve, which is all the argument needs.
An idea this cheap deserves scepticism, so the paper ran the comparison directly: replace dropout with each of the token-level augmentations everyone had been proposing, keep everything else identical, and read the STS-B development score.
| Positive pair construction | STS-B dev (Spearman ×100) | Cost relative to the best |
|---|---|---|
| None — two dropout masks (unsup. SimCSE) | 82.5 | — |
| Crop 10% of tokens | 77.8 | −4.7 |
| Crop 20% | 71.4 | −11.1 |
| Crop 30% | 63.6 | −18.9 |
| Word deletion 10% | 75.9 | −6.6 |
| Word deletion 20% | 72.2 | −10.3 |
| Word deletion 30% | 68.8 | −13.7 |
| Delete exactly one word | 75.9 | −6.6 |
| Synonym replacement | 77.4 | −5.1 |
| MLM 15% (mask and refill) | 62.2 | −20.3 |
The same bars, sorted, with the dropout result marked. Tap a bar to see what that augmentation does to a real sentence and why it costs what it costs.
Two rows deserve individual attention.
"Delete exactly one word" scores 75.9. This is as gentle as a discrete augmentation can possibly be — a single token, out of maybe twenty. It still costs 6.6 points. The reason is that "one token" is not a small perturbation in a code with almost no redundancy. Delete not, and the sentence means the opposite. Delete the, and nothing happens. The augmenter cannot tell those cases apart, so a fraction of your positive pairs are actively teaching the model that a sentence and its negation are the same thing. Every one of those is a wrong label, and Chapter 5 will show that at τ = 0.05 wrong labels are punished with enormous force.
MLM refill scores 62.2, the worst of all. This is the most surprising row, because it is the most sophisticated method — use a language model to keep the sentence fluent. But fluency is not fidelity. Mask 15% of "the concert was cancelled because of the storm" and a well-trained MLM will cheerfully produce "the concert was cancelled because of the singer". Grammatical, plausible, and a different fact. The better your refiller, the more confidently wrong the augmentation becomes.
Two more families of alternative are worth recording, because they are the ideas most people propose next.
Use a neighbouring sentence as the positive. If the sentence after this one is on the same topic, it is a kind of positive. This is the idea behind several 2020 methods. As reported in the paper's second ablation table, taking the next sentence as the positive scores 67.1, and taking one of the next three scores 67.4 — both far below 82.5. The reason is a mismatch between "related" and "equivalent". STS asks whether two sentences mean the same thing; discourse adjacency only says they are about the same thing. Training on the second and evaluating on the first blurs precisely the distinction being measured.
Use two different encoders. Contrastive frameworks often use a query encoder and a separate key encoder (MoCo's momentum encoder, for instance). The paper tried it: with a shared encoder, dropout-SimCSE scores 82.5; with two independently parameterised encoders, 80.1. Sharing wins. And it wins for a structural reason: with two encoders, the model can satisfy the positive term by making the two encoders agree rather than by making the representation good, which is a cheaper and less useful solution. A single encoder removes that escape route.
The unsupervised model trains on 106 sentences sampled from English Wikipedia. Not filtered for quality, not paired, not deduplicated by topic — just a million sentences. That deserves a moment, because it is where the "no labels" claim cashes out.
| Knob | Value | Why |
|---|---|---|
| Corpus | 106 English Wikipedia sentences | Diverse, freely available, and needs no annotation of any kind |
| Max sequence length | 32 tokens | Sentence-level task; short sequences make the quadratic attention cost trivial |
| Batch size | 64 (so 128 rows after duplication) | Each row gets 63 in-batch negatives |
| Epochs | 1 | 106/64 ≈ 15,625 optimiser steps. That is a single-GPU afternoon |
| Learning rate | 3e-5 (base models) | Standard BERT fine-tuning territory |
| Temperature | 0.05 | Fixed, not learned. Chapter 5 explains the number |
| Pooling | [CLS] with the MLP head during training, MLP dropped at test | An empirical finding; Chapter 8 has the ablation |
Fifteen thousand steps on unlabelled text, and the resulting model beats a supervised system trained on a million human annotations. The asymmetry between the effort and the result is the reason this paper has 4,000+ citations.
Before the next chapter builds the proper lens, here is the ablation that will drive it — the dropout-rate sweep, on STS-B dev:
| p | 0.0 | 0.01 | 0.05 | 0.1 | 0.15 | 0.2 | 0.5 | Fixed 0.1 |
|---|---|---|---|---|---|---|---|---|
| STS-B | 71.1 | 72.6 | 81.1 | 82.5 | 81.4 | 80.5 | 71.0 | 43.6 |
Read the two ends. At p = 0 the two views are identical, and the score falls 11 points. At p = 0.5 the two views are barely related, and the score falls 11 points. There is a sweet spot, and BERT's default happens to sit almost exactly on it — a coincidence worth noticing, since nobody chose 0.1 with this in mind.
But the column that should stop you is the last one. Fixed 0.1 means dropout is on, at the standard rate, but the same mask is reused for both views. The two representations are again identical, exactly as at p = 0 — and yet the score is 43.6 rather than 71.1. Same "identical twins" situation, twenty-seven points worse. Chapter 3 is the machinery that explains this, and Chapter 8 comes back for the full reading.
We now have a method that works and no account of why. "Dropout is a good augmentation" is a restatement, not an explanation. This chapter installs the lens the paper borrows from Wang & Isola (2020), and it is the most portable idea in the whole lesson: two numbers that between them diagnose almost every failure of a representation space.
Think about what you are actually asking for. Two requirements, and they pull in opposite directions.
One: things that mean the same thing must be close. Otherwise similarity search misses obvious matches. Call this alignment.
Two: the space must be used. If every sentence maps to nearly the same point, then requirement one is trivially satisfied and the embedding is worthless — that is the collapse from Chapter 1. The embeddings should spread out and cover the sphere. Call this uniformity.
Alignment alone is maximised by the constant function. Uniformity alone is maximised by a random hash, which spreads everything perfectly and puts paraphrases in unrelated places. Any useful embedding is a compromise, and the value of Wang & Isola's framework is that it lets you measure where on that compromise you are, with two scalars, on a held-out set.
Assume every embedding has been L2-normalised, so it lives on the unit sphere. Let ppos be the distribution of semantically equivalent pairs. Then
The expected squared distance between the embeddings of positive pairs. Lower is better. Zero would mean every paraphrase pair maps to exactly the same point.
Because the vectors are unit length, this quantity has a much friendlier form. Expand the square:
So alignment is just a rescaled cosine: ℓalign = 2(1 − E[cos]). A positive-pair cosine of 0.99 gives ℓalign = 0.02; a cosine of 0.80 gives 0.40. The range is [0, 4], with 4 meaning every pair is antipodal.
Uniformity is subtler, because "spread out on a sphere" needs a functional form. Wang & Isola use the Gaussian potential — the same object physicists use for repulsive energy:
Take two independent samples, measure their squared distance, and evaluate a decaying exponential of it. Pairs that are close contribute nearly 1; pairs that are far contribute nearly 0. Average, then take the log. Lower is better here too.
Three questions people always have, answered.
Why the exponential? Because it is a soft nearest-neighbour count. e−2d2 asks "is there another point right here?" and the average answers "on average, how crowded is a random point's neighbourhood?" A collapsed representation is maximally crowded.
Why the log? Purely for scale. The raw average lives in (0, 1]; the log maps it to (−∞, 0], which makes differences between well-spread configurations legible instead of all being squashed near zero.
Why is the maximum 0? If every point maps to the same place, every distance is 0, every exponential is 1, the average is 1, and log 1 = 0. So ℓuniform = 0 is total collapse, and every real model produces a negative number. The more negative, the more spread.
Four sentences, embedded on a unit circle so the arithmetic is visible. We will compute uniformity over all six distinct pairs, and alignment on one positive pair.
Configuration A — the cone (what pretrained BERT looks like): the four points sit at 0°, 10°, 20°, 30°. Every pair is close. The six pairwise angles are 10°, 20°, 30°, 10°, 20°, 10°.
Squared distances, using ‖a−b‖2 = 2(1 − cosθ):
Average over the six pairs (three at 10°, two at 20°, one at 30°):
Barely below zero. This configuration is nearly collapsed.
Configuration B — spread: the same four points at 0°, 90°, 180°, 270°. Four of the six pairs are 90° apart (d2 = 2) and two are antipodal (d2 = 4).
From −0.19 to −4.40. The same four points, rearranged, and the uniformity number moves by 4.2 nats. This measure is extremely sensitive to exactly the pathology we care about.
Now alignment. Suppose sentence 1, at 0°, has a paraphrase that the model places at 5°. Then
Excellent alignment. But suppose that in the process of spreading the points out to reach configuration B, the model also pushed that paraphrase to 25° away:
Twenty-five times worse. That is the trade-off, quantified: the spreading that fixed uniformity destroyed alignment. Everything in this chapter is about which methods pay that price and which do not.
Here is the connection that makes the lens more than a diagnostic. Split Chapter 1's loss into its two pieces using log(a/b) = log a − log b:
The first term wants sii (the positive-pair cosine) as large as possible. That is alignment, up to the constant factors in ℓalign = 2(1 − sii).
The second term is a log-sum-exp of all similarities, which is dominated by the largest ones; minimising it pushes down whichever pairs are currently closest. That is a soft-max flavoured cousin of uniformity, whose log-mean-exp of −2d2 = −4(1−cos) has the same structure with a different constant. Wang & Isola made this rigorous: as the number of negatives goes to infinity, the contrastive loss converges to a weighted sum of exactly these two objectives.
So the contrastive objective is not related to alignment and uniformity. It is them, added together, with τ setting the exchange rate.
Now we can prove — not describe, prove — why removing dropout wrecks the model.
Suppose the two views are produced by the same deterministic function: hi+ = hi exactly, for every i, for every setting of the parameters. Then the positive similarity is
The alignment term is a constant. Its gradient is identically zero. It contributes nothing to any update, ever. The loss reduces to the uniformity term alone:
And "everything" includes sentence pairs that genuinely mean the same thing. The model has no term telling it that some pairs should stay together, so it separates them along with all the others. Held-out alignment collapses. That is the mechanism, and it is exact: the missing gradient is not weak, it is zero.
Dropout restores it. With independent masks, hiz ≠ hiz′, so sii < 1 and depends on θ. The model must now actively arrange its parameters so that a sentence's representation is stable under its own internal noise. That stability requirement is a smoothness constraint, and smoothness generalises: a model that cannot distinguish a sentence from its noisy self also cannot violently separate two sentences that differ only slightly in meaning. Alignment on held-out semantic pairs survives.
A working toy. Twelve sentences live on a circle; six pairs of them are secretly semantic twins (the thin links) and the training objective never sees those links — it only sees each sentence paired with its own dropout-jittered self, exactly as in unsupervised SimCSE. Press Run to descend on the InfoNCE loss and watch both coordinates move. The scatter on the right plots (uniformity, alignment) with a trail. Set dropout to 0 to reproduce the paper's collapse: uniformity plunges beautifully while alignment falls apart.
Run the three dropout settings and watch the trail take three different shapes.
| Setting | What the trail does | Why |
|---|---|---|
| none | Uniformity drops fast; alignment climbs badly. The trail heads down-and-right | The alignment gradient is exactly zero. Nothing resists the spreading, so semantic twins are separated along with everyone else |
| p = 0.1 | Uniformity drops; alignment stays roughly flat. The trail heads down-and-left, into the good corner | The positive term is alive and demands local stability, which generalises to nearby meanings |
| p = 0.5 | Alignment stays excellent — and almost nothing spreads. The trail barely leaves the right-hand edge | The two views are now so different that the only way to make them agree is to make the encoder nearly constant. Perfect alignment is bought by re-creating the collapse of Chapter 1 from the opposite direction |
This is the paper's Figure 3, reproduced from scratch in a toy small enough to read. The real version tracks the two measures every ten steps of actual BERT training and shows the same three shapes.
Applying the lens to the actual methods produces a map that explains why each one succeeds or fails, and why the post-hoc fixes plateau.
| Model | Alignment | Uniformity | Avg. STS | Reading |
|---|---|---|---|---|
| Avg. BERT | good (≈ 0.17) | terrible (≈ −1.3) | 56.70 | Everything is close to everything — including the paraphrases, which is why alignment looks fine. The space is a cone |
| BERT-flow | worse | much better | 66.55 | A post-hoc transform buys uniformity by spreading the cone, and pays for it in alignment |
| BERT-whitening | worse | much better | 66.28 | Same trade, different transform. Both land ~10 points above raw BERT and then stop |
| SBERT (supervised) | good | somewhat better | 74.89 | Labelled pairs fix alignment properly but do comparatively little for spreading |
| Unsup. SimCSE | kept | much better | 76.25 | The only method that moves one coordinate without sacrificing the other |
| Sup. SimCSE | improved | much better | 81.57 | Entailment pairs sharpen alignment on top of the contrastive spreading |
(The two numeric anchors are approximate coordinates from the paper's Figure 2; read the pattern rather than the digits. The STS column is exact.)
The story the table tells is clean. Raw BERT's problem was never alignment — it was uniformity. The post-hoc methods correctly diagnosed that and fixed it, but they operate on frozen embeddings and can only redistribute what is already there, so improving uniformity necessarily costs alignment. SimCSE fixes uniformity by training, which lets it simultaneously hold alignment in place with the positive term. Two coordinates, one method that moves the right one.
Because both quantities are cheap to compute — one pass over a held-out set and a pairwise matrix — they make a genuine diagnostic instrument. Here is the decision table.
| Uniformity good (very negative) | Uniformity bad (near 0) | |
|---|---|---|
| Alignment good (near 0) | Working. Ship it and go measure your real task | The pretrained-encoder starting point. Your negatives are too weak, your batch too small, or τ too large. Add repulsion. |
| Alignment bad (large) | Your positives are wrong. The augmentation or pair-mining is producing pairs that are not actually equivalent — or, as in the no-dropout ablation, are so identical that the positive term contributes no gradient. Fix the pairs. | Everything is broken. Usually a bug: mis-aligned batch reshape, wrong labels, or an encoder in eval() mode |
python — the two numbers, on a held-out setimport torch, torch.nn.functional as F def align_uniform(model, pairs, singles): # pairs: list of (sent_a, sent_b) that HUMANS called equivalent # singles: a flat list of sentences from the same distribution A = F.normalize(model.encode([p[0] for p in pairs]), dim=-1) # (P, d) B = F.normalize(model.encode([p[1] for p in pairs]), dim=-1) # (P, d) align = (A - B).norm(dim=-1).pow(2).mean() # scalar, lower better S = F.normalize(model.encode(singles), dim=-1) # (M, d) d2 = torch.cdist(S, S).pow(2) # (M, M) iu = torch.triu_indices(len(S), len(S), offset=1) # distinct pairs only unif = torch.log(torch.exp(-2 * d2[iu[0], iu[1]]).mean()) # scalar, lower better return align.item(), unif.item()
Log these two next to your loss on every evaluation. A contrastive loss that falls while alignment climbs is the single most common silent failure in this whole family of methods, and no other instrument catches it.
Everything so far assumed you have no labels. Now suppose you do. The interesting question is not "can labels help" — obviously they can — but "does the same objective absorb them, or do you need a different method?" SimCSE's answer is that you change one thing: where the positive comes from. And then you get a bonus that turns out to be worth more than the positive itself.
Natural Language Inference is the task of deciding whether one sentence follows from another. Annotators are given a premise and asked to write three hypotheses:
| Label | Definition | Example (premise: "Two dogs are running through a field.") |
|---|---|---|
| Entailment | Must be true if the premise is true | "There are animals outdoors." |
| Neutral | Might be true; the premise does not decide | "Some puppies are running to catch a stick." |
| Contradiction | Cannot be true if the premise is true | "The pets are sitting on a couch." |
Two large corpora exist: SNLI (570,152 pairs, premises taken from image captions) and MNLI (392,702 pairs, spanning ten genres from fiction to government reports). SimCSE takes the union and keeps the entailment pairs — 314,315 of them — as its supervised training set.
Set (xi, xi+) = (premise, entailment hypothesis) and run Chapter 1's loss unchanged. That is the entire supervised method, at first pass.
It is worth being uncomfortable about this for a moment, because entailment is not equivalence. Entailment is asymmetric and lossy: "There are animals outdoors" follows from the premise but is far more general. If you embed them at the same point, you are asserting something the logic does not license.
The paper's defence is empirical, and it is the right kind of defence: this pair source beats every alternative that is symmetric.
| Source of positive pairs | What the pair is | STS-B dev |
|---|---|---|
| Unsupervised SimCSE (dropout) | Same sentence, two masks | 82.5 |
| QQP | Two Quora questions marked duplicate | ≈ 81.8 |
| Flickr30k | Two human captions of the same photo | ≈ 81.5 |
| ParaNMT | Back-translated paraphrase pairs | ≈ 79.7 |
| SNLI + MNLI entailment | Premise and a sentence it entails | 84.1 |
| … plus contradiction as a hard negative | See below | 86.2 |
(The ordering is the finding; the middle decimals are as reported in the paper's Table 4.) Note first that three of these labelled sources are no better than free dropout twins. Collecting paraphrase data is not automatically worth the money.
Why does entailment win? Two reasons, and both are about difficulty.
The surface forms are maximally different. A dropout twin shares every token with its partner. A back-translated paraphrase shares most of them. An entailment hypothesis was written from scratch by a human who read the premise and produced a genuinely different sentence — different words, different length, often different syntactic frame. Matching it requires the model to work in meaning space, because there is nothing else to work with.
The annotation is checked. ParaNMT pairs are machine-generated and inherit MT artefacts; QQP duplicates were crowd-labelled with known noise; Flickr30k captions of the same photo often describe different aspects of it ("a man in a red shirt" / "someone climbing a rock face"). NLI entailment was written under an explicit logical instruction and validated by multiple annotators.
The empirical defence is not a licence to stop thinking, so let us look at what the asymmetry actually costs. Entailment is a one-way relation:
| Premise | Entailment hypothesis | Does the reverse hold? |
|---|---|---|
| "A woman is slicing a red onion with a chef's knife." | "A person is preparing food." | No. Preparing food does not imply onions or a knife |
| "The train from Leeds arrived eleven minutes late." | "A train arrived." | No. Enormously more general |
| "Three children are playing in a fountain." | "Children are outside." | No. |
Train on these as positives and you are asserting similarity where the data only licenses implication. The visible consequence is a real and documented one: models trained this way place a specific sentence very close to its own generalisation, so retrieval over a mixed corpus tends to surface short generic sentences for specific queries. "A person is preparing food" becomes a strong match for a great many cooking queries, because it sits near all of them.
Two things keep the damage contained, and both are worth knowing because they generalise.
The loss only ever sees relative comparisons. InfoNCE never asks "is this pair similar in absolute terms?" It asks "is this pair more similar than those other N−1 pairs?" A premise and its generalisation genuinely are more related than a premise and a random Wikipedia sentence, so the constraint being imposed is true even though the equality it implies is not.
The errors are unbiased in direction. Because NLI annotators wrote hypotheses at many levels of generality, the corpus contains generalisations, specialisations and rephrasings in roughly comparable measure. What the model absorbs is not "specific implies general" but the union of the ways two sentences can be semantically linked — averaged over 314,315 human judgements. The systematic component largely cancels; the shared semantic component survives. This is the same "five noisy targets around a truth beat one clean target of the wrong kind" argument that made multiple captions per clip valuable in the caption-supervision literature.
Now the part that matters more than the positive. Each NLI premise comes with a contradiction hypothesis, and it has a remarkable property: it is topically identical, lexically overlapping, and semantically opposite.
An in-batch negative is a random sentence. Separating it from the premise is nearly free — different topic, different vocabulary, different everything. The contradiction shares dogs/pets, the outdoor-scene frame, the animal-activity frame, and differs on exactly the thing that matters. Learning to separate those two is where a similarity model earns its keep.
A hard negative is a negative example that the current model finds confusable with the positive. In Chapter 1 we saw that InfoNCE mines them automatically, weighting each negative by pik. But the softmax can only weight negatives that are present in the batch, and a random batch of Wikipedia sentences contains almost none that are genuinely hard. NLI contradiction hypotheses inject hard negatives directly, one per example, for free, because a human already wrote them.
Each training example is now a triple (xi, xi+, xi−) = (premise, entailment, contradiction). The loss extends by adding the hard negatives to the denominator:
Read the denominator carefully, because the detail is easy to miss: the sum over j runs to N in both terms. Row i's denominator contains not only its own contradiction xi−, but every row's contradiction. The multiple-choice test now has 2N candidates instead of N, and exactly one of them is right.
| Quantity | Unsupervised | Supervised with hard negatives |
|---|---|---|
| Candidates per row | N | 2N |
| Chance-level loss | ln N = 4.16 at N = 64 | ln 2N = 6.24 at N = 512 → ln 1024 = 6.93 |
| Rows in the forward pass | 2N (sentence duplicated) | 3N (premise, entailment, contradiction) |
| Similarity matrix | (N, N) | (N, 2N) — a positive block and a hard-negative block, concatenated |
This is the calculation that justifies the whole design. Take a batch of two supervised examples and the following cosines for row 1, at τ = 0.05 (multiplier 20):
| Candidate | What it is | cos | logit (cos × 20) |
|---|---|---|---|
| h1+ | the correct entailment | 0.75 | 15.0 |
| h2+ | another row's entailment — a random in-batch negative | 0.30 | 6.0 |
| h1− | this row's contradiction — the hard negative | 0.62 | 12.4 |
| h2− | another row's contradiction | 0.25 | 5.0 |
Factor e15 out of the denominator so the arithmetic stays on human scale. The denominator becomes e15(1 + e−9 + e−2.6 + e−10):
Now delete the two hard negatives and recompute with in-batch negatives only:
The same encoder, the same premise, the same positive. Adding one human-written contradiction multiplies this row's loss — and therefore its gradient — by nearly six hundred.
Look at where the repulsive force goes, using Chapter 1's gradient formula (the weight on negative k is pik):
Ninety-nine point eight percent of the repulsion in this row is being spent on the one negative that is worth learning from. Random negatives are not merely less useful — once a hard negative is present, they are numerically invisible.
The anchor (premise) sits at the top. Its entailment is the green point; the contradiction is the red one, deliberately placed nearby because it shares vocabulary and topic; grey points are random in-batch negatives. Arrow thickness is the actual gradient weight pik from Chapter 1. Toggle the hard negatives off and watch every arrow but one vanish — that is the 582×, drawn.
A good ablation section tells you where the idea stops working, and this one does.
Weighting the hard negatives. You can add a coefficient α on the hard-negative term, eα·sim/τ — turning their influence up or down. The paper swept α and found α = 1 (no weighting at all) to be the best or tied-best setting. The softmax's own weighting was already correct; a second knob on top of it just distorts the mining that Chapter 1 showed happens automatically.
Adding the neutral hypotheses as extra negatives. Every NLI premise also has a neutral hypothesis, and it is sitting right there. Using it as another hard negative does not help. The reason is a labelling subtlety: "neutral" means the premise does not decide, which is entirely compatible with the two sentences being very similar. Training the model to push neutrals away teaches it something false.
Adding ANLI. ANLI is an adversarially-collected NLI dataset, built specifically so that models fail on it. Adding it does not improve STS. Adversarially hard inference examples are not the same thing as informative similarity examples; ANLI's difficulty comes from requiring multi-step reasoning, which a bag-of-meaning sentence vector is never going to represent anyway.
python — supervised SimCSE, one step# Each example is a TRIPLE. features has 3 sentences per row. # input_ids: (N, 3, T) -> [premise, entailment, contradiction] ids = input_ids.view(-1, T) # (3N, T) out = mlp(encoder(ids).last_hidden_state[:, 0]) # (3N, 768) out = out.view(-1, 3, 768) # (N, 3, 768) z1, z2, z3 = out[:,0], out[:,1], out[:,2] # anchor / positive / hard neg z1 = F.normalize(z1, dim=-1) z2 = F.normalize(z2, dim=-1) z3 = F.normalize(z3, dim=-1) sim_pos = z1 @ z2.T / 0.05 # (N, N) diagonal = the answers sim_hard = z1 @ z3.T / 0.05 # (N, N) EVERY row's contradiction logits = torch.cat([sim_pos, sim_hard], dim=1) # (N, 2N) labels = torch.arange(N, device=z1.device) # (N,) — answer still on the diagonal loss = F.cross_entropy(logits, labels) # scalar
Two lines carry the whole chapter. torch.cat([sim_pos, sim_hard], dim=1) widens the multiple-choice test from N options to 2N, and the labels do not change — the correct answer is still at index i, because the hard negatives were appended after the positive block. Append them before it and every label is wrong by N; the loss will sit near ln 2N and you will spend an hour blaming the learning rate.
The supervised recipe otherwise differs from the unsupervised one in exactly three numbers: batch size 512 instead of 64, learning rate 5e-5 instead of 3e-5, and 3 epochs over 314k triples instead of 1 epoch over 1M sentences. And unlike the unsupervised setting, the MLP pooling head is kept at test time.
τ = 0.05 looks like a detail. It is not. It is the single hyperparameter most likely to be responsible for a contrastive model that will not train, and the mechanism by which it breaks things is worth understanding in full, because the same τ appears in CLIP, MoCo, SimCLR, E5, BGE and every embedding model shipped since.
The softmax over scaled scores is
Consider what happens at the two extremes, because the limits tell you what the knob is for.
τ → ∞. Every score is divided by an enormous number, so every logit approaches 0, so every exponential approaches 1, so pij → 1/N. The distribution is uniform: the model expresses no preference at all, and the loss is pinned at ln N regardless of what the encoder does.
τ → 0. Differences between scores are multiplied by a huge factor, so the largest score's exponential dwarfs all others: p → a one-hot vector on the argmax. The softmax becomes a hard maximum, which is where the name comes from.
So τ interpolates between "no opinion" and "absolute certainty". The name is borrowed from statistical physics, where the Boltzmann distribution p ∝ e−E/kT has exactly this form: high temperature means a system spread across many states, low temperature means it settles into the ground state.
Chapter 1 gave the argument; here is the number that makes it undeniable. Cosine similarities live in [−1, 1]. At τ = 1, the very best possible row — positive at cosine 1, all N−1 negatives at cosine −1 — produces
A perfect encoder cannot get the loss below 2.254, while a random one sits at ln 64 = 4.159. The entire dynamic range available to learning is 1.9 nats, and the gradient is correspondingly feeble everywhere in it.
At τ = 0.05 the same expression gives log(1 + 63 e−40) = log(1 + 2.7 × 10−16) ≈ 2.7 × 10−16. The floor is zero for all practical purposes, and the full range from 4.159 down to 0 is available.
Take the row-1 cosines from Chapter 1: positive 0.72, negatives 0.54 and 0.19. Nothing about the encoder changes across the four columns below. Only the divisor changes.
| τ | logits (cos/τ) | p11 (positive) | ℓ1 | |∂ℓ/∂s11| = (1−p11)/τ | p12 : p13 (hard : easy) |
|---|---|---|---|---|---|
| 1.00 | 0.72, 0.54, 0.19 | 0.4126 | 0.8853 | 0.587 | 1.42 : 1 |
| 0.20 | 3.60, 2.70, 0.95 | 0.6769 | 0.3902 | 1.616 | 5.75 : 1 |
| 0.10 | 7.20, 5.40, 1.90 | 0.8545 | 0.1573 | 1.455 | 33.1 : 1 |
| 0.05 | 14.4, 10.8, 3.80 | 0.9734 | 0.0270 | 0.532 | 1100 : 1 |
| 0.01 | 72.0, 54.0, 19.0 | 1.0000 | 1.5 × 10−8 | 1.5 × 10−6 | 1.6 × 1015 : 1 |
Walk the τ = 0.20 row by hand so you trust the rest. Multiply the cosines by 5: 3.60, 2.70, 0.95. Subtract the max: 0, −0.90, −2.65. Exponentiate: 1, 0.40657, 0.07065. Sum: 1.47722. So p11 = 1/1.47722 = 0.67695 and ℓ1 = ln(1.47722) = 0.39016. The positive-pair gradient magnitude is (1 − 0.67695)/0.20 = 0.32305/0.20 = 1.6153.
Now read the fifth column, which is the one that matters, and notice that it is not monotonic.
That maximum can be located exactly, which turns a vague "tune it" into a rule of thumb worth carrying around.
Take the simplest case: one positive at similarity s and one negative at s − Δ, so Δ is the cosine gap the encoder currently achieves. The softmax over two candidates is a logistic function:
So the positive-pair gradient magnitude is
Substitute x = Δ/τ (so τ = Δ/x) and the Δ factors out:
Maximise the bracket. Differentiating x/(1+ex) and setting the numerator to zero gives (1 + ex) − x ex = 0, that is
Check: e1.2785 = 3.5917, and 3.5917 × 0.2785 = 1.0003. So
Set the temperature to about three quarters of the cosine gap your model currently produces. Verify it on our numbers: with Δ = 0.72 − 0.54 = 0.18, the formula predicts τ* = 0.1408, and a numerical sweep over the two-candidate row finds the peak at 0.1410. Adding the third candidate shifts it to 0.158, because a second negative adds probability mass off the diagonal.
The final column is the other half of the story: as τ falls, the ratio of attention paid to the hard negative versus the easy one explodes from 1.42 to 1015. Small τ does not make the model push harder overall; it makes the model push almost exclusively on whatever is currently closest. Contrastive learning at low temperature is hard-negative mining with extra steps.
Six candidates with fixed cosine scores — the encoder never changes. The left column is the raw cosines; the middle is the softmax at your chosen τ; the right is the gradient weight each candidate receives. Sweep τ and watch the probability mass and the repulsive force migrate. The loss and the chance level (ln 6) are printed below.
Press that last button. The scenario is one every practitioner meets: a negative in the batch is actually a paraphrase of the anchor, so the model correctly scores it above the designated positive. The loss now punishes the model for being right.
Suppose the positive scores 0.50 and this false negative scores 0.70, with a third candidate at 0.20. At τ = 1:
At τ = 0.05 the logits are 10, 14, 4. Factor out e14:
The penalty for the same mislabelling is 3.7 times larger. And since this row's loss now dominates the batch, its (incorrect) gradient dominates the update.
Let q be the probability that two randomly drawn sentences from your corpus are semantically equivalent. In a batch of N, the expected number of false negatives is N(N−1)q.
| Corpus | Plausible q | Expected false negatives, N = 64 | Expected, N = 512 |
|---|---|---|---|
| 1M random Wikipedia sentences | 10−5 | 0.04 — effectively never | 2.6 |
| Product reviews, one category | 10−3 | 4.0 | 261 |
| Support tickets, one product | 10−2 | 40 — two thirds of the batch | 2,614 |
Read row three. On a support-ticket corpus with a batch of 64, roughly forty of your sixty-three "negatives" are things the model should be pulling closer. The loss is fighting itself, and lowering τ makes the fight fiercer. This single table explains why so many teams reproduce SimCSE perfectly on Wikipedia and then watch it fail on their own data.
The mitigations, in order of how often they are the right answer:
| Fix | What it does | When to reach for it |
|---|---|---|
| Deduplicate and diversify the sampling | Lowers q directly — sample across clusters, not within them | Always. Cheapest and most effective |
| Raise τ | Softens the penalty on every negative, false ones included | When you cannot clean the corpus |
| Mask known duplicates out of the denominator | Removes the wrong constraints exactly | When you have duplicate labels or a clustering |
| Smaller batches | N(N−1)q falls quadratically | Counter-intuitive but correct on dirty corpora |
On batch size, the paper is worth quoting for what it does not say. Unsupervised SimCSE uses a batch of 64 — tiny by contrastive standards, where vision models routinely use 4,096 — and the appendix reports that the model is not very sensitive to batch size provided the learning rate is adjusted with it. That is a meaningful negative result. In image contrastive learning, batch size is a headline hyperparameter; here the negatives arrive already-diverse from a million-sentence pool, so more of them add little.
One structural difference from CLIP is worth naming since people port code between them. CLIP's loss is symmetric: it computes cross-entropy along the rows and the columns of the similarity matrix and averages. SimCSE's unsupervised loss, as published, is one-directional — rows only.
The asymmetry is defensible here and not there. In CLIP the two towers are different networks over different modalities, so "which caption matches this image" and "which image matches this caption" are genuinely different questions with different failure modes. In SimCSE both views come from the same encoder applied to the same sentence, so the similarity matrix is nearly symmetric already and the second direction adds little beyond compute. Adding it is harmless; expecting it to matter is not.
Chapter 3 gave us a name for BERT's problem — bad uniformity — and a number for it. This chapter explains where that pathology comes from, why two post-hoc fixes almost solve it, and derives, from the contrastive loss itself, the reason training fixes it better than any transform can.
Take two sentences with nothing in common. "The 1957 Chevrolet Bel Air was produced in Flint, Michigan." and "She practises the cello every morning before school." Embed both with BERT and take the cosine.
You will get something in the neighbourhood of 0.6 to 0.8. Not 0.0, which is what "unrelated" ought to look like on a sphere. Repeat with a thousand random pairs and the average barely moves. Ethayarajh (2019) measured this systematically across BERT, ELMo and GPT-2 and found that in the upper layers the expected cosine similarity between randomly chosen words and sentences is high — often above 0.5, sometimes far above.
The name for this is anisotropy. A distribution is isotropic if it looks the same in every direction — its second moment is a multiple of the identity, so variance is spread evenly across all d axes. It is anisotropic when the mass concentrates in a small subspace: a narrow cone rather than a ball.
That toy is the whole of BERT-whitening in two lines, and it also tells you why raw BERT looked like it had "good alignment" in Chapter 3's table: when everything is at cosine 0.8 from everything, paraphrases are close — but so is every other pair, which is precisely what uniformity measures.
Anisotropy is not a bug in BERT's code. It is a predictable consequence of how language models are trained, and the mechanism was identified by Gao et al. (2019) under the name representation degeneration.
A language model ends in a softmax over the vocabulary: for hidden state h and output embedding wv of token v, the score is hTwv. Now think about a rare token — one that appears as the target almost never. What gradient does its output embedding receive?
Almost every update in which it participates is a negative one: it appears in the softmax denominator, so the loss pushes hTwrare down, for whatever hidden states happen to be in the batch. Over millions of steps, the accumulated push drives wrare toward the direction that minimises its dot product with the typical hidden state — roughly, toward the negative of the average hidden direction. Every rare token is pushed toward the same place, because they all see the same average. The rare tokens pile into a shared cone.
Meanwhile hidden states are pushed to have large dot products with the output embeddings of frequent tokens, which are themselves few and concentrated. Both halves of the model converge on a preferred direction, and the whole representation drifts off-centre.
| Contributor | Effect on the geometry |
|---|---|
| Rare tokens in the softmax denominator | Their embeddings receive almost only negative gradient, collapsing into a common direction |
| Frequent tokens dominating the targets | Hidden states are optimised to align with a handful of directions |
| No objective term anywhere | Nothing in MLM or next-token prediction rewards spreading. Only relative scores matter, and a global offset changes none of them |
| LayerNorm's constraint | Puts representations on a sphere (of radius √d, after scaling) but says nothing about how they distribute over it |
If the problem is a common component, subtract it. Two 2020–21 methods do essentially that, without touching the encoder.
BERT-whitening (Su et al. 2021). Collect embeddings over a corpus, compute the mean μ and covariance Σ, eigendecompose Σ = UΛUT, and transform
Subtracting μ removes the shared offset; multiplying by UΛ−1/2 rotates onto the principal axes and rescales each so that every direction has unit variance. The transformed covariance is exactly the identity. Perfect isotropy, achieved with a matrix multiply and no gradient steps. Result: 66.28.
BERT-flow (Li et al. 2020). Same goal, more machinery: train an invertible normalising flow that maps the BERT embedding distribution onto a standard Gaussian, then use the mapped vectors. Result: 66.55.
Both take raw BERT from 56.70 to about 66.4, a ten-point gain for essentially no cost. And then both stop, ten points short of SimCSE. Chapter 3 told us why in the abstract — they trade alignment for uniformity. Here is the concrete version:
This is the paper's theoretical contribution and it is short enough to do in full. Assemble the m embeddings of your corpus as rows of a matrix W ∈ Rm×d, all rows L2-normalised.
Step 1: isolate the repulsive term. Chapter 3 split the loss as −sii/τ + log∑jesij/τ. Write the second term as an expectation over the data:
Step 2: apply Jensen's inequality. For any random variable Z, log E[eZ] ≥ E[Z], because the exponential is convex. Setting Z = hiThj/τ with j uniform:
Averaging over i as well:
where Sum(·) means the sum of all entries of the matrix. So the repulsive term, times τm2, is an upper bound on Sum(WWT). Driving the term down drives the bound down.
Step 3: recognise what Sum(WWT) is. Expand the double sum:
It is the squared norm of the mean embedding (times m2). Minimising it is centring the embeddings. The contrastive loss derives, all by itself, the first half of what whitening does by hand.
Step 4: the trace is pinned. Because every row is a unit vector, the diagonal of WWT is all ones, so
The eigenvalues must always sum to m, no matter what the encoder does. This is the constraint that makes the whole argument work: you cannot shrink the spectrum, only redistribute it.
Step 5: Merikoski's bound. For a matrix all of whose entries are non-negative, the sum of the entries is an upper bound on the largest eigenvalue: Sum(WWT) ≥ λ1. So minimising Sum squeezes λ1 downward — while ∑λk stays fixed at m. Mass taken from the top eigenvalue must reappear in the smaller ones. The spectrum flattens.
Four unit vectors in two dimensions. We compute Sum(WWT), the eigenvalues of WTW (which are the non-zero eigenvalues of WWT, and whose square roots are the singular values of W), and the uniformity from Chapter 3.
Configuration A — the cone, at 0°, 10°, 20°, 30°. Rows: (1, 0), (0.98481, 0.17365), (0.93969, 0.34202), (0.86603, 0.50000).
Sum of rows: x-component 1 + 0.98481 + 0.93969 + 0.86603 = 3.79053; y-component 0 + 0.17365 + 0.34202 + 0.50000 = 1.01567. So
Now WTW, a 2×2 matrix of column inner products:
Trace = 3.60287 + 0.39713 = 4.000, exactly m as promised. Determinant = 3.60287 × 0.39713 − 0.925422 = 1.43075 − 0.85640 = 0.57436. Eigenvalues of a 2×2 with trace T and determinant D are (T ± √(T2 − 4D))/2:
Condition number λ1/λ2 = 25.8. Singular values √λ = 1.962 and 0.386. One direction carries 96% of the energy; the space is effectively one-dimensional. And check Merikoski: 15.400 ≥ 3.851. It holds, because every cosine here is positive.
Configuration B — a quarter turn, at 0°, 30°, 60°, 90°. Here ∑x2 = ∑y2 = 2.000 and ∑xy = 0.86603, so λ = 2 ± 0.86603:
Configuration C — fully spread, at 0°, 90°, 180°, 270°. The rows sum to the zero vector, so Sum(WWT) = 0. And WTW = 2I, giving λ1 = λ2 = 2 and ratio 1.00 — a perfectly flat spectrum.
| Configuration | Sum(WWT) | λ1 | λ2 | λ1/λ2 | ℓuniform |
|---|---|---|---|---|---|
| A — cone (0–30°) | 15.400 | 3.851 | 0.149 | 25.8 | −0.186 |
| B — quarter (0–90°) | 11.196 | 2.866 | 1.134 | 2.53 | −1.077 |
| C — spread (0–270°) | 0.000 | 2.000 | 2.000 | 1.00 | −4.396 |
Every column moves together. Sum(WWT) falls, λ1 falls while the trace stays at 4, the condition number collapses toward 1, and uniformity improves by more than four nats. The theoretical quantity the loss provably minimises, the spectral quantity we care about, and the empirical measure from Chapter 3 are three views of one thing.
Sixty-four embeddings in eight dimensions, drawn from a distribution whose cone width you control. Left: a 2-D projection onto the top two principal directions. Right: the eight singular values as a bar chart, with the trace (their squared sum) pinned at 64 — watch the mass slide from the first bar into the others without the total changing. Below: Sum(WWT), the mean pairwise cosine, the condition number, and the Chapter 3 uniformity, all live.
Two things to notice while you play. First, "+ centring" alone — just subtracting the mean, no rescaling — recovers most of the improvement, which is the same lesson as the (1, ±0.3) example: the dominant pathology is a shared offset. Second, "+ whitening" produces a perfectly flat spectrum, flatter than the contrastive preset, and yet we know it scores ten points worse on STS. Flatness is not the goal. Flatness with the right structure preserved is the goal, and only a trained objective with an alignment term can tell the difference.
Anisotropy is not a theoretical concern; it is a property of whatever encoder you happen to be holding, and it takes under a minute to check.
python — is your embedding space a cone?import numpy as np E = model.encode(sentences) # (m, d), m ≈ 2000 is plenty E = E / np.linalg.norm(E, axis=1, keepdims=True) # unit rows — required G = E @ E.T # (m, m) all pairwise cosines iu = np.triu_indices(len(E), 1) print("mean random-pair cosine :", G[iu].mean()) # > 0.4 = a cone. Should be ≈ 0 print("||mean embedding|| :", np.linalg.norm(E.mean(0))) # Sum(WW^T) = m^2 * ||mean||^2 — the exact quantity Chapter 6 minimises s = np.linalg.svd(E, compute_uv=False) # (d,) singular values, descending print("spectrum decay :", (s[:6] / s[0]).round(3)) q = s**2 / (s**2).sum() # eigenvalue shares, sums to 1 print("effective rank :", np.exp(-(q * np.log(q + 1e-12)).sum()))
The last line is worth explaining because it is the most useful single number. Treating the normalised eigenvalue shares qk as a probability distribution, its spectral entropy H = −∑ qk log qk measures how many directions the mass is spread over, and eH — the effective rank — converts that back into a dimension count.
Sanity-check it on our three toy configurations. For configuration C, q = (0.5, 0.5), so H = ln 2 and the effective rank is exactly 2 — both dimensions fully used. For configuration A, q = (3.8508/4, 0.1492/4) = (0.9627, 0.0373), giving H = −(0.9627 ln 0.9627 + 0.0373 ln 0.0373) = −(−0.0366 − 0.1227) = 0.1593 and an effective rank of e0.1593 = 1.17. Four vectors in two dimensions, using 1.17 of them.
Run this on raw BERT with d = 768 and the effective rank comes out in the low tens rather than the hundreds. That number, more than any plot, is what "the space is a cone" means operationally: you are paying to store 768 floats and getting the discriminative power of a couple of dozen.
The paper plots the singular value distribution of the sentence embedding matrix for BERT, BERT-whitening, and SimCSE, and the picture matches the derivation exactly: BERT's spectrum decays steeply, SimCSE's is markedly flatter. It also reports the alignment/uniformity coordinates we saw in Chapter 3, showing SimCSE moving down the uniformity axis while holding its horizontal position.
And there is a satisfying consistency check available. Chapter 3 defined uniformity through a pairwise Gaussian potential; this chapter derived flattening through a matrix spectrum. They are the same phenomenon in different coordinates — the pairwise view and the second-moment view of "is the mass spread out". Our three-configuration table demonstrates the equivalence numerically, and it is the reason you can diagnose anisotropy either way: measure the average cosine of random pairs (cheap, one number) or eigendecompose the embedding matrix (informative, tells you how many directions are being used).
Six chapters of machinery. Now we run the whole thing on paper: three sentences, two dropout masks each, every cosine, every exponential, and a final loss number you can check against real PyTorch. If you can reproduce this page with a pencil, you understand SimCSE.
Three sentences in the batch:
| i | Sentence | Pre-dropout representation ui |
|---|---|---|
| 1 | "A man is playing a guitar." | (4, 3, 2, 1) |
| 2 | "A person plays a musical instrument." | (3, 4, 1, 2) |
| 3 | "The stock market closed lower today." | (1, −2, 4, −3) |
The four units are not arbitrary. Read them as: unit 1 ≈ "human agent", unit 2 ≈ "music", unit 3 ≈ "finance/abstract", unit 4 ≈ "temporal reference". Sentences 1 and 2 have similar profiles because they mean nearly the same thing; sentence 3 does not. This is what a good encoder is supposed to produce, and our toy encoder already has it — we are studying what the loss does, given a representation.
Dropout at p = 0.5, so each mask keeps two of the four units and multiplies survivors by 1/(1 − 0.5) = 2. (Real SimCSE uses 0.1; we use 0.5 so the masks are visible.) The six masks — two per sentence, drawn independently:
| Sentence | mask z (view A, the "query") | hA = 2 · u ⊙ z | mask z′ (view B, the "key") | hB |
|---|---|---|---|---|
| 1 | keep {1, 2} | (8, 6, 0, 0) | keep {1, 3} | (8, 0, 4, 0) |
| 2 | keep {1, 2} | (6, 8, 0, 0) | keep {2, 4} | (0, 8, 0, 4) |
| 3 | keep {3, 4} | (0, 0, 8, −6) | keep {1, 3} | (2, 0, 8, 0) |
Check one cell before continuing. Sentence 1, view A, keeps units 1 and 2 of (4, 3, 2, 1) and doubles them: (2×4, 2×3, 0, 0) = (8, 6, 0, 0). Units 3 and 4 were dropped, so they are exactly zero.
Norms, which we will need for every cosine:
We need sij = cos(hiA, hjB) for all nine combinations. Three in full, the rest in the table.
The positive pair, s11. h1A = (8, 6, 0, 0), h1B = (8, 0, 4, 0). Only unit 1 survived in both views:
The near-neighbour, s12. h1A = (8, 6, 0, 0) against h2B = (0, 8, 0, 4). The overlap is unit 2 only:
The far one, s13. h1A = (8, 6, 0, 0) against h3B = (2, 0, 8, 0). Overlap on unit 1:
Continuing the same way for rows 2 and 3 (row 3's second entry involves the only negative product, 8 × 0 + 0 × 8 + 8 × 0 + (−6)(4) = −24, over 89.4427):
| sij | key 1 | key 2 | key 3 |
|---|---|---|---|
| query 1 | 0.715542 | 0.536656 | 0.194029 |
| query 2 | 0.536656 | 0.715542 | 0.145521 |
| query 3 | 0.357771 | −0.268328 | 0.776114 |
(If these look familiar: Chapter 1's three-sentence worked example used exactly this matrix, rounded to two decimals. It was a preview of this page.)
Three sanity checks. The diagonal is the largest entry in every row — the model already prefers the right answer, so this batch is "easy" and we expect a small loss. The 1–2 cross terms (0.5367) are much larger than the 1–3 cross terms (0.194), which is correct: the guitar and the musical instrument really are related. And s32 is negative, which is only possible because u3 has negative components — the finance sentence actively points away from the music direction.
τ = 0.05, so every entry is multiplied by 20:
| logits | key 1 | key 2 | key 3 |
|---|---|---|---|
| query 1 | 14.3108 | 10.7331 | 3.8806 |
| query 2 | 10.7331 | 14.3108 | 2.9104 |
| query 3 | 7.1554 | −5.3666 | 15.5223 |
Row 1. Subtract the row maximum (14.3108) from every entry — this changes nothing about the softmax and keeps the exponentials on human scale:
Row 2. Maximum is again 14.3108, on the diagonal. Differences: −3.5777, 0, −11.4004.
Row 3. Maximum 15.5223. Differences: −8.3669, −20.8889, 0.
That is the number. Chance level for a batch of three is ln 3 = 1.0986, so this batch is being solved almost perfectly — which is what we expected from a similarity matrix whose diagonal already dominates.
Row 3 contributes essentially nothing (0.00023). The finance sentence is trivially distinguishable from the two music sentences, so it is done: no gradient will flow from it. All the learning in this batch happens in rows 1 and 2, where the two music sentences confuse each other. This is InfoNCE's automatic curriculum from Chapter 1, visible in three numbers.
Rerun step 2 with no scaling and everything else identical:
| Row | softmax at τ = 1 | ℓi | ℓi at τ = 0.05 |
|---|---|---|---|
| 1 | 0.4116, 0.3441, 0.2443 | 0.8878 | 0.0276 |
| 2 | 0.3482, 0.4164, 0.2355 | 0.8762 | 0.0276 |
| 3 | 0.3274, 0.1751, 0.4975 | 0.6981 | 0.0002 |
| mean | — | 0.8207 | 0.01846 |
At τ = 1 the batch loss is 0.821 against a chance level of 1.099 — the model is barely a quarter of a nat better than random, despite the diagonal winning every row. Row 3's probability of being right is 0.4975: a coin flip, on a pair the geometry separates by 0.42 in cosine. Chapter 5's argument, now on our own numbers.
Rerun the whole thing with z′ = z for every sentence — the paper's "Fixed 0.1" pathology. Now hB = hA, so the diagonal is exactly 1 and the off-diagonals are computed within the same two-unit subspace:
| sij | key 1 | key 2 | key 3 |
|---|---|---|---|
| query 1 | 1.000000 | 0.960000 | −0.178885 |
| query 2 | 0.960000 | 1.000000 | −0.447214 |
| query 3 | −0.178885 | −0.447214 | 1.000000 |
Row 1 at τ = 0.05: logits 20, 19.2, −3.578; differences 0, −0.8, −23.578; exponentials 1, 0.449329, 4 × 10−11; sum 1.449329; so p11 = 0.689975 and ℓ1 = ln(1.449329) = 0.371101. By symmetry ℓ2 is the same and ℓ3 ≈ 0, giving L = 0.24740.
The loss is thirteen times larger than the dropout version. A naive reading would call that a harder, better training signal. It is the opposite, and here is the precise statement of why:
The loss wants very badly to raise s11, and it structurally cannot. Every gradient that actually reaches the parameters comes from the repulsive terms, pushing sentences 1 and 2 apart — two sentences that mean nearly the same thing. Chapter 3's argument, on our own numbers, with the two factors of the chain rule laid side by side.
python — reproduces every number on this pageimport torch, torch.nn.functional as F u = torch.tensor([[4., 3, 2, 1], [3., 4, 1, 2], [1., -2, 4, -3]]) # (3, 4) # the six masks from the table, as 0/1 rows; inverted dropout scale = 1/(1-0.5) = 2 zA = torch.tensor([[1.,1,0,0], [1.,1,0,0], [0.,0,1,1]]) zB = torch.tensor([[1.,0,1,0], [0.,1,0,1], [1.,0,1,0]]) hA, hB = 2 * u * zA, 2 * u * zB # (3, 4) each sim = F.normalize(hA, dim=-1) @ F.normalize(hB, dim=-1).T # (3, 3) print(sim) # matches the table to 6 dp print(F.cross_entropy(sim / 0.05, torch.arange(3))) # 0.018462 print(F.cross_entropy(sim / 1.00, torch.arange(3))) # 0.820716 # the pathology: same mask for both views hB2 = 2 * u * zA sim2 = F.normalize(hA, dim=-1) @ F.normalize(hB2, dim=-1).T print(F.cross_entropy(sim2 / 0.05, torch.arange(3))) # 0.247400 — and zero useful gradient
A method is only as good as the evaluation that measured it, and this paper's evaluation section contains a methodological correction that is arguably as valuable as the model. Let us do the numbers properly: what was measured, how, what it establishes, and what it does not.
Each STS task is not one dataset but several sub-datasets. STS12, for instance, contains five topical subsets. When you report "STS12 Spearman", you have three defensible options, and they give different numbers:
| Aggregation | What it does | What it implicitly assumes |
|---|---|---|
all | Concatenate every subset's pairs into one list and compute a single Spearman | The model's scores are comparable across subsets — a real and demanding requirement |
mean | Compute Spearman per subset, then average unweighted | Only within-subset ranking matters. A model can use a different scale per topic and lose nothing |
wmean | Same, weighted by subset size | As above, plus large subsets count more |
Papers before SimCSE were not consistent about which they used, and some reported mean while comparing against numbers computed with all. That is not a rounding-level discrepancy: mean is a strictly easier setting, because it forgives a model whose similarity scale drifts between topics.
SimCSE's appendix documents this, adopts all throughout, and re-evaluates every baseline under the same protocol. The 56.70 for averaged BERT and the 74.89 for SBERT in this lesson are the paper's re-computed figures, not the ones from the original publications.
all and mean on STS is several points — larger than the margin separating many published "improvements". A paper that stops to fix its own benchmark before reporting its result is telling you something about how much to trust the rest of it.| Model | Supervision | Avg. Spearman, 7 STS tasks | Delta vs. raw BERT |
|---|---|---|---|
| GloVe embeddings (avg.) | none | 61.32 | +4.62 |
| BERT-base (first-last avg.) | none | 56.70 | — |
| BERT-flow | none (post-hoc) | 66.55 | +9.85 |
| BERT-whitening | none (post-hoc) | 66.28 | +9.58 |
| IS-BERT | none | 66.58 | +9.88 |
| CT-BERT | none | 72.05 | +15.35 |
| Unsup. SimCSE-BERT-base | none | 76.25 | +19.55 |
| SBERT-base | NLI labels | 74.89 | +18.19 |
| SBERT-whitening | NLI + post-hoc | 77.00 | +20.30 |
| Sup. SimCSE-BERT-base | NLI labels | 81.57 | +24.87 |
| Sup. SimCSE-RoBERTa-large | NLI labels | 83.76 | — |
Four readings worth extracting.
The unsupervised model beats a supervised one. 76.25 over 74.89. This is the number that made the paper famous, and the honest framing is: dropout twins are a better positive-pair source than the paraphrase supervision SBERT was built on, once you pair them with in-batch negatives and a sensible temperature.
The post-hoc methods plateau together. BERT-flow 66.55, BERT-whitening 66.28, IS-BERT 66.58 — three quite different techniques landing within 0.3 of each other. When several independent approaches converge on the same ceiling, the ceiling is a property of the setup rather than of any one method. Here the setup is frozen embeddings: you can only redistribute the information BERT already put in the vector, and Chapter 6 explained why that trades alignment for uniformity.
Supervision and the contrastive objective stack. 76.25 unsupervised, 81.57 supervised. If NLI labels merely duplicated what dropout provides, the two would not add. They are doing different work: dropout supplies local stability, entailment pairs supply semantic identity across different surface forms, and contradiction pairs supply hard negatives. Three different jobs.
Scale still helps, but modestly. RoBERTa-large gets 83.76 versus BERT-base's 81.57 — 2.2 points for roughly three times the parameters. Compare that with the 24.9 points the objective bought. In sentence embedding, the loss function has historically mattered far more than model size.
We have met this table three times. Here is the full reading, with each row explained by the machinery we have built.
| Setting | STS-B dev | Alignment gradient? | Explanation |
|---|---|---|---|
| p = 0.0 (no dropout) | 71.1 | Exactly zero | Views identical ⇒ sii = 1 for all θ ⇒ only repulsion acts. Held-out alignment collapses (Chapter 3) |
| p = 0.01 | 72.6 | Nearly zero | One activation in a hundred differs; the positive term is technically alive but carries almost no information |
| p = 0.05 | 81.1 | Healthy | Enough perturbation to make the matching task non-trivial |
| p = 0.1 (BERT's default) | 82.5 | Healthy | The sweet spot — and, remarkably, nobody chose it for this purpose |
| p = 0.15 | 81.4 | Strong | Slightly past the peak |
| p = 0.2 | 80.5 | Strong | Views drifting apart; the positive is becoming a weaker constraint |
| p = 0.5 | 71.0 | Overwhelming | Half the units gone. Satisfying the positive now requires near-invariance to the encoder's own content |
| Fixed 0.1 (one mask, reused) | 43.6 | Exactly zero | Collapse (as at p = 0), plus a train/test mismatch: training optimises one scaled sub-network, evaluation runs the full one |
The curve is an inverted U with a hard floor at both ends, and the two ends fail for opposite reasons — too little perturbation kills the gradient, too much kills the meaning. That is the signature of a genuine sweet spot rather than a monotone "more is better" knob.
Pooling. Options are the [CLS] token with BERT's MLP pooler, [CLS] with the MLP used only during training, and mean-pooling over tokens. The paper's finding is asymmetric and slightly odd: for the unsupervised model, keeping the MLP during training and discarding it at test time is best; for the supervised model, keeping it throughout is best. The differences are around a point. The practical advice is to treat pooling as a hyperparameter to check, not a principle — and to note that this is a real reproducibility trap, because "which pooler at test time" is exactly the kind of detail that gets lost between a paper and a re-implementation.
An MLM auxiliary objective. Adding λ · (masked language modelling loss) to the contrastive loss, with a small λ, improves transfer tasks and slightly hurts STS. The reason is legible: MLM preserves token-level information that a purely contrastive objective is free to discard, and downstream classification probes benefit from that information while sentence-level similarity does not. It is a clean demonstration that "a better embedding" is task-relative.
Transfer tasks. Evaluated on the SentEval classification suite (MR, CR, SUBJ, MPQA, SST-2, TREC, MRPC), SimCSE is roughly on par with previous methods — not the blowout it achieves on STS. This is honest reporting and it makes sense: those tasks train a logistic-regression probe on top of the frozen embedding, and a probe can undo an arbitrary linear transformation. The anisotropy that wrecks cosine similarity is largely invisible to a trained probe, so fixing it buys little there.
A lesson that only reports the wins is advertising. Four genuine limitations, in rough order of how likely they are to bite you.
1. STS is symmetric; retrieval is not. Every result here concerns pairs of sentences of similar length and register. Real retrieval matches a short question against a long passage — an asymmetric task with a completely different similarity structure. The BEIR benchmark (2021) measured exactly this and found that SimCSE-style models trained on STS-flavoured objectives frequently underperform BM25, a keyword-matching algorithm from the 1990s, on out-of-domain retrieval. Strong STS is not evidence of strong retrieval, and treating it as such has cost real teams real quarters.
2. Checkpoint selection uses labels. The unsupervised recipe evaluates on STS-B dev every 250 steps and keeps the best checkpoint. With a single training run this is mild; across a hyperparameter sweep it is not negligible. "Unsupervised" here means "no labels in the loss", and the number of labelled pairs touched by model selection is small but not zero.
3. English, general domain, short sentences. Max sequence length 32 tokens. Wikipedia prose. If your text is 500-token legal clauses, none of the reported numbers transfer, and Chapter 5's false-negative table says the failure will be sharp rather than gradual.
4. Sentence embeddings are a bag of meaning. A single vector cannot represent compositional structure with much fidelity. SimCSE improves the geometry; it does not give the representation a syntax. Negation, quantifier scope, and argument order remain systematically underserved — "the dog bit the man" and "the man bit the dog" will be close under any of these models.
| Resource | Unsupervised SimCSE-BERT-base |
|---|---|
| Training data | 106 unlabelled Wikipedia sentences |
| Optimiser steps | 106 / 64 ≈ 15,625 (one epoch) |
| Rows per step | 128 (batch 64, duplicated) at 32 tokens |
| New parameters | Zero. Only the existing encoder plus its pooler are fine-tuned |
| Human annotation | Zero in the loss |
| Wall clock | Single mid-range GPU, hours not days |
Fifteen thousand steps of ordinary fine-tuning, on free text, moving a well-studied benchmark by twenty points. That ratio — not the absolute score — is the paper's real claim on your attention.
| Symptom | Most likely cause | Check |
|---|---|---|
| Loss stuck at ln N | Temperature far too large, or the labels do not match the batch layout | Print the similarity matrix. Is the diagonal the row max? Is 1/τ = 20? |
| Loss near zero from step 1, embeddings useless | Dropout is off — the model is in eval() mode, or you re-used one mask | Forward the same sentence twice and assert the outputs differ |
| Trains well, STS barely moves | Pairs mis-aligned by the reshape (i paired with i + N/2) | Assert ids.view(-1,2,T)[:,0] == ids.view(-1,2,T)[:,1] |
| Great on Wikipedia, collapses on your corpus | False negatives (Chapter 5) | Sample 100 random in-batch pairs and read them. How many are duplicates? |
| Numbers 2–4 points below the paper | Aggregation setting, or the test-time pooler | Confirm all aggregation; try with and without the MLP head at test |
SimCSE's lasting contribution is not 76.25. It is a template. Every text embedding model shipped since is a variation on four decisions this paper fixed, and knowing which of the four a new model changed is usually enough to understand it in a paragraph.
| # | Decision | SimCSE's choice | Still standard in 2026? |
|---|---|---|---|
| 1 | Encoder and pooling | Pretrained transformer, [CLS] or mean pool, L2-normalised | Yes — universally |
| 2 | Objective | InfoNCE, in-batch negatives, τ = 0.05 | Yes — almost without exception |
| 3 | Cheap positives | Dropout twins on unlabelled text | Replaced — by mined web pairs at 109 scale |
| 4 | Curated positives + hard negatives | NLI entailment / contradiction | Generalised — retriever-mined hard negatives |
Rows 1 and 2 did not move at all. Rows 3 and 4 are where five years of progress happened, and the direction of travel is consistent: the objective was right; the data was the bottleneck.
| System | What it kept from SimCSE | What it changed, and why |
|---|---|---|
| sentence-transformers v2 | The whole loss (MultipleNegativesRankingLoss is InfoNCE with in-batch negatives) | Packaged it. The library made the recipe the default way anyone trains an embedder |
| E5 (2022) | InfoNCE, small τ, in-batch negatives, two-stage structure | Stage 1 becomes ~270M mined web pairs (post–comment, title–body, question–answer) instead of dropout twins. Adds query: / passage: prefixes so one model can serve asymmetric retrieval |
| GTE (2023) | Same objective, same normalisation | Multi-stage training over a deliberately diverse mixture, arguing that data variety matters more than volume |
| BGE / C-Pack (2023) | Same objective; NLI-style curated stage survives as the final stage | Adds a RetroMAE-style pretraining stage before contrastive training, plus instruction prefixes and a Chinese-language suite |
| Instructor (2022) | InfoNCE unchanged | Conditions the embedding on a natural-language task instruction, so one vector space serves many notions of "similar" |
| E5-Mistral, LLM embedders (2023–) | InfoNCE unchanged | Swaps BERT for a decoder LLM and generates the training pairs synthetically — the positive-pair problem solved by asking a model to write them |
| Matryoshka embeddings (2022) | InfoNCE unchanged | Applies the loss at several truncation lengths at once, so the first 64 dimensions are usable on their own. A storage and latency innovation, not a semantic one |
| GradCache / cross-device negatives | The loss, exactly | Engineering that decouples batch size from GPU memory, so "more negatives" stops being a hardware question |
Read the middle column. In every row it says the same thing.
Two of Chapter 8's limitations shaped the field's next five years, so they are worth restating as forward-looking facts rather than caveats.
Symmetric similarity is not retrieval. BEIR made this unmissable in 2021, and the entire prefix-and-instruction line of work (E5's query:/passage:, Instructor, BGE's instructions) exists to give one model two different notions of similarity. If you take one design lesson from this lesson into your own system, make it this: decide whether your task is symmetric or asymmetric before you pick an objective, because the same InfoNCE loss trained on the wrong pair type produces a model that benchmarks well and retrieves badly.
The bottleneck moved from method to data. After SimCSE, the objective stopped being where the gains were. MTEB (2022) turned embedding evaluation into a 50-task suite precisely because single-task STS had been saturated by a method anyone could implement in an afternoon. The frontier moved to pair mining, hard-negative mining, instruction conditioning, and multilingual coverage — all data questions.
The symbols.
| Symbol | Meaning | Value / shape |
|---|---|---|
| xi | Input sentence | tokenised to (T,), T ≤ 32 |
| z, z′ | Two independent dropout masks | ~7.6 × 105 Bernoulli draws each, p = 0.1 |
| hiz | Encoder output for xi under mask z | R768 (base), L2-normalised |
| sij | cos(hiz, hjz′) | scalar in [−1, 1]; matrix is (N, N) |
| τ | Temperature | 0.05, fixed → logit multiplier 20 |
| N | Batch size = number of candidates | 64 unsupervised, 512 supervised |
| x+, x− | Entailment / contradiction hypothesis (supervised only) | 314,315 NLI triples |
| ℓalign | E ‖f(x) − f(x+)‖2 = 2(1 − E[cos]) | [0, 4]; lower better; measured on held-out pairs |
| ℓuniform | log E e−2‖f(x)−f(y)‖2 | (−∞, 0]; lower better; 0 = total collapse |
The four equations.
The numbers worth remembering.
| Number | What it is |
|---|---|
| 76.25 | Unsupervised SimCSE-BERT-base, avg. over 7 STS tasks — above supervised SBERT's 74.89 |
| 81.57 / 83.76 | Supervised SimCSE, BERT-base / RoBERTa-large |
| 56.70 | Raw averaged BERT — below GloVe's 61.32 |
| 82.5 vs 71.1 vs 43.6 | STS-B dev at p = 0.1, p = 0, and one fixed mask. The whole argument in three numbers |
| 84.1 → 86.2 | Supervised STS-B dev, before and after adding contradiction hard negatives |
| 0.05 | Temperature — multiplier 20 on cosine logits |
| 106 / 314,315 | Unlabelled Wikipedia sentences / labelled NLI triples |
| 64 / 512 | Batch size, unsupervised / supervised |
| 582× | How much one hard negative multiplied a row's loss in Chapter 4's worked example |
| ln N | Chance-level loss. 4.159 at N = 64. If you sit here, check τ first |
| Step | What to do | The decision that matters |
|---|---|---|
| 1. Text | 200k–1M unlabelled sentences from your own domain | Diversity, not volume. Deduplicate hard — Chapter 5's false-negative table is the reason |
| 2. Encoder | Any pretrained transformer. Keep its dropout at its default | Do not disable dropout "for stability". It is the method |
| 3. Pooling | [CLS] + a Linear–Tanh head, or mean pooling | Try dropping the head at test time; it is worth a point and costs one experiment |
| 4. Pairs | Duplicate each sentence inside the batch; one forward pass | Interleave, then view(-1,2,d). Assert the two token rows are equal |
| 5. Loss | Chapter 1's six lines, τ = 0.05 | If your corpus is narrow, start at τ = 0.1 and lower it only if the loss stalls |
| 6. Batch | 64 to start | Bigger is not automatically better here; sample across topics rather than within one |
| 7. Schedule | One epoch, lr 3e-5, evaluate often | Contrastive fine-tuning overfits fast. The best checkpoint is usually early |
| 8. Instrument | Log ℓalign and ℓuniform on a held-out pair set | Chapter 3. These two numbers tell you which half is broken. Nothing else does |
| 9. Then supervise | Add whatever curated pairs you have, with explicit hard negatives | One good hard negative beats a thousand random ones (Chapter 4) |
| 10. Evaluate honestly | Your own retrieval task, not STS | If your task is asymmetric, STS will lie to you (Chapter 8) |
| If you want… | Go to |
|---|---|
| Contrastive objectives in general, from the ground up | Contrastive learning |
| The same recipe across two modalities | CLIP and CLAP |
| The encoder SimCSE fine-tunes | BERT and embedding layers |
| What the vectors are for once you have them | Vector embeddings, similarity metrics, vector databases |
| The system that consumes them | RAG and multimodal RAG |
| Dropout itself, in depth | Dropout variants and regularisation |
Without scrolling up: (1) write the unsupervised objective and say what z and z′ are; (2) define alignment and uniformity, and state which one raw BERT fails; (3) prove that removing dropout makes the attractive gradient exactly zero; (4) compute the loss of a 2×2 batch whose cosines are 0.8 on the diagonal and 0.3 off it, at τ = 0.05; (5) explain why one contradiction hypothesis is worth more than doubling the batch. If any of the five stalls, its chapter is one tap away.