TAI BUI
Back to curriculum
CapstonePython

Vision Encoder Patches

A vision model that reads pixels needs a tokenizer for pixels, Patch embedding is that tokenizer. Cut the image into a grid of squares, flatten each square, and project it through one linear layer. Then add a 2D position signal so the transformer knows where each square sat in the original image.

TIME: ~90 minutesPREREQUISITES: Phase 19 lessons 30-37 (Track B foundations)

Learning Objectives

  • Tokenize an image into a fixed-length sequence of patch embeddings.
  • Implement a `Conv2d`-based patch projection that matches the math of unfold-then-linear.
  • Build a deterministic 2D sinusoidal position embedding so token order encodes spatial position.
  • Verify patch count, embedding shape, and `Conv2d`/unfold equivalence on a synthetic fixture.

The Problem

Patch embedding is the front end that turns an image into a sequence of tokens a transformer can read. It cuts the pixel grid into square regions and maps each region to one vector, so a few hundred tokens each summarize one square of the image.

The need comes from a mismatch. A transformer consumes a sequence of vectors, and an image is a 3-channel grid. Reading every pixel as a token explodes the sequence length: a 224x224 RGB image is 150,528 tokens, which a 12-layer transformer cannot afford in attention. Reading the image as one giant flat vector throws away locality, and the attention layer cannot recover it.

One linear projection handles this. A 224x224 image cut into 16x16 patches produces a 14x14 grid of 196 patches. Each patch is flattened from (3, 16, 16) = 768 pixel values into one vector, then a linear layer maps it to the model's hidden dimension. The transformer sees 196 tokens of dimension hidden (commonly 768) plus a CLS token, a sequence the rest of the network can handle.

Warm-up0/3
1. A 2D convolution has kernel size 3 and stride 3, with no padding, and runs over a single-channel 9x9 grid. How many output positions does it produce?Warm-up
2. A 3-channel image that is 8x8 pixels is flattened into a single vector. How many values does the vector hold?Warm-up
3. Attention treats its input as a set of vectors with no built-in order. What must you add so the model can tell where in an image a token came from?Warm-up
Check: The Problem0/3
1. What does sequence length mean for a transformer reading patch tokens?Define
2. Why does a transformer-based vision model avoid treating every pixel of a 224x224 image as its own token?Explain
3. You change patch_size to 32 on a 224x224 image and still prepend one CLS token. How many tokens does the transformer see?Apply

The Concept

Mermaid diagram

Why patches, not pixels

Attention cost grows with the square of the sequence length. A 196-token sequence costs 196 * 196 = 38,416 attention scores per head per layer, while a 150,528-token sequence costs 150,528 * 150,528 = 22.6 billion. Patches buy a 590,000x reduction in attention compute, and a single 16x16 region carries enough signal for high-level vision tasks. You pay with fine-grained spatial detail inside one patch, which is why downstream multimodal stacks often run a second high-resolution branch when fine localization matters.

Why a linear projection is enough

A patch embedding treats each patch as an independent vector and projects it with one learned matrix. That projection learns a basis: edge detectors, color filters, simple textures. It is small (768 * 768 = 589,824 parameters for ViT-Base) and trains fast. Deeper convolutional stems exist (the "hybrid" ViT), but the flat linear projection is the standard, and most modern open-weight encoders ship with this exact shape.

The Conv2d trick

A Conv2d(in_channels=3, out_channels=hidden, kernel_size=patch_size, stride=patch_size) with no padding gives the same numerical result as unfold-then-linear, because each output position dot-products the patch pixels against one filter. The convolution is the patch projection. Most production codebases ship it this way: it is faster on GPU and uses one fewer reshape.

Position embeddings

A position embedding is a vector added to each token that says where in the grid the token came from. The projection alone leaves tokens with no order, so this signal has to be supplied. The 2D sinusoidal version gives each token a fixed signal that encodes its (row, col) position. Half the embedding dimension encodes row position with sin/cos at multiple frequencies, and the other half encodes column position. The encoding is deterministic, so the table can be rebuilt for a different grid without retraining, and it interpolates cleanly to grids the model never saw at training time. In this lesson's code, PatchEmbed raises ValueError unless the input matches cfg.image_size, so a new resolution means a new config. sinusoidal_2d also requires dim divisible by 4, because each half splits again into sin and cos.

