Search…

Vision Transformers (ViT) and When to Use Them

In this series (30 parts)
  1. Computer Vision Roadmap: From Basics to Real Projects
  2. What is Computer Vision? From Pixels to Decisions
  3. A Short History of Computer Vision: 1545 to Now
  4. Math for CV Beginners: Vectors, Matrices, Convolutions
  5. Image Fundamentals: Color, Histograms, Noise, Filtering
  6. How Images and Video Are Stored: Colour, JPEG, Frames
  7. OpenCV Setup + First 10 Tasks in Python
  8. Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
  9. CV Project Workflow: Dataset, Baseline, Error Iteration
  10. Intensity Transforms and Frequency-Domain Filtering
  11. Edge Detection and Thresholding That Actually Work
  12. Morphology, Contours, and Shape Analysis for Real Images
  13. Feature Matching (SIFT/ORB) and Image Stitching
  14. Camera Calibration and Perspective Correction
  15. Epipolar Geometry, Stereo Vision, and Depth Estimation
  16. Optical Flow and Motion Tracking in Video
  17. HOG, HOF and MBH: Descriptors Before Deep Learning
  18. Your First Image Classifier (PyTorch + Transfer Learning)
  19. Data Pipelines and Augmentation for Vision Models
  20. CNN Architectures Explained: From LeNet to ResNet
  21. YOLO Detection Pipeline: Data to Inference
  22. Semantic and Instance Segmentation: U-Net to Mask R-CNN
  23. Vision Transformers (ViT) and When to Use Them
  24. CV Explainability: Grad-CAM, Failures, Bias Checks
  25. 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
  26. Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
  27. Video Understanding: Flow Networks, Interpolation, Stabilisation
  28. Real-Time CV Systems: Tracking, Latency, Streaming
  29. Deploying CV Models: ONNX, TensorRT, Edge, and APIs
  30. 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.

8×8 image, patch size 4 (8×8)
12
14
11
13
90
88
91
89
13
12
14
12
89
92
90
88
11
13
12
14
91
89
88
92
14
11
13
12
88
90
92
89
40
42
41
39
15
17
16
14
41
39
40
42
16
14
15
17
42
41
39
40
17
16
14
15
39
40
42
41
14
15
17
16
patch 1 (4×4)
12
14
11
13
13
12
14
12
11
13
12
14
14
11
13
12
patch 2 (4×4)
90
88
91
89
89
92
90
88
91
89
88
92
88
90
92
89
patch 3 (4×4)
40
42
41
39
41
39
40
42
42
41
39
40
39
40
42
41
patch 4 (4×4)
15
17
16
14
16
14
15
17
17
16
14
15
14
15
17
16

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.

N=(22416)2=142=196 patchesN = \left(\frac{224}{16}\right)^2 = 14^2 = 196 \text{ patches}

Each patch flattened:

16×16×3=768 values16 \times 16 \times 3 = 768 \text{ values}

That flattened vector goes through a linear projection to the model dimension D=768D = 768:

# 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:

Attention(Q,K,V)=softmax ⁣(QKdk)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right) V

Work it with dk=4d_k = 4 and three tokens. Take one query vector and three key vectors:

q=[1,0,1,0]q = [1, 0, 1, 0] k1=[1,0,1,0],k2=[0,1,0,1],k3=[1,1,0,0]k_1 = [1, 0, 1, 0], \quad k_2 = [0, 1, 0, 1], \quad k_3 = [1, 1, 0, 0]

Step 1 — dot products.

qk1=1+0+1+0=2q \cdot k_1 = 1 + 0 + 1 + 0 = 2 qk2=0+0+0+0=0q \cdot k_2 = 0 + 0 + 0 + 0 = 0 qk3=1+0+0+0=1q \cdot k_3 = 1 + 0 + 0 + 0 = 1

Step 2 — scale by dk=4=2\sqrt{d_k} = \sqrt{4} = 2.

[2,0,1]/2=[1.0,  0.0,  0.5][2, 0, 1] / 2 = [1.0,\; 0.0,\; 0.5]

Step 3 — softmax.

e1.0=2.7183,e0.0=1.0000,e0.5=1.6487e^{1.0} = 2.7183, \quad e^{0.0} = 1.0000, \quad e^{0.5} = 1.6487 sum=5.3670\text{sum} = 5.3670 weights=[0.5065,  0.1863,  0.3072]\text{weights} = [0.5065,\; 0.1863,\; 0.3072]

Step 4 — weighted sum of values. With v1=[10,0]v_1 = [10, 0], v2=[0,10]v_2 = [0, 10], v3=[5,5]v_3 = [5, 5]:

