AI Architectures

The Modality Gap

CLIP is sold as a single shared space where a photo and its caption land in the same place. Open the embeddings and you find two separate islands that never touch — a picture is more similar to an unrelated picture than to its own caption. This lesson shows you the gap, derives where it comes from, computes it by hand, and tells you exactly which of your systems it breaks.

Prerequisites: a vector is a list of numbers + cosine similarity is a dot product of unit vectors. Everything else is built here.
10
Chapters
9
Simulations
0
Assumed Knowledge

Chapter 0: Two Islands

It is your first afternoon with CLIP. You have a photograph of a golden retriever sitting on a porch, and you have the caption someone wrote for it: “a golden retriever sitting on a wooden porch.”

You run both through the model. The image goes through the vision encoder and comes out as a list of 512 numbers. The caption goes through the text encoder and comes out as a different list of 512 numbers. Both lists are scaled so their length is exactly 1 — this is called L2 normalization, and it means every embedding lives on the surface of a 512-dimensional ball. Then you compute the cosine similarity: multiply the two lists element by element and add up the products. For unit-length vectors that single number is the cosine of the angle between them, so it runs from −1 (opposite) through 0 (perpendicular) to 1 (identical direction).

The whole promise of CLIP is that these two vectors describe the same thing. You expect something like 0.9.

cos(image, its own caption) = 0.31

Thirty-one hundredths. That is not “the same thing.” That is barely leaning in the same direction. You assume you made a mistake, so you run a control: the same photo against a caption about something else entirely, “a red fire truck parked outside a station.”

cos(image, an unrelated caption) = 0.21

So the model does know the difference — 0.31 beats 0.21 — but only by a tenth. And then you run the control that actually breaks your model of the world. You take the golden-retriever photo and compare it to a photo of a fire truck. Two images, nothing whatsoever in common.

cos(dog photo, fire-truck photo) = 0.58

The photo of the fire truck is nearly twice as similar to your dog photo as your dog photo is to its own caption. And if you compare two unrelated captions to each other you get about 0.62, higher still.

The shock, stated plainly. In CLIP’s “shared” space, any two images are more similar to each other than any image is to any text — including its own caption. The space is not one space with pictures and words mixed together. It is two disjoint regions with a wide, empty channel between them. That channel is the modality gap.

The four numbers you have to hold in your head

Everything in this lesson is a consequence of four measurements. These are typical values for CLIP ViT-B/32 evaluated on MS-COCO image–caption pairs; a different checkpoint or dataset will move them by a few hundredths but never changes the ordering.

ComparisonSymbolTypical cosineWhat you expected
two different imagessII0.55low — they are unrelated
two different captionssTT0.62low — they are unrelated
an image and its own captionsmatch0.30very high — same content
an image and someone else’s captionscross0.20low — unrelated

Read that table twice. Every within-modality number is far above every cross-modality number. The only comparison the model was trained to make large — an image and its own caption — is the second smallest number in the table.

How far from random is any of this?

Before we panic, let us calibrate. What does “no relationship at all” look like in 512 dimensions? Take two vectors drawn uniformly at random from the unit sphere in d dimensions. Their expected cosine is 0, and the standard deviation of that cosine is

σrandom = 1 ÷ √d = 1 ÷ √512 = 1 ÷ 22.63 = 0.0442

That is the yardstick. Now measure the four numbers in units of that yardstick — how many standard deviations away from “random” each one sits:

two images: 0.55 ÷ 0.0442 = 12.4σ    two captions: 0.62 ÷ 0.0442 = 14.0σ
matched pair: 0.30 ÷ 0.0442 = 6.8σ    mismatched pair: 0.20 ÷ 0.0442 = 4.5σ

Two facts fall out. First, nothing here is random — even the smallest number, 0.20, is four and a half standard deviations from chance. The embeddings are all crammed into a small corner of the sphere. Second, the within-modality crowding (12–14σ) is roughly twice the cross-modality crowding (4.5–6.8σ). Images huddle with images; captions huddle with captions; the two huddles are somewhere else from each other.

The picture to carry. Not one cloud with pictures and words interleaved. Two tight clumps, each about the size of a fist, sitting apart on a very large sphere. Training pulled each caption slightly toward its own image — that is the 0.30 versus 0.20 — but it never moved the clumps toward each other.
The textbook picture versus the measurement

Four distributions of cosine similarity on one axis. Press the button to switch between what everyone draws on a whiteboard and what the model actually produces. Then drag the threshold: in the real world there is no cutoff that keeps matched pairs and rejects unrelated images, because the image–image bump sits entirely to the right of the matched bump.

threshold0.45

Why nobody noticed for a year

Here is the uncomfortable part. CLIP works. Zero-shot ImageNet classification, image–text retrieval, guiding diffusion models — all of it works, and all of it works while the gap is wide open. How?

Because almost every use of CLIP only ever asks a ranking question, and ranking only needs the matched pair to beat the mismatched pairs. It does not care whether the winning score is 0.31 or 0.93. Retrieval says “of these 5,000 captions, which one goes with this image?” The answer is whichever has the highest cosine. 0.31 versus 0.21 is a comfortable win, and the gap — being roughly the same for every caption — cancels out of the comparison.

So the gap hid in plain sight, invisible to every benchmark, until somebody asked a question that was not a within-gallery ranking question. Three such questions, all of which people ask in production:

QuestionWhat breaks
“Is this caption relevant to this image, yes or no?”Needs an absolute threshold. Yours is calibrated on text–text similarity and rejects every correct pair.
“Search this mixed corpus of documents and figures with one text query.”Every text document scores ~0.6, every image scores ~0.25. The images never surface, no matter how relevant.
“Train a classifier on text embeddings, run it on image embeddings.”The classifier has never seen a vector from the image island. It fires on nothing, or on everything.

Every one of those is a real system somebody shipped and then had to debug at 2am. Chapters 7 and 8 are about fixing exactly them. But first we need to see the thing, and then understand why it exists at all — because the obvious explanation, “the model is undertrained,” is wrong, and the second-most-obvious explanation, “the loss function wants it,” is also wrong.

What “joint” was supposed to mean, written as a model

It is worth writing down the mental model the numbers just destroyed, because seeing exactly which assumption breaks tells you what to replace it with.

The intuitive model is two noisy views of one meaning. There is a true meaning vector z for “golden retriever on a porch,” and the image encoder recovers it with some error, and the text encoder recovers it with some other, independent error:

u = normalize(z + a)    v = normalize(z + b),    ‖z‖ = 1,   ‖a‖ = ‖b‖ = ε

Work out what this predicts. In high dimensions two independent error vectors are nearly perpendicular to each other and to z, so the cross terms vanish and the norms are √(1+ε2):

matched: u · v ≈ (z·z + z·b + a·z + a·b) ÷ (1 + ε2) ≈ 1 / (1 + ε2)

Fine so far — a noisy encoder gives a matched cosine below 1, and to reach 0.30 you would need 1 + ε2 = 3.33, that is ε = 1.53. Large, but not absurd; you could tell yourself the encoders are noisy. Now compute the number that kills it. Two unrelated images, with meaning vectors zi and zj pointing in different directions:

ui · uj ≈ (zi·zj + ai·aj) ÷ (1 + ε2) ≈ 0

Both terms in the numerator are near zero — unrelated meanings, independent errors. So the two-view model predicts that the matched cosine is far larger than the image–image cosine. We measured the opposite, 0.30 against 0.55. The model does not merely mis-estimate a number; it gets the inequality backwards. That is a structural failure, and structural failures are informative.

For the within-image number to be large, every image must share a component with every other image — a vector that is the same for all of them and absent from the texts. Call it mI. Redo the calculation with it, assuming for a moment that meanings are uncorrelated and mI is perpendicular to meaning:

ui · uj = ‖mI2 ÷ (1 + ‖mI2) = 0.55  →  ‖mI2 = 0.55 ÷ 0.45 = 1.222
‖mI‖ = √1.222 = 1.105     and for text, 0.62/0.38 = 1.632 → ‖mT‖ = 1.277
The modality component is longer than the meaning. Take the semantic part of an embedding to have length 1. Then the shared “I am a picture” part has length 1.11, and the shared “I am a sentence” part has length 1.28. More of each vector encodes what kind of thing it is than what it is about. Everything in the rest of this lesson is a consequence of that one sentence.

(This crude model reproduces the two within-modality numbers exactly by construction and gets the cross-modal ones only roughly — real captions and images in a dataset are topically correlated with each other, and the modality component is not perfectly perpendicular to meaning. Chapter 4 builds a version with all the pieces, where every number closes.)

Measuring the four numbers without fooling yourself

Before you quote these on your own model, three ways the measurement goes wrong.

Prompt templates inflate text–text similarity. If you embed class names with the standard CLIP template, every string starts with the same four tokens: “a photo of a…”. Those shared tokens push the text–text cosine up by a large amount that has nothing to do with the modality gap. Measure sTT on natural captions, and report separately if you use templates.

Near-duplicates inflate everything. Web-scraped corpora are full of the same image at different resolutions and the same caption with different punctuation. A few percent of duplicates will lift both within-modality numbers. Deduplicate before measuring, or you will attribute a data problem to the model.

The four numbers must be reported together. A matched cosine of 0.30 on its own means nothing. It only becomes a diagnosis next to the within-modality numbers, because the whole claim is a comparison. Any paper or dashboard that reports only “average matched similarity” has thrown away the signal.

Concept → realization: the five lines that produce the shock

This is not a subtle statistical effect you need a paper to observe. It takes ninety seconds and a laptop.

python
import torch, open_clip

model, _, preprocess = open_clip.create_model_and_transforms('ViT-B-32', pretrained='openai')
tok = open_clip.get_tokenizer('ViT-B-32')

with torch.no_grad():
    I = model.encode_image(images)          # (N, 512) float32
    T = model.encode_text(tok(captions))    # (N, 512) float32

I = I / I.norm(dim=-1, keepdim=True)      # every row now has length 1
T = T / T.norm(dim=-1, keepdim=True)

print('matched   ', (I * T).sum(-1).mean().item())       # ~0.30
print('img x img ', (I @ I.T).mean().item())                # ~0.55
print('txt x txt ', (T @ T.T).mean().item())                # ~0.62
print('img x txt ', (I @ T.T).mean().item())                # ~0.20

Note the shapes, because they matter later: I and T are both (N × 512) matrices of unit rows. I @ T.T is the (N × N) matrix of all cross-modal cosines — its diagonal is the matched pairs, everything off-diagonal is mismatched. I @ I.T is the within-image block. The whole of this lesson lives inside that one 2N × 2N block matrix, and Chapter 4 will build a four-by-four version of it that you can compute with a pencil.

The one-sentence diagnosis, and the two-sentence preview

Here is the answer, so you know where we are going. The gap is not created by training. It is created by initialization — a randomly-wired deep network already squeezes whatever you feed it into a narrow cone, and two networks with two different random seeds produce two cones pointing in two different directions. Training then carves fine structure inside each cone, because that is all the loss function can see.

And the reason training never fixes it is almost funny: the contrastive loss is blind to the gap. A softmax over a row of scores only sees differences between scores. Moving the whole image cone closer to the whole text cone adds nearly the same amount to every score in the row, so the differences are unchanged and the loss does not move. The gap sits in a flat valley of the loss landscape, and gradient descent has no reason to walk out of it.

What you will be able to do by the end. Measure the gap on your own checkpoint in four lines. Predict which of your downstream systems it will break and which it will not, from first principles rather than by testing. Compute a corrected score that makes an image and a text passage comparable in the same ranked list. And decide — with evidence on both sides — whether closing it is worth your time.
In CLIP’s embedding space, an image and its own caption have cosine similarity around 0.30, while two unrelated images have around 0.55. What does this tell you?

Chapter 1: Seeing the Cones

Numbers are one thing. Let us actually look at the space — because when you do, the gap stops being a statistical curiosity and becomes a picture you cannot unsee.

The obstacle is that the space has 512 dimensions and your screen has two. The standard tool is principal component analysis (PCA): find the direction along which the cloud of points varies most, call it PC1; find the direction of most remaining variation perpendicular to it, call it PC2; plot every point by its coordinates along those two. It is the shadow of the cloud cast onto the plane where the shadow is widest.

Do that to the union of a few thousand image embeddings and a few thousand text embeddings from CLIP, colour the images one colour and the texts another, and you get two compact blobs with clear daylight between them. Not overlapping. Not touching. Two islands.

A trap, and why it is not one here. A reasonable objection: PCA is chosen to maximise spread, so of course it finds a direction that spreads things out — maybe the separation is an artefact of the projection. It is not, and the reason is the point of this chapter. PC1 finds the direction of greatest variance in the data. If PC1 turns out to be almost exactly the direction from the text centroid to the image centroid, that is not the projection inventing structure — it is the projection reporting that “which modality is this?” is the single largest source of variation in the entire cloud.

