Build a CLIP zero-shot classifier by ensembling several text prompts per class, then classify a batch of image embeddings and report accuracy.
CLIP classifies without any classification head: each class name is written into a handful of prompt templates ("a photo of a {c}.", "a blurry photo of a {c}.", "a sketch of a {c}."), each prompt is encoded, and the class's weight vector is the average of those embeddings. The class weights become the rows of a linear classifier over image embeddings - a head synthesised from text alone.
The order of operations is what makes or breaks it:
wc=∥tˉc∥2tˉc,tˉc=P1∑p=1P∥tc,p∥2tc,p
Then normalise each image embedding and take the argmax of I @ W.T. Prompt ensembling done this way is worth about a point and a half of ImageNet top-1 over a single template, for zero extra training.
Implement:
def zero_shot_classify(image_emb, class_prompt_emb, labels):
Return [preds, accuracy] where preds is the list of predicted class indices (argmax; ties break to the lowest class index) and accuracy is the fraction correct, rounded to 4 decimals.
[[int, ...], float]
imgs = [[1.0, 0.1], [0.0, 2.0]]
prompts = [[[1.0, 0.0], [0.9, 0.2]], [[0.0, 1.0], [0.1, 0.9]]]
print(zero_shot_classify(imgs, prompts, [0, 1]))
Output:
[[0, 1], 1.0]
imgs = [[1.0, 0.1], [0.0, 2.0]] prompts = [[[1.0, 0.0], [0.9, 0.2]], [[0.0, 1.0], [0.1, 0.9]]] print(zero_shot_classify(imgs, prompts, [0, 1]))
[[0, 1], 1.0]
Class 0's two unit prompts average to a vector near [0.99, 0.11] which renormalises to a unit vector pointing mostly along x; class 1's points mostly along y. Image 0 leans x and image 1 is pure y, so both are classified correctly: accuracy 1.0.
1 <= n <= 200, 1 <= C <= 50, 1 <= d <= 64; no vector is the zero vector