Skip to content
SDB
Computer Vision

Chapter 06 · advanced · 60 min

Vision Transformers

ViT, Swin Transformer, DINO, and the end of convolutional dominance

Subhendu Datta BhowmikAI Tutorials

Vision Transformer (ViT)

ViT (Dosovitskiy et al., 2020) treats an image as a sequence of non-overlapping patches and applies a standard Transformer encoder.

Patch embedding:

  1. Split image H×W×CH \times W \times C into NN patches of size P×PP \times P: N=HWP2N = \frac{HW}{P^2}
  2. Linearly project each flattened patch (P2CP^2 C dims) → embedding dimension DD
  3. Prepend a learned [CLS][CLS] token (used for classification)
  4. Add learnable position embeddings

z0=[xcls;xp1E;;xpNE]+Eposz_0 = [x_{cls}; x_p^1 E; \ldots; x_p^N E] + E_{pos}

Transformer encoder (LL layers): z=MSA(LN(z1))+z1z'_\ell = \text{MSA}(\text{LN}(z_{\ell-1})) + z_{\ell-1} z=MLP(LN(z))+zz_\ell = \text{MLP}(\text{LN}(z'_\ell)) + z'_\ell

Classification uses the [CLS][CLS] token of the final layer.

ViT Variants

ModelLayersHidden dimHeadsParamsImageNet Top-1
ViT-Ti/161219235.7M72.2%
ViT-S/1612384622M81.4%
ViT-B/16127681286M85.5%
ViT-L/1624102416307M87.1%

Swin Transformer

Swin addresses ViT's quadratic complexity and fixed resolution by introducing:

  • Shifted Window Attention: attention within local M×MM \times M windows → O(MN)O(MN) instead of O(N2)O(N^2)
  • Shifted windows in alternating layers create cross-window connections for global information flow
  • Hierarchical stages: patch merging produces multi-scale feature maps (like ResNet), enabling FPN for detection/segmentation

DINO Self-Supervised Learning

DINO (Caron et al., 2021) trains a ViT with self-supervised knowledge distillation:

  • Student processes small local crops; Teacher (EMA of student) processes large global crops
  • Loss: cross-entropy between student and sharpened teacher outputs

L=xPt(x)logPs(x)\mathcal{L} = -\sum_x P_t(x) \log P_s(x)

Emergent properties: DINO attention maps naturally segment objects without any annotation. DINOv2 (2023) scales this with curated data (LVD-142M) and an iBOT objective.

ViT from scratch (educational)python
import torch
import torch.nn as nn
import math

