Zero-Shot Classifier by Prompt Ensembling
Problem Statement
Build a CLIP zero-shot classifier by ensembling several text prompts per class, then classify a batch of image embeddings and report accuracy.
Background
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:
- L2-normalise each prompt embedding - so every template contributes equally regardless of its norm
- Average the normalised prompt embeddings for the class
- Re-normalise the average - a mean of unit vectors is not a unit vector, and unless you renormalise, classes whose prompts disagree get a shorter weight vector and are systematically under-predicted
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.
Your Task
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.
Input Format
- image_emb - n x d nested list of unnormalised image embeddings
- class_prompt_emb - list of C entries; entry c is a list of P_c unnormalised prompt embeddings of dim d (classes may have different prompt counts)
- labels - list of n ground-truth class indices
Output Format
[[int, ...], float]
Sample
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]
Example
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.
Constraints
1 <= n <= 200,1 <= C <= 50,1 <= d <= 64; no vector is the zero vector- Normalise each prompt embedding BEFORE averaging, and re-normalise the class mean AFTER
- Classes may have different numbers of prompts
- Argmax ties break to the lowest class index
- Round accuracy to 4 decimals