Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
In this series (30 parts)
- Computer Vision Roadmap: From Basics to Real Projects
- What is Computer Vision? From Pixels to Decisions
- A Short History of Computer Vision: 1545 to Now
- Math for CV Beginners: Vectors, Matrices, Convolutions
- Image Fundamentals: Color, Histograms, Noise, Filtering
- How Images and Video Are Stored: Colour, JPEG, Frames
- OpenCV Setup + First 10 Tasks in Python
- Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
- CV Project Workflow: Dataset, Baseline, Error Iteration
- Intensity Transforms and Frequency-Domain Filtering
- Edge Detection and Thresholding That Actually Work
- Morphology, Contours, and Shape Analysis for Real Images
- Feature Matching (SIFT/ORB) and Image Stitching
- Camera Calibration and Perspective Correction
- Epipolar Geometry, Stereo Vision, and Depth Estimation
- Optical Flow and Motion Tracking in Video
- HOG, HOF and MBH: Descriptors Before Deep Learning
- Your First Image Classifier (PyTorch + Transfer Learning)
- Data Pipelines and Augmentation for Vision Models
- CNN Architectures Explained: From LeNet to ResNet
- YOLO Detection Pipeline: Data to Inference
- Semantic and Instance Segmentation: U-Net to Mask R-CNN
- Vision Transformers (ViT) and When to Use Them
- CV Explainability: Grad-CAM, Failures, Bias Checks
- 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
- Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
- Video Understanding: Flow Networks, Interpolation, Stabilisation
- Real-Time CV Systems: Tracking, Latency, Streaming
- Deploying CV Models: ONNX, TensorRT, Edge, and APIs
- Responsible CV: Privacy, Fairness, Security, Governance
A clothing retailer has 400,000 product photos and a search box that only matches the words someone typed into a spreadsheet three years ago. Search for “beige linen shirt” and you get nothing, because the product was tagged “sand cotton-blend top”. Every classifier so far needed a fixed list of classes and labelled examples for each. This problem has no fixed list.
Prerequisites: Vision transformers and your first image classifier.
One space for two things
The trick is a single vector space that both images and sentences map into.
graph LR A["Product photo"] --> B["Image encoder<br/>ViT-B/32"] C["'beige linen shirt'"] --> D["Text encoder<br/>Transformer"] B --> E["512-d vector"] D --> F["512-d vector"] E --> G["Same space<br/>compare with cosine"] F --> G
Once both live in one space, search is arithmetic. Encode the query sentence, compare it against every stored image vector, sort.
Cosine similarity, computed
Similarity is the cosine of the angle between two vectors:
Take a 4-dimensional image embedding and three candidate text embeddings.
Candidate 1 — “a photo of a beige linen shirt”:
Candidate 2 — “a photo of a leather boot”:
Candidate 3 — “a photo of a garment”:
| # | Caption | Cosine similarity target | Rank |
|---|---|---|---|
| 1 | a photo of a beige linen shirt | 0.9621 | 1 |
| 2 | a photo of a garment | 0.7000 | 2 |
| 3 | a photo of a leather boot | 0.1746 | 3 |
The specific correct caption wins, the vague-but-true caption comes second, the wrong one is far behind.
The practical shortcut. If you normalise every vector to unit length once, when you store it, then cosine similarity is exactly the dot product — no division at query time. Every production system does this, and it is the single most useful implementation detail in retrieval.
emb = model.encode_image(images)
emb = emb / emb.norm(dim=-1, keepdim=True) # do this ONCE, at index time
# later: similarity is just a matmul
scores = query_emb @ index.T
Zero-shot classification
There is no separate mechanism for this. You compare the image against sentences instead of against stored images.
labels = ["shirt", "trousers", "shoes", "bag"]
prompts = [f"a photo of a {c}, a type of clothing." for c in labels]
text_emb = model.encode_text(clip.tokenize(prompts))
text_emb = text_emb / text_emb.norm(dim=-1, keepdim=True)
probs = (100.0 * image_emb @ text_emb.T).softmax(dim=-1)
The 100.0 is the inverse temperature — CLIP learns it during training and it lands near 100.
What temperature does
Take the three similarities above, 0.9621, 0.1746, 0.7000, and softmax them at different temperatures.
At : logits are the raw similarities.
At : logits become 9.621, 1.746, 7.000. Subtracting the max for stability gives 0, −7.875, −2.621.
| # | Temperature τ | P(shirt) target | P(boot) | P(garment) | Character |
|---|---|---|---|---|---|
| 1 | 1.00 | 0.4496 | 0.2045 | 0.3459 | soft, barely committed |
| 2 | 0.50 | 0.6172 | 0.1200 | 0.2628 | leaning |
| 3 | 0.10 | 0.9319 | 0.000355 | 0.06775 | confident (CLIP's regime) |
| 4 | 0.01 | ≈1.000 | ≈0 | ≈0 | effectively argmax |
Same three similarities, four temperatures. Cosine scores are all crammed into a narrow band, so a low temperature is needed to separate them.
Temperature never changes the ordering, so it never changes top-1 accuracy. It changes how confident the numbers look, which matters whenever a threshold controls a decision. This is the same idea as the temperature scaling used for calibration.
Prompt wording matters
| # | Prompt template | ImageNet zero-shot top-1 target | Gain |
|---|---|---|---|
| 1 | "{label}" | 58.2% | — |
| 2 | "a photo of a {label}" | 62.1% | +3.9 |
| 3 | "a photo of a {label}, a type of clothing." | 63.4% | +5.2 |
| 4 | Ensemble of 80 templates, averaged | 64.9% | +6.7 |
Approximate figures from the CLIP paper. Rewording the prompt is worth several points, for free.
The reason is mundane. CLIP was trained on image–caption pairs scraped from the web, and web captions are sentences, not bare nouns. A bare noun is out of distribution for the text encoder. Wrapping it in a sentence puts it back in.
How CLIP is trained
Take a batch of image–caption pairs. Encode all of them, giving an similarity matrix. The diagonal entries are the true pairs. Every off-diagonal entry is a wrong pairing, and the loss pushes the diagonal up and everything else down.
InfoNCE loss Cross-entropy over the similarity row: the correct pairing must beat all the other captions in the batch. Every other item in the batch acts as a negative example.
Worked, for one row of a batch of 4. Cosine similarities of image 1 against all four captions, with :
| # | Caption | Cosine | ÷ τ | exp target | Is it the true pair? |
|---|---|---|---|---|---|
| 1 | 1 (true pair) | 0.92 | 9.2 | 9897.1 | yes |
| 2 | 2 | 0.31 | 3.1 | 22.20 | no |
| 3 | 3 | 0.18 | 1.8 | 6.05 | no |
| 4 | 4 | 0.25 | 2.5 | 12.18 | no |
Row 1 of the similarity matrix. The true pair's exponential dominates.
Nearly zero loss — this pair is already well learned. Now a badly learned row, where the true pair scores 0.35 and a distractor scores 0.28:
A loss 212 times larger, so the gradient goes overwhelmingly to the pairs the model has not learned.
Row 2 is the weak one — its diagonal, 0.35, barely beats the 0.30 next to it. Rows 1, 3, and 4 have diagonals far above their neighbours and contribute almost nothing to the gradient.
What a real similarity matrix looks like
The matrix above uses round numbers to make the arithmetic readable. A real trained CLIP model produces something that surprises people the first time they see it.
Here is a zero-shot run: seven images, eight candidate captions, cosine similarity for every pair. The correct pairing for each image is on the diagonal.
The diagonal values are around 0.30. Off-diagonal values are around 0.19. Correct pairings win by roughly 0.10, not by 0.6.
This also explains why the temperature parameter is so important. Dividing by turns a 0.30-versus-0.19 gap into 30 versus 19, which after softmax is a decisive difference. Without it, the softmax over raw cosines would be nearly uniform and the loss would carry almost no signal.
Why batch size matters so much
Each row has negatives, so the batch size is the difficulty of the task.
| # | Batch size | Negatives per sample target | Task difficulty | Note |
|---|---|---|---|---|
| 1 | 256 | 255 | easy | Random captions are trivially distinguishable |
| 2 | 1024 | 1023 | moderate | |
| 3 | 8192 | 8191 | hard | Some genuinely similar items now compete |
| 4 | 32768 | 32767 | very hard | CLIP's actual batch size |
Contrastive learning is one of the few settings where batch size is a genuine model-quality decision, not just a throughput one.
With 255 random negatives, telling a shirt from a boot is easy and the model learns coarse categories. With 32,767, the batch contains other shirts, and the model must learn what distinguishes beige linen from white cotton. That is where the fine-grained ability comes from.
Building the search system
flowchart TD subgraph Offline A["400,000 product photos"] --> B["Encode with image encoder"] B --> C["Normalise to unit length"] C --> D["Build ANN index<br/>HNSW / IVF-PQ"] end subgraph "Per query" E["'beige linen shirt'"] --> F["Encode with text encoder"] F --> G["Normalise"] G --> H["ANN search → top 200"] H --> I["Filter: in stock, size, price"] I --> J["Rerank by business rules"] J --> K["Show top 24"] end D -.-> H
Why brute force stops working
Store 400,000 vectors of 512 dimensions as float32:
Every query is a dot product against all of them:
| # | Method | Index size | Query latency target | Recall@10 | When |
|---|---|---|---|---|---|
| 1 | Brute force (float32) | 819 MB | ~40 ms | 1.000 | under ~100k vectors |
| 2 | HNSW | 1.3 GB | ~1.2 ms | 0.98 | the usual default |
| 3 | IVF-PQ (64 bytes/vec) | 26 MB | ~0.8 ms | 0.91 | when memory is the constraint |
| 4 | Binary hashing | 3.2 MB | ~0.3 ms | 0.74 | first stage of a two-stage system |
400,000 vectors of 512 dimensions. IVF-PQ is 31x smaller than brute force and loses 9 points of recall.
Precision and recall at answer different questions and both matter:
Precision@10 says 7 of the 10 results shown were good, which is what the user experiences. Recall@10 says only a third of the relevant catalogue made it onto page one, which is what the merchandising team cares about.
| Property | CLIP zero-shot | Fine-tuned classifier | Keyword search |
|---|---|---|---|
| Labelled data needed | none | hundreds per class | manual tags |
| New category at 3am | type a sentence | collect data, retrain | re-tag everything |
| Accuracy on a fixed known set | moderate | high | depends on tag quality |
| Handles unusual phrasing | yes | n/a | no |
| Explains its ranking | similarity score only | class probabilities | yes, exact matches |
| Cost to add a class | zero | days | hours of tagging |
- The set of categories changes often, or is not known in advance
- You have images but no labels
- Users search with free text rather than picking from a menu
- You need to find rare examples in a large unlabelled pool
- You have a fixed set of classes and labelled data — a classifier will be more accurate
- The domain is far from web imagery, such as medical scans or industrial X-ray
- Fine distinctions matter more than broad ones, like counting or exact colour matching
- You need a calibrated probability rather than a ranking
Practice task
Use open_clip and a few thousand images from any public dataset.
- Encode 2,000 images. Confirm every embedding has unit norm after normalising.
- Verify that the dot product of two normalised vectors matches
cosine_similarityexactly. - Run text queries and inspect the top 10. Note which query wordings work best.
- Run zero-shot classification with bare labels, then with
"a photo of a {label}". Compare accuracy. - Build a template ensemble of 5 prompts per class by averaging their embeddings. Measure the gain.
- Sweep the temperature over 1, 0.5, 0.1, 0.01. Confirm accuracy does not change but confidence does.
- Build 30 queries with hand-labelled relevant sets. Compute precision@10 and recall@10.
- Build an HNSW index with FAISS. Measure latency and recall@10 against brute force.
Step 6 is the one that fixes a common misunderstanding. Watching the probabilities move dramatically while accuracy stays fixed makes it clear that temperature is a presentation choice, not a modelling one.
Summary
CLIP puts images and sentences in one 512-dimensional space, so search is a cosine similarity. The correct caption scored 0.9621, a vague-but-true one 0.7000, and a wrong one 0.1746. Normalise once at index time and every query becomes a plain dot product.
Zero-shot classification is the same operation aimed at label sentences. Temperature sharpens the softmax without ever changing the ranking: at τ=1 the top class got 0.450, at τ=0.1 it got 0.932. Wrapping labels in "a photo of a {label}" is worth about 4 points, and a template ensemble another 3.
Training uses InfoNCE over an similarity matrix. A well-learned row gave loss 0.00408; a poorly learned one gave 0.8653, 212 times more, so the gradient concentrates where the model is weak. Batch size is the number of negatives, which is why CLIP trained at 32,768.
Four hundred thousand 512-d float32 vectors are 819 MB and 205M multiply-adds per query. HNSW is 33× faster at 0.98 recall; IVF-PQ is 31× smaller at 0.91.
- CLIP maps images and text into one space, so comparing them is just a cosine similarity.
- Normalise embeddings once at index time — then similarity is a dot product, and it is a matmul.
- Zero-shot classification is retrieval against label sentences, not a separate mechanism.
- Temperature changes confidence, never ranking, so it affects calibration and not accuracy.
- 'a photo of a {label}' beats a bare label by about 4 points; a template ensemble adds ~3 more.
- InfoNCE makes every other item in the batch a negative, so batch size is a quality decision.
- A well-fit pair contributed loss 0.004; a poor one contributed 0.865 — gradient goes where it is needed.
- 400k×512 float32 is 819 MB and 205M ops per query. Beyond ~100k vectors, use an ANN index.
- Precision@k is what the user sees; recall@k is what the catalogue owner cares about.
- CLIP's coverage follows web imagery — measure on your own data before trusting it on a niche domain.
What comes next
One note on the other direction. CLIP learns a shared image–text space in order to match them. Run the same idea generatively — condition an image generator on a text embedding — and you get DALL·E and the diffusion models that followed. They are siblings from the same insight: once images and text live in one space, you can move in either direction between them. Retrieval finds an existing image for your sentence; generation makes one.
Retrieval at 1.2 ms per query is fast because it is one matrix operation over precomputed vectors. Live video is a harder problem: frames arrive whether or not you are ready. Real-time computer vision systems covers latency budgets, what happens when the queue fills up, and why average FPS is the wrong thing to measure.