CLIP and Contrastive Learning
Reading time: ~28 min | Interview relevance: Very High | Target roles: ML Engineer, AI Engineer, Research Engineer
The Night a Model Recognized Something It Had Never Seen
Your team has just launched an e-commerce image search feature. The pitch was straightforward: users upload a photo and find similar products. You trained a ResNet-50 on 500K product images, carefully labeled by your data team over six months. The model does well - 89% top-5 accuracy on your held-out test set. You ship it, celebrate with your team, and go home.
Three weeks later your product manager calls. Engagement is a fraction of what was expected. You dig into the user behavior data and immediately see why. Users are not photographing the exact products you sell. They are photographing things they see in the world - a jacket on a stranger on the street, a lamp in a coffee shop, a plant arrangement at a friend's house - and expecting your model to understand what they are looking for. Your model was trained on product catalog images with white backgrounds. It has never seen a jacket in the wild, on a person, in motion, in various lighting conditions. It fails almost entirely on real-world photos.
The deeper problem is a fundamental one. To train a classifier on product images, you need labeled product images. To get labeled product images, you need human annotators. To annotate at internet scale, you need an army of humans working for years. And even then, your vocabulary is fixed - 500K product categories, and nothing outside that set. The label space is the ceiling of what your model can understand. Whatever your annotators did not name, your model cannot see.
This is the supervised learning bottleneck. It does not just apply to image search. It applies to every computer vision task that tries to bridge the gap between the visual world and human concepts. The world is infinitely diverse; your annotation budget is finite. The concepts humans describe in language are vastly richer than the categories your labeling infrastructure can produce.
CLIP (Contrastive Language-Image Pre-training, Radford et al., OpenAI 2021) solved this by replacing the annotation budget with the internet. The insight was deceptively simple: images and their captions are naturally paired. Every product page has a description. Every news article has a photo caption. Every social media post has alt-text or hashtags. These pairs exist at a scale that human annotation can never match - 400 million of them, collected from the web. And they are already labeled, in the richest possible way: by the humans who wrote them, for humans who would read them.
Why Supervised Learning Hits a Wall
Before understanding what CLIP does, it is worth being precise about what traditional supervised image learning cannot do.
Fixed vocabulary problem. A classifier trained on ImageNet's 1,000 classes can recognize those 1,000 classes. If you encounter a concept not in that vocabulary - say, "a photo taken in the style of Warhol" or "a high-end versus budget version of the same product" - the model has no representation for it. Its output space is closed.
Distribution shift. Models trained on labeled datasets overfit to the data collection process. ImageNet images are typically centered, well-lit, single-subject. Real-world photos are not. A model trained on one distribution fails silently on another.
Transfer rigidity. A model fine-tuned on medical imaging has forgotten everything about natural images. Transfer learning mitigates this but does not eliminate it. The more specialized the fine-tuning, the more the model loses its general visual vocabulary.
Cost of annotation. Labeling 1 million images requires roughly 1,000 hours of human labor at a minimum (60 seconds per image, being optimistic). Labeling 400 million images is essentially impossible. The internet, however, already contains 400 million image-caption pairs - for free.
CLIP: The Core Idea
CLIP is a dual encoder model: one encoder for images, one for text. Given a batch of image-text pairs, CLIP learns to embed matched pairs close together in a shared embedding space and push unmatched pairs apart.
The training data consists of 400 million (image, caption) pairs collected from the internet. The image encoder is a Vision Transformer (ViT-L/14 in the largest variant) or a ResNet. The text encoder is a 63M-parameter transformer. Both encoders project to a shared 512-dimensional embedding space.
The key constraint is that both encoders project into the same space. After training, you can compute the cosine similarity between any image embedding and any text embedding, and the value tells you how semantically related they are.
The InfoNCE Loss
The loss function that makes this work is InfoNCE (Noise Contrastive Estimation). For a batch of image-text pairs:
Let be the normalized image embedding for image and be the normalized text embedding for caption .
The similarity matrix is:
where is a learned temperature parameter (initialized to 0.07 in CLIP). The temperature controls how sharply the model distinguishes between matched and unmatched pairs.
The loss is computed symmetrically - image-to-text and text-to-image:
The diagonal elements are the similarity scores for matched pairs; all off-diagonal elements where are scores for unmatched pairs. The model is trained to maximize diagonal similarity and minimize off-diagonal similarity. With a batch of (CLIP's training batch size), each image must be distinguished from 32,767 negative text captions. This forces the embeddings to be highly specific.
Why Temperature Matters
A small temperature () makes the softmax very sharp - the model is penalized heavily for any confusion between similar-but-not-matched pairs. A large temperature () makes the loss more lenient. CLIP learns during training, starting at 0.07. In practice, the temperature is one of the most important hyperparameters in contrastive learning.
If is too small: gradients vanish for pairs that are clearly different, learning is slow. If is too large: the model cannot distinguish semantically similar but distinct pairs.
Zero-Shot Classification: The Key Result
After training, CLIP can classify images into any set of categories - including ones never seen during training - without any fine-tuning. Here is how:
- For each class name (e.g., "dog", "cat", "airplane"), encode the text prompt "a photo of a {class}" through the text encoder. This gives you text embeddings for classes.
- Encode the query image through the image encoder. This gives you one image embedding.
- Compute cosine similarity between the image embedding and all text embeddings.
- The class with the highest similarity is the prediction.
This is remarkable. CLIP achieved 76.2% top-1 accuracy on ImageNet using this zero-shot approach - matching a supervised ResNet-50 trained on 1.28 million labeled ImageNet images. The model had never seen ImageNet during training. It generalized because its training data - 400M image-text pairs from the internet - implicitly covered the same concepts.
Prompt Engineering for Zero-Shot Classification
The text prompt matters. "a photo of a {class}" consistently outperforms just "{class}" as the prompt. More specific prompts further improve accuracy:
- "a photo of a {class}, a type of food" (for food categories)
- "a photo of a {class}, a type of pet" (for animal categories)
- "a centered satellite photo of a {class}" (for remote sensing)
CLIP's paper showed that ensembling 80 different prompt templates and averaging the resulting text embeddings improved ImageNet zero-shot accuracy by 3.5 percentage points. This is essentially prompt engineering for embeddings.
Scaling Laws: ALIGN and the Noise Tolerance Insight
ALIGN (Jia et al., Google 2021) showed that scale beats data quality. Instead of carefully filtering 400M pairs down to a clean set, ALIGN trained on 1.8 billion noisy image-text pairs with minimal filtering. The only filtering applied: remove images smaller than 200×200 pixels, remove captions shorter than 3 tokens or longer than 80 tokens.
The result matched or exceeded CLIP on most benchmarks. The conclusion: at sufficient scale, noise in the training data is essentially irrelevant. The signal-to-noise ratio needed for contrastive learning is much lower than for supervised learning. Unmatched captions are already in the denominator of the InfoNCE loss - they are "hard negatives" that make the loss harder and the representations more discriminative.
SigLIP: A Better Loss at Scale
SigLIP (Zhai et al., Google 2023) replaces the softmax in InfoNCE with a sigmoid loss. The standard InfoNCE loss normalizes across the entire batch - for a batch of , every loss computation involves a softmax over 32K values, requiring all-reduce communication across all GPUs. This is expensive.
The sigmoid loss decouples this: each pair is scored independently:
where if pair is matched, 0 otherwise, and is a learned bias.
This is a standard binary cross-entropy loss with a bias term. The advantages:
- No need for all-reduce communication across GPUs during the softmax
- Better handling of large batches (the softmax denominator grows with batch size, creating gradient issues at extreme scale)
- Empirically stronger performance on downstream tasks
SigLIP achieved better performance than CLIP on most benchmarks while being more efficient to train. It is now the preferred contrastive objective for training VLM vision encoders.
OpenCLIP: Open-Source Reproduction
OpenCLIP (LAION-AI, 2022) reproduced and extended CLIP using the LAION-400M and LAION-5B datasets - open-source image-text datasets scraped from the internet using Common Crawl. This made CLIP-style training accessible to the research community.
Key findings from OpenCLIP:
- Models trained on LAION-5B matched and exceeded OpenAI's CLIP models on many benchmarks
- The largest OpenCLIP model (ViT-G/14 trained on LAION-2B) achieved 80.1% zero-shot ImageNet top-1 accuracy
- Data quality matters more than quantity at smaller scales; at large scales, quantity dominates
CLIP Embeddings for Retrieval
Beyond zero-shot classification, CLIP embeddings enable a powerful family of retrieval applications.
Text-to-image search: Encode a text query, find images with nearest embeddings. This enables natural language image search - "a red car parked in front of a white building" - without any structured metadata.
Image-to-image search: Encode a query image, find visually similar images. Unlike pixel-level similarity (e.g., perceptual hash), CLIP similarity is semantic - two images with different pixels but similar content rank similarly.
Cross-modal matching: Given a product photo, find the matching product description. Given a code screenshot, find the corresponding documentation. CLIP generalizes to any domain represented in its training data.
Code: CLIP Zero-Shot Classification and Retrieval
import torch
import clip
from PIL import Image
import requests
from io import BytesIO
import numpy as np
from typing import list as List
def load_clip_model(model_name: str = "ViT-L/14"):
"""Load CLIP model and preprocessing transform."""
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load(model_name, device=device)
model.eval()
print(f"Loaded {model_name} on {device}")
return model, preprocess, device
def encode_images(model, preprocess, image_paths: List[str], device: str) -> torch.Tensor:
"""Encode a list of images to CLIP embeddings."""
images = []
for path in image_paths:
if path.startswith("http"):
response = requests.get(path, timeout=10)
img = Image.open(BytesIO(response.content)).convert("RGB")
else:
img = Image.open(path).convert("RGB")
images.append(preprocess(img))
image_tensor = torch.stack(images).to(device)
with torch.no_grad():
image_features = model.encode_image(image_tensor)
# L2 normalize for cosine similarity
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
return image_features.float()
def encode_texts(model, texts: List[str], device: str) -> torch.Tensor:
"""Encode a list of text strings to CLIP embeddings."""
text_tokens = clip.tokenize(texts, truncate=True).to(device)
with torch.no_grad():
text_features = model.encode_text(text_tokens)
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
return text_features.float()
def zero_shot_classify(
model,
preprocess,
device: str,
image_path: str,
class_names: List[str],
prompt_template: str = "a photo of a {}",
) -> List[tuple[str, float]]:
"""
Classify an image into one of the given classes using zero-shot CLIP.
Returns ranked list of (class_name, probability) tuples.
"""
# Encode the image
image_features = encode_images(model, preprocess, [image_path], device)
# Encode all class prompts
prompts = [prompt_template.format(cls) for cls in class_names]
text_features = encode_texts(model, prompts, device)
# Compute similarities
# image_features: (1, D), text_features: (K, D)
similarities = (image_features @ text_features.T).squeeze(0) # (K,)
probabilities = similarities.softmax(dim=0).cpu().numpy()
results = sorted(
zip(class_names, probabilities.tolist()),
key=lambda x: x[1],
reverse=True,
)
return results
def image_text_similarity(
model,
preprocess,
device: str,
image_path: str,
texts: List[str],
) -> List[tuple[str, float]]:
"""Compute similarity scores between an image and multiple texts."""
image_features = encode_images(model, preprocess, [image_path], device)
text_features = encode_texts(model, texts, device)
similarities = (image_features @ text_features.T).squeeze(0).cpu().numpy()
return list(zip(texts, similarities.tolist()))
def build_image_index(
model,
preprocess,
device: str,
image_paths: List[str],
) -> tuple[torch.Tensor, List[str]]:
"""Build a CLIP embedding index for a corpus of images."""
# Process in batches to avoid OOM
batch_size = 32
all_features = []
for i in range(0, len(image_paths), batch_size):
batch = image_paths[i:i + batch_size]
features = encode_images(model, preprocess, batch, device)
all_features.append(features.cpu())
all_features = torch.cat(all_features, dim=0) # (N, D)
return all_features, image_paths
def text_image_search(
query_text: str,
model,
device: str,
image_index: torch.Tensor,
image_paths: List[str],
top_k: int = 5,
) -> List[tuple[str, float]]:
"""Search image index using a text query."""
text_features = encode_texts(model, [query_text], device).cpu() # (1, D)
similarities = (text_features @ image_index.T).squeeze(0) # (N,)
top_indices = similarities.argsort(descending=True)[:top_k]
results = [
(image_paths[i], similarities[i].item())
for i in top_indices
]
return results
# Prompt ensemble: average embeddings across multiple prompt templates
IMAGENET_PROMPT_TEMPLATES = [
"a photo of a {}.",
"a blurry photo of a {}.",
"a black and white photo of a {}.",
"a low contrast photo of a {}.",
"a high contrast photo of a {}.",
"a bad photo of a {}.",
"a good photo of a {}.",
"a photo of a small {}.",
"a photo of a big {}.",
"a photo of the {}.",
"itap of a {}.",
"a pixelated photo of a {}.",
"a photo of the cool {}.",
"a dark photo of the {}.",
]
def encode_texts_ensemble(
model,
class_names: List[str],
device: str,
templates: List[str] = IMAGENET_PROMPT_TEMPLATES,
) -> torch.Tensor:
"""
Encode class names using multiple prompt templates and average embeddings.
This improves zero-shot accuracy by ~3-5 percentage points on ImageNet.
"""
all_embeddings = []
for template in templates:
prompts = [template.format(cls) for cls in class_names]
text_tokens = clip.tokenize(prompts, truncate=True).to(device)
with torch.no_grad():
embeddings = model.encode_text(text_tokens)
embeddings = embeddings / embeddings.norm(dim=-1, keepdim=True)
all_embeddings.append(embeddings.float())
# Stack and average across templates
stacked = torch.stack(all_embeddings, dim=0) # (T, K, D)
mean_embeddings = stacked.mean(dim=0) # (K, D)
# Renormalize after averaging
mean_embeddings = mean_embeddings / mean_embeddings.norm(dim=-1, keepdim=True)
return mean_embeddings
if __name__ == "__main__":
model, preprocess, device = load_clip_model("ViT-B/32")
# Zero-shot classification example
image_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"
classes = ["cat", "dog", "bird", "horse", "car", "airplane"]
results = zero_shot_classify(model, preprocess, device, image_url, classes)
print("Zero-shot classification results:")
for cls, prob in results:
print(f" {cls}: {prob:.4f}")
# Image-text similarity
texts = [
"a cat sitting on a surface",
"a dog playing in a park",
"a car on a highway",
]
sims = image_text_similarity(model, preprocess, device, image_url, texts)
print("\nImage-text similarities:")
for text, sim in sims:
print(f" '{text}': {sim:.4f}")
Code: Fine-tuning CLIP for a Domain
import torch
import torch.nn as nn
import clip
from torch.utils.data import Dataset, DataLoader
from PIL import Image
class ImageTextDataset(Dataset):
"""Dataset of (image_path, caption) pairs for CLIP fine-tuning."""
def __init__(self, pairs: list[tuple[str, str]], preprocess):
self.pairs = pairs
self.preprocess = preprocess
def __len__(self):
return len(self.pairs)
def __getitem__(self, idx):
image_path, caption = self.pairs[idx]
image = self.preprocess(Image.open(image_path).convert("RGB"))
text = clip.tokenize([caption], truncate=True)[0]
return image, text
def fine_tune_clip(
model,
device: str,
train_pairs: list[tuple[str, str]],
preprocess,
epochs: int = 5,
lr: float = 1e-6,
batch_size: int = 32,
):
"""
Fine-tune CLIP on domain-specific image-text pairs.
Use a very small learning rate to avoid catastrophic forgetting.
"""
dataset = ImageTextDataset(train_pairs, preprocess)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# Fine-tune only the final projection layers, not the full model
# This is more stable than full fine-tuning
params_to_tune = (
list(model.visual.proj.parameters())
if hasattr(model.visual, "proj")
else list(model.visual.parameters())[-10:]
)
optimizer = torch.optim.AdamW(params_to_tune, lr=lr, weight_decay=0.01)
model.train()
for epoch in range(epochs):
total_loss = 0.0
for images, texts in dataloader:
images = images.to(device)
texts = texts.to(device)
# Encode
image_features = model.encode_image(images)
text_features = model.encode_text(texts)
# Normalize
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
# InfoNCE loss
logit_scale = model.logit_scale.exp()
logits_per_image = logit_scale * image_features @ text_features.T
logits_per_text = logits_per_image.T
labels = torch.arange(len(images)).to(device)
loss_i = nn.CrossEntropyLoss()(logits_per_image, labels)
loss_t = nn.CrossEntropyLoss()(logits_per_text, labels)
loss = (loss_i + loss_t) / 2
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
avg_loss = total_loss / len(dataloader)
print(f"Epoch {epoch + 1}/{epochs}, Loss: {avg_loss:.4f}")
model.eval()
return model
Production Engineering: CLIP as a Feature Extractor
Building a Product Image Search System
CLIP's most common production use case is powering semantic image search - particularly in e-commerce, content moderation, and document retrieval.
import numpy as np
import faiss
import pickle
from pathlib import Path
class CLIPImageSearchIndex:
"""Production-ready CLIP-based image search index using FAISS."""
def __init__(self, model, preprocess, device: str, embedding_dim: int = 512):
self.model = model
self.preprocess = preprocess
self.device = device
self.embedding_dim = embedding_dim
# FAISS index with inner product (cosine similarity for normalized vectors)
self.index = faiss.IndexFlatIP(embedding_dim)
self.image_paths = []
def add_images(self, image_paths: list[str], batch_size: int = 64):
"""Add images to the search index."""
for i in range(0, len(image_paths), batch_size):
batch = image_paths[i:i + batch_size]
features = encode_images(self.model, self.preprocess, batch, self.device)
features_np = features.cpu().numpy().astype(np.float32)
self.index.add(features_np)
self.image_paths.extend(batch)
print(f"Indexed {min(i + batch_size, len(image_paths))}/{len(image_paths)}")
def search_by_text(self, query: str, top_k: int = 10) -> list[tuple[str, float]]:
"""Search by text query."""
text_features = encode_texts(self.model, [query], self.device)
query_np = text_features.cpu().numpy().astype(np.float32)
scores, indices = self.index.search(query_np, top_k)
results = []
for score, idx in zip(scores[0], indices[0]):
if idx != -1:
results.append((self.image_paths[idx], float(score)))
return results
def search_by_image(self, image_path: str, top_k: int = 10) -> list[tuple[str, float]]:
"""Search by image similarity."""
img_features = encode_images(self.model, self.preprocess, [image_path], self.device)
query_np = img_features.cpu().numpy().astype(np.float32)
scores, indices = self.index.search(query_np, top_k)
results = []
for score, idx in zip(scores[0], indices[0]):
if idx != -1 and self.image_paths[idx] != image_path:
results.append((self.image_paths[idx], float(score)))
return results
def save(self, path: str):
"""Save index to disk."""
faiss.write_index(self.index, f"{path}.faiss")
with open(f"{path}.meta", "wb") as f:
pickle.dump(self.image_paths, f)
def load(self, path: str):
"""Load index from disk."""
self.index = faiss.read_index(f"{path}.faiss")
with open(f"{path}.meta", "rb") as f:
self.image_paths = pickle.load(f)
CLIP in Content Moderation
CLIP zero-shot classification is a fast, cheap first-pass filter for content moderation. You define a set of policy-violating categories as text prompts and flag images where the similarity to any violation category exceeds a threshold:
MODERATION_CATEGORIES = [
"graphic violence or gore",
"explicit sexual content",
"hate symbols or logos",
"drug paraphernalia",
"weapons with intent to harm",
]
SAFE_CATEGORY = "a safe, normal image"
def clip_content_moderation(
model,
preprocess,
device: str,
image_path: str,
violation_threshold: float = 0.25,
) -> dict:
"""
Fast CLIP-based content moderation.
Returns a dict with is_flagged, top_category, and all scores.
"""
all_categories = MODERATION_CATEGORIES + [SAFE_CATEGORY]
results = zero_shot_classify(model, preprocess, device, image_path, all_categories)
scores = dict(results)
safe_score = scores.pop(SAFE_CATEGORY)
max_violation_category = max(scores.items(), key=lambda x: x[1])
is_flagged = max_violation_category[1] > violation_threshold
return {
"is_flagged": is_flagged,
"safe_score": safe_score,
"top_violation": max_violation_category,
"all_scores": scores,
}
:::note CLIP is a first-pass filter, not a final decision maker CLIP content moderation has a non-trivial false positive rate on edge cases and a false negative rate on cleverly obfuscated content. Always follow CLIP with a more specialized classifier or human review for policy enforcement. Use CLIP to dramatically reduce the volume that reaches human review, not to replace it. :::
Common Mistakes
:::danger Forgetting to L2-Normalize Embeddings Before Similarity
CLIP embeddings must be L2-normalized before computing cosine similarity. Without normalization, the dot product measures both direction (semantic similarity) and magnitude (confidence) - the magnitude component introduces noise that degrades retrieval quality. Always apply: features = features / features.norm(dim=-1, keepdim=True) immediately after encoding.
:::
:::danger Using Just the Class Name as a Prompt Passing bare class names ("cat", "dog") to the text encoder consistently underperforms prompt-engineered versions ("a photo of a cat"). The text encoder was trained on natural language captions, not isolated nouns. Always use at least "a photo of a {class}". For high-stakes classification, ensemble 10+ templates. :::
:::warning Assuming CLIP Works Well on All Domains CLIP was trained on internet images and captions. It works well for concepts well-represented on the internet. It underperforms for: medical images (radiology, pathology), satellite/aerial imagery, microscopy, proprietary industrial images, and any domain underrepresented in internet text. For these domains, fine-tune CLIP on domain data before deploying. :::
:::warning Ignoring Batch Size in Contrastive Learning The InfoNCE loss uses all other samples in the batch as negatives. With a batch size of 32, each image has 31 negative captions - too few for the loss to learn fine-grained distinctions. CLIP's training batch size was 32,768. For fine-tuning, use the largest batch size that fits in memory, supplement with in-batch mining of hard negatives, or use memory banks (MoCo-style) to maintain a large pool of negatives. :::
Interview Questions and Answers
Q1: Why does contrastive learning work without labeled data?
Contrastive learning does not eliminate supervision - it changes the source of supervision from human annotations to naturally co-occurring pairs. Image-caption pairs from the internet are implicitly labeled: the caption describes the image, and that relationship was created by a human who was trying to communicate something about the image. The contrastive objective - pull matched pairs together, push unmatched pairs apart - forces the model to learn which visual features correspond to which textual concepts. The key insight is that at internet scale (400M+ pairs), the diversity of image-text co-occurrences covers virtually all visual concepts humans care to describe in text. You do not need labels if you have rich enough co-occurrence signal.
Q2: What is the InfoNCE loss and why does temperature matter?
InfoNCE (Noise Contrastive Estimation) treats the correct image-text pair as a classification problem: given an image, identify the matching caption from a batch of captions. The loss is a cross-entropy over the normalized similarity scores between the image and all captions in the batch. Temperature scales the similarity scores before the softmax. Low temperature makes the distribution sharp - the model must be very confident about the correct match and is heavily penalized for any confusion. High temperature makes the distribution flat - even rough similarity is acceptable. In practice, is learned and typically settles around 0.01-0.1. Too-small temperature causes gradient instability; too-large temperature leads to poor discriminability.
Q3: How does CLIP achieve zero-shot ImageNet classification? Why is this significant?
For zero-shot classification, CLIP encodes each class name as a text prompt ("a photo of a {class}") and computes cosine similarity between the query image embedding and all class text embeddings. The predicted class is the one with highest similarity. The significance: CLIP achieves 76.2% top-1 accuracy on ImageNet without seeing a single ImageNet image during training, matching supervised ResNet-50 trained on 1.28M labeled ImageNet images. This demonstrates that the shared embedding space learned from internet-scale image-text pairs generalizes across visual concepts - the model has implicitly learned what a "golden retriever" or "analog clock" looks like from captions, without explicit class labels. It also demonstrates that natural language is a much more flexible supervision signal than fixed label sets.
Q4: How would you use CLIP to build a product image search system?
The architecture: (1) Offline indexing - for every product in the catalog, run the product image through CLIP's image encoder, L2-normalize the embedding, store it in a vector database (Faiss, Pinecone, Weaviate) with the product ID as metadata. (2) Online query - for a text query ("red running shoes for women"), encode the query through CLIP's text encoder, query the vector database for the top-K nearest image embeddings by cosine similarity, return the corresponding products. For image-based queries (upload a photo), encode the uploaded image instead of text. Key engineering decisions: batch size for embedding generation (64-128 is typical), index type (FAISS flat for accuracy, HNSW for speed-accuracy trade-off), whether to fine-tune CLIP on product data, and how to handle multi-image products (average embeddings vs. index all images separately).
Q5: What are the failure modes of CLIP, and how would you diagnose them?
Key failure modes: (1) Domain gap - CLIP was trained on internet images; it fails on medical, satellite, and industrial imagery. Diagnosis: compute zero-shot accuracy on a held-out domain-specific test set. Fix: fine-tune with domain data. (2) Spurious correlations - CLIP inherits biases from internet text. "A photo of a nurse" may embed closer to female than male images. Diagnosis: audit retrieval results for demographic bias. (3) Compositional reasoning - CLIP struggles with "a red ball on a blue table" vs "a blue ball on a red table." The embeddings are not strongly sensitive to spatial and relational composition. Diagnosis: test on Winoground or ARO benchmarks. (4) Fine-grained distinctions - CLIP cannot distinguish between very similar subcategories (e.g., 150 dog breeds) as reliably as specialized classifiers. Diagnosis: compare against a fine-tuned classifier on your specific taxonomy.
:::tip 🎮 Interactive Playground
Visualize this concept: Try the CLIP Contrastive Learning demo on the EngineersOfAI Playground - no code required.
:::
