Given a batch of image embeddings and their paired text embeddings, compute CLIP's symmetric InfoNCE loss.
CLIP trains two encoders so that the i-th image matches the i-th caption and nothing else in the batch. Both embeddings are first L2-normalised, so their dot product is a cosine similarity in [-1, 1], then scaled by a learned logit_scale (the inverse temperature, exp(t), roughly 100 at convergence):
logits=s⋅I^T^⊤,I^i=∥Ii∥2Ii
The diagonal holds the n positive pairs; every off-diagonal entry is a negative. The loss is cross-entropy against the labels 0, 1, ..., n-1 computed in both directions and averaged:
L=21(image→textn1i∑−log∑jesI^i⋅T^jesI^i⋅T^i+text→imagen1j∑−log∑iesI^i⋅T^jesI^j⋅T^j)
The two directions differ: the first normalises over each row of the logit matrix, the second over each column. Averaging them is what makes CLIP usable for both image-to-text and text-to-image retrieval.
Because logit_scale is large, exp overflows on raw logits. Use the log-sum-exp trick: subtract each row's max before exponentiating.
Implement:
def clip_loss(image_emb, text_emb, logit_scale):
image_emb and text_emb are n x d nested lists, row i of each being a positive pair. Return the scalar loss rounded to 4 decimals.
A single float rounded to 4 decimals.
img = [[1.0, 0.0], [0.0, 1.0]]
txt = [[0.9, 0.1], [0.2, 1.0]]
print(clip_loss(img, txt, 10.0))
Output:
0.0003
img = [[1.0, 0.0], [0.0, 1.0]] txt = [[0.9, 0.1], [0.2, 1.0]] print(clip_loss(img, txt, 10.0))
0.0003
After normalising, the cosine matrix is [[0.9939, 0.1961], [0.1104, 0.9806]]. Scaled by 10 the diagonal dominates every row and every column, so the image-to-text cross-entropy is 0.000254, the text-to-image one is 0.000268, and their average rounds to 0.0003.
1 <= n <= 64, 1 <= d <= 64; no row is the zero vectorlogit_scale can be 100