Multimodal transformers
How models see images and hear audio.
Create a free account Try part of it here
Why it matters
Today's assistants read screenshots, charts, PDFs, product photos and handwriting. Some listen and speak too. Underneath, almost all of it is the transformer you just built. Images and audio are cut into small pieces. Each piece becomes a vector, and the model treats those vectors as tokens. Knowing this tells you what these models are good at and what images cost. It also explains why they still misread small text and miscount objects.
What you'll be able to do
- Turn an image into a sequence of tokens with patches, as a vision transformer does
- Explain contrastive training (CLIP) and use it for zero-shot classification
- Describe how vision-language models feed image tokens into a language model
- Estimate what images cost in tokens, and where multimodal models still fail
What it covers
- An image is a sequence of patches
- Connecting pictures and words: contrastive training
- Zero-shot classification
- Vision-language models
- In practice
- Worked example: what one photo costs
An image is a sequence of patches
Think of a jigsaw puzzle. To sort the pieces, you first need pieces. A transformer has the same need. It reads a sequence of vectors, one per token, and a photo isn't a sequence. So the first job is to cut the photo up.
The vision transformer (ViT) does this. Google introduced it in 2020, in a paper titled "An Image is Worth 16x16 Words". It takes four steps.
- Cut the image into small squares called patches.
- Flatten each patch. That means writing all its pixel numbers out in one long row.
- Pass each row through a linear layer, called the patch embedding. It turns the row into a vector of a fixed width.
- Add a position embedding to each vector, so the model knows where each patch sat.
Now the image is a sequence of tokens, like any sentence. The code below paints a tiny picture and runs those four steps.
import matplotlib.pyplot as plt import numpy as np import torch import torch.nn as nn # a 32 × 32 picture with 3 color channels: sky, sun, ground and a red tent H = W = 32 yy, xx = np.mgrid[0:H, 0:W] img = np.zeros((H, W, 3)) img[:] = [0.35, 0.6, 0.9] # sky img[(yy - 7) ** 2 + (xx - 24) ** 2 < 20] = [1.0, 0.85, 0.2] # sun img[yy >= 22] = [0.25, 0.6, 0.3] # ground img[(yy >= 13) & (yy < 24) & (abs(xx - 11) <= (yy - 13))] = [0.85, 0.2, 0.15] # tent p = 8 # patch size in pixels patches = img.reshape(H // p, p, W // p, p, 3).transpose(0, 2, 1, 3, 4).reshape(-1, p * p * 3) print("image:", img.shape, "→ patches:", patches.shape) torch.manual_seed(0) d = 64 to_token = nn.Linear(p * p * 3, d) # the patch embedding position = nn.Parameter(torch.randn(len(patches), d) * 0.02) # one learned vector per patch position tokens = to_token(torch.tensor(patches, dtype=torch.float32)) + position print("image tokens:", tuple(tokens.shape)) plt.imshow(img) for k in range(1, H // p): plt.axhline(k * p - 0.5, color="white", linewidth=1) plt.axvline(k * p - 0.5, color="white", linewidth=1) for i in range(H // p): for j in range(W // p): plt.text(j * p + 1, i * p + 2.5, str(i * (W // p) + j), color="white", fontsize=9) plt.axis("off")
Here's what each piece does.
imgis a 32 × 32 picture. Each pixel has 3 color numbers: red, green and blue. The next four lines paint a sky, a sun, the ground and a red tent.p = 8sets the patch size. The longpatchesline cuts the picture into 8 × 8 squares and flattens each one. It prints(16, 192): 16 patches, each a row of 192 numbers. That's 8 × 8 pixels × 3 colors.to_tokenis the patch embedding. It turns each row of 192 numbers into 64 numbers.positionholds one learned vector for each patch position. Adding it tells the model where each patch came from. The tokens print as(16, 64): 16 tokens, each 64 numbers wide.- The plot shows the picture with a white grid and each patch's number.
From here on, it's the transformer from this phase. Attention lets patch 5, the tent's peak, connect to patch 9, its base. Stacked blocks build up from edges and colors to whole objects. (torch here is the course's in-browser PyTorch subset.)
How many tokens is an image?
Real models use bigger images, with patches of 14 or 16 pixels. To count the tokens, use this rule. Here is the patch size in pixels.
In plain words: count how many patches fit across. Count how many fit down. Multiply the two.
Try it with small numbers. The tent picture is 32 × 32 pixels, with 8-pixel patches. So 32 / 8 = 4 fit across and 4 fit down. That's 4 × 4 = 16 tokens, as the code printed. A 224 × 224 image with 16-pixel patches fits 14 across and 14 down. That gives tokens. In Python, // divides and drops any remainder:
for size, p in [(32, 8), (224, 16)]: across = size // p print(f"{size} x {size} image, {p}-pixel patches: {across} x {across} = {across * across} tokens")
One difference from the CNNs of Phase 4 matters here. A CNN is built to look at nearby pixels together. Nothing in a vision transformer says nearby pixels matter more. The model has to learn that from data. That's why vision transformers need large training sets.
Go deeper How the long reshape line cuts out patches
The array img has the shape (32, 32, 3): rows, columns, colors. The patch line works in three moves.
reshape(4, 8, 4, 8, 3)splits the rows into 4 bands of 8 and the columns into 4 bands of 8. The five axes are now: patch row, row inside the patch, patch column, column inside the patch, color.transpose(0, 2, 1, 3, 4)swaps the middle two axes. Now the order is patch row, patch column, then the pixels inside the patch. Each patch's pixels sit together.reshape(-1, 192)flattens each patch into one row. The-1means "work out how many rows", which comes to 16.
Patches come out in reading order: left to right, then top to bottom. You'll write the same function in the exercise.
An image model is a text model with one new first step. That's why it fits so neatly in a language model.
print((64 // 16) * (48 // 16))
Show the answer
12
4 patches fit across (64 / 16) and 3 fit down (48 / 16). Multiply: 4 × 3 = 12 tokens.
Connecting pictures and words: contrastive training
Picture a party game. You get a pile of photos and a pile of captions, all shuffled. Your job is to pair each photo with its caption. You get better by learning which words go with which sights.
OpenAI's CLIP (2021) learned in much the same way. It has two encoders. One turns an image into a vector. The other turns a caption into a vector. CLIP was trained on about 400 million image and caption pairs from the web.
The training game is called contrastive learning. Each batch holds some images and their captions. The model scores every image against every caption. Then training pushes each image's score with its own caption up. It pushes its scores with all the other captions down.
The score is cosine similarity. Each vector is first scaled to length 1. Then the dot product of two vectors says how closely they point the same way. The top score is 1, when the two vectors point exactly the same way.
Here's a batch of two, with made-up scores. Image 1 goes with caption A. Image 2 goes with caption B.
| blank | caption A | caption B |
|---|---|---|
| image 1 | 0.9 | 0.2 |
| image 2 | 0.3 | 0.8 |
The right answers sit on the diagonal, from top left to bottom right. The grading is a cross-entropy loss, which you met in Phase 2. Each image is graded on how much probability it gives its own caption. The code below trains a small CLIP this way.
import math import torch import torch.nn as nn import torch.nn.functional as F torch.manual_seed(0) concepts = torch.randn(10, 6) # 10 hidden "things", shared by pictures and words to_pixels, to_words = torch.randn(6, 40), torch.randn(6, 20) def make_pairs(n): c = torch.randint(0, 10, (n,)) z = concepts[c] + 0.3 * torch.randn(n, 6) images = z @ to_pixels + 0.5 * torch.randn(n, 40) # stand-in images: 40 pixel features captions = z @ to_words + 0.5 * torch.randn(n, 20) # stand-in captions: 20 word features return images, captions, c image_encoder, text_encoder = nn.Linear(40, 16), nn.Linear(20, 16) temperature = 0.1 def unit(v): return v / v.norm(dim=-1, keepdim=True) def clip_loss(img, txt): logits = unit(img) @ unit(txt).T / temperature # every image against every caption labels = torch.arange(len(img)) # the matching pair is on the diagonal return (F.cross_entropy(logits, labels) + F.cross_entropy(logits.T, labels)) / 2 @torch.no_grad() def zero_shot_accuracy(): images, _, truth = make_pairs(500) label_texts = text_encoder(concepts @ to_words) # one clean caption per concept guess = (unit(image_encoder(images)) @ unit(label_texts).T).argmax(dim=-1) return (guess == truth).float().mean().item() print(f"zero-shot accuracy before training: {zero_shot_accuracy():.0%}") opt = torch.optim.Adam(list(image_encoder.parameters()) + list(text_encoder.parameters()), lr=1e-2) for step in range(201): images, captions, _ = make_pairs(32) loss = clip_loss(image_encoder(images), text_encoder(captions)) opt.zero_grad() loss.backward() opt.step() if step % 50 == 0: print(f"step {step:>3} loss {loss.item():.3f}") print(f"zero-shot accuracy after training: {zero_shot_accuracy():.0%}")
The images and captions here are simulated, to keep the example fast. The training is CLIP's.
conceptsholds 10 hidden "things".make_pairsbuilds fake images (40 numbers each) and fake captions (20 numbers each) from them, plus some noise.image_encoderandtext_encodereach turn their input into 16 numbers. These two are what training changes.unitscales each vector to length 1.clip_lossbuilds the score table, every image against every caption, and divides it bytemperature. It grades both ways: each image picks a caption, and each caption picks an image. The loss is the average of the two.- The loop trains for 201 steps on batches of 32 pairs. It prints the loss every 50 steps.
After training, images and text live in one shared embedding space. A picture and its caption land close together. The test score climbs from chance, about 10% with 10 concepts, to over 90%.
The loss doesn't reach zero. Part of the reason: a batch of 32 drawn from only 10 concepts holds look-alikes. Two images of the same concept still count as "wrong" for each other's captions. Real web data has the same problem.
Go deeper What the temperature does
Dividing by a temperature of 0.1 is the same as multiplying by 10. So scores of 0.9 and 0.2 become 9 and 2.
That matters because cross-entropy works on softmax probabilities, which you met in Phase 2. Softmax on 0.9 and 0.2 gives a split of about 67% to 33%. Softmax on 9 and 2 gives about 99.9% to 0.1%. A small temperature makes the model commit to its best match. Real CLIP learns its temperature during training.
Show the answer
Its own caption, which should score higher than every other caption in the batch
The loss is cross-entropy over the batch's captions, with the matching caption as the target.
Zero-shot classification
A shared space gives you something for free. Say you want to sort photos into labels. Write one caption per label, such as "a photo of a tent" and "a photo of a jacket". Turn each caption into a vector. Then give each image the label whose caption is most similar.
This is zero-shot classification. "Zero-shot" means the model saw zero labeled examples for your task. You never trained it on photos tagged "tent". The zero_shot_accuracy test in the code above works this way. It writes one clean caption per concept and picks the closest.
CLIP-style encoders power image search, content moderation and duplicate detection. They're also the eyes of many vision-language models, which come next.
Show the answer
Embed a caption for each label, then give each photo the label whose caption is most similar
Images and captions share one embedding space. So a caption per label is enough: pick the closest one. No labeled photos are needed.
Vision-language models
How do you let a chatbot see? The common recipe, made popular by LLaVA in 2023, joins three parts.
- A vision encoder, often CLIP-style, turns the image into patch vectors.
- A small projector maps those vectors into the language model's embedding space. Afterwards, each one looks like a token embedding.
- The language model receives the image tokens alongside the text tokens. It attends over all of them together.
The projector works like a travel plug adapter. The vision encoder's output has the wrong shape for the language model. The projector reshapes it so it fits.
Training usually starts with the projector alone, on image and caption pairs. Then more of the system is fine-tuned on instructions about images. Some newer models train on images and text together from the start. They're called "natively multimodal". The idea is the same: everything becomes a sequence of vectors in one model.
Audio works the same way. OpenAI's Whisper (2022) cuts sound into 30-second clips. It turns each clip into a spectrogram: a picture of how much energy each frequency has over time. Think of sheet music, which shows which notes sound at each moment. A transformer encoder reads the spectrogram, and a decoder writes the transcript. Whisper was trained on 680,000 hours of audio. Some speech-capable assistants feed audio tokens straight to the language model. Others pair the model with separate tools for speech recognition and speech synthesis.
Generating images is usually a different technique. Most image generators are diffusion models. They start from random noise and remove it step by step, steered by a text encoder. Some newer systems instead generate image tokens the way an LLM generates words.
Show the answer
Vision encoder, then projector, then the language model with the text tokens
The vision encoder makes patch vectors. The projector maps them into the language model's embedding space. Then the language model attends over image and text tokens together.
In practice
Images cost tokens. Each image becomes hundreds to thousands of tokens, and you pay for tokens. Models merge patches, and providers publish their own counting rules. Anthropic's documentation has given an estimate of about (width × height) / 750 tokens. Large images are shrunk first.
Worked example: what one photo costs
A product photo is 1,000 × 1,000 pixels. Count its whole 16-pixel patches. Then compare that with the estimate of (width × height) / 750 tokens.
width, height = 1000, 1000 patch = 16 patches = (width // patch) * (height // patch) estimate = width * height / 750 print("whole 16-pixel patches:", patches) print("estimated tokens:", round(estimate))
Across, 1,000 / 16 is 62 whole patches, with 8 pixels left over. Down is the same. So 62 × 62 = 3,844 patches.
The estimate is 1,000 × 1,000 = 1,000,000 pixels, divided by 750. That's about 1,333 tokens, roughly a third of the raw patch count. Real models merge patches and follow their own counting rules, so the raw patch count is only a starting point. Either way, ten such photos add more than 13,000 tokens to a prompt.
Fine detail is the weak spot. Small print, dense tables, exact counts and precise positions are harder than "what's in this picture". Detail can fall between or inside patches, and it gets compressed. Cropping to the part that matters helps. So does sending a higher-resolution crop.
Charts and screenshots are read, not measured. A model can describe a chart's trend well and still misread the exact values. When the numbers matter, give it the data too.
For marketers: vision models make it practical to tag product photos, check ads against brand guidelines, or pull text from screenshots of competitors' pages. Check any extracted numbers and small print before they go into a report.
Show the answer
The image was cut into patches and compressed, so fine detail like exact values is easy to misread
Models read charts rather than measure them. The overall shape survives the patches well, but small labels and exact values are a known weak spot. When numbers matter, send the data too.
Practice what you just read
- 4 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
How do most vision-language models let a language model 'see' an image?