Vision Transformers (ViT) and When to Use Them
In this series (30 parts)
- Computer Vision Roadmap: From Basics to Real Projects
- What is Computer Vision? From Pixels to Decisions
- A Short History of Computer Vision: 1545 to Now
- Math for CV Beginners: Vectors, Matrices, Convolutions
- Image Fundamentals: Color, Histograms, Noise, Filtering
- How Images and Video Are Stored: Colour, JPEG, Frames
- OpenCV Setup + First 10 Tasks in Python
- Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
- CV Project Workflow: Dataset, Baseline, Error Iteration
- Intensity Transforms and Frequency-Domain Filtering
- Edge Detection and Thresholding That Actually Work
- Morphology, Contours, and Shape Analysis for Real Images
- Feature Matching (SIFT/ORB) and Image Stitching
- Camera Calibration and Perspective Correction
- Epipolar Geometry, Stereo Vision, and Depth Estimation
- Optical Flow and Motion Tracking in Video
- HOG, HOF and MBH: Descriptors Before Deep Learning
- Your First Image Classifier (PyTorch + Transfer Learning)
- Data Pipelines and Augmentation for Vision Models
- CNN Architectures Explained: From LeNet to ResNet
- YOLO Detection Pipeline: Data to Inference
- Semantic and Instance Segmentation: U-Net to Mask R-CNN
- Vision Transformers (ViT) and When to Use Them
- CV Explainability: Grad-CAM, Failures, Bias Checks
- 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
- Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
- Video Understanding: Flow Networks, Interpolation, Stabilisation
- Real-Time CV Systems: Tracking, Latency, Streaming
- Deploying CV Models: ONNX, TensorRT, Edge, and APIs
- Responsible CV: Privacy, Fairness, Security, Governance
A shipping container with a dented side panel looks completely normal in close-up. The dent only reads as damage when you compare that panel against the straight edges of the container three metres away. A convolution kernel sees a 3×3 neighbourhood. That comparison is not available to it until many layers deep.
Prerequisites: CNN architectures and your first image classifier.
Why convolution struggles here
A container yard inspector flags damage. The visual cue for a dent is not local texture — it is that one panel is no longer coplanar with the panels far from it.
A 3×3 convolution’s receptive field grows slowly. It takes many stacked layers before any single unit can see both the dent and the reference edge, and by then the signal has passed through dozens of nonlinearities.
inductive bias An assumption baked into an architecture. Convolution assumes nearby pixels matter most and that a pattern means the same thing anywhere in the image.Convolution’s bias is usually right, which is why CNNs work so well on limited data. But when a task genuinely depends on long-range relationships, that bias is a constraint. ViT removes it and pays for the removal with data.
An image as a sequence
The whole idea is one preprocessing step.
Each patch flattens into a vector of length 16 and goes through one shared linear layer. Now you have four vectors, and everything after this point is the standard transformer used for text.
The real numbers for ViT-Base/16
Input 224×224×3, patch size 16.
Each patch flattened:
That flattened vector goes through a linear projection to the model dimension :
| # | Component | Shape | Parameters target |
|---|---|---|---|
| 1 | Patch embedding (768 → 768) | 768 × 768 + 768 | 590,592 |
| 2 | CLS token | 1 × 768 | 768 |
| 3 | Positional embeddings | 197 × 768 | 151,296 |
| 4 | 12 transformer blocks | ~7.1M each | 85,054,464 |
| 5 | Classification head (768 → 1000) | 768 × 1000 + 1000 | 769,000 |
| 6 | Total ViT-Base/16 | ≈ 86.6M |
ViT-Base/16 parameter breakdown. Compare ResNet-50 at 25.6M.
The CLS token is an extra learned vector prepended to the 196 patches, giving 197 tokens. It belongs to no patch. Its job is to gather information from all of them through attention, and its final state is what the classifier reads.
positional embedding A learned vector added to each token that encodes where the patch came from. Without it, the transformer has no idea which patch was top-left, because attention is order-blind.This matters more than it sounds. Shuffle the patches and a transformer without positional embeddings produces exactly the same output. A CNN could never be confused this way, because position is implicit in its structure.
Attention, computed
The mechanism, in one formula:
Work it with and three tokens. Take one query vector and three key vectors:
Step 1 — dot products.
Step 2 — scale by .
Step 3 — softmax.
Step 4 — weighted sum of values. With , , :
The output is a blend, weighted by how similar this query was to each key. Token 1 matched best and contributed half.
Why divide by √d
Repeat step 3 without the scaling, using the raw scores 2, 0, 1:
| # | Token | Weight with √d scaling target | Weight without scaling | Change |
|---|---|---|---|---|
| 1 | 1 (best match) | 0.5065 | 0.6653 | +0.159 |
| 2 | 2 (worst match) | 0.1863 | 0.0900 | −0.096 |
| 3 | 3 (middle) | 0.3072 | 0.2447 | −0.063 |
Unscaled attention is sharper. At d=768 rather than 4, it becomes almost one-hot.
Dot products of -dimensional random vectors have variance proportional to . At per head, raw scores routinely reach ±20, and against is a ratio of — softmax becomes a hard argmax, and the gradient through it is essentially zero for every other token. Dividing by normalises the variance back to roughly 1 and keeps the distribution soft enough to learn from.
Multi-head attention and the full block
One attention operation produces one weighting. ViT-Base uses 12 heads, each with , run in parallel and concatenated back to 768. Different heads reliably specialise: some track texture, some track position, some latch onto the CLS token.
graph TD A["Input tokens 197×768"] --> B["LayerNorm"] B --> C["Multi-head attention<br/>12 heads × 64 dims"] C --> D["+ residual"] A --> D D --> E["LayerNorm"] E --> F["MLP: 768 → 3072 → 768<br/>GELU"] F --> G["+ residual"] D --> G G --> H["Output 197×768"]
Note the layer norm comes before each sublayer, not after. The original text transformer put it after; ViT and everything since moved it in front because post-norm needs a careful learning-rate warmup to train at all, while pre-norm just works.
The MLP expands 768 to 3072 and back. Per block:
| # | Sublayer | Parameters target | Share |
|---|---|---|---|
| 1 | Attention Q,K,V,O (4 × 768 × 768) | 2,362,368 | 33% |
| 2 | MLP (768×3072 + 3072×768) | 4,722,432 | 67% |
| 3 | LayerNorms | 3,072 | <1% |
| 4 | Per-block total | ≈ 7,087,872 | 100% |
Two thirds of a transformer block's parameters are in the MLP, not attention.
Most people assume attention dominates. It does not — it dominates the compute at long sequences, but the MLP holds most of the weights.
The quadratic cost
Every token attends to every token, so the attention matrix is .
| # | Input size | Patch 16 tokens | Token pairs | Relative cost target |
|---|---|---|---|---|
| 1 | 128 × 128 | 64 + 1 = 65 | 4,225 | 0.11× |
| 2 | 224 × 224 | 196 + 1 = 197 | 38,809 | 1.00× |
| 3 | 384 × 384 | 576 + 1 = 577 | 332,929 | 8.58× |
| 4 | 512 × 512 | 1024 + 1 = 1025 | 1,050,625 | 27.1× |
| 5 | 1024 × 1024 | 4096 + 1 = 4097 | 16,785,409 | 432× |
Doubling the image side quadruples the tokens and multiplies attention cost by 16.
This is the reason Swin Transformer exists. It computes attention only within local 7×7 windows and shifts the windows between layers, making cost linear in the number of tokens while still mixing information globally over several layers. For detection and segmentation at high resolution, Swin-style models are the practical choice.
The data requirement — the part that decides your project
The original ViT paper’s most useful result is not that ViT wins. It is when it wins.
| # | Pretraining data | Images | ViT-Base/16 | ResNet-152 | Winner target |
|---|---|---|---|---|---|
| 1 | None (ImageNet-1k only) | 1.3M | 77.9% | 79.4% | ResNet |
| 2 | ImageNet-21k | 14M | 83.97% | 82.3% | ViT |
| 3 | JFT-300M | 300M | 84.15% | 83.0% | ViT |
ImageNet top-1 after fine-tuning. ViT needs roughly 14M pretraining images before it overtakes.
The interpretation is clean. Convolution’s inductive bias is free knowledge — locality and translation invariance are true facts about images that the architecture gets without learning. ViT must learn them from examples. With enough examples it learns something better; without them it learns something worse.
Fine-tuning in practice
import timm, torch
model = timm.create_model('vit_base_patch16_224.augreg_in21k',
pretrained=True, num_classes=4)
# Small dataset: freeze the backbone, train the head only
for p in model.parameters():
p.requires_grad = False
for p in model.head.parameters():
p.requires_grad = True
opt = torch.optim.AdamW(model.head.parameters(), lr=1e-3, weight_decay=0.05)
If you unfreeze everything, drop the learning rate hard — ViT is far more sensitive than a CNN:
| # | Setting | CNN typical | ViT typical target | Why |
|---|---|---|---|---|
| 1 | Fine-tune LR | 1e-3 | 1e-5 to 3e-5 | ViT diverges easily at CNN learning rates |
| 2 | Optimiser | SGD momentum | AdamW | Adaptive steps are near-mandatory |
| 3 | Weight decay | 1e-4 | 0.05 | Much stronger regularisation needed |
| 4 | Warmup | optional | essential, 5–10% of steps | Early large steps destabilise attention |
| 5 | Augmentation | moderate | heavy (mixup, randaug) | Compensates for the missing inductive bias |
| 6 | Gradient clipping | rarely | usually, at 1.0 | Attention produces occasional huge gradients |
ViT training recipe differences. Using CNN defaults is the most common cause of a failed ViT run.
- A strong pretrained checkpoint exists for your domain
- The task needs long-range relationships across the image
- You are doing multimodal work — CLIP-style models are transformers
- You want attention maps as a built-in explanation of what the model looked at
- You have under about 10,000 images and no suitable pretrained checkpoint
- You are deploying to edge hardware with limited transformer support
- You need very high input resolution and cannot use windowed attention
- A ResNet or ConvNeXt already meets your accuracy target
Practice task
Use the same four-class dataset from the classifier post.
- Compute the token count for patch sizes 8, 16, and 32 at 224×224. Then compute the attention pairs for each.
- Fine-tune
vit_base_patch16_224with only the head unfrozen. Record accuracy. - Fine-tune a
resnet50the same way. Compare on your dataset size. - Unfreeze the full ViT at learning rate 1e-3. Watch it diverge. Then retry at 1e-5.
- Add 10% warmup and gradient clipping at 1.0. Compare final accuracy.
- Extract the CLS token’s attention map from the last block and overlay it on a few images.
- Shuffle the patch order at inference. See how badly it breaks, which shows what positional embeddings were doing.
- Repeat step 2 at 384×384 and measure the actual latency increase against the predicted 8.58×.
Step 4 is worth doing deliberately rather than reading about. The divergence is fast and unmistakable, and it makes the learning-rate table above stop being arbitrary advice.
Summary
ViT splits a 224×224 image into 196 patches of 16×16, flattens each to 768 values, projects them, adds a CLS token for 197 tokens, and adds learned positional embeddings — 151,296 parameters just to say where each patch came from.
Attention scores each token pair by dot product, divides by , softmaxes, and takes a weighted sum of values. With scores 2, 0, 1 and , the weights are 0.5065, 0.1863, 0.3072. Skip the scaling and they become 0.6653, 0.0900, 0.2447 — sharper, and at real dimensions sharp enough to kill the gradient.
Two thirds of each block’s 7.1M parameters live in the MLP, not attention. But attention costs O(n²), so 224 → 384 pixels multiplies token pairs by 8.58 and 512 pixels by 27.
The decision is about data. Below roughly 10M pretraining images a ResNet wins; above it ViT does. And ViT needs its own recipe: AdamW at 1e-5, warmup, weight decay 0.05, gradient clipping.
- ViT treats an image as a sequence of patches — 196 of them at 224×224 with patch size 16.
- The CLS token is a learned extra token whose final state feeds the classifier.
- Positional embeddings are essential: attention is order-blind and would not know top from bottom.
- Attention = softmax(QKᵀ/√d)V, a similarity-weighted average of value vectors.
- The √d divisor stops softmax saturating; without it gradients vanish within a few thousand steps.
- Two thirds of a block's parameters sit in the MLP, but attention dominates compute at long sequences.
- Attention is O(n²): 224→384 pixels costs 8.58×, 224→512 costs 27×.
- Windowed attention (Swin) makes high-resolution transformers affordable.
- ViT overtakes CNNs only above roughly 10M pretraining images — below that, convolution's bias wins.
- ViT needs AdamW at ~1e-5 with warmup and clipping; CNN hyperparameters will make it diverge.
What comes next
You now have a model that produces attention maps, which look like an explanation. They are not, quite. Model explainability covers Grad-CAM, what attention maps do and do not tell you, and how to run a failure analysis that finds the bias you did not know your dataset had.