How much of the variance is just “which modality”?

We can compute this from the four numbers in Chapter 0, with no data and no code. Let cI be the mean of all image embeddings and cT the mean of all text embeddings. Note these means are not unit length — averaging vectors that point in slightly different directions shortens the result, and how much it shortens tells you how tight the clump is.

Start with the squared length of the image centroid. Writing it out,

‖cI2 = (1/n2) ∑ij ui · uj

The double sum has n diagonal terms, each equal to 1 because every embedding is unit length, and n(n−1) off-diagonal terms, each on average equal to sII = 0.55. For n in the thousands the diagonal is negligible, so

‖cI2 ≈ sII = 0.55  →  ‖cI‖ = √0.55 = 0.742

The same argument for text gives ‖cT2 ≈ 0.62, so ‖cT‖ = 0.787. And the dot product of the two centroids is the average of every image-with-every-text cosine, which for large n is dominated by the mismatched pairs:

cI · cT = (1/n2) ∑ij ui · vj ≈ 0.25

(That 0.25 is the average over the whole n × n cross block: one matched entry at 0.30 per row and n−1 mismatched entries at 0.20, which for large n sits close to 0.20 — we use 0.25 here because real datasets have topical clustering that lifts it, and because it is the value that reproduces the published gap. Nothing below depends on the third decimal.)

The gap distance, computed

The gap vector is simply the difference of the two centroids, Δ = cI − cT, and its length is the standard way to report how bad the gap is. Expand the square:

‖Δ‖2 = ‖cI2 + ‖cT2 − 2 cI · cT
= 0.55 + 0.62 − 2(0.25) = 1.17 − 0.50 = 0.67
‖Δ‖ = √0.67 = 0.82

That number, 0.82, is the modality gap for CLIP ViT-B/32 as reported by Liang and colleagues in Mind the Gap — and we just recovered it from four averages and one line of algebra. Put it in perspective: the maximum possible distance between two points on the unit sphere is 2. The two modality centroids are 0.82 apart while each individual embedding is only about 0.74 from the origin. The gap is comparable in size to the entire cloud.

Now the variance share

Total variance of the whole union of embeddings, images and texts pooled, is the mean squared distance to the overall mean. Since every embedding has squared length 1,

Vartotal = E‖x‖2 − ‖x̄‖2 = 1 − ‖x̄‖2,   where x̄ = (cI + cT)/2
‖x̄‖2 = (0.55 + 0.62 + 2 · 0.25) / 4 = 1.67 / 4 = 0.4175
Vartotal = 1 − 0.4175 = 0.5825

The variance explained by modality alone is the between-group term: two groups of equal size whose centres are ‖Δ‖ apart contribute (‖Δ‖/2)2 along the gap direction.

Varbetween = (0.82 / 2)2 = 0.412 = 0.1681
share = 0.1681 ÷ 0.5825 = 0.289 → about 29%
Twenty-nine percent of everything. In a 512-dimensional space, a single binary attribute — is this a picture or a sentence? — accounts for nearly a third of the total variance of the embedding cloud. That is why PC1 lands on the gap direction. The first thing PCA discovers about CLIP is not “animals versus vehicles” or “indoors versus outdoors.” It is “pixels versus words.”

Perfectly separable, and the free modality detector

If 29% of the variance is one direction, a linear classifier on that direction should be trivially good. It is. Fit a logistic regression to predict “image or text” from the 512 numbers, and it reaches 100% accuracy — not 99.8%, 100% — on held-out data. Liang and colleagues report exactly this, and you can reproduce it in a minute.

You do not even need to fit anything. The projection onto the normalized gap direction Δ̂ = Δ/‖Δ‖ already separates them. Compute where each centroid sits along that axis:

cI · Δ̂ = (‖cI2 − cI·cT) / ‖Δ‖ = (0.55 − 0.25) / 0.82 = 0.30 / 0.82 = +0.366
cT · Δ̂ = (cI·cT − ‖cT2) / ‖Δ‖ = (0.25 − 0.62) / 0.82 = −0.37 / 0.82 = −0.451

Sanity check: the difference between those two projections must be the gap length itself, and 0.366 − (−0.451) = 0.817 ≈ 0.82. It closes. So the rule “call it an image if x · Δ̂ > −0.04” is a free, training-free modality detector, and its two classes are centred 0.82 apart.

How much do the two clouds spread along that same axis? Total within-image variance is 1 − 0.55 = 0.45, spread over the directions the embeddings actually use. If that variance were spread evenly over all 512 dimensions the standard deviation along any one direction would be √(0.45/512) = 0.030; real CLIP embeddings concentrate their variance in perhaps 40–60 effective directions, giving √(0.45/50) = 0.095. So the separation is somewhere between 8σ and 27σ. Either way, the two clouds do not touch, which is precisely what “100% linearly separable” means.

The two cones, and what a rotation shows you

Points on a sphere, seen edge-on. Orange dots are image embeddings, teal dots are text embeddings; each cluster is a cone of directions around its own axis. Spin the view to convince yourself the separation is not an artefact of one particular angle — the two clumps stay apart from every viewpoint. Tighten the cones to see the within-modality similarity climb while the cross-modality similarity barely moves. The readouts are computed live from the points actually drawn.

view angle35°
cone tightness20
cone separation76°

Which projection, and what each one lies about

Three tools get used for this picture, and only one of them is admissible as evidence.

methodwhat it optimizescan you trust a gap you see in it?
PCAThe linear projection preserving the most variance.Yes. It is linear, so a separation you see corresponds to a separation that exists along a real direction, and you can quantify it as we just did.
t-SNELocal neighbourhood structure, with a tuneable perplexity.No. t-SNE produces crisp clusters from uniform random noise if you pick the wrong perplexity. It is a hypothesis generator, never evidence.
UMAPA fuzzy topological approximation of the manifold.No, for the same reason, and it additionally exaggerates the distance between clusters in a way that is not metrically meaningful.

The rule to internalize: a nonlinear embedding can manufacture separation; a linear one cannot. If you want to claim two groups are apart, project onto one linear direction and report the two means and standard deviations along it. That is one line of numpy and it is unfalsifiable in a way no coloured scatter plot is.

How many dimensions is CLIP actually using?

While we have the variance numbers out, compute something that will matter a great deal in Chapter 8. The participation ratio is a standard measure of how many directions a cloud really occupies. If λ1, λ2, … are the eigenvalues of the covariance (the variance along each principal direction),

PR = ( ∑k λk )2 ÷ ∑k λk2

It equals d when all directions carry equal variance and 1 when a single direction carries everything. We know λ1 ≈ 0.168 (the gap) and the total is 0.5825, so the remaining 0.4145 is spread over the rest. Suppose it were spread evenly over 50 directions — a generous assumption for real embeddings:

∑λ = 0.5825,    ∑λ2 = 0.1682 + 50 × (0.4145/50)2 = 0.02822 + 0.003436 = 0.03166
PR = 0.58252 ÷ 0.03166 = 0.3393 ÷ 0.03166 = 10.7

Roughly eleven effective directions out of 512. Now delete the gap and recompute with only the 0.4145 spread over 50:

PR′ = 0.41452 ÷ (50 × 0.0082902) = 0.1718 ÷ 0.003436 = 50.0
One direction costs you a factor of five in effective dimensionality. The gap does not merely add a nuisance axis — by being so much larger than every other axis, it makes the whole representation look nearly one-dimensional to any method that weights directions by variance. That is the seed of the argument in Chapter 8 that the gap is a symptom of a low-rank problem rather than a translation problem.

What the picture is not telling you

Two honest caveats, because a compelling visual is exactly where people stop thinking.

First: a 2D shadow of a 512-dimensional cloud loses almost everything. The two blobs look like discs; they are actually thin shells on a hypersphere with structure in dozens of directions. What the shadow does faithfully report is the one thing we computed above — that the between-modality direction carries more variance than any within-modality direction. That claim is projection-independent.

Second: “two cones” is a model, not a photograph. The real clouds are not perfect cones and the gap is not a perfect rigid translation. Chapter 8 covers a line of recent work arguing that treating the gap as a single translation vector is too crude and misses the real problem, which is that each modality occupies a lower-dimensional sliver than it should. Hold that thought; the cone model is right enough to predict every practical failure in Chapter 7, and wrong enough that fixing the gap by translation alone does not buy as much as you would hope.

Concept → realization: measure it on your checkpoint

python
import torch

def modality_gap(I, T):
    # I: (N, d) unit rows, images.  T: (N, d) unit rows, texts.
    cI, cT = I.mean(0), T.mean(0)        # (d,) each — NOT unit length
    delta  = cI - cT                       # (d,) the gap vector
    return {
        'gap_norm'   : delta.norm().item(),                    # ~0.82 for ViT-B/32
        'img_spread' : (1 - cI.norm().pow(2)).item(),           # 1 - 0.55 = 0.45
        'txt_spread' : (1 - cT.norm().pow(2)).item(),           # 1 - 0.62 = 0.38
        'sep_acc'    : separability(I, T, delta / delta.norm()) # ~1.00
    }

def separability(I, T, dhat):
    pi, pt = I @ dhat, T @ dhat            # (N,) projections onto the gap axis
    thr    = (pi.mean() + pt.mean()) / 2   # midpoint decision rule
    return ((pi > thr).float().mean().item() + (pt < thr).float().mean().item()) / 2

Four lines of real work. gap_norm is the headline number you will quote for the rest of this lesson; sep_acc is the sanity check that the gap is a wall and not a smudge. If sep_acc comes back at 1.00, you have two islands. Every checkpoint anyone has published comes back at 1.00.

PC1 of the pooled image+text embedding cloud points almost exactly along the gap direction. Why is that not just an artefact of choosing a projection that maximises spread?

Chapter 2: Where It Comes From

The obvious hypothesis is that training put the gap there — that the contrastive objective, for some subtle reason, prefers two separate clumps. It is a reasonable guess and it is wrong, and the experiment that kills it is embarrassingly simple.

Take a CLIP architecture. Do not train it. Initialize both encoders with random weights — the standard initialization, no pretrained anything. Now push a few thousand images through the vision tower and a few thousand captions through the text tower, normalize, and plot.

You get two separate clumps. Already. Before a single gradient step. And they are further apart than they will be after training.

The gap is not learned. It is inherited. Random initialization already produces two disjoint regions, and contrastive training merely fails to remove them. Any explanation that starts with the loss function is explaining the wrong stage. The explanation has to start with what a randomly-wired deep network does to whatever you feed it.

The cone effect

Here is the phenomenon, and it is general enough to deserve its own name. Take any deep neural network with random weights. Feed it a batch of completely unrelated inputs — even pure noise. Normalize the outputs. Now measure the average cosine similarity between pairs of outputs.

It is not zero. It is large, and it grows with depth. The network maps the entire input space into a narrow cone of directions — a small patch of the output sphere — and everything you feed it lands in that patch. This is the cone effect, and it is the root cause of the modality gap.

Why does it happen? The linear layers are not the culprit — a random matrix applied to two vectors preserves their angle in expectation (it is close to a random rotation plus a rescaling, and rotations do not change angles). The culprit is the nonlinearity, and we can prove it in one line for the simplest case.

Worked example: what one ReLU does to two perpendicular vectors

Let x and y be two independent random vectors in d dimensions, each entry drawn from a standard normal. Independent means their expected cosine similarity is exactly 0 — the angle between them is uniformly random. Now apply ReLU, the function max(0, ·), entry by entry: r = max(0, x) and s = max(0, y).

Both r and s now have only non-negative entries. Two vectors in the non-negative orthant cannot have a negative dot product — the angle between them is at most 90°, never more. So just by clipping we have thrown away half of the available directions. Let us compute exactly how much cosine similarity that buys.

The expected numerator, using independence across coordinates and between x and y:

E[r · s] = d · E[max(0,X)] · E[max(0,Y)] = d · (1/√(2π))2 = d / (2π)

because for a standard normal X, E[max(0,X)] is the half-normal mean 1/√(2π) = 0.3989. The expected squared length:

E[‖r‖2] = d · E[max(0,X)2] = d · (1/2) = d/2

because half the time the entry is 0 and the other half it contributes X2 from a standard normal whose full second moment is 1, so the conditional contribution is exactly 1/2. Divide:

E[cos(r, s)] ≈ (d / 2π) ÷ (d / 2) = 2 / (2π) = 1/π = 0.3183
One ReLU takes you from 0 to 0.318. Two vectors that were perfectly uncorrelated now have a cosine similarity of about a third, and the d cancelled — it does not matter whether the layer is 64 wide or 4096 wide. Every single nonlinearity in the stack does this again, on top of what the previous one did.

What happens after the second layer, and the third

The composition has a closed form. If two inputs to a random ReLU layer have cosine ρ, the outputs have expected cosine

ρ′ = [ sinθ + (π − θ) cosθ ] ÷ π,    where θ = arccos(ρ)

This is the arc-cosine kernel of Cho and Saul (2009), and it is exact in the wide-layer limit. Check it against what we just derived: at ρ = 0 we have θ = π/2, so sinθ = 1 and cosθ = 0, giving ρ′ = 1/π = 0.318. It agrees.

Now iterate it. Start at ρ0 = 0 (two unrelated inputs) and apply the map once per layer:

depth (ReLU layers)θ (degrees)average cosine ρreading
090.0°0.000uncorrelated — the whole sphere is available
171.4°0.318the non-negative orthant
260.4°0.494a wide cone
447.1°0.681a visible clump
833.4°0.834a narrow cone — everything looks alike
16~22°~0.93near-collapse

Let us do one step by hand so the table is not magic. From ρ = 0.318:

θ = arccos(0.318) = 1.2469 rad    sinθ = √(1 − 0.3182) = √0.8987 = 0.9480
(π − θ) · cosθ = (3.1416 − 1.2469) × 0.318 = 1.8947 × 0.318 = 0.6025
ρ′ = (0.9480 + 0.6025) ÷ 3.1416 = 1.5505 ÷ 3.1416 = 0.4937

The fixed point of this map is ρ = 1: pushed deep enough, a purely-ReLU random network maps everything to a single direction and the outputs become indistinguishable. Real networks do not go all the way there — residual connections carry a copy of the input past each nonlinearity, LayerNorm removes the per-vector mean, and attention mixes tokens — but they slow the collapse rather than stop it. A 12-layer transformer at initialization sits comfortably in the 0.6–0.9 range.

Two random networks, two cones

The left panel iterates the arc-cosine map: drag depth and watch the average cosine between two unrelated inputs climb from 0. The right panel draws the two cones that result — one per encoder, each pointing along its own random axis. Switch off the nonlinearity to see the effect vanish entirely: with linear layers only, the cosine stays pinned at zero and there are no cones at all.

depth (layers)6
seed separation88°

Two cones, not one: why the axes differ

The cone effect alone would give you one narrow cone. The modality gap needs two, pointing in different directions. Where does the second one come from?

From the random seed. The direction a cone points is determined by the particular random weights drawn for that network. Draw a different seed and you get a cone with the same width around a completely different axis. And the vision encoder and the text encoder are separate networks with separate weights, so their cone axes are two independent random directions in d-dimensional space.

Now use the fact from Chapter 0: two random directions in 512 dimensions have expected cosine 0 with standard deviation 1/√512 = 0.044. They are, to a very good approximation, perpendicular. So the picture at initialization is two narrow cones whose axes are at essentially 90° to each other.

And now we can predict the gap at initialization without running anything. With cone tightness ρ inside each modality and perpendicular axes, the two centroids have squared lengths ρ each and dot product approximately 0, so

‖Δinit2 ≈ ρI + ρT − 0 = 2ρ
with ρ = 0.68 (a depth-4-equivalent cone): ‖Δinit‖ = √1.36 = 1.17

Compare that to the trained value of 0.82 from Chapter 1. Training does move the cones toward each other — from 1.17 down to 0.82, which is a real 30% reduction — and then stops, with the gap still wider than the radius of either clump. That arc, 1.17 → 0.82 → stuck, is the entire story of the modality gap in three numbers.

Why architecture asymmetry makes it worse, not better. Even if you somehow used the same seed for both towers, a Vision Transformer over 7×7 patch grids and a text transformer over 77 word-piece positions are different functions with different depths, different normalization statistics, and different final projections. There is no mechanism that would make their cones coincide. Sharing the final projection layer helps a little, which is why some multimodal models do it — but the cones are formed by everything before that layer.

Why LayerNorm and residuals do not save you

The natural objection: real transformers are not naked ReLU stacks. They have LayerNorm after every block and a residual path around every sublayer. Surely those fix it? Take them one at a time, because the answers are different and both are instructive.

LayerNorm operates in the wrong direction. It takes one vector and normalizes across its feature dimensions: subtract the mean of the 512 numbers in that vector, divide by their standard deviation. It does not subtract the mean across the batch. So a shared cone direction survives untouched unless that direction happens to be the all-ones vector, which it will not be. Concretely: if every output is c + δi for a shared c and small per-input δi, then LayerNorm maps it to LN(c) plus a small perturbation, and the shared part is still shared. LayerNorm normalizes scale; the cone effect is about direction.

Residuals dilute it but do not remove it. A residual block computes x + F(x), so a copy of the input survives the nonlinearity. Model the branch as having relative scale β. The cross terms between x and F are small, and the squared norm of the sum is 1 + β2, giving

ρout ≈ ( ρ + β2 f(ρ) ) ÷ ( 1 + β2 )

where f is the arc-cosine map from above. At β = 1, starting from ρ = 0:

ρ1 = (0 + 1 × 0.318) ÷ 2 = 0.159    (against 0.318 with no residual)

So a residual block tightens the cone at roughly half the rate. Eight residual layers behave like four plain ones — you get to ρ ≈ 0.68 instead of 0.83. That is a delay, not a cure, and real networks are much deeper than eight layers.

The activation function matters, and this one is testable. ReLU tightens the cone because it maps everything into the non-negative orthant. GELU, being a smoothed ReLU, does the same thing slightly more gently. But tanh is odd-symmetric — it maps negatives to negatives — so it does not push outputs into a half-space and its cone effect is far weaker. If you want to feel this rather than believe it, take the twenty-line script below and swap nn.ReLU() for nn.Tanh(): the mean cosine at depth eight drops from about 0.83 to a small number. It is the clearest possible demonstration that the cone comes from the shape of the nonlinearity, not from depth or width.

The same effect has other names in other fields

If this feels familiar, it should. The cone effect is a rediscovery, in the multimodal setting, of something language-model people have studied for years under the name anisotropy: contextual embeddings from BERT and GPT-2 occupy a narrow cone rather than filling the space, with average cosine similarity between random words as high as 0.5–0.9 in later layers (Ethayarajh, 2019). The word-embedding community found the same thing earlier and called it the representation degeneration problem (Gao et al., 2019), and the standard fix — subtract the mean and remove the top principal components, the “all-but-the-top” recipe of Mu et al. (2018) — is exactly the centering fix we will use in Chapter 6.

That cross-field connection is worth more than a citation. It tells you the modality gap is not a quirk of CLIP or of contrastive learning. It is what deep networks do to representations, showing up in a place where you happen to have two networks and can therefore see the two cones instead of just one.

Concept → realization: reproduce the cone in twenty lines

python
import torch, torch.nn as nn

def random_mlp(depth, width=512, act=True):
    layers = []
    for _ in range(depth):
        layers.append(nn.Linear(width, width))
        if act: layers.append(nn.ReLU())
    return nn.Sequential(*layers)          # never trained — random init only

x = torch.randn(2000, 512)                    # 2000 UNRELATED inputs
for depth in [0, 1, 2, 4, 8]:
    net = random_mlp(depth)
    with torch.no_grad():
        z = net(x) if depth else x
    z = z / z.norm(dim=-1, keepdim=True)      # (2000, 512) unit rows
    S = z @ z.T                              # (2000, 2000) cosine matrix
    off = S[~torch.eye(len(S), dtype=torch.bool)]
    print(depth, off.mean().item())           # 0.00, 0.32, 0.49, 0.68, 0.83

Then do it twice with different seeds and take the two mean vectors: the angle between them will be close to 90°, and the distance between them close to √(2ρ). You have just built the modality gap out of nothing but random numbers and a ReLU, with no data, no captions, and no training.

A randomly initialized CLIP, before any training, already shows two separated clumps. What is the mechanism?

Chapter 3: The Gap Vector

We have a number, 0.82, and a direction, Δ̂. This chapter is about what that direction actually is as an object you can manipulate — and about the single most surprising experimental result in the original paper: if you deliberately move the embeddings along the gap direction, downstream zero-shot accuracy changes, and the gap that training landed on is not the best one.

Three claims, each one line of algebra

Before the experiment, get the geometry exactly right. Nearly every argument people have about the modality gap comes down to confusing these three cases.

Claim A — translating the gallery does not change rankings. You have a query image u and a gallery of captions {vj}. You rank by the score u · vj. Now translate every caption by the same vector t:

u · (vj + t) = u · vj + u · t

The added term u · t does not depend on j. It is the same constant for every candidate, so it shifts all scores equally and the ordering is exactly unchanged. Closing the gap this way is a no-op for retrieval.

Claim B — renormalizing after translation does change rankings. In practice you do not stop at vj + t; you put it back on the unit sphere. Then

scorej = ( u · vj + u · t ) ÷ ‖vj + t‖,    ‖vj + t‖ = √(1 + 2 vj·t + ‖t‖2)

Now the denominator depends on j, through vj · t. Candidates whose embedding already leans along t get a bigger denominator and are penalized; candidates leaning away get a smaller one and are promoted. The translation has become a per-candidate reweighting. This is the entire mechanism by which “adjusting the gap” changes accuracy.

Claim C — translating the query side changes rankings even without renormalization. Move the query instead:

(u + t) · vj = u · vj + t · vj

Here the extra term t · vj does depend on j. So shifting the query is a genuine change of ranking, equivalent to blending a fixed “background query” t into every search.

Keep these straight and half the literature makes sense. Papers reporting “we closed the gap and retrieval did not improve” usually did a gallery-side translation (Claim A, a no-op). Papers reporting “closing the gap changed everything” renormalized (Claim B) or shifted the query side (Claim C). Both are honest. They ran different experiments.

Worked example: shifting the gap flips a prediction

Zero-shot classification with CLIP means: embed the image once, embed one text prompt per class (“a photo of a cat”, “a photo of a dog”), and take the class with the highest cosine. Take a genuinely borderline image — a fluffy animal in a doorway — with these measurements:

u · vcat = 0.302    u · vdog = 0.295    (cat wins by 0.007)

And these projections onto the gap axis. Remember from Chapter 1 that texts sit at about −0.45 along Δ̂ and images at about +0.37; individual items scatter around those:

vcat · Δ̂ = −0.25    vdog · Δ̂ = −0.45    u · Δ̂ = +0.41

Now translate both text embeddings by αΔ̂ with α = 0.6 — that is, move the text island six tenths of the way toward the image island — and renormalize. Numerators first; both get the same additive term, 0.6 × 0.41 = 0.246:

cat: 0.302 + 0.246 = 0.548     dog: 0.295 + 0.246 = 0.541

Now the denominators, using ‖v + αΔ̂‖ = √(1 + 2α(v·Δ̂) + α2):

cat: √(1 + 2(0.6)(−0.25) + 0.36) = √(1 − 0.30 + 0.36) = √1.06 = 1.0296
dog: √(1 + 2(0.6)(−0.45) + 0.36) = √(1 − 0.54 + 0.36) = √0.82 = 0.9055

Divide:

cat: 0.548 ÷ 1.0296 = 0.532     dog: 0.541 ÷ 0.9055 = 0.598

The prediction flipped. Cat won before the shift by 0.007; dog wins after the shift by 0.066, nearly ten times the original margin. Nothing about the image changed, nothing about the model changed, and no training happened. We moved one modality along one direction and renormalized.

Read the mechanism, not the magic. Dog won because vdog was more anti-aligned with the gap direction (−0.45 versus −0.25), so moving toward the image island shortened it more, so renormalizing inflated its score more. The gap shift is a per-class prior. It is doing the same job as a per-class bias term in a logistic regression — which means it can genuinely improve accuracy when the classes were badly calibrated, and it means the improvement is calibration, not understanding.

What the original experiment found

Liang and colleagues did exactly this sweep on real models: shift the embeddings along Δ̂ by varying amounts, from closing the gap entirely to widening it well past its trained value, and measure downstream performance at every point. Two findings, both counter-intuitive:

FindingWhat it means
The trained gap is not the optimum. Accuracy at α = 0 is not the peak of the curve.Whatever geometry training converged on, it was not chosen to maximise your downstream task. It was chosen by initialization plus a loss that is blind to this direction.
Sometimes increasing the gap helps. The best α can be negative.The direction of the fix is task-dependent, which is the signature of a calibration effect rather than a representation fix. And it kills the intuition that “closer is better.”
Fairness metrics move too — rates of harmful misclassification on face datasets change with α.Per-class prior reweighting hits rare and stereotyped classes hardest. A geometric knob nobody knew existed is quietly setting social outcomes.

The size of the effect is modest — a point or two of accuracy, in either direction, depending on the dataset. That modesty is itself informative and we will return to it in Chapter 8. A one-line post-hoc shift that moves accuracy by a point or two is not a breakthrough; it is a hyperparameter you did not know you had.