0.5065[10,0]+0.1863[0,10]+0.3072[5,5]0.5065[10,0] + 0.1863[0,10] + 0.3072[5,5] =[5.065,0]+[0,1.863]+[1.536,1.536]=[6.601,  3.399]= [5.065, 0] + [0, 1.863] + [1.536, 1.536] = [6.601,\; 3.399]

The output is a blend, weighted by how similar this query was to each key. Token 1 matched best and contributed half.

Self-Attention
Self-attention lets each word look at every other word and decide which ones are most relevant. Click through to see how "The cat sat" computes attention scores, applies softmax, and produces a weighted mix of values.

Why divide by √d

Repeat step 3 without the scaling, using the raw scores 2, 0, 1:

e2=7.389,e0=1.000,e1=2.718,sum=11.107e^2 = 7.389, \quad e^0 = 1.000, \quad e^1 = 2.718, \quad \text{sum} = 11.107 weights=[0.6653,  0.0900,  0.2447]\text{weights} = [0.6653,\; 0.0900,\; 0.2447]

# 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 dd-dimensional random vectors have variance proportional to dd. At dk=64d_k = 64 per head, raw scores routinely reach ±20, and e20e^{20} against e20e^{-20} is a ratio of 101710^{17} — softmax becomes a hard argmax, and the gradient through it is essentially zero for every other token. Dividing by dk\sqrt{d_k} 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 dk=768/12=64d_k = 768/12 = 64, run in parallel and concatenated back to 768. Different heads reliably specialise: some track texture, some track position, some latch onto the CLS token.

Transformer Block
A transformer encoder block has 4 stages: Multi-Head Attention → Add & Norm → Feed-Forward Network → Add & Norm. Data flows upward through these layers. Residual connections (skip arrows) let gradients flow directly, preventing vanishing gradients.

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 N×NN \times N.

# 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.

Practice task

Use the same four-class dataset from the classifier post.

  1. Compute the token count for patch sizes 8, 16, and 32 at 224×224. Then compute the attention pairs for each.
  2. Fine-tune vit_base_patch16_224 with only the head unfrozen. Record accuracy.
  3. Fine-tune a resnet50 the same way. Compare on your dataset size.
  4. Unfreeze the full ViT at learning rate 1e-3. Watch it diverge. Then retry at 1e-5.
  5. Add 10% warmup and gradient clipping at 1.0. Compare final accuracy.
  6. Extract the CLS token’s attention map from the last block and overlay it on a few images.
  7. Shuffle the patch order at inference. See how badly it breaks, which shows what positional embeddings were doing.
  8. 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 dk\sqrt{d_k}, softmaxes, and takes a weighted sum of values. With scores 2, 0, 1 and dk=4d_k = 4, 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.

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.

Test your understanding
You fine-tune ViT-Base on 6,000 images and it performs worse than a ResNet-50 baseline. What is the most likely explanation?
Test your understanding
You move a ViT from 224×224 to 448×448 input and inference time increases far more than the 4x pixel count suggests. Why?

Frequently asked questions

Is ViT replacing CNNs?
No, and the picture got more nuanced after ConvNeXt showed a modernised ResNet matching ViT at comparable cost. That suggests much of ViT's early advantage came from better training recipes rather than the architecture itself. CNNs still dominate deployment because they export cleanly to ONNX and TensorRT, run on every edge accelerator, and have predictable memory at any resolution. Transformers dominate multimodal and promptable models, where their strengths are decisive.
What patch size should I use?
16 is the standard and the right default. Patch 32 gives 49 tokens at 224×224, so it is roughly 16 times cheaper in attention but loses fine detail badly. Patch 8 gives 784 tokens and much better detail at 16 times the attention cost. Since pretrained checkpoints are tied to a specific patch size, in practice you pick the checkpoint rather than the patch size.
Can I change input resolution after pretraining?
Yes, but the positional embeddings must be interpolated, because there are now a different number of patches than the 197 the embeddings were learned for. Every good library does this automatically — timm interpolates on load. Expect a small accuracy drop unless you fine-tune briefly at the new resolution, which usually recovers it and often exceeds the original.
Do attention maps explain the model?
Partly, and less than they appear to. They show which tokens each layer attended to, which is genuinely informative, but attention weight is not the same as causal importance — a token can be attended to heavily and still barely affect the output. Attention rollout, which composes attention across layers, is better than reading a single layer. For a claim about what actually drove a prediction, use a gradient-based method.
Why does ViT need so much more augmentation?
Augmentation substitutes for the inductive bias ViT does not have. A CNN knows a cat is a cat wherever it appears because convolution is translation-equivariant by construction; ViT has to see cats in many positions to learn the same thing. Heavy augmentation — mixup, cutmix, randaugment — manufactures that variety. It is why ViT recipes look excessive next to CNN ones and why dropping augmentation costs ViT several points.
Start typing to search across all content
navigate Enter open Esc close