CLIP › Code Implementation
Language AI HandbookAll chapters CLIP › Code ImplementationOne section · Sections in this chapter7 / 11 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's implement CLIP-based zero-shot classification using the Hugging Face Transformers library. We'll walk through loading the model, processing inputs, and performing classification.
from transformers import CLIPModel, CLIPProcessor
# Load pre-trained CLIP model and processor
model_name = "openai/clip-vit-base-patch32"
model = CLIPModel.from_pretrained(model_name)
processor = CLIPProcessor.from_pretrained(model_name)
# Model configuration details (uncomment to view after loading)
# print(f"Model loaded: {model_name}")
# print(f"Text encoder layers: {model.text_model.config.num_hidden_layers}")
# print(f"Image encoder layers: {model.vision_model.config.num_hidden_layers}")
# print(f"Embedding dimension: {model.config.projection_dim}")Model loaded: openai/clip-vit-base-patch32 Text encoder layers: 12 Image encoder layers: 12 Embedding dimension: 512
The CLIP ViT-B/32 model uses a 12-layer vision transformer with patch size 32 and a 12-layer text transformer, both projecting to a 512-dimensional shared space. This shared dimension is the key architectural constraint that makes cross-modal comparison possible.
Now let's download a sample image and prepare candidate labels for zero-shot classification:
import io
import requests
from PIL import Image, ImageDraw
# Download a real dog photo so CLIP can actually classify it correctly
dog_url = "https://images.unsplash.com/photo-1543466835-00a7907e9de1?w=320&auto=format"
headers = {"User-Agent": "Mozilla/5.0"}
try:
response = requests.get(dog_url, headers=headers, timeout=15)
response.raise_for_status()
image = Image.open(io.BytesIO(response.content)).convert("RGB")
except requests.RequestException:
image = Image.new("RGB", (320, 240), color=(210, 180, 130))
draw = ImageDraw.Draw(image)
draw.ellipse((95, 55, 225, 185), fill=(140, 95, 45))
draw.text((118, 190), "dog", fill=(40, 30, 20))
# Define candidate classes for zero-shot classification
candidate_labels = ["a dog", "a cat", "a bird", "a car", "a tree"]
prompt_template = "a photo of a {}"
# Create full prompts using the template
text_inputs = [prompt_template.format(label) for label in candidate_labels]
print(f"Image size: {image.size}")
print(f"Candidate labels: {candidate_labels}")Image size: (320, 240) Candidate labels: ['a dog', 'a cat', 'a bird', 'a car', 'a tree']
Note the use of the prompt template. We construct "a photo of a dog" rather than just "dog" because this matches the distribution of captions in the training data and produces more stable embeddings.
Next, we process the inputs through CLIP. The processor handles tokenization for text and normalization plus reshaping for images:
# Process inputs: tokenize text and preprocess image
inputs = processor(
text=text_inputs, images=image, return_tensors="pt", padding=True
)
print(f"Input IDs shape: {inputs['input_ids'].shape}")
print(f"Pixel values shape: {inputs['pixel_values'].shape}")
print(f"Attention mask shape: {inputs['attention_mask'].shape}")Input IDs shape: torch.Size([5, 8]) Pixel values shape: torch.Size([1, 3, 224, 224]) Attention mask shape: torch.Size([5, 8])
The processor converts our 5 text prompts into token tensors of shape (5, 77) where 77 is CLIP's maximum context length, and the image into a pixel tensor of shape (1, 3, 224, 224) representing batch size, RGB channels, and resolution. CLIP's processor normalizes the image using the mean and standard deviation of the training data, centering each channel around zero and scaling to unit variance. This preprocessing ensures the image encoder receives inputs in the same statistical regime it saw during training.
Now we compute the embeddings and similarities:
import torch
# Forward pass through CLIP
with torch.no_grad():
outputs = model(**inputs)
# Extract embeddings
image_embeds = outputs.image_embeds # Shape: (1, 512)
text_embeds = outputs.text_embeds # Shape: (5, 512)
# L2 normalization (already normalized by CLIP, but applying explicitly for clarity)
image_embeds = image_embeds / image_embeds.norm(dim=-1, keepdim=True)
text_embeds = text_embeds / text_embeds.norm(dim=-1, keepdim=True)
# Compute cosine similarities (dot product of normalized vectors)
similarities = (image_embeds @ text_embeds.T).squeeze(0)
# Convert to probabilities using softmax with temperature scaling
temperature = model.logit_scale.exp().item()
probs = (similarities * temperature).softmax(dim=0)
print(f"Image embeddings shape: {image_embeds.shape}")
print(f"Text embeddings shape: {text_embeds.shape}")
print(f"Temperature (learned): {temperature:.4f}")Image embeddings shape: torch.Size([1, 512]) Text embeddings shape: torch.Size([5, 512]) Temperature (learned): 100.0000
The model.logit_scale stores the learned log-temperature parameter. Taking its exponential gives the actual temperature value, typically around 100 (the inverse of 0.01 in terms of ). This means the model uses an extremely sharp softmax that concentrates essentially all probability mass on the highest-scoring candidate. The learned temperature is much larger than the initialization value because training on 400 million diverse examples allows the model to become very confident in its similarity judgments for semantically distinct categories. Finally, let's visualize the classification results:

The results demonstrate CLIP's ability to classify without task-specific training. The model typically assigns probability greater than 0.95 to "a dog" for clear dog photographs, with negligible probability distributed among the other candidates. This extreme concentration reflects the large learned temperature, which sharpens the softmax so strongly that even a modest advantage in cosine similarity translates into near-certain prediction.
Embedding Extraction and Similarity Search
Beyond classification, CLIP embeddings enable semantic image search and clustering. Because the embedding space is shared across modalities, you can use text queries to retrieve images, or use images to retrieve similar images, all within the same vector space. Let's extract embeddings for multiple images and compute pairwise similarities:
# Download real images of semantically distinct subjects so CLIP embeddings
# reflect semantic content rather than colour alone.
import io
import requests
image_urls = [
(
"dog",
"https://images.unsplash.com/photo-1543466835-00a7907e9de1?w=160&auto=format",
),
(
"cat",
"https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=160&auto=format",
),
(
"car",
"https://images.unsplash.com/photo-1541899481282-d53bffe3c35d?w=160&auto=format",
),
(
"dog2",
"https://images.unsplash.com/photo-1587300003388-59208cc962cb?w=160&auto=format",
),
]
headers = {"User-Agent": "Mozilla/5.0"}
images = []
labels = []
for label, url in image_urls:
try:
resp = requests.get(url, headers=headers, timeout=15)
resp.raise_for_status()
img = Image.open(io.BytesIO(resp.content)).convert("RGB")
except requests.RequestException:
img = Image.new("RGB", (160, 160), color=(220, 220, 220))
ImageDraw.Draw(img).text((40, 70), label, fill=(30, 30, 30))
images.append(img)
labels.append(label)
# Process all images
image_inputs = processor(images=images, return_tensors="pt", padding=True)
# Extract embeddings
with torch.no_grad():
vision_out = model.get_image_features(
pixel_values=image_inputs["pixel_values"]
)
image_features = vision_out.pooler_output
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
# Compute similarity matrix
similarity_matrix = (image_features @ image_features.T).numpy()
print(f"Similarity matrix shape: {similarity_matrix.shape}")
print(f"Labels: {labels}")Similarity matrix shape: (4, 4) Labels: ['dog', 'cat', 'car', 'dog2']
Now let's visualize the similarity matrix to see how CLIP clusters semantically similar images:

The similarity matrix is best read comparatively. The diagonal is 1.0 by construction, and each off-diagonal entry shows how these particular images relate in CLIP's embedding space. The dog pair should be compared with the dog-cat and dog-car pairs, but one four-image sample is not enough to establish categorical separation: exact scores depend on image content, composition, and preprocessing. A proper evaluation would repeat this analysis across many labeled examples and test whether same-class pairs are consistently more similar than different-class pairs.