Shifting the gap, and watching accuracy move

A twelve-class zero-shot setup. Drag α to translate all class-prompt embeddings along the gap direction and renormalize. The top strip shows the gap closing and reopening; the curve below is zero-shot accuracy over the whole sweep with your current α marked. Turn renormalization off to watch the accuracy curve go perfectly flat — that is Claim A, drawn.

α (shift along Δ̂)0.00

Why the gap direction is special, in one number

Claim B says a translation becomes a reweighting through the denominator ‖vj + t‖ = √(1 + 2vj·t + ‖t2). The size of the effect is controlled entirely by how much vj · t varies across candidates. So compare two choices of t with the same length.

A random direction. For a unit t unrelated to anything, each vj · t is about 0 ± 1/√512 = 0.044. The denominators differ between candidates by roughly 2α(0.044) = 0.088α.

The gap direction. From Chapter 1, text embeddings project onto Δ̂ at about −0.45 systematically. The denominators shift by 2α(0.45) = 0.90α — and, more importantly, they shift the same way for every text, which is what makes the effect a coherent recalibration instead of noise.

0.90 ÷ 0.088 ≈ 10× the systematic effect, from a direction of the same length

That factor of ten is what makes Δ̂ worth naming. Out of 512 directions you could shift along, this is the one where the whole corpus has a large, shared, non-zero projection. Shifting along anything else is close to a no-op.

How the gap distance itself moves under a shift

One more piece of bookkeeping, because the sim in this chapter reports it. If you translate every text by αΔ̂ and do not renormalize, the text centroid moves to cT + αΔ̂ and the new gap is

Δ(α) = cI − cT − αΔ̂ = (‖Δ‖ − α) Δ̂  →  ‖Δ(α)‖ = |0.82 − α|

A straight line down to zero at α = 0.82 and back up again. Note what that means for the sweep: the same gap distance occurs at two different α values — one on each side — and there is no reason for them to give the same downstream accuracy, because accuracy depends on the reweighting, not on the distance. If you ever see a paper plot accuracy against gap distance rather than against the shift, that folding is happening and the plot is ambiguous.

python
def gap_sweep(I, T, labels, prompts, alphas):
    # I: (N,d) image embs. prompts: (C,d) class-prompt embs, unit rows.
    d_hat = (I.mean(0) - T.mean(0))
    d_hat = d_hat / d_hat.norm()                # (d,) the gap direction
    out = []
    for a in alphas:
        P = prompts + a * d_hat                 # (C,d) — off the sphere
        P = P / P.norm(dim=-1, keepdim=True)  # back on — this is where it bites
        pred = (I @ P.T).argmax(-1)             # (N,)
        out.append(((pred == labels).float().mean().item(), abs(0.82 - a)))
    return out    # (accuracy, gap distance) per alpha — plot against ALPHA, not distance

Is the gap really just one vector?

We have been treating Δ as a rigid translation: every image sits at the same offset from where the corresponding text sits. If that were exactly true, the fix would be trivial — subtract Δ and go home. How true is it?

Test it directly. For each matched pair, compute the difference vector di = uivi, and ask how much of each di is the shared Δ and how much is item-specific residual. Decompose:

di = Δ + ei,    with ∑i ei = 0 by construction
fraction explained = ‖Δ‖2 ÷ ( ‖Δ‖2 + mean‖ei2 )

Compute the mean squared difference from our four numbers. Since both are unit vectors,

E‖di2 = E‖ui2 + E‖vi2 − 2E[ui·vi] = 1 + 1 − 2(0.30) = 1.40
mean‖ei2 = 1.40 − ‖Δ‖2 = 1.40 − 0.67 = 0.73
fraction explained = 0.67 ÷ 1.40 = 0.48

So the shared gap vector accounts for a little under half of the image-to-text displacement, and the other half is item-specific. The rigid-translation model is half right. That single number explains a great deal of the literature: methods that subtract Δ get roughly half the benefit they hoped for, and methods that inject noise to cover the residual (Chapter 6) do better than methods that subtract a mean and stop.

Why the residual is not noise. Half of di being item-specific does not mean it is random. Some of it is real information asymmetry: a photograph carries the exact shade of the porch, the weather, the dog’s posture; the caption carries none of that. A caption carries the writer’s framing and word choice; the photograph carries none of that. There is no vector you can add to a caption to recover the pixels. Chapter 8 revisits this under the name information imbalance, and it is the strongest argument that the gap cannot be fully closed by geometry.
You translate every caption embedding by the same vector t and do not renormalize. What happens to image→text retrieval rankings?

Chapter 4: By Hand

Everything so far has been about a 512-dimensional space you cannot see. Now we build the smallest object that has a real modality gap — four unit vectors in three dimensions — and compute every number in it with a pencil. By the end of this chapter you will have reproduced, at toy scale, the four Chapter 0 measurements, the gap vector, the free modality detector, and the exact reason mixed-modality search collapses.

Building the toy

Three ingredients. First, an image axis a — the direction the image cone points. Second, a text axis b — where the text cone points. Third, a semantic axis e, perpendicular to both, along which the two items in our dataset differ. Item 1 is a dog, item 2 is a fire truck; that difference lives on e and is shared by both modalities, because a dog photo and a dog caption are about the same thing.

a = (1, 0, 0)    b = (0.28, 0.96, 0)    e = (0, 0, 1)

Check that b is a unit vector: 0.282 + 0.962 = 0.0784 + 0.9216 = 1. Good. And the two axes are a · b = 0.28 apart in cosine, which is arccos(0.28) = 73.7° — two clearly different directions, the way two randomly-seeded encoders come out. The semantic axis e is perpendicular to both, since both have a zero third coordinate.

Now build four embeddings. Each is 96% modality axis and 28% semantic axis, with the sign of the semantic part carrying the item identity (+ for the dog, − for the fire truck):

I1 = 0.96a + 0.28e = (0.96, 0, 0.28)     I2 = 0.96a − 0.28e = (0.96, 0, −0.28)
T1 = 0.96b + 0.28e = (0.2688, 0.9216, 0.28)     T2 = 0.96b − 0.28e = (0.2688, 0.9216, −0.28)

All four have length 1, because 0.962 + 0.282 = 0.9216 + 0.0784 = 1 and the modality axis is perpendicular to e. Verify one explicitly:

‖T12 = 0.26882 + 0.92162 + 0.282 = 0.0723 + 0.8493 + 0.0784 = 1.0000

The matched pairs are (I1, T1) — dog photo with dog caption — and (I2, T2). Now compute the whole four-by-four similarity matrix. Because every vector is unit length, cosine is just the dot product.

The six dot products

Within images. The modality parts are identical so they contribute 0.96 × 0.96 = 0.9216, and the semantic parts have opposite signs so they contribute −(0.28 × 0.28) = −0.0784:

I1 · I2 = 0.9216 − 0.0784 = 0.8432

Within texts. Same structure, same answer:

T1 · T2 = 0.9216 − 0.0784 = 0.8432

Matched pair. The modality parts contribute 0.96 × 0.96 × (a · b) = 0.9216 × 0.28, and the semantic parts have the same sign so they add:

0.9216 × 0.28 = 0.258048
I1 · T1 = 0.258048 + 0.0784 = 0.336448

Mismatched pair. Identical except the semantic parts now cancel:

I1 · T2 = 0.258048 − 0.0784 = 0.179648

Assemble the block matrix. Read it the way you would read torch.cat([I,T]) @ torch.cat([I,T]).T:

I1I2T1T2
I11.00000.84320.33640.1796
I20.84321.00000.17960.3364
T10.33640.17961.00000.8432
T20.17960.33640.84321.0000

Compare it to the Chapter 0 table: within-image 0.84 against a measured 0.55, within-text 0.84 against 0.62, matched 0.34 against 0.30, mismatched 0.18 against 0.20. The toy is tighter than real CLIP but every relationship is reproduced — and it was produced by nothing but two axes 73.7° apart plus a little shared semantic wiggle. That is all the modality gap is.

The two-block signature. The matrix has bright diagonal blocks (within-modality, ~0.84) and a dim off-diagonal block (cross-modality, 0.18–0.34). Every real CLIP similarity matrix looks like this. When someone shows you a heat map of a “joint” embedding space and it has two bright squares on the diagonal, you are looking at the gap.

The gap vector, exactly

The centroids are the averages of each pair. The semantic parts cancel — one is +0.28e, the other −0.28e — leaving only the modality axis:

cI = (I1 + I2)/2 = 0.96a = (0.96, 0, 0)
cT = (T1 + T2)/2 = 0.96b = (0.2688, 0.9216, 0)

Notice they are shorter than 1 — the averaging shortened them to 0.96, and 0.962 = 0.9216 is exactly the within-modality cosine we computed plus the semantic loss, matching the ‖c‖2 ≈ s formula from Chapter 1. The gap vector:

Δ = cI − cT = (0.96 − 0.2688, 0 − 0.9216, 0) = (0.6912, −0.9216, 0)
‖Δ‖2 = 0.69122 + 0.92162 = 0.477757 + 0.849347 = 1.327104
‖Δ‖ = √1.327104 = 1.1520

Cross-check it with the Chapter 1 formula, which used only the averaged cosines:

‖Δ‖2 = 0.9216 + 0.9216 − 2(0.9216 × 0.28) = 1.8432 − 0.516096 = 1.327104  

Same answer from two directions. Now normalize:

Δ̂ = (0.6912, −0.9216, 0) ÷ 1.1520 = (0.6, −0.8, 0)

A perfectly clean unit vector, which is not a coincidence — 0.6–0.8–1.0 is a right triangle. Project all four embeddings onto it:

I1 · Δ̂ = 0.96(0.6) + 0(−0.8) + 0.28(0) = +0.576    (same for I2)
T1 · Δ̂ = 0.2688(0.6) + 0.9216(−0.8) = 0.16128 − 0.73728 = −0.576    (same for T2)

Both images at exactly +0.576, both texts at exactly −0.576, and the distance between them is 0.576 + 0.576 = 1.152, which is ‖Δ‖. Everything closes. The rule “image if x · Δ̂ > 0” separates the two modalities with a margin of 0.576 on each side, with no training and no labels — the free modality detector from Chapter 1, now in numbers you verified yourself.

The toy, drawn and computed

Left: the four unit vectors and their two cone axes, seen looking down the semantic axis and then tilted. Right: the four-by-four cosine matrix, every cell computed live from the vectors on the left. Drag the axis separation to watch the off-diagonal block brighten as the cones converge, and press the button to apply the centering fix and see what it does to every cell at once.

axis separation a·b0.28
semantic strength0.28

Retrieval still works. Mixed search does not.

Now run the two experiments that matter, on the matrix we just built.

Experiment 1: image→text retrieval. Query with I1, rank the two captions. 0.3364 for T1 beats 0.1796 for T2. Correct. Query with I2: 0.3364 for T2 beats 0.1796 for T1. Correct. Two out of two. A gap of 1.152 — larger than in real CLIP — and retrieval is perfect, because both candidates suffer the same handicap and it cancels.

Experiment 2: mixed-modality search. Now the corpus contains images and texts together, the way a real document index does. Query with the dog caption T1, and rank everything else:

rankcandidatescoreactually relevant?
1T2 — the fire-truck caption0.8432no. Wrong topic entirely.
2I1 — the dog photo0.3364yes. This is the answer.
3I2 — the fire-truck photo0.1796no

The completely irrelevant caption beats the correct image by a factor of 2.5. And notice this is not a close call you could fix with a better model: the gap is 1.152 while the entire semantic signal is worth 2 × 0.0784 = 0.157 of cosine. Modality is seven times louder than meaning. No amount of relevance can overcome it.

This is the production bug. You put your documentation pages and your architecture diagrams in one vector index. Users search in text. They never, ever see a diagram — not because the diagrams are bad matches, but because every text chunk in the corpus scores 0.6 and every image scores 0.25 before relevance is even considered. Chapter 7 fixes it in four different ways, all cheap.

The general formulas, and two limits worth knowing

Our four numbers came from three parameters: the axis separation R = a·b, the semantic strength s, and the modality weight m = √(1−s2). Write the whole toy in closed form, which is what the sliders in the widget above are driving:

within-modality = m2 − s2     matched = m2R + s2     mismatched = m2R − s2
‖Δ‖ = m √(2(1 − R))     margin = matched − mismatched = 2s2

That last identity is the most important line in the chapter. The entire cross-modal signal — everything that lets retrieval work at all — is 2s2, and it is completely independent of R. Move the cones together or apart as much as you like; the margin does not budge. The gap and the signal are decoupled by construction, and that is exactly why Chapter 5 will find that the loss cannot see the gap.

Limit one: perpendicular axes (R = 0). This is the situation at random initialization, where the two cone axes are essentially orthogonal.

matched = s2 = 0.0784    mismatched = −s2 = −0.0784    within = m2 − s2 = 0.8432

The matched cosine equals the square of the semantic strength. Semantics enters cross-modal similarity at second order while the modality axis enters at first order, and squaring a number smaller than one makes it much smaller. A within-to-matched ratio of 0.8432/0.0784 = 10.8×. That single second-order-versus-first-order asymmetry is the deepest reason matched cosines look so disappointing.

Limit two: coincident axes (R = 1). The gap closes completely, ‖Δ‖ = m√0 = 0, and

matched = m2 + s2 = 1.0000    mismatched = m2 − s2 = 0.8432 = within-modality

Matched pairs coincide exactly, and every other pair — image to image, text to text, image to wrong text — collapses to the same number. The block structure disappears; there is one cloud. This is what a genuinely joint space would look like, and it is worth staring at the contrast with the table above.

What the loss thinks of all this

Compute the contrastive loss on the toy, because it will be the punchline of Chapter 5. For image I1 with the two captions as candidates, InfoNCE at temperature τ is the negative log of the softmax probability on the correct one. With a two-way choice that reduces to

L = ln( 1 + e−margin/τ ) = ln( 1 + e−0.1568/τ )

At τ = 0.1: exponent = −1.568, e−1.568 = 0.2085, L = ln(1.2085) = 0.1893. At τ = 0.01: exponent = −15.68, e−15.68 = 1.55×10−7, L = 1.55×10−7. The loss depends on the margin and on nothing else — not on R, not on the gap, not on the absolute similarities. Hold on to that.

What centering does, computed exactly

The standard cheap fix: subtract each modality’s own centroid, then renormalize. Take Δ/2 = (0.3456, −0.4608, 0) and move the images back by it while moving the texts forward by it — a symmetric closing that meets in the middle.

I1′ = I1 − Δ/2 = (0.96 − 0.3456, 0 + 0.4608, 0.28) = (0.6144, 0.4608, 0.28)
T1′ = T1 + Δ/2 = (0.2688 + 0.3456, 0.9216 − 0.4608, 0.28) = (0.6144, 0.4608, 0.28)

They are the same vector. The matched pair has been mapped exactly on top of itself — cosine 1.0000. That is not luck: our toy was built so the only difference between a matched image and its caption is the modality offset, so removing the offset removes the entire difference. Real embeddings have the item-specific residual from Chapter 3, which is why real centering gets you to about 0.7, not 1.0.

The mismatched pair, though. T2′ = (0.6144, 0.4608, −0.28), and the vectors are no longer unit length, so we must divide by their norms:

‖I1′‖2 = 0.377487 + 0.212337 + 0.0784 = 0.668224  →  ‖I1′‖ = 0.81745
I1′ · T2′ = 0.377487 + 0.212337 − 0.0784 = 0.511424
cos = 0.511424 ÷ 0.668224 = 0.7653

So after centering: matched 1.0000, mismatched 0.7653. Compare to before: 0.3364 and 0.1796. Both went up enormously — and here is the number that decides whether centering was worth it:

margin before = 0.3364 − 0.1796 = 0.1568
margin after = 1.0000 − 0.7653 = 0.2347

The margin grew by 50%. Retrieval is not just still correct, it is more robustly correct. And the mixed-search disaster is gone: T1′ against T2′ is now 0.511424/0.668224 = 0.7653 — the same as its score against the wrong image, so the correct image (1.0000) finally wins. Centering fixed the actual bug.

Do not over-generalize from a toy this clean. The margin grew here because our residual was zero. On real embeddings, centering typically improves cross-modal margins slightly, sometimes hurts, and always changes the ranking (Claim B from Chapter 3). What it reliably does is make cross-modal and within-modal scores live on the same scale, which is the thing you actually needed.
In the toy, the gap contributes 1.152 of distance between centroids while the entire semantic signal is worth 0.157 of cosine. What follows for a mixed corpus of images and texts searched with a text query?

Chapter 5: Temperature

We know where the gap comes from — initialization. We still owe an answer to the harder question: why does training, which has millions of steps and a loss whose entire job is to pull matched pairs together, not simply close it?

The answer is that the loss cannot see it. And the knob that decides how blind the loss is turns out to be a single scalar that CLIP learns and almost nobody thinks about: the temperature.

The loss, written out

CLIP’s objective is InfoNCE, a softmax cross-entropy over a batch. Take a batch of N image–text pairs. For image i, treat its own caption as the correct answer and the other N−1 captions as wrong answers, and score each candidate by cosine divided by the temperature τ:

Li = − log [ exp(ui·vi / τ) ÷ ∑j exp(ui·vj / τ) ]

The full loss averages this over all images and then does the mirror-image version over all captions. Temperature τ is a divisor on every score: small τ magnifies differences, large τ flattens them. CLIP does not fix it — it stores log(1/τ) as a learnable parameter, initializes it at log(1/0.07) ≈ 2.66, and clamps 1/τ at 100. Training drives it to the clamp. So the shipped model runs at

τ = 0.01,   i.e. every cosine is multiplied by 100 before the softmax

Why the loss is blind to the gap

Softmax has a property so basic it is easy to forget: adding the same constant to every logit changes nothing. The constant factors out of the numerator and out of every term in the denominator and cancels. Only differences between logits matter.

Now ask what closing the gap does to the logits. Take the toy from Chapter 4 and rotate the image cone axis a until it lands exactly on the text axis b. That is the gap closed completely, done properly on the sphere so everything stays unit length. Recompute:

gap open (a·b = 0.28)gap closed (a·b = 1.00)
matched cosine0.9216(0.28) + 0.0784 = 0.33640.9216(1.00) + 0.0784 = 1.0000
mismatched cosine0.9216(0.28) − 0.0784 = 0.17960.9216(1.00) − 0.0784 = 0.8432
difference0.15680.1568
logits at τ = 0.0133.64 and 17.96100.00 and 84.32
softmax lossln(1 + e−15.68) = 1.6×10−7ln(1 + e−15.68) = 1.6×10−7

Identical. Both cosines rose by exactly 0.6636, so their difference — the only thing the softmax sees — is unchanged, and the loss is bit-for-bit the same at both geometries. Closing the gap is free and closing the gap is worthless, as far as the objective is concerned.

The gap lives in a flat valley. The loss surface has a direction along which it does not change. Gradient descent, by construction, does not move along directions where the gradient is zero. So the model stays wherever initialization dropped it, forever, no matter how long you train. This is the whole answer, and it is why “train longer” and “use more data” have never closed the gap for anyone.

One honest caveat: the valley is flat exactly only because our toy put the semantic variation perpendicular to the modality axes. In real CLIP the two are not perfectly perpendicular, so the valley has a gentle slope rather than being level. The rest of this chapter is about how steep that slope is — and that is where temperature enters.

The gradient, and why τ freezes it

Differentiate the InfoNCE loss with respect to the image embedding. Writing pij for the softmax weight the model puts on candidate j:

∂Li / ∂ui = −(1/τ) [ vi − ∑j pij vj ] = −(1/τ) [ vi − v̄i ]

Read that in English: the image is pulled toward its own caption and away from the softmax-weighted average of all captions, i. The pull vanishes when i = vi, that is, when the softmax has become a spike on the right answer.

Now put numbers on it, using the toy’s margin of 0.1568.

At τ = 0.01 (what CLIP ships). The logit difference is 0.1568/0.01 = 15.68, so the probability the model assigns to the wrong caption is

pwrong = 1 ÷ (1 + e15.68) = 1 ÷ (1 + 6,452,600) = 1.55 × 10−7

The pull is proportional to (1/τ) × pwrong — a big prefactor times a vanishing probability:

pull ∝ 100 × 1.55×10−7 = 1.55 × 10−5

At τ = 0.5. The logit difference is 0.1568/0.5 = 0.3136, so

pwrong = 1 ÷ (1 + e0.3136) = 1 ÷ (1 + 1.3683) = 1 ÷ 2.3683 = 0.4222
pull ∝ 2 × 0.4222 = 0.8444

Take the ratio:

0.8444 ÷ (1.55 × 10−5) ≈ 54,000×
Fifty-four thousand times weaker. At CLIP’s shipped temperature, once matched pairs beat their negatives by a margin much larger than τ, the softmax saturates and every gradient in the model — including the tiny one that would nudge the cones together — collapses to nothing. Low temperature does not cause the gap. It freezes whatever gap initialization handed you, by switching off the only force that could have removed it.

Where the gap-closing force actually comes from

Be precise about the direction, not just the magnitude, because the magnitude alone would predict that huge τ is best — and it is not (huge τ also has a 1/τ prefactor going to zero, and it destroys the model’s ability to discriminate).

Project the pull onto the gap axis. The image moves along Δ̂ by an amount proportional to

〈 vi − ∑j pij vj , Δ̂ 〉 = (vi·Δ̂) − ∑j pij (vj·Δ̂)

Which captions get large pij? The ones with high cosine to the image — and those are systematically the captions that sit closer to the image cone, meaning higher vj · Δ̂. So the weighted average ∑j pij(vj·Δ̂) is pulled above the plain average, the bracket comes out negative, and the image is pushed in the −Δ̂ direction — toward the texts. That is the gap-closing force, and it is a covariance: how much the softmax weight correlates with position along the gap axis.

A covariance needs spread in the weights. When p is a spike on the correct answer, every other weight is zero, there is nothing to correlate with, and the covariance is zero. That is the same conclusion as the magnitude argument, now with the mechanism attached: low temperature closes the softmax, and a closed softmax has no opinion about the gap.

This predicts the empirical finding in Mind the Gap: train the same model at different temperatures and the gap distance shrinks as τ grows. It also predicts why nobody wants to just raise τ — temperature is not a gap knob, it is the model’s discrimination knob, and turning it up trades away exactly the hard-negative pressure that makes CLIP good.

Temperature: the knob that freezes the geometry

Drag τ on a log scale. Top: the softmax distribution over one correct caption and seven negatives, from a razor spike to nearly uniform. Middle: the gradient pressure available to move the geometry, computed as (1/τ) × (1 − pcorrect) — note it dies at both extremes. Bottom: the resulting equilibrium gap. CLIP’s shipped τ = 0.01 is marked.

temperature τ0.010
margin (cosine)0.16

Batch size is the other half of the story

There is one thing that does put pressure back on the geometry at low temperature, and it explains why the trained gap (0.82) is smaller than the gap at initialization (1.17) rather than equal to it. The pressure came from the number of negatives.

With N negatives all sitting at the same margin m below the correct answer, the softmax probability on the correct one is

pcorrect = 1 ÷ (1 + N e−m/τ)  →  1 − pcorrect ≈ N e−m/τ

The total mass on wrong answers scales linearly with the batch size. Put CLIP’s actual numbers in: margin 0.1568, τ = 0.01, so e−m/τ = e−15.68 = 1.55×10−7.

negatives per rowmass on wrong answerspressure = (1/τ)(1 − p)
1 (a toy pair)1.55 × 10−71.55 × 10−5
255 (batch 256)3.95 × 10−53.95 × 10−3
32,767 (CLIP’s batch)32,767 × 1.55×10−7 = 5.1 × 10−30.51

Work the last row: 32,767 × 1.55×10−7 = 5.08×10−3, and multiplying by 1/τ = 100 gives 0.51. Compare that to the 0.84 we computed at τ = 0.5 — it is the same order of magnitude. A batch of thirty-two thousand recovers almost all the gradient pressure that a temperature of 0.01 destroyed.

The two knobs are the same knob. Multiplying the batch by N has the same effect on softmax saturation as reducing the margin by τ ln N. At τ = 0.01, going from batch 256 to batch 32,768 is worth 0.01 × ln(128) = 0.049 of cosine margin. That is why CLIP’s enormous batch is not merely a throughput decision — it is the thing keeping the gradient alive at a temperature that would otherwise freeze the model solid. And it is why the gap shrinks from 1.17 to 0.82 during training instead of staying put: there is pressure, it is just not enough to finish the job.

Why you cannot simply raise the temperature

If large τ shrinks the gap, why not use it? Because temperature is not a gap knob, it is the discrimination knob, and the same saturation that freezes the geometry is what makes CLIP good.

The gradient the model gets from a negative is proportional to the softmax weight it assigns that negative. At τ = 0.01 the weights concentrate on the handful of negatives that are genuinely confusable — the hard negatives — and the model spends its capacity separating those. At τ = 0.5 the weights are nearly uniform: a caption about a fire truck receives almost as much repulsive force as a caption about a different dog. You have replaced a curriculum of hard cases with an undifferentiated shove.

