13. Vision Transformers
The previous chapter built an encoder for sequences of text tokens. In 2021 Dosovitskiy et al. asked a deliberately blunt question: what if you feed an image to that same encoder, unchanged?1 The answer β "An Image is Worth 16Γ16 Words" β was that with enough data a near-vanilla Transformer beats a CNN at image classification, with no convolutions anywhere.
The result mattered less as a classification score than as a demonstration: the architecture does not need to know it is looking at an image. That is why the ViT is the bridge chapter. Once you have it, the image encoders inside CLIP, inside every vision-language model, inside Stable Diffusion and DiT all stop being separate things to learn.
The inductive-bias trade, priced honestly
Chapter 10 called a convolution a strong, cheap, fixed prior. A self-attention layer has none of it: every patch may attend to every other patch from layer one, and any notion that neighbouring pixels are related has to be learned from data.
That is a trade, not an improvement, and the original paper measured both sides of it:
| Pretraining data | Result |
|---|---|
| ImageNet-1k only (~1.3M images) | ViT loses to a comparable ResNet |
| ImageNet-21k (~14M) | Roughly level |
| JFT-300M (~300M) | ViT wins, and transfers better |
Read the table as a single sentence: the convolutional prior is a floor at small scale and a ceiling at large scale. Below some data threshold it gives you for free what the Transformer has to buy; above it, it forbids relationships the Transformer is free to discover.
This threshold is not a law of nature, and it moved
DeiT2 showed that most of the "ViT needs 300M images" claim was really "ViT needs a training recipe nobody had written yet" β with the right augmentation, regularization and a distillation token, a ViT trains competitively on ImageNet-1k alone. ConvNeXt8 then made the symmetric point from the other side. The modern reading is that recipe and scale explain most of the gap the 2021 papers attributed to architecture.
The pipeline
The only genuinely new mechanism is the first step. Everything after it is the encoder you already know.
An image \(x \in \mathbb{R}^{H \times W \times C}\) is cut into non-overlapping \(P \times P\) patches, each flattened and projected by one shared linear layer:
For \(224 \times 224\) and \(P = 16\), that is \(N = 196\) tokens. Positional embeddings are added, because attention alone cannot tell the top-left patch from the bottom-right one, and the sequence goes through \(L\) standard pre-norm encoder blocks.
Patch embedding is a convolution
A linear projection over non-overlapping \(P \times P\) patches is exactly Conv2d(C, d, kernel_size=P, stride=P), and that is how every implementation does it β one op that patchifies and projects at once. So the "no convolutions" claim has an asterisk: there is exactly one, at the stem, and it is the only place a spatial prior enters the model.
Patch size is the whole design decision
\(P\) controls two things at once, in opposite directions, and there is nothing else in a ViT with that much leverage.
Everything inside a patch is compressed into one \(d\)-dimensional vector before attention ever runs. No later layer can recover what the projection discarded. Drag the patch size to 32 and watch a face disappear into four tokens.
Pull it the other way and the count explodes: halving \(P\) quadruples \(N\), and the attention term grows with \(N^2\). Doubling the resolution does the same. This single quadratic is why:
- 14 and 16 are the near-universal patch sizes, and have been for five years;
- high-resolution vision-language models tile a large image into several 224- or 336-pixel crops rather than feeding one huge grid to the encoder;
- NaViT-style patch-and-pack9 exists at all β pack variable-resolution images into one sequence, keep the native aspect ratio, and skip the tokens you do not need.
What changed since 2021
The 2021 ViT is still recognizable, but a model you would download today differs in five places, and all five are worth knowing because they show up in the vision encoder of every multimodal model.
-
The
[CLS]token mostly went away
Reading the class from one special token was a BERT habit. Most current encoders use global average pooling or a small attention-pooling head over the patch tokens instead β as good or better, and it removes an odd token that has to be trained to be a summary.
And in a vision-language model the classification head is gone entirely: what gets passed to the language model is the patch tokens, projected into the LLM's embedding space.
-
Learned positions β 2-D RoPE
Learned absolute position embeddings have to be interpolated whenever the resolution changes, and that interpolation costs accuracy. 2-D RoPE β the rotation from chapter 11, applied separately to the row and column axes β is relative by construction and handles a change of resolution without surgery.
-
Register tokens
Trained ViTs put enormous attention on a few meaningless background patches β using them as scratch space, which wrecks the attention maps and hurts dense tasks. Adding a handful of unused register tokens10 gives the model somewhere to put that state. The artifacts vanish, the attention maps become interpretable again, and dense-prediction scores improve. A one-line fix to a five-year-old bug.
-
Hierarchy, where it is needed
A plain ViT holds one resolution throughout, which is wasteful for detection and segmentation. Swin3 reintroduced windowed attention and a pyramid; ViTDet showed a plain ViT can also be adapted with a simple feature pyramid on top. Both are alive; the plain backbone won for pretraining, the hierarchical one for dense downstream tasks.
What a ViT is actually for in 2026
Very little of the deployed use is "classify this image into 1000 classes". The ViT won by becoming the standard way to turn pixels into tokens for something else:
| Use | What the ViT provides |
|---|---|
| Vision-language models | The image encoder. Patch tokens are projected and prepended to the text sequence β this is how GPT-4o-class, Claude-class and Gemini-class models see. |
| Contrastive image-text | The image tower of CLIP and SigLIP (chapter 19), and therefore the text conditioning of every image generator. |
| Self-supervised features | DINOv2/v37 produces general-purpose dense features without labels that beat supervised ones on segmentation, depth and correspondence β used frozen, as a feature extractor, across robotics and medical imaging. |
| Generative backbones | DiT and MMDiT are ViTs operating on latent patches instead of pixel patches (chapter 22). |
| Masked pretraining | MAE6 β mask 75% of patches, reconstruct them. Cheap (the encoder only sees the visible 25%) and a strong initializer. |
The reason it won is not accuracy
A ViT and a modern ConvNet score about the same on ImageNet. The ViT is everywhere anyway, because it emits a sequence of tokens in the same format text uses. Concatenating patch tokens with word tokens and running one Transformer over both is trivial; doing the equivalent with a convolutional feature map is not. Multimodality is the argument, not classification.
CNN vs. ViT, updated
| CNN | Vision Transformer | |
|---|---|---|
| Core operation | Convolution (local, fixed) | Self-attention (global, learned) |
| Inductive bias | Strong | Weak β bought with data |
| Receptive field | Grows with depth | Global at layer 1 |
| Cost | \(O(\text{pixels})\) | \(O(N^2)\) in patches |
| Small data | Wins | Needs pretraining or a DeiT-style recipe |
| Web-scale data | Saturates earlier | Keeps improving |
| Multimodal use | Awkward | Native β it already emits tokens |
| Still standard for | Latent autoencoders, medical segmentation, on-device | Everything upstream of an LLM |
Implementation reference
import torch, torch.nn as nn
class PatchEmbed(nn.Module):
"""Image -> sequence of patch tokens. One Conv2d does patchify AND projection."""
def __init__(self, patch=16, in_ch=3, dim=768):
super().__init__()
self.proj = nn.Conv2d(in_ch, dim, kernel_size=patch, stride=patch)
def forward(self, x): # (B, C, H, W)
x = self.proj(x) # (B, dim, H/p, W/p)
return x.flatten(2).transpose(1, 2) # (B, N, dim)
class ViT(nn.Module):
"""Registers instead of a CLS token, mean pooling instead of a CLS head."""
def __init__(self, dim=768, depth=12, heads=12, n_classes=1000,
n_patches=196, n_registers=4):
super().__init__()
self.patch_embed = PatchEmbed(dim=dim)
self.reg = nn.Parameter(torch.zeros(1, n_registers, dim))
self.pos = nn.Parameter(torch.zeros(1, n_patches, dim))
layer = nn.TransformerEncoderLayer(dim, heads, dim * 4, activation='gelu',
norm_first=True, batch_first=True)
self.encoder, self.norm = nn.TransformerEncoder(layer, depth), nn.LayerNorm(dim)
self.head = nn.Linear(dim, n_classes)
self.n_reg = n_registers
def forward(self, x):
x = self.patch_embed(x) + self.pos
x = torch.cat([self.reg.expand(x.size(0), -1, -1), x], dim=1)
x = self.encoder(x)[:, self.n_reg:] # drop the registers; they were scratch space
return self.head(self.norm(x).mean(dim=1))
import timm
# a supervised ViT, fine-tuned on your classes
model = timm.create_model('vit_base_patch16_224', pretrained=True, num_classes=10)
# DINOv2 features, frozen β usually the stronger option when labels are scarce
dino = torch.hub.load('facebookresearch/dinov2', 'dinov2_vitb14')
feats = dino.forward_features(img)['x_norm_patchtokens'] # (B, N, 768)
Key takeaways
- A ViT is the text encoder applied to image patches. The only new part is patchify β and that is one strided convolution.
- The convolutional prior is a floor at small scale and a ceiling at large scale. That is the whole trade.
- The 2021 data threshold was largely a recipe problem. DeiT and ConvNeXt closed the gap from both directions.
- Patch size is the design decision: information inside a patch is destroyed before attention runs, and \(N\) grows quadratically as \(P\) shrinks or resolution grows. 14β16 is where the compromise lands.
- A current encoder uses pooling instead of
[CLS], 2-D RoPE instead of learned positions, and register tokens to absorb the attention artifacts. - ViTs won because they emit tokens, which is what a language model eats. The classification score was never the point.
- In production a ViT is usually the vision encoder of something else: a VLM, CLIP/SigLIP, DINOv2 features, or a diffusion backbone.
-
Dosovitskiy, A., et al. (2021). An Image is Worth 16Γ16 Words: Transformers for Image Recognition at Scale β ICLR. β©
-
Touvron, H., et al. (2021). Training data-efficient image transformers & distillation through attention (DeiT) β ICML. The recipe that removed the JFT-300M requirement. β©
-
Liu, Z., et al. (2021). Swin Transformer: Hierarchical Vision Transformer using Shifted Windows β ICCV. β©
-
Vaswani, A., et al. (2017). Attention Is All You Need β NeurIPS. (The encoder block ViT reuses, unchanged.) β©
-
Steiner, A., et al. (2021). How to train your ViT? Data, Augmentation, and Regularization in Vision Transformers β TMLR. β©
-
He, K., et al. (2022). Masked Autoencoders Are Scalable Vision Learners β CVPR. Mask 75%, encode only what is visible. β©
-
Oquab, M., et al. (2024). DINOv2: Learning Robust Visual Features without Supervision β TMLR. Self-supervised features good enough to use frozen. β©
-
Liu, Z., et al. (2022). A ConvNet for the 2020s (ConvNeXt) β CVPR. β©
-
Dehghani, M., et al. (2023). Patch n' Pack: NaViT, a Vision Transformer for any Aspect Ratio and Resolution β NeurIPS. β©
-
Darcet, T., Oquab, M., Mairal, J., & Bojanowski, P. (2024). Vision Transformers Need Registers β ICLR. The high-norm artifact tokens, diagnosed and fixed. β©