ComponentShapeParameters
Patch projection (Conv2d)(hidden, 3, patch, patch)3 * P * P * hidden + hidden
Position embedding (fixed)(num_patches, hidden)0 (computed, not learned)
CLS token (learned)(1, hidden)hidden

For ViT-Base/16 at 224 resolution: 590,592 parameters in the projection, 768 in the CLS token, and zero for sinusoidal position. The next lesson (59) stacks a 12-layer transformer on top of this front end.

Equivalence as a sanity check

The patch step has two spellings: a Conv2d projection and an explicit unfold-then-linear. With the same weights they must produce the same output. If they do not, the unfold math is wrong and the rest of the encoder is built on sand. The tests in this lesson exercise that equivalence.

Check: The Concept0/17
1. In a vision transformer front end, what is a patch?Define
2. What is a patch embedding?Define
3. What is a 2D sinusoidal position embedding?Define
4. What is the CLS token in this front end?Define
5. Why can a single learned linear projection per patch be enough for the front end?Explain
6. Why does a Conv2d with kernel size and stride both equal to the patch size, and no padding, match unfold-then-linear?Explain
7. Why is the Conv2d-versus-unfold-then-linear comparison a useful sanity check on the patch front end?Explain
8. How does the 2D sinusoidal table encode where a token sits in the grid?Explain
9. Why does this lesson use a deterministic 2D sinusoidal position table rather than a learned one?Explain
10. What does a model give up by using 16x16 patches instead of single pixels as tokens?Explain
11. Using the lesson's parameter formula 3 * P * P * hidden + hidden, how many parameters does the Conv2d projection have for patch size 8 and hidden size 64?Apply
12. A teammate writes Conv2d(3, 768, kernel_size=16) and leaves stride at its default of 1. The model then fails when the position table is added. What is the root cause?Analyze
13. A model with 16x16 patches fails to localize an object only a few pixels wide. Which property of the front end explains this?Analyze
14. Your unfold-then-linear output differs from Conv2d by a max of about 100 with identical weights, far above float tolerance. Your unfold flattens each patch as (height, width, channel), while the Conv2d weight flattens as (channel, height, width). How should you read this?Analyze
15. Your deployment will change the input resolution after training. Which position signal fits best?Design
16. Your model will only ever see 224x224 images and you want the strongest accuracy. What does the lesson's reasoning suggest for position, and how would you check it?Design
17. You will ship a patch front end and worry a later refactor could silently break the projection math. Which design guards against that?Design

Build It

code/main.py implements:

  • PatchEmbed, an nn.Module wrapping Conv2d for patch projection.
  • sinusoidal_2d(grid_h, grid_w, dim), a stateless function that builds the 2D position table.
  • VisionFrontEnd, which composes patch embedding, CLS prepend, and position addition into one forward pass.
  • A synthesize_image(seed) helper that builds a deterministic 224x224x3 fixture from numpy.random.
  • A demo that runs one fixture image through the front end and prints the output shape, the CLS token norm, the norms of the first eight tokens, the first eight values of one position row, a batch-consistency drift, and the max difference between the Conv2d projection and unfold-then-linear.

Run it:

bash
python3 code/main.py

Output: the 224x224 fixture is tokenized to a sequence of shape (1, 197, 768). The first token is the CLS and the next 196 are patch tokens. Every patch row of the position table has the same norm, which is the sinusoidal signature. The demo does not print that norm, so check it yourself with model.pos_embed[0, 1:].norm(dim=-1).