Concretely, at τ = 0.5 and 32,767 negatives, pcorrect = 1/(1 + 32,767 e−0.314) = 1/(1 + 23,940) = 4.2×10−5. The model is assigning essentially zero probability to the right answer; the loss is ln(23,941) = 10.1 and barely decreasing, because at that temperature no achievable margin can beat thirty thousand competitors. Training would not converge. This is the actual reason CLIP’s learnable temperature runs to its 1/τ = 100 clamp: it is the only regime where the objective is solvable at scale, and the frozen gap is the price of admission.

What about losses that are not a softmax?

A fair objection: the flat valley came from softmax’s shift invariance. What about SigLIP, which replaces the row-wise softmax with an independent sigmoid on every pair — a binary “do these two go together, yes or no” for each of the N2 cells?

L = − ∑ij log σ( zij ( ui·vj / τ + b ) ),    zij = +1 if matched, −1 otherwise

A sigmoid is not shift-invariant — it has a genuine absolute threshold, so raising every cosine by 0.66 really does change the loss. In principle SigLIP has pressure on the absolute similarity that CLIP lacks. In practice, look at the b in that formula: SigLIP includes a learnable bias, initialized strongly negative because the matrix is overwhelmingly negatives, and that bias absorbs exactly the constant offset a gap produces. The model can satisfy the sigmoid by moving b instead of moving the cones, and moving one scalar is much cheaper than moving a billion parameters. Gaps are reported for SigLIP-style models too.

The general principle worth keeping. Any objective that only constrains relative or rank information cannot determine absolute geometry. If you want your two modalities to actually coincide, something in the loss has to reward coincidence in absolute terms — a reconstruction term, an explicit centroid penalty, a shared decoder. Contrastive learning was never going to give it to you, and no amount of scale changes that.
Why does CLIP’s learned temperature of τ = 0.01 preserve the modality gap?

Chapter 6: Close It or Use It

Two camps have grown up around the gap. One says: it is a defect, remove it. The other says: it is a well-behaved, nearly-constant offset, so stop fighting it and start using it as a coordinate. Both camps have shipped useful things. This chapter builds the tools from each.

Camp one: closing it

1. Post-hoc centering — the five-minute fix. Subtract each modality’s own mean, renormalize. No training, no data beyond a sample of your corpus, works on any frozen checkpoint.

python
def fit_centering(I_ref, T_ref):
    # I_ref, T_ref: (M, d) unit rows from a held-out sample, M ~ a few thousand
    return I_ref.mean(0), T_ref.mean(0)          # (d,), (d,)

def apply_centering(X, mu):
    Y = X - mu                                    # (N, d) — leaves the unit sphere
    return Y / Y.norm(dim=-1, keepdim=True)      # (N, d) — back onto it

cI, cT = fit_centering(I_ref, T_ref)
Ic, Tc = apply_centering(I, cI), apply_centering(T, cT)
# cross-modal cosines now land in the same range as within-modal ones

Two things to watch. The means must come from a reference sample, not from the query batch — if you recompute the mean per batch, your scores stop being comparable across batches and your thresholds wobble. And the renormalization step is exactly Claim B from Chapter 3, so rankings will change: validate on your own retrieval set, do not assume it is free. Typical effect on real cross-modal retrieval is between −1 and +2 points of Recall@1, which is to say: sometimes worth it, never dramatic.

2. Whitening. Centering equalizes the means. Whitening also equalizes the shapes: estimate each modality’s covariance and apply the inverse square root, so both clouds become isotropic before you compare them.

x̃ = Σm−1/2 (x − μm),  then renormalize

This is stronger and it addresses something centering cannot: the two modalities do not merely sit in different places, they have different anisotropy. Text embeddings are more tightly clustered than image embeddings (0.62 versus 0.55 mean cosine) and their variance is spread over fewer effective directions. The cost is that you now need a stable estimate of a d×d covariance — a few thousand samples for d = 512, with shrinkage — and whitening can amplify noise directions if you are careless.

3. Train it away, with an explicit term. If you control training, add a penalty that the softmax cannot ignore:

Ltotal = LInfoNCE + λ ‖ mean(U) − mean(V) ‖2

This works, in the narrow sense that the gap shrinks. Whether downstream metrics improve is another question and the published answer is “a little, sometimes.” A more surgical variant is geodesic mixup: construct synthetic hard negatives by interpolating along the great-circle arc between an image embedding and its caption embedding, so the model is forced to make distinctions in the empty channel between the cones instead of never visiting it.

4. Architectural coupling. Share the final projection layer between the towers, or add a shallow shared transformer on top of both. This helps a little for the reason Chapter 2 gives — the cone is formed by everything before the shared part, so sharing the last layer only shares the last rotation.

5. Cross-modal reconstruction. Train a small map f so that f(u) predicts v and vice versa, with a plain squared-error loss. Squared error is not shift-invariant, so unlike InfoNCE it genuinely punishes the offset. In the linear case this is the orthogonal Procrustes problem and has a closed-form solution from an SVD:

R* = argminR ‖UR − V‖F2 subject to RR = I,    R* = AB where UV = AΣB

One SVD of a 512×512 matrix, and you have the best rigid rotation taking the image cone onto the text cone. It is worth running just as a measurement: how much of the displacement a single rotation can absorb tells you how “rigid” your gap really is. From Chapter 3, expect about half.

methodneeds training?changes rankings?what it actually fixes
centeringnoyes (via renormalization)puts cross-modal and within-modal scores on one scale — the thing you needed
whiteningnoyesalso equalizes the two clouds’ shapes; needs covariance estimates
centroid penaltyyes, full retrainyesshrinks ‖Δ‖ directly; downstream gains are small
geodesic mixupyes, fine-tuneyesforces discrimination in the empty channel between cones
Procrustes / reconstructionclosed form or lightyesabsorbs the rigid part; the residual half stays

Camp two: exploiting it

Now the more interesting direction. If the gap is roughly a constant vector, then it is a known, measurable, reusable constant — and constants are gifts.

1. A linear probe costs exactly one bias term. This is the cleanest result in the chapter. Suppose you train a linear classifier on text embeddings, because text is cheap and you have millions of unlabelled captions. Its score on a text is w · v + b. Now apply it to an image, modelling the image as its caption plus the gap:

w · u + b ≈ w · (v + Δ) + b = ( w · v + b ) + w · Δ

The entire cost of crossing the modality gap, for any linear head, is a single scalar added to every score. Not a distortion, not a rotation — one number, and it is the same number for every input. Estimate it once on a handful of paired examples and subtract it, and your text-trained probe works on images.

How big is that number in practice? A useful classifier direction w encodes semantics, and semantics lives mostly perpendicular to the modality axis. For a unit w essentially unrelated to Δ̂ in 512 dimensions,

|w · Δ| ≈ ‖Δ‖ ÷ √d = 0.82 ÷ 22.63 = 0.036

— a shift of three hundredths on a score whose useful range is a few tenths. Meaningful for a tightly-calibrated threshold, negligible for an argmax. This single estimate explains a puzzle we keep hitting: why a gap that is enormous geometrically is often invisible functionally.

2. Text-only training of image models. Push the same idea further. You want an image captioner but you have no paired data — only a pile of sentences. Train a text decoder to reconstruct a sentence from its own CLIP text embedding. At inference, feed it an image embedding. It has never seen one.

This is the CapDec recipe, and the trick that makes it work is Gaussian noise. During training, perturb each text embedding before feeding the decoder, so the decoder learns to be right about a whole ball of vectors rather than a point. Make the ball big enough to reach the image island and the image embedding falls inside a region the decoder already understands.

How big should the ball be? We computed the answer in Chapter 3 without knowing it. The mean squared distance between a matched image and text is 1.40, so the typical displacement has norm √1.40 = 1.183. Spread isotropically over 512 dimensions, that is a per-coordinate standard deviation of

σ = 1.183 ÷ √512 = 1.183 ÷ 22.63 = 0.052

And if you subtract the known gap vector first, the decoder only has to cover the item-specific residual, whose norm is √0.73 = 0.854:

σresidual = 0.854 ÷ 22.63 = 0.038

The published CapDec noise levels, found by hyperparameter search, live between roughly 0.015 and 0.08. Our derivation lands inside that range from four averaged cosines — which is a good sign that the geometric model is the right one. It also tells you the tuning direction: too little noise and the decoder never generalizes across the gap; too much and it stops distinguishing sentences from each other.

3. Synthetic cross-modal data. Same arithmetic, other direction. Need image embeddings to train something and have none? Generate them:

synthetic = normalize( v + Δ + ε ),   ε ~ N(0, σ2I) with σ ≈ 0.038

You are sampling from the model’s own estimate of “what the image embedding for this caption would look like.” Half of the true displacement is captured by Δ and the rest is covered by the noise. This is the engine behind text-only training of retrieval heads, captioners and classifiers.

4. A free modality router. The projection x · Δ̂ is a perfect, zero-cost modality classifier (Chapters 1 and 4). In a mixed index that means you can tell what kind of thing a vector is without storing a type tag — useful for per-modality calibration (Chapter 7), for auditing an index you inherited, and for detecting when someone has written image vectors into the text shard.

Gap arithmetic: walking a text embedding into the image island

One text embedding, dragged along the gap direction by α. The dashed circle is the familiarity ball a text-trained decoder has learned to handle, sized by the noise you injected during training. Watch three readouts: which island the vector now belongs to, whether the real matching image falls inside the ball, and what the modality router says. Turn the noise down to see CapDec fail; turn α to 0.82 to see why subtracting the gap lets you use a much smaller ball.

α (walk along Δ̂)0.00
training noise σ0.038

Concept → realization: the bias correction, end to end

The probe result is worth writing as code, because it is the cheapest useful thing in this lesson and it takes eight paired examples to fit.

python
import torch

# 1. train the head on text only — millions of cheap unlabelled captions
w, b = train_linear_head(T_train, y_train)      # w: (d,), b: scalar

# 2. estimate the single correction term on a HANDFUL of paired examples
with torch.no_grad():
    delta = I_pair.mean(0) - T_pair.mean(0)      # (d,) — 8 pairs is enough
    shift = (w @ delta).item()                  # ONE number, ~0.036 in practice

# 3. score images with the text-trained head
scores = I_test @ w + b - shift                 # (N,)

Why eight pairs is enough: you are estimating a single scalar w · Δ, not a d-dimensional vector. The variance of the estimate falls as 1/n in the number of pairs, and the quantity itself is small, so a handful gets you well inside the noise floor of the head. Contrast that with fitting a full 512×512 alignment map, which needs thousands of pairs and a regularizer. Know what you are estimating and the data requirement collapses.

The one caveat: this assumes the head’s direction w is stable when you move to images, which is the same “half rigid” assumption from Chapter 3. It holds well for coarse semantic heads (“is this about animals?”) and poorly for heads that key on fine detail the caption never contained. If accuracy after correction is still bad, that is not a bias problem — it is the item-specific residual telling you the caption did not carry the information you are asking the image about.

Which one should you actually pick?

A short decision procedure, because five methods with overlapping benefits is where good engineering goes to die.

Start by asking whether you can change the model at all. If the checkpoint is frozen — and it usually is — four of the five closing methods are unavailable and the question reduces to “centering or whitening?” Use centering. It has one estimated parameter per modality, it cannot blow up, and it delivers most of the benefit. Reach for whitening only when you have measured that the two modalities have genuinely different covariance shapes and you have thousands of reference samples and a shrinkage estimator.

Then ask whether you need the embeddings changed, or only the scores. This is the distinction most teams get wrong. If your problem is “images never surface in a mixed ranked list,” you do not need to touch the embeddings at all — calibrate the scores, which is Chapter 7, and keep your index bit-for- bit identical. Rewriting an index is a migration; rewriting a scoring function is a deploy.

Only if you are training the encoder should you consider the loss-side fixes, and then measure uniformity rather than gap distance, for the reason Chapter 8 gives.

Two, six, or a hundred modalities

The exploiting camp has a strong argument from an unexpected place: ImageBind. It binds six modalities — image, text, audio, depth, thermal, IMU — into one space, training each one only against images, never against each other. And yet audio-to-text retrieval works, with no audio–text pair ever seen. That is called emergent alignment, and it is the best evidence available that gaps do not prevent cross-modal function.

Geometrically, ImageBind has not two cones but six, each with its own axis and therefore its own gap vector to every other. Fifteen pairwise gaps. If gaps were fatal, nothing about that system would work. It works because — per Claim A — retrieval within a fixed gallery is invariant to a constant offset, so six disjoint cones behave, for ranking purposes, exactly like one.

The reframe worth taking away. Stop asking “how do I make the space truly joint?” and start asking “is my downstream operation invariant to a per-modality offset?” If yes, ignore the gap entirely. If no, either make the operation invariant (rank-based instead of score-based) or supply the offset explicitly (centering, the w·Δ bias correction, per-modality calibration). Both are cheaper than retraining a foundation model.
You train a linear classifier on CLIP text embeddings and want to run it on image embeddings. Modelling the image as its caption plus a constant gap vector, what does the modality gap cost you?

