Build an Image Captioning Model
Learn to build an image captioning model in Python. This Applied AI engineering lesson covers the core concepts, a step-by-step walkthrough, and practical tips for troubleshooting.
Focus: build an image captioning model
You’ve built models that classify images and models that generate text, but bringing the two together — teaching a machine to look at a picture and describe it in natural language — feels like a whole new level of intelligence. It’s also deceptively hard: a naive approach that feeds raw pixels into an RNN produces gibberish, and a model that memorizes captions fails on new images. In this lesson, you’ll learn how to build an image captioning model the right way — using a CNN encoder, a transformer-based decoder, and modern PyTorch tooling — and you’ll walk away with a complete, runnable example you can extend to your own datasets.
The problem this lesson solves
Image captioning is one of the most visible challenges in applied AI. From automatically generating alt‑text for accessibility to creating product descriptions at scale, the ability to describe an image in words unlocks a huge range of real‑world applications. But the problem is fundamentally different from image classification: instead of predicting a single label, you must generate a variable‑length sequence of words that are both semantically correct and grammatically coherent.
A common mistake is to treat captioning as a pure vision problem or a pure language problem. If you train a vision model to output a fixed sentence, you’ll quickly realize that sentences are not single labels — they have structure, order, and dependencies. Likewise, a language model that ignores the image will produce fluent but hallucinated descriptions. The core challenge is fusion: how do you combine visual features with language generation in a way that the model learns to attend to the right parts of the image at each word?
This lesson gives you a clear path: understand the problem, build a mental model, then implement a modern encoder–decoder architecture that handles variable‑length output and attention.
Core concept / mental model
Think of image captioning as translation with a twist. In machine translation, you translate from one language (French) to another (English). Here, you translate from a visual language — the pixels — into natural language. The intermediate representation is not a sentence but a feature vector that captures the semantic content of the image.
The architecture that dominates modern captioning is the encoder–decoder framework.
- Encoder (CNN) — Takes the raw image and compresses it into a rich feature representation. Think of it as the model’s "eyes" that see the image and extract objects, colors, spatial relationships.
- Decoder (Transformer) — Takes that visual representation plus the words generated so far, and produces the next word. It’s the model’s "mouth" that speaks the caption, one word at a time.
A crucial component is cross‑attention. When the decoder produces the word "dog," it should focus on the region of the image that contains a dog, not the background. Cross‑attention mechanisms allow the decoder to dynamically weigh different spatial regions of the image at each decoding step.
Mental picture: Imagine a tour guide (the encoder) who looks at a room and gives a rich summary. The narrator (the decoder) then speaks a sentence, and at each word, the narrator points to the exact part of the room that the word refers to. That pointing is the cross‑attention.
How it works step by step
Let’s break down the training and inference pipeline.
1. Data preparation
You need pairs of images and their captions. For training, each image usually has multiple captions (e.g., COCO has 5 per image). During data loading, you tokenize the captions into a sequence of word IDs and pad them to a fixed length. The vocabulary is built from all word tokens that appear more than a minimum frequency.
2. Feature extraction with a CNN
A pretrained CNN (e.g., ResNet‑50 or EfficientNet) is used to encode the image. You typically remove the final classification layer and use the output of the last convolutional block, which is a feature map of shape (height × width, feature_dim). This retains spatial information — important for attention.
3. Decoder with cross‑attention
A transformer decoder takes the visual features as K and V in cross‑attention layers, and the previously generated word embeddings as Q. At each step, it predicts the probability distribution over the vocabulary for the next word.
4. Training with cross‑entropy loss
The decoder is trained to maximize the likelihood of the ground‑truth caption word by word, given the image and the previous words. This is called teacher forcing. The loss is the cross‑entropy between the predicted distribution and the actual word ID.
5. Inference with greedy or beam search
At inference time, you start with a special <start> token and feed the decoder’s previous output back as input to generate the next word, until you hit the <end> token or a maximum length. Beam search keeps the top‑B hypotheses and improves coherence.
Hands-on walkthrough
We’ll build a complete, trainable image captioning model using PyTorch and the HuggingFace Transformers library. We’ll use a small synthetic dataset to keep the example runnable in minutes — you can swap in COCO later.
Step 1: Set up environment
pip install torch transformers pillow requests
Step 2: Model definition
We’ll use a pretrained ResNet‑50 as the encoder and a GPT‑2 decoder. The encoder outputs spatial features; the decoder receives them via cross‑attention.
import torch
import torch.nn as nn
from transformers import GPT2LMHeadModel, GPT2Tokenizer, ResNetModel, AutoImageProcessor
class ImageCaptioningModel(nn.Module):
def __init__(self, decoder_name='gpt2', embed_dim=768):
super().__init__()
# Encoder: ResNet from transformers (or torchvision)
self.encoder = ResNetModel.from_pretrained('microsoft/resnet-50')
self.processor = AutoImageProcessor.from_pretrained('microsoft/resnet-50')
# Projection layer: map ResNet features to decoder's embedding dim
self.proj = nn.Linear(2048, embed_dim)
# Decoder: GPT-2
self.decoder = GPT2LMHeadModel.from_pretrained(decoder_name)
self.tokenizer = GPT2Tokenizer.from_pretrained(decoder_name)
self.tokenizer.pad_token = self.tokenizer.eos_token
def forward(self, images, captions):
# images: (B, C, H, W)
with torch.no_grad():
enc_features = self.encoder(images).last_hidden_state # (B, H*W, 2048)
enc_features = self.proj(enc_features) # (B, H*W, embed_dim)
# Decoder: input captions (B, T), output logits
outputs = self.decoder(input_ids=captions, encoder_hidden_states=enc_features)
return outputs.logits
def generate_caption(self, image, max_len=30):
self.eval()
with torch.no_grad():
enc = self.encoder(image.unsqueeze(0)).last_hidden_state
enc = self.proj(enc)
# Start with <eos>? GPT-2 uses <eos> as start
input_ids = torch.tensor([[self.tokenizer.eos_token_id]])
for _ in range(max_len):
outputs = self.decoder(input_ids=input_ids, encoder_hidden_states=enc)
next_token = outputs.logits[0, -1].argmax()
input_ids = torch.cat([input_ids, next_token.unsqueeze(0).unsqueeze(0)], dim=1)
if next_token == self.tokenizer.eos_token_id:
break
return self.tokenizer.decode(input_ids[0], skip_special_tokens=True)
Step 3: Training loop
We’ll use a dummy dataset to demonstrate the loop. In practice, you’ll replace this with your caption dataset.
from torch.utils.data import DataLoader, Dataset
import torch.optim as optim
class DummyCaptionDataset(Dataset):
def __init__(self, num_samples=32, vocab_size=100):
self.num_samples = num_samples
self.vocab_size = vocab_size
def __len__(self):
return self.num_samples
def __getitem__(self, idx):
image = torch.randn(3, 224, 224) # random image
caption = torch.randint(0, 50, (20,)) # random token ids
return image, caption
ds = DummyCaptionDataset()
dl = DataLoader(ds, batch_size=8, shuffle=True)
model = ImageCaptioningModel()
optimizer = optim.AdamW(model.parameters(), lr=1e-4)
for epoch in range(3):
total_loss = 0
for images, captions in dl:
optimizer.zero_grad()
logits = model(images, captions)
loss = nn.CrossEntropyLoss()(logits.view(-1, logits.size(-1)), captions.view(-1))
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}: loss = {total_loss/len(dl):.4f}")
Expected output (approx):
Epoch 1: loss = 3.9123
Epoch 2: loss = 3.8894
Epoch 3: loss = 3.8562
The loss decreases as the model learns to predict the next token (even on random data, it picks up the basic distribution).
Step 4: Inference in action
Here’s how you would use a trained model to caption a new image:
# Load a real image (e.g., from a URL)
from PIL import Image
import requests
url = "https://images.unsplash.com/photo-1507146426996-ef05306b995a?w=400"
img = Image.open(requests.get(url, stream=True).raw).convert("RGB")
processed = model.processor(img, return_tensors="pt").pixel_values
caption = model.generate_caption(processed)
print(caption)
Compare options / when to choose what
There are several ways to build an image captioning model. Here’s a quick comparison:
| Approach | Description | Pros | Cons | When to use |
|---|---|---|---|---|
| CNN + LSTM | Classic encoder–decoder with recurrent decoder | Simple, well‑documented | Slow training, no long‑range dependencies | Learning or small datasets |
| CNN + Transformer (this lesson) | CNN vision encoder, transformer decoder | Fast, state‑of‑the‑art, attends to image regions | Requires more memory | Production, large datasets |
| Vision‑Language Pretrained (e.g., BLIP, BLIP‑2) | Huge pretrained multimodal models | Zero‑shot, excellent quality | Heavy, not easily fine‑tunable for specific domains | Out‑of‑the‑box needs, minimal training |
When to choose: if you’re just starting, the CNN + LSTM approach can help you grasp the fundamentals. But for any real‑world project, go with CNN + Transformer — it’s the architecture behind most modern captioning systems. If you have a domain‑specific task (like medical images) and limited compute, fine‑tuning a small transformer decoder on top of a frozen CNN often beats using a massive pretrained model.
Pro tip: Freeze the CNN encoder during early training. This saves memory and forces the decoder to learn the mapping first. Unfreeze later for fine‑tuning.
Troubleshooting & edge cases
- Loss doesn’t decrease. First, check if the data pipeline is correct — shallow debug with a single batch. Print shapes and ensure no NaNs. If using a pretrained decoder, make sure you’re not freezing it unintentionally.
- The model generates the same caption for every image. This usually means the encoder isn’t contributing enough. Increase the projection dimension or unfreeze the CNN later. Also verify that the cross‑attention is implemented — the decoder must receive
encoder_hidden_states. - Out‑of‑memory (OOM) errors. Reduce batch size, use gradient accumulation, or lower image resolution. For large images, extract features offline and cache them.
- Tokenization mismatch. GPT‑2 has a limited vocabulary — you’ll need to add new tokens for domain‑specific words (e.g., medical terms). Alternatively, train your own tokenizer.
- Beam search produces repetitive output. Set a repetition penalty (e.g.,
no_repeat_ngram_size=2).
What you learned & what's next
You now have a solid mental model of image captioning: a CNN encoder extracts visual features, a transformer decoder generates words using cross‑attention, and the training process uses teacher forcing and cross‑entropy loss. You’ve built a complete, runnable model and seen how to generate captions on a new image.
You achieved the two key learning objectives: you can explain the core idea behind image captioning, and you can complete a practical exercise to build one. You also now understand how to compare different architectures and troubleshoot common issues.
Next lesson: you’ll dive into evaluation metrics for multimodal models, where you’ll learn BLEU, ROUGE, and CIDEr scores, and how to measure caption quality objectively. That’s essential for any serious deployment.
Keep experimenting — try fine‑tuning on a real dataset like COCO, and see how your captions improve. The sky’s the limit.
Practice recap
Try fine-tuning this model on a small dataset like Flickr8k (8k images with captions). Pre-extract features using the CNN, then train the decoder only — you'll see a dramatic improvement in caption quality. Compare results with greedy vs. beam search, and note which produces more natural descriptions.
Common mistakes
- Treating captioning as a classification task: trying to output a fixed-length sentence as a single label leads to poor results.
- Forgetting to remove the classification head from a pretrained CNN — you need the feature map, not the final class probabilities.
- Not padding captions correctly in the batch: mismatched lengths cause shape errors or silent misalignment.
- Failing to freeze the encoder in early training, which can slow convergence and degrade the decoder's learning.
Variations
- Swap the CNN encoder for a vision transformer (ViT) if you need even better spatial reasoning.
- Use beam search instead of greedy decoding for more coherent captions (at a slight speed cost).
- Fine-tune a large vision-language model (e.g., BLIP) instead of training from scratch when you have a domain-specific dataset.
Real-world use cases
- Automatically generating alt-text for images on a news website to improve accessibility and SEO.
- Creating product descriptions for e-commerce listings from photos, reducing manual copywriting effort.
- Helping visually impaired users by describing their surroundings through a smartphone camera feed in real time.
Key takeaways
- Image captioning combines a vision encoder and a language decoder — it's sequence generation conditioned on visual features.
- Cross-attention is the key mechanism that lets the decoder focus on the right image regions for each word.
- Modern implementations use a CNN encoder plus a transformer decoder, outperforming older LSTM-based approaches.
- Freeze the encoder initially and unfreeze later for best training stability.
- Generated captions need evaluation with BLEU/ROUGE/CIDEr metrics to be objectively measured.
- Pretrained vision-language models offer a fast path to production when fine-tuning data is limited.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.