CLIP Contrastive Loss (InfoNCE) - Representation Learning & Retrieval
Implement CLIP Contrastive Loss (InfoNCE) in Python with NumPy. medium exercise with examples, hints, and instant feedback. No setup or signup required.
Symmetric InfoNCE (CLIP Loss)
What you'll build
The contrastive loss CLIP is trained with: L2-normalize image and text embeddings, build the temperature-scaled cosine similarity matrix, and average the cross-entropy over both directions with the matching pairs on the diagonal.
Background
CLIP (Radford et al., 2021) learns one embedding space for images and text from 400M (image, caption) pairs, with no class labels. In a batch of N pairs, image i and caption i are a positive pair; the other N - 1 captions are negatives for that image, and vice versa. The same loss trains text embedding models for retrieval (with (query, passage) pairs), SimCLR (two augmentations of one image) and many more. More negatives make the task harder and the representation better, which is why these models are trained with very large batches.
Formula
img_n = img / max(||img||, 1e-12) # per row, (N, D)
txt_n = txt / max(||txt||, 1e-12) # per row, (N, D)
logits = img_n @ txt_n.T / tau # (N, N); row = image, column = text
CE(Z) = mean_i [ logsumexp(Z[i, :]) - Z[i, i] ] # correct class of row i is column i
loss_i2t = CE(logits) # each image picks its caption among N
loss_t2i = CE(logits.T) # each caption picks its image among N
loss = (loss_i2t + loss_t2i) / 2
A lower temperature tau sharpens the softmax; CLIP learns it (initialised to 0.07).
Inputs
img_emb: float array(N, D); rowipairs withtxt_embrowitxt_emb: float array(N, D)temperature: float> 0, default 0.07
Expected Return Format
A dictionary:
'logits':(N, N)array of scaled cosine similarities (row = image, column = text)'loss_i2t': image-to-text cross-entropy (Python float)'loss_t2i': text-to-image cross-entropy (Python float)'loss': their mean (Python float)
Edge cases and conventions
- Normalize the rows (each embedding), not the columns; rescaling an embedding must not change the loss.
- Use a stable log-sum-exp: with
tau = 0.07logits reach about 14, and smaller temperatures go higher. - The matrix is not symmetric in general, so
loss_i2t != loss_t2i.
Constraints
NumPy only, vectorised (no loop over the batch).
Follow-up
- With
Npairs, the loss of a random model is aboutlog N. Why? What does that say about comparing losses across batch sizes? - SigLIP replaces the softmax with independent sigmoid losses per pair. What does that buy for distributed training?
Examples
Input: clip_loss(np.eye(2), np.eye(2), temperature=1.0)
Output: {'logits': [[1.0, 0.0], [0.0, 1.0]], 'loss_i2t': 0.3133, 'loss_t2i': 0.3133, 'loss': 0.3133}Each row is softmax([1, 0]), so CE = log(1 + e^-1) = 0.3133 in both directions.
Input: clip_loss(np.array([[1.0, 0.0], [0.6, 0.8], [0.0, 1.0]]), np.array([[0.8, 0.6], [0.0, 1.0], [1.0, 1.0]]), temperature=0.5)
Output: {'loss_i2t': 1.1121, 'loss_t2i': 1.1433, 'loss': 1.1277, ...}logits = [[1.6, 0, 1.4142], [1.92, 1.6, 1.9799], [1.2, 2.0, 1.4142]]: image 1 is closer to captions 0 and 2 than to its own, so the loss is high, and the two directions differ.
Practice in Python
import numpy as np
def clip_loss(img_emb, txt_emb, temperature=0.07):
"""
Symmetric InfoNCE loss used by CLIP.
Args:
img_emb: (N, D) image embeddings (row i pairs with txt_emb row i)
txt_emb: (N, D) text embeddings
temperature: softmax temperature tau
Returns:
dict with 'logits' (N, N) = cos / tau (row = image), and the Python
floats 'loss_i2t', 'loss_t2i' and 'loss' (their mean)
"""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 6 tests, or use the progressive hints.