ML coding · Embeddings and retrieval
The InfoNCE contrastive loss
Given queries Q and keys K (both n × d, no zero rows) and a temperature
tau > 0: normalise every row to unit length, form the logits
Lij = q̂i·k̂j / τ, and return the mean over i of
log Σj exp(Lij) − Lii. Key i is the positive for query i; the other keys are its
negatives.
Examples
Input Q = [[1, 0], [0, 1]], K = [[1, 0], [0, 1]], tau = 1
Output 0.3133
Each query matches its key exactly: loss log(e + 1) − 1.
Input Q = [[1, 0], [0, 1]], K = [[1, 0], [0, 1]], tau = 0.001
Output 0
A tiny temperature: logits of 1000. The naive exp overflows.
Constraints
- 1 ≤ n ≤ 512, 1 ≤ d ≤ 256
- 10⁻³ ≤ τ ≤ 10
- Checked to a relative tolerance of 10⁻⁹
Hints
Hint 1
Normalise with Q / np.linalg.norm(Q, axis=1, keepdims=True).
Hint 2
The loss is the cross-entropy of each row of the logits with the diagonal as the target.
Hint 3
Subtract each row's maximum before exponentiating, and add it back after the log.
Where candidates go wrong
- Forgetting to normalise: then the loss rewards long vectors.
- np.log(np.exp(L).sum(axis=1)) with L in the thousands: inf, then nan.
- Averaging over all n² entries instead of over the n rows.
The idea
Normalise both matrices, build the n × n logits with one matrix product, and compute a row-wise log-sum-exp with the maximum subtracted. The loss is the mean of the log-sum-exp minus the diagonal.
Time O(n²d) · Space O(n²)
Reference solution
import numpy as np
def info_nce(Q, K, tau):
Q = np.asarray(Q, dtype=float)
K = np.asarray(K, dtype=float)
Qn = Q / np.linalg.norm(Q, axis=1, keepdims=True)
Kn = K / np.linalg.norm(K, axis=1, keepdims=True)
L = (Qn @ Kn.T) / tau
m = L.max(axis=1, keepdims=True)
lse = m[:, 0] + np.log(np.exp(L - m).sum(axis=1)) # stable log-sum-exp per row
return float(np.mean(lse - np.diag(L)))
Follow-ups an interviewer asks
- The symmetric version used by CLIP: average this loss with the same loss on the transposed logits.
- The gradient with respect to the logits: softmax(L) minus the identity, divided by n.
- Why larger batches help: more negatives per query, a tighter bound on the mutual information.
Python runs in your browser with NumPy. The first run downloads the runtime, which takes a few seconds.
Why interviewers ask it
Contrastive pre-training is how most modern embedding models are trained. Writing its loss tests whether a candidate can combine normalisation, a temperature and a numerically stable cross-entropy without a library.
The theory behind it is in the lesson Cosine search, contrastive loss and negative sampling.
Keep going
Top-k search by cosine similarity
Return the k rows most similar to a query by cosine similarity, with zero vectors and ties handled by stated rules, vectorised in NumPy..
Open the problem → Medium15 minThe InfoNCE contrastive loss
The contrastive loss behind CLIP and SimCLR in NumPy: normalised embeddings, a temperature, and a log-sum-exp that survives tiny temperatures..
Open the problem → Hard20 minSkip-gram with negative sampling: loss and gradients
The word2vec skip-gram loss with k negative samples and its exact gradients, written with a log-sigmoid that never overflows..
Open the problem →