Implement SigLIP's sigmoid contrastive loss over a batch of image and text embeddings.
SigLIP replaces CLIP's softmax with an independent binary classification per pair. Every one of the n x n image-text pairs gets a label
zij={+1−1i=ji=j
and a logit built from the scaled cosine similarity plus a learned bias b (initialised strongly negative, around -10, because the batch is overwhelmingly negatives):
ℓij=s⋅I^i⋅T^j+b
The loss is the mean over the batch of the summed pairwise logistic losses:
L=n1∑i=1n∑j=1nlog(1+e−zijℓij)
Note the normalisation: the double sum is divided by n, not by n^2.
The point of the reformulation is that no term needs any other pair's logit. CLIP's denominator sums the whole row, forcing an all-gather across every device in the batch; SigLIP's terms are independent, so it trains at small batch sizes and shards trivially. That independence is why -log sigmoid must be computed pairwise here.
log(1 + exp(-z*l)) overflows for large negative z*l. Use numpy.logaddexp(0.0, -z*l), which is the stable softplus.
Implement:
def siglip_loss(image_emb, text_emb, logit_scale, bias):
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 = [[1.0, 0.0], [0.0, 1.0]]
print(siglip_loss(img, txt, 10.0, -5.0))
Output:
0.0134
img = [[1.0, 0.0], [0.0, 1.0]] txt = [[1.0, 0.0], [0.0, 1.0]] print(siglip_loss(img, txt, 10.0, -5.0))
0.0134
The cosine matrix is the identity. Diagonal logits are 101 - 5 = 5, giving softplus(-5) = 0.0067 each. Off-diagonal logits are 100 - 5 = -5 with label -1, so z*l = +5 and softplus(-5) = 0.0067 each. Four terms of 0.006715 summed and divided by n = 2 gives 0.0134.
1 <= n <= 64, 1 <= d <= 64; no row is the zero vectorn (NOT by n * n)np.logaddexp (stable softplus) - naive log(1 + exp(-x)) overflows