class PatchEmbedding(nn.Module):
    def __init__(self, img_size=224, patch_size=16, in_ch=3, embed_dim=768):
        super().__init__()
        self.n_patches = (img_size // patch_size) ** 2
        self.proj = nn.Conv2d(in_ch, embed_dim, kernel_size=patch_size, stride=patch_size)

    def forward(self, x):
        x = self.proj(x).flatten(2).transpose(1, 2)  # (B, N, D)
        return x

class MultiHeadSelfAttention(nn.Module):
    def __init__(self, embed_dim, n_heads, dropout=0.0):
        super().__init__()
        self.n_heads = n_heads
        self.head_dim = embed_dim // n_heads
        self.scale = self.head_dim ** -0.5
        self.qkv = nn.Linear(embed_dim, 3 * embed_dim)
        self.proj = nn.Linear(embed_dim, embed_dim)
        self.drop = nn.Dropout(dropout)

    def forward(self, x):
        B, N, D = x.shape
        qkv = self.qkv(x).reshape(B, N, 3, self.n_heads, self.head_dim).permute(2,0,3,1,4)
        q, k, v = qkv.unbind(0)
        attn = (q @ k.transpose(-2,-1)) * self.scale
        attn = self.drop(attn.softmax(dim=-1))
        return (attn @ v).transpose(1,2).reshape(B, N, D)

class TransformerBlock(nn.Module):
    def __init__(self, embed_dim, n_heads, mlp_ratio=4.0):
        super().__init__()
        self.norm1 = nn.LayerNorm(embed_dim)
        self.attn  = MultiHeadSelfAttention(embed_dim, n_heads)
        self.norm2 = nn.LayerNorm(embed_dim)
        self.mlp   = nn.Sequential(
            nn.Linear(embed_dim, int(embed_dim*mlp_ratio)), nn.GELU(),
            nn.Linear(int(embed_dim*mlp_ratio), embed_dim),
        )
    def forward(self, x):
        x = x + self.attn(self.norm1(x))
        x = x + self.mlp(self.norm2(x))
        return x

class VisionTransformer(nn.Module):
    def __init__(self, img_size=224, patch_size=16, in_ch=3, num_classes=1000,
                 embed_dim=768, depth=12, n_heads=12):
        super().__init__()
        self.patch_embed = PatchEmbedding(img_size, patch_size, in_ch, embed_dim)
        N = self.patch_embed.n_patches
        self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
        self.pos_embed = nn.Parameter(torch.zeros(1, N+1, embed_dim))
        nn.init.trunc_normal_(self.pos_embed, std=0.02)
        self.blocks = nn.ModuleList([TransformerBlock(embed_dim, n_heads) for _ in range(depth)])
        self.norm = nn.LayerNorm(embed_dim)
        self.head = nn.Linear(embed_dim, num_classes)

    def forward(self, x):
        B = x.shape[0]
        x = self.patch_embed(x)
        x = torch.cat([self.cls_token.expand(B,-1,-1), x], dim=1) + self.pos_embed
        for block in self.blocks: x = block(x)
        return self.head(self.norm(x[:, 0]))

# ViT-B/16: 224x224 → 196 patches
vit = VisionTransformer()
print(vit(torch.randn(2, 3, 224, 224)).shape)  # (2, 1000)
DINOv2 feature extraction and ViT fine-tuningpython
import torch
import torch.nn.functional as F
from torchvision import transforms

# DINOv2 feature extraction
dinov2 = torch.hub.load('facebookresearch/dinov2', 'dinov2_vits14')
dinov2.eval()

transform = transforms.Compose([
    transforms.Resize(224), transforms.CenterCrop(224), transforms.ToTensor(),
    transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),
])

from PIL import Image
img = transform(Image.open("photo.jpg").convert("RGB")).unsqueeze(0)
with torch.no_grad():
    feats = dinov2.forward_features(img)
cls_feat   = feats["x_norm_clstoken"]    # (1, 384) for classification
patch_feat = feats["x_norm_patchtokens"] # (1, 256, 384) for dense tasks

# ViT fine-tuning with HuggingFace
from transformers import ViTForImageClassification, ViTImageProcessor, TrainingArguments, Trainer
from datasets import load_dataset
from sklearn.metrics import accuracy_score

dataset = load_dataset("food101", split={"train": "train[:5000]", "test": "validation[:500]"})
processor = ViTImageProcessor.from_pretrained("google/vit-base-patch16-224")

def preprocess(batch):
    inputs = processor(images=[img.convert("RGB") for img in batch["image"]], return_tensors="pt")
    inputs["labels"] = batch["label"]
    return inputs

dataset = dataset.map(preprocess, batched=True, remove_columns=["image"])
dataset.set_format("torch")

model = ViTForImageClassification.from_pretrained(
    "google/vit-base-patch16-224",
    num_labels=101, ignore_mismatched_sizes=True,
)
args = TrainingArguments(
    output_dir="vit-food101", per_device_train_batch_size=32,
    num_train_epochs=5, learning_rate=2e-5, evaluation_strategy="epoch",
)
trainer = Trainer(
    model=model, args=args,
    train_dataset=dataset["train"], eval_dataset=dataset["test"],
    compute_metrics=lambda ep: {"accuracy": accuracy_score(ep.label_ids, ep.predictions.argmax(-1))},
)
trainer.train()

Knowledge check

For a ViT-B/16 processing a 224×224 image, how many patch tokens are in the sequence (excluding the CLS token)?

Knowledge check

What is the key motivation for Swin Transformer's shifted window attention?

Knowledge check

In DINO self-supervised training, what is the role of the EMA teacher?

Computer Vision