Check: Build It0/9
1. You build FrontEndConfig with image_size=224 and feed a 256x256 image into this lesson's PatchEmbed. What happens?Apply
2. You call sinusoidal_2d(14, 14, 770). What happens?Apply
3. The default front end reports 591,360 parameters. How many of them belong to the position table?Apply
4. The demo prints pos[1, :8] and every value is +0.000. Why?Apply
5. FrontEndConfig(image_size=224, patch_size=15) is built and cfg.grid_size is read. What happens?Apply
6. Three different images go through VisionFrontEnd in one batch. How do their output CLS vectors (position 0) compare?Apply
7. In sinusoidal_2d(2, 2, 8) the tokens are in row-major order (0,0), (0,1), (1,0), (1,1). Which two rows of the (4, 8) table share the same last four values, the column half?Apply
8. The demo's last check prints a max abs difference near 1e-6 between unfold-then-linear and Conv2d, then 'ok'. What does that show?Apply
9. A teammate adds sinusoidal_2d(14, 14, 768) directly to the 197-token sequence and gets a shape error. What did they miss?Analyze

Use It

The same patch front end sits at the start of the modern vision-language models: CLIP ViT-L/14, SigLIP, DINOv2, the Qwen-VL family, and the InternVL stack all begin with a Conv2d patch projection plus a position signal. They differ downstream: CLS vs no-CLS pooling, register tokens, patch sizes of 14 vs 16, dynamic resolution via interpolated positions. The front end you build here is the base each of those models stands on.

Check: Use It0/2
1. Why can the front end built in this lesson serve as the base for CLIP ViT-L/14, SigLIP and DINOv2?Explain
2. Some encoders use 14x14 patches instead of 16x16. With a 224x224 input and one CLS token, what sequence length does the front end produce?Apply

Tests

code/test_main.py covers:

  • patch count matches (image_size / patch_size) ** 2
  • output shape matches (batch, num_patches + 1, hidden)
  • the Conv2d projection equals manual unfold-then-linear on a small fixture
  • sinusoidal position table is deterministic across calls
  • CLS token broadcasts across batch dim without leakage

Run them:

bash
python3 -m unittest code/test_main.py
Check: Tests0/3
1. A test builds FrontEndConfig(image_size=96, patch_size=16, hidden=64). What are num_patches and the sequence length with CLS?Apply
2. A test feeds torch.randn(2, 3, 64, 64) to VisionFrontEnd with patch_size=16 and hidden=32. What output shape does it check?Apply
3. A profile at batch sizes 1, 8 and 64 shows the attention layers taking most of the time and the patch projection very little. Where should you spend optimization effort first?Design

Exercises

  1. Replace the sinusoidal position with a learned nn.Parameter and compare the first-epoch loss on a tiny synthetic classification task. Learned positions win at fixed resolution, and sinusoidal wins when you change resolution after training.

  2. Swap the Conv2d for an explicit nn.Unfold plus nn.Linear and assert the outputs match to within float tolerance. The math is the same, spelled two ways.

  3. Add support for non-square patch sizes (e.g. 32x16 for wide-aspect inputs) and verify the position table handles non-square grids.

  4. Profile the patch step at batch sizes 1, 8, 64. The patch projection is rarely the bottleneck, and the attention layers downstream dominate.

  5. Train the front end as a frozen feature extractor on a 4-class synthetic shape dataset (circles, squares, triangles, stars). The CLS token output should linearly separate.

Key Terms

TermWhat it means
PatchA square sub-region of the image, typically 14x14 or 16x16
Patch embeddingLinear projection of one flattened patch to the hidden dim
Sequence lengthNumber of tokens after patch tokenization, usually plus CLS
Sinusoidal positionFixed sin/cos signal that encodes 2D grid coordinates
CLS tokenLearned vector prepended to the sequence as the pooling head
Author's Note

My bet is on the sinusoidal table if you expect to change resolution after training, since it is computed rather than learned and carries zero parameters. I'd reach for a learned position parameter when the resolution is fixed, and exercise 1 is how I'd check that instead of taking my word for it.

For the projection itself, I'd ship the Conv2d spelling and keep the unfold-then-linear version around as a test reference. It costs one reshape more, but it is the version where the math is visible, so if the encoder ever misbehaves it is the first thing I'd compare against.

I'd also hold off on tuning the patch step. Exercise 4 has you profile it, and I expect you'll find the attention layers downstream deserve the attention.

Further Reading

  • An Image is Worth 16x16 Words (ViT, 2021) for the original patch-embed framing.
  • Attention Is All You Need (2017) for the sinusoidal position formula adapted here to 2D.
  • DINOv2 paper for register tokens, an extension you can add as exercise 6.

Code