Chapter 7: Search Calibration

This is the chapter you will actually use. Everything before it was diagnosis; this is treatment, on the system where the gap does real damage: a search index containing more than one kind of thing.

The setup is completely ordinary. You have internal documentation: text passages and architecture diagrams. You embed all of it with CLIP into one vector index — text through the text tower, images through the image tower, everything into the same 512-dimensional space, because that is what a joint embedding space is for. A user types “diagram of a transformer block.”

They get five text passages. They never get the diagram. The diagram is right there, correctly embedded, and it is invisible.

The five candidates, with real numbers

Here is the state of the index at query time. Two pools with completely different score distributions, which is the whole problem:

text pool: mean 0.62, std 0.05     image pool: mean 0.25, std 0.04
candidatemodalityraw cosinetruly relevant?
A — passage on attention maskstext0.68tangential
B — passage on tokenizer designtext0.64no
C — passage on optimizer choicetext0.61no
D — the transformer block diagramimage0.34yes. This is the answer.
E — a photo of a whiteboardimage0.27no

Raw ranking: A, B, C, D, E. The correct answer is fourth, behind three passages that are not about diagrams at all. And crucially it is not close — C beats D by 0.27, roughly seven standard deviations of the image pool. No reranker downstream of a top-3 cut will ever see D.

Fix 1: per-modality centering of scores

The cheapest correction. Subtract each pool’s mean score:

s′ = s − μmodality
A: 0.68 − 0.62 = +0.06   B: +0.02   C: −0.01   D: 0.34 − 0.25 = +0.09   E: +0.02

New ranking: D, A, B, E, C. The diagram is first. One subtraction, no model changes, no retraining. Note this is score-space centering, which is subtly different from the embedding-space centering of Chapter 6 — it is cruder (it cannot change the within-modality ordering at all) and correspondingly safer.

Fix 2: per-modality z-scores

Centering assumes the two pools have the same spread. They do not: 0.05 against 0.04. Divide by the standard deviation too:

z = (s − μm) ÷ σm
A: 0.06/0.05 = 1.20    B: 0.02/0.05 = 0.40    C: −0.01/0.05 = −0.20
D: 0.09/0.04 = 2.25    E: 0.02/0.04 = 0.50

Ranking: D (2.25), A (1.20), E (0.50), B (0.40), C (−0.20). D wins by nearly a full point of z now, and the ordering is defensible: the one image that is unusually good for its pool beats the one passage that is unusually good for its pool.

A z-score also gives you a portable threshold. “Return anything above z = 1.5” means the same thing in both modalities and survives a checkpoint change, because μ and σ are re-estimated per checkpoint. “Return anything above cosine 0.5” means two completely different things and survives nothing.

Fix 3: rank fusion

Both fixes above assume the score distributions are roughly Gaussian. If yours are skewed, throw the scores away and keep only the ranks. Score each pool separately, then merge with reciprocal rank fusion:

RRF(item) = ∑lists 1 ÷ (k + rank),    k = 60 by convention
A is text-rank 1: 1/61 = 0.01639     D is image-rank 1: 1/61 = 0.01639
B is text-rank 2: 1/62 = 0.01613     E is image-rank 2: 1/62 = 0.01613

Since every item appears in exactly one list, RRF here reduces to interleaving: best text, best image, second text, second image. That is a policy, not an inference — it guarantees images appear, and it guarantees they appear even when there are no good images. Use it when you want a coverage floor per modality and you do not trust the scores at all.

Fix 4: per-modality quotas plus a learned calibration

The production answer, if you have even a hundred labelled examples per modality. Fit a one-dimensional logistic calibration per modality mapping raw cosine to probability of relevance:

P(relevant | s, modality m) = σ( am s + bm )

Now the scores are comparable because they are both probabilities of the same event. Sort by that. This is strictly better than z-scoring — z-scoring is the special case where you assume both pools have the same relevance rate and a Gaussian shape — and it costs one sklearn call. It also gives you a threshold with a meaning your product manager understands: “show it if it is more than 60% likely to be relevant.”

The mixed-index calibrator

A ten-item mixed index — five text passages, five images — ranked live. Cycle the calibration mode and watch the ranked list reorder. The bars show the score under the current mode; a filled dot marks a truly relevant item. Drag the threshold to draw the cut line, and drag “best image relevance” to make the correct answer better or worse and see how much relevance the raw mode is capable of ignoring. Precision@3 and “relevant images surfaced” are computed live.

threshold percentile55
best image relevance0.90

Concept → realization: the calibrated scorer

Everything above, as the object you would actually deploy. Note where the statistics live — frozen, alongside the index, versioned with the checkpoint — because that is the part people get wrong.

python
import numpy as np, json

class ModalityCalibrator:
    # Fitted ONCE per checkpoint on a fixed reference corpus, then frozen
    # and shipped next to the index. Never re-estimated from a query batch.
    def fit(self, scores_by_modality):
        self.stats = {m: {'mu': float(np.mean(s)), 'sd': float(np.std(s) + 1e-6)}
                      for m, s in scores_by_modality.items()}
        return self

    def score(self, raw, modality):
        st = self.stats[modality]
        return (raw - st['mu']) / st['sd']        # comparable across modalities

    def save(self, path):
        json.dump(self.stats, open(path, 'w'))     # version this WITH the checkpoint

# reference corpus: a few thousand items per modality, sampled from YOUR index,
# scored against a few hundred representative queries. Refit whenever either
# the checkpoint or the corpus composition changes materially.
cal = ModalityCalibrator().fit({'text': ref_text_scores, 'image': ref_image_scores})
ranked = sorted(hits, key=lambda h: -cal.score(h.cos, h.modality))

Three implementation details that decide whether this works in production.

Do not estimate μ and σ from the retrieved candidates. It is tempting — you already have the scores — but the retrieved set is exactly the biased top of the distribution, so the estimate moves with the query and your scores stop being comparable between queries. Estimate on a fixed reference sample and freeze.

How many reference samples? The standard error of a mean is σ/√n. With σ ≈ 0.05 and a target accuracy of 0.002 on the mean — small compared to the 0.37 difference you are correcting — you need n = (0.05/0.002)2 = 625 per modality. A couple of thousand is comfortable; a hundred is not.

You cannot z-score inside an approximate nearest-neighbour index. HNSW and IVF search on the raw vectors and return the top k by raw score — the calibration happens after retrieval, by which point the images have already been filtered out. If your index is a single mixed shard and you ask for k = 50, all fifty slots go to text and no amount of re-scoring can recover an image that was never returned. The fix is one of two things, and you must choose deliberately:

approachhowtrade-off
Shard by modality (recommended)One index per modality. Retrieve k from each, calibrate, merge.Guarantees per-modality recall. Costs one extra index and one extra query.
Bake the correction into the vectorsSubtract the modality mean and renormalize before insertion, so the single index already sees comparable geometry.One index, but any change to the correction means a full re-index, and you have permanently altered the stored vectors.

The arithmetic is stark: single mixed shard at k = 50 returns 0 images; two shards at k = 25 each returns 25 images and the calibrator decides which of them deserve to be shown. Retrieval recall is a property of the index; ranking quality is a property of the scorer. The gap breaks the first one and no scorer can repair it after the fact.

A post-mortem, in the order it actually happens

The failure never announces itself. Here is how the ticket reads and how it resolves.

Day 1. “Image search doesn’t work.” Someone verifies that the diagrams are in the index — they are — and that the image embeddings are non-zero — they are. The model is declared “bad at diagrams” and a ticket is filed to fine-tune it.

Day 3. Somebody tries the query “transformer block diagram” against only the image shard and the correct diagram comes back first, with a cosine of 0.34. The model is fine. The retrieval is fine. Something about the mixture is broken.

Day 3, ten minutes later. Print the mean score per modality: 0.62 and 0.25. Done. The diagnosis is one groupby, and it would have been day 1 if anyone had thought to look at the score distributions before looking at the model.

The habit worth forming. Whenever a ranked list looks wrong, plot the score distribution grouped by every categorical attribute you have — modality, source, language, document length. If any group’s distribution is shifted relative to another’s, that group is being systematically promoted or buried, and no amount of relevance tuning will fix it. The modality gap is the most extreme instance of a completely general failure.

The threshold that follows you across checkpoints

One more trap, and it has bitten more teams than the mixed-index bug because it fails silently.

You calibrate a relevance threshold on ViT-B/32: cosine 0.28 means “relevant.” It works, it ships, it sits in a config file for a year. Then someone upgrades to a larger checkpoint for better accuracy. The new model has a different gap, so its matched pairs sit at a different absolute cosine — and your threshold, which encoded “a bit above the gap floor,” now encodes something else entirely. Recall collapses, or precision does, and nothing in the diff explains it.

what you hard-codedsurvives a checkpoint change?why
absolute cosine thresholdnoThe gap floor moves. This is the number that breaks.
top-k cutoffyesPure ranking — invariant to any per-modality offset.
z-score thresholdyes, if you re-estimate μ and σThe statistics absorb the new gap automatically.
calibrated probability thresholdyes, if you refit the calibrationSame reason, with a better-shaped mapping.

The operational rule: never hard-code an absolute cosine anywhere. Store μ and σ per modality per checkpoint, computed on a fixed reference corpus, and express every threshold relative to them. That one discipline immunizes you against every failure mode in this chapter.

The debugging checklist, in order. (1) Compute the gap norm and the per-modality score means on your own index — four lines from Chapter 1. (2) If the two means differ by more than a standard deviation, every absolute threshold in your system is wrong. (3) Check whether your operation is a pure ranking within one gallery; if it is, the gap is not your bug and you should stop here. (4) Otherwise apply per-modality calibration — centering if you have nothing, z-scores if you have a reference corpus, a fitted logistic if you have labels. (5) Only then consider touching the embeddings themselves.
A single vector index holds text passages (mean cosine to queries 0.62) and images (mean 0.25). Which single change most reliably makes relevant images appear in results?

Chapter 8: Does It Matter?

Time to be honest. We have spent seven chapters on a geometric fact that is real, large, and easy to measure. Now the question a senior engineer asks: so what? Should you do anything about it, and what happens if you do nothing?

The literature genuinely disagrees, and the disagreement is productive rather than confused. Here are both cases, as strongly as each can be made.

The case that it does not matter

1. Ranking within a gallery is provably invariant. This is not an empirical claim, it is Claim A from Chapter 3. Add a constant to every candidate in a fixed gallery and the ordering cannot change. Every cross-modal retrieval benchmark, every zero-shot classification benchmark that argmaxes over prompts, and every top-k nearest-neighbour lookup falls into this category. The gap is exactly zero cost for all of them.

2. CLIP works. The strongest evidence is the seven years of results. Zero-shot ImageNet, retrieval on COCO and Flickr, CLIP-guided diffusion, CLIPScore as a captioning metric, ImageBind’s six-way emergent alignment — all built on top of a wide-open gap that nobody noticed for the first year. A defect that costs nothing on every headline benchmark deserves the label “cosmetic” until proven otherwise.

3. Closing it barely helps. Papers that close the gap — by centering, by penalty terms, by fine-tuning — report retrieval changes in the range of one or two points, and not always positive. If removing a defect that accounts for 29% of your embedding variance moves Recall@1 by a point, the defect was not carrying much function.

4. Linear transfer costs one scalar. Chapter 6: a text-trained linear probe applied to images picks up exactly the bias w · Δ, which for a semantic direction in 512 dimensions is about 0.036. Cross-modal transfer of linear heads is nearly free, gap or no gap.

Invariance, drawn: translate the gallery and watch nothing happen

Eight gallery items ranked against one query. Slide t to translate every gallery vector along the gap direction. With renormalization off, the bars all move together and the ranking is frozen — that is Claim A, and the rank-change counter stays at zero forever. Switch renormalization on and the same slider starts scrambling the order, because each item is now divided by its own length. The difference between “the gap matters” and “the gap does not” is that one toggle.

t (gallery shift along Δ̂)0.00

The case that it absolutely does

1. Mixed-modality search is broken, not degraded. Chapter 4 computed it exactly: the modality term was seven times the semantic term, so every text outranks every image regardless of content. This is not a percentage-point regression, it is a total failure of a feature, and it is invisible in every benchmark because every benchmark ranks within one modality.

2. Absolute thresholds are meaningless and portable ones are not obvious. The most common CLIP-in- production pattern — “is this pair a match, yes or no?” for moderation, deduplication, auto-tagging, or filtering a training set — needs a number, and the number depends on a geometry nobody documented. Chapter 7’s checkpoint-upgrade failure is the classic version.

3. Anything that mixes embeddings across modalities is nonsense without correction. Averaging an image embedding and a text embedding to form a “multimodal query” produces a vector on the segment between the two islands — a region no real embedding ever occupies, and one your index has never seen. So does interpolating for a slider UI, so does adding a text “edit direction” to an image embedding, so does mean-pooling a multimodal document into one vector.

4. The gap has social consequences. Liang and colleagues showed that shifting along Δ̂ changes not just accuracy but rates of harmful misclassification on face datasets. A geometric parameter nobody knew existed, set by random initialization, is quietly modulating who gets mislabelled as what.

The third position: the gap is a symptom, not the disease

A more recent line of work argues both camps have the wrong object. Two threads.

The gap is not one vector. We measured this ourselves in Chapter 3: the shared Δ explains only about half of the image-to-text displacement. Work under titles like “It’s Not a Modality Gap” argues that the real pathology is a uniformity failure — each modality occupies a much lower-dimensional sliver of the sphere than it should, in the sense of Wang and Isola’s alignment-versus-uniformity decomposition of contrastive learning. Translation does not fix low-dimensionality; only training or whitening does. This explains the disappointing one-point gains: people have been subtracting a mean when the problem was a rank deficiency.

The gap is driven by information imbalance. Schrodi and colleagues argue the gap emerges from a mismatch in how much information each modality carries about the pair — an image contains far more detail than its caption, so no amount of alignment pressure can make the two representations coincide, and the residual shows up as a separation. On this view the gap is a measurement of an irreducible asymmetry rather than an artefact you can remove, and the crucial follow-up is that closing it does not fix the downstream biases people hoped it would.

The synthesis, and the sentence to remember. The modality gap is a calibration problem, not a representation problem — except when it is a symptom of one. If your operation is rank-based within a gallery, ignore it. If your operation compares across modalities in absolute terms, calibrate per modality and you are done in an afternoon. If you are training the encoder and want representations that genuinely coincide, do not chase the gap distance — chase uniformity and the information asymmetry underneath it, because the gap is the thermometer, not the fever.

Measuring uniformity, so you can argue with numbers

The “symptom, not disease” camp rests on a quantity you can compute in three lines, so compute it. Wang and Isola define uniformity as the log of the average Gaussian-kernel similarity between pairs — lower is better, meaning the points are spread out:

Lunif = log Ex,y [ e−2‖x−y‖2 ]

For unit vectors, ‖x−y‖2 = 2 − 2 cos(x,y), so we can evaluate it from the same four numbers we have been using all lesson. Within-image pairs sit at cosine 0.55:

‖x−y‖2 = 2 − 2(0.55) = 0.90  →  e−1.80 = 0.165

Cross-modal pairs sit at 0.25:

‖x−y‖2 = 2 − 2(0.25) = 1.50  →  e−3.00 = 0.0498

Roughly half the pairs in the pooled cloud are within-modality and half are cross-modality, so the average is about (0.165 + 0.0498)/2 = 0.107 and

Lunif = ln(0.107) = −2.23

Now the reference point. On a truly uniform 512-dimensional sphere, the squared distance between two random points concentrates tightly at 2, giving

Lunifideal = ln( e−4 ) = −4.00

So CLIP sits at −2.23 against an ideal of −4.00, and — here is the point — look at which term is responsible. The within-modality term, 0.165, is more than three times the cross-modal term, 0.0498. The uniformity deficit is dominated by points being crowded inside each cone, not by the cones being apart. Translating one cone onto the other does not change any within-modality distance at all, so it cannot improve this number.

The one-line refutation of “just subtract the mean.” A rigid translation leaves every within-modality distance exactly unchanged. If most of the deficit is within-modality crowding — and by the arithmetic above, it is — then translation is provably incapable of fixing most of the problem. This is why the reported gains from gap-closing are one or two points instead of ten.

Run the ablation yourself

You do not have to take anyone’s side. The experiment takes twenty minutes on a frozen checkpoint and it settles the question for your system, which is the only version of the question that matters.

python
# Four measurements. Run them in this order; stop as soon as one is decisive.

# A. Is there a gap at all, and how big?
delta = I.mean(0) - T.mean(0);  gap = delta.norm()          # expect 0.7 – 0.9

# B. Is your operation rank-based? Translate the GALLERY, do not renormalize.
#    If top-k is byte-identical, the gap is provably not your bug. Stop here.
base = (Q @ T.T).topk(10).indices
shft = (Q @ (T + delta).T).topk(10).indices
print('rank-invariant:', torch.equal(base, shft))                # True, always

# C. Does calibration change your metric? Cheapest real intervention.
evaluate(rank_raw(hits))
evaluate(rank_zscored(hits, per_modality_stats))

# D. Does closing the gap in EMBEDDING space help beyond calibration?
Ic = renorm(I - I.mean(0));  Tc = renorm(T - T.mean(0))
evaluate(rank_raw(retrieve(Ic, Tc)))          # usually within ±1 point of C

The order is deliberate and it is a claim about where the value is. Test B costs nothing and eliminates most systems. Test C is a scoring change, deployable in an afternoon, and captures nearly all the available gain. Test D is an index migration and, on the published evidence, buys about a point beyond C. If your results come out differently, you have learned something specific about your corpus — which is the whole reason to run it rather than read about it.

A decision table you can actually use

your operationgap-sensitive?what to do
image → text retrieval, fixed text gallerynoNothing. Provably invariant.
zero-shot classification, argmax over promptsbarelyNothing, unless you also renormalize after a shift — then you have added an unlogged prior.
mixed text+image index, one queryyes, fatallyPer-modality centering, z-scores, or a fitted calibration. Chapter 7.
“is this pair a match?” with a thresholdyesExpress the threshold in z-units or calibrated probability, per modality, per checkpoint.
text-trained linear probe on imagesmildlySubtract the single bias w·Δ, estimated on a few paired examples.
averaging or interpolating across modalitiesyesCenter both modalities first, or you are sampling from a region with no data.
text-only training of an image headyes, but usefullyExploit it: subtract Δ, inject noise of scale ~0.04 per coordinate. Chapter 6.
you are pretraining the encoder yourselfyes, deeplyDo not optimize gap distance. Watch uniformity, effective rank, and the size of the item-specific residual.
Which single sentence best captures the current state of the evidence about the modality gap?

Chapter 9: Connections

Nothing new here — just the map, the limits of what we built, and where each thread continues.

The whole lesson in eight lines

1. In CLIP’s shared space, an image and its own caption sit at cosine ~0.30, while two unrelated images sit at ~0.55. Within-modality similarity beats every cross-modality similarity. The space is two islands.

2. The gap vector Δ = cI − cT has norm 0.82 for ViT-B/32, recoverable from four averaged cosines, and “which modality” explains about 29% of the total embedding variance. The two modalities are 100% linearly separable and x · Δ̂ is a free modality detector.

3. The gap is not learned. Random deep networks squeeze everything into a narrow cone — one ReLU takes the expected cosine between unrelated inputs from 0 to 1/π = 0.318, and the arc-cosine recursion drives it to 0.83 by depth eight. Two encoders, two seeds, two near-perpendicular cone axes, gap at birth.

4. Training cannot remove it because the softmax only sees differences between logits, and closing the gap adds nearly the same amount to every logit in a row. The gap sits in a flat valley.

5. Temperature decides how flat. At the shipped τ = 0.01, once the margin exceeds τ the softmax saturates and the gap-closing force — a covariance between softmax weight and position along Δ̂ — collapses by about 54,000×.

6. The rigid-translation model is half right: Δ explains 0.67 of the 1.40 mean squared displacement. That single number predicts why centering gets you halfway and why CapDec needs noise of scale ~0.04 per coordinate.

7. Ranking within a gallery is invariant to the gap. Absolute thresholds, mixed-modality indexes, and cross-modal averaging are not. That line divides the systems you should fix from the ones you should leave alone.

8. Never hard-code an absolute cosine. Store μ and σ per modality per checkpoint and express every threshold relative to them.

What we did not cover

topicwhy it was left out
Effective rank and uniformity metricsThe right measurement for the “symptom, not disease” view, and it deserves its own treatment alongside alignment–uniformity theory.
Gaps in generative multimodal modelsModels that splice visual vectors into an LLM’s token stream have a related but different problem — the adapter is trained to land in the text distribution, so the loss is not shift-invariant.
Hubness in high-dimensional retrievalA separate pathology — a few vectors are everyone’s nearest neighbour — that compounds with the gap in mixed indexes.
Sharpness of the gap under fine-tuningFine-tuning a CLIP on a narrow domain moves both cones; how the gap responds is checkpoint-specific and under-studied.

Keep exploring

Contrastive Learning & CLIP — the model whose geometry this whole lesson dissects
Contrastive Learning — InfoNCE, negatives, and the alignment-versus-uniformity view of the objective
Vector Embeddings — what an embedding is before we start worrying about where it lives
Similarity Metrics — cosine, dot product, Euclidean, and why normalization changes the question
Vector Databases — where the mixed-index failure of Chapter 7 actually happens
Multimodal RAG — retrieval over text and images together, the system this lesson is a prerequisite for
Embedding Benchmarks — why leaderboard numbers hide calibration problems like this one
Multimodal Foundation Models — the wider family of joint-space designs
Vision-Language Models — the generative branch, where a trained adapter changes the geometry story
ImageBind — six modalities, fifteen pairwise gaps, and emergent alignment anyway
SigLIP — the sigmoid loss, and the learnable bias that absorbs the offset
OpenCLIP — the open reproduction you can measure your own gap on
EVA-CLIP — scaling the recipe, with the gap intact

References

Liang, Zhang, Kwon, Yeung-Levy, Zou. “Mind the Gap: Understanding the Modality Gap in Multi-modal Contrastive Representation Learning.” NeurIPS 2022. arXiv:2203.02053 — the source of the gap distance, the cone-effect analysis, and the shift-along-Δ experiments.
Radford et al. “Learning Transferable Visual Models From Natural Language Supervision.” ICML 2021. arXiv:2103.00020 — CLIP itself, including the learnable temperature clamped at 1/τ = 100.
Cho, Saul. “Kernel Methods for Deep Learning.” NeurIPS 2009. proceedings — the arc-cosine kernel, which gives the exact depth recursion in Chapter 2.
Wang, Isola. “Understanding Contrastive Representation Learning through Alignment and Uniformity on the Hypersphere.” ICML 2020. arXiv:2005.10242 — the decomposition the “uniformity, not translation” argument rests on.
Ethayarajh. “How Contextual are Contextualized Word Representations?” EMNLP 2019. arXiv:1909.00512 — anisotropy in language models: the cone effect, discovered a modality earlier.
Gao et al. “Representation Degeneration Problem in Training Natural Language Generation Models.” ICLR 2019. arXiv:1907.12009 — the same narrow cone in word embeddings, with a training-time explanation.
Mu, Bhat, Viswanath. “All-but-the-Top: Simple and Effective Postprocessing for Word Representations.” ICLR 2018. arXiv:1702.01417 — the original centering-and-remove-top-components recipe reused in Chapter 6.
Nukrai, Mokady, Globerson. “Text-Only Training for Image Captioning using Noise-Injected CLIP” (CapDec). EMNLP Findings 2022. arXiv:2211.00575 — exploiting the gap: the noise-ball trick and its scale.
Li et al. “DeCap: Decoding CLIP Latents for Zero-shot Captioning via Text-Only Training.” ICLR 2023. arXiv:2303.03032 — the projection-onto-a-support-set alternative to noise injection.
Schrodi et al. “Two Effects, One Trigger: On the Modality Gap, Object Bias, and Information Imbalance in Contrastive Vision-Language Models.” ICLR 2025. arXiv:2404.07983 — the information-imbalance account, and the argument that closing the gap does not fix what people hoped.
Fahim, Murphy, Fyshe. “It’s Not a Modality Gap: Characterizing and Addressing the Contrastive Gap.” 2024. arXiv:2405.18570 — the uniformity-deficiency reframing of the gap.
Zhai et al. “Sigmoid Loss for Language Image Pre-Training” (SigLIP). ICCV 2023. arXiv:2303.15343 — the pairwise sigmoid objective and its learnable bias.
Girdhar et al. “ImageBind: One Embedding Space To Bind Them All.” CVPR 2023. arXiv:2305.05665 — six cones, emergent alignment, and the best evidence that gaps do not stop retrieval.

“What I cannot create, I do not understand.” You can now create this one: draw two unit vectors 73.7° apart, hang a shared semantic wiggle off each, and you have built a modality gap from four numbers. You can measure it on any checkpoint in four lines, predict from the geometry alone which of your systems it breaks, and fix those with per-modality statistics instead of a retraining run. The most valuable thing the gap teaches is not about CLIP at all — it is that a “shared space” is a claim about a loss function, and a loss function that only reads differences will never tell you where anything actually is.
You inherit a CLIP-backed system and want to know in ten minutes whether the modality gap is your bug. What do you check first?