Index Ai Academy
Phase 5 · Transformers · Scale & inference

Multimodal transformers

How models see images and hear audio.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. An image is a sequence of patches
  2. Connecting pictures and words: contrastive training
  3. Zero-shot classification
  4. Vision-language models
  5. In practice
  6. 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.

  1. Cut the image into small squares called patches.
  2. Flatten each patch. That means writing all its pixel numbers out in one long row.
  3. Pass each row through a linear layer, called the patch embedding. It turns the row into a vector of a fixed width.
  4. 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")
It runs right here in your browser, with nothing to install.

Here's what each piece does.

  • img is 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 = 8 sets the patch size. The long patches line 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_token is the patch embedding. It turns each row of 192 numbers into 64 numbers.
  • position holds 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 pp is the patch size in pixels.

tokens=widthp×heightp\text{tokens} = \frac{\text{width}}{p} \times \frac{\text{height}}{p}

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 14×14=19614 \times 14 = 196 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.

  1. 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.
  2. 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.
  3. reshape(-1, 192) flattens each patch into one row. The -1 means "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.

Try it now

An image model is a text model with one new first step. That's why it fits so neatly in a language model.

Quick check
Predict what this prints. It counts the patch tokens for a 64 × 48 image with 16-pixel patches.
print((64 // 16) * (48 // 16))
  1. 192
  2. 7
  3. 12
  4. 3072
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.

blankcaption Acaption B
image 10.90.2
image 20.30.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.

  • concepts holds 10 hidden "things". make_pairs builds fake images (40 numbers each) and fake captions (20 numbers each) from them, plus some noise.
  • image_encoder and text_encoder each turn their input into 16 numbers. These two are what training changes.
  • unit scales each vector to length 1.
  • clip_loss builds the score table, every image against every caption, and divides it by temperature. 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.

Quick check
In contrastive (CLIP) training, what is each image's 'correct answer' in a batch?
  1. The pixels of the next image
  2. The average of all captions
  3. Its own caption, which should score higher than every other caption in the batch
  4. A class label from a fixed list
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.

Quick check
A shop wants to sort product photos into 'shoes', 'bags' and 'hats' with a CLIP-style model. It has no labeled photos. What should it do?
  1. Embed a caption for each label, then give each photo the label whose caption is most similar
  2. Ask the model to generate a label one word at a time
  3. Compare the photos' pixels with each other
  4. Label thousands of photos by hand first, then train a new classifier
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.

  1. A vision encoder, often CLIP-style, turns the image into patch vectors.
  2. A small projector maps those vectors into the language model's embedding space. Afterwards, each one looks like a token embedding.
  3. 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.

Quick check
In a vision-language model, in what order does an image travel?
  1. Straight into the language model as raw pixel numbers
  2. Language model, then projector, then vision encoder
  3. Projector, then vision encoder, then a separate image-answering model
  4. Vision encoder, then projector, then the language model with the text tokens
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

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))
In the lesson you can change this and run it.

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.

Quick check
A vision model describes a sales chart's trend correctly but gets one monthly figure wrong. What's the most likely reason?
  1. The image was cut into patches and compressed, so fine detail like exact values is easy to misread
  2. Vision models round every number on purpose
  3. The model can't see charts at all and guessed the trend
  4. The chart used too many colors for the model
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

Create a free account

Try a quiz question

How do most vision-language models let a language model 'see' an image?

  1. A vision encoder turns patches into vectors, a projector maps them into the LLM's embedding space, and the LLM attends over them with the text
  2. The image is converted to a text description by a separate caption model first, and after that only the text ever reaches the LLM, which never sees the picture itself
  3. The LLM reads the raw pixel values as long lists of numbers in the prompt, one token per number, and it learns during training what those numbers tend to mean
  4. A separate image model answers all the image questions on its own, without the LLM, and the app then pastes its answer into the conversation as if the LLM had written it

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson