Phase 04 - Lesson 14

Vision Transformers (ViT)

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Corte a imagem em patches, trate cada patch como uma palavra, rode um transformer padrao. Nao olhe para tras.

Tipo: Build Linguagens: Python Pre-requisitos: Fase 7 Licao 02 (Self-Attention), Fase 4 Licao 04 (Classificacao de Imagens) Tempo: ~45 minutos

Objetivos de Aprendizado

  • Implementar patch embedding, positional embedding aprendido, class token e blocos de encoder transformer do zero para construir um ViT minimo
  • Explicar por que se acreditava que o ViT precisava de pretreinamento massivo de dados ate que DeiT e MAE provaram o contrario
  • Comparar ViT, Swin e ConvNeXt em seus priors arquiteturais (nenhum, atencao em janela local, backbone convolucional)
  • Fazer fine-tune de um ViT pre-treinado em um dataset pequeno usando timm e a receita padrao de linear-probe / fine-tune

O Problema

Por uma decada, convolucao foi sinonimo de visao computacional. CNNs tinham fortes vieses indutivos — localidade, equivariancia a translacao — que ninguem achava que dava para substituir. Entao Dosovitskiy et al. (2020) mostraram que um transformer puro aplicado a patches de imagem achatados, sem nenhuma maquinaria convolucional, podia igualar ou superar as melhores CNNs em escala.

O detalhe era "em escala". ViT no ImageNet-1k perdia para o ResNet. ViT pre-treinado no ImageNet-21k ou JFT-300M e depois fine-tunado no ImageNet-1k o superava. A conclusao foi que transformers careciam de priors uteis, mas podiam aprende-los com dados suficientes. Trabalhos subsequentes (DeiT, MAE, DINO) mostraram que, com as receitas de treinamento certas — augmentation forte, pretreinamento auto-supervisionado, destilacao — ViTs treinam bem em dados pequenos tambem.

Em 2026, CNNs puras ainda sao competitivas em dispositivos de borda (ConvNeXt e a mais forte), mas transformers dominam todo o resto: segmentacao (Mask2Former, SegFormer), deteccao (DETR, RT-DETR), multimodal (CLIP, SigLIP), video (VideoMAE, VJEPA). A estrutura do bloco ViT e a que vale conhecer.

O Conceito

O pipeline

flowchart LR
    IMG["Imagem<br/>(3, 224, 224)"] --> PATCH["Patch embedding<br/>conv 16x16 s=16<br/>-> (768, 14, 14)"]
    PATCH --> FLAT["Achata para<br/>(196, 768) tokens"]
    FLAT --> CAT["Adiciona no inicio<br/>token [CLS]"]
    CAT --> POS["Soma positional<br/>embed aprendido"]
    POS --> ENC["N blocos de<br/>encoder transformer"]
    ENC --> CLS["Pega a saida do<br/>token [CLS]"]
    CLS --> HEAD["Classificador MLP"]

    style PATCH fill:#dbeafe,stroke:#2563eb
    style ENC fill:#fef3c7,stroke:#d97706
    style HEAD fill:#dcfce7,stroke:#16a34a

Sete passos. Patches -> tokens -> atencao -> classificador. Cada variante (DeiT, Swin, ConvNeXt, pretreinamento MAE) muda um ou dois dos sete e deixa o resto intacto.

Patch embedding

A primeira conv e o segredo. Tamanho de kernel 16, stride 16, entao uma imagem 224x224 vira uma grade 14x14 de patches 16x16, cada um projetado para um embedding de 768 dimensoes. Aquela unica conv tanto patchifica quanto projeta linearmente.

Input:  (3, 224, 224)
Conv (3 -> 768, k=16, s=16, no padding):
Output: (768, 14, 14)
Flatten spatial: (196, 768)

196 patches = 196 tokens. A dimensao de feature de cada token e 768 (ViT-B), 1024 (ViT-L) ou 1280 (ViT-H).

Class token

Um unico vetor aprendido adicionado no inicio da sequencia:

tokens = [CLS; patch_1; patch_2; ...; patch_196]   shape (197, 768)

Apos N blocos transformer, a saida do [CLS] e a representacao global da imagem. A cabeca de classificacao le apenas esse unico vetor.

Positional embedding

Transformers nao tem nocao embutida de posicao espacial. Some um vetor aprendido a cada token:

tokens = tokens + learned_pos_embedding   (also shape (197, 768))

O embedding e um parametro do modelo; o treinamento baseado em gradiente o adapta a estrutura 2D da imagem. Existem alternativas sinusoidais 2D, mas raramente sao usadas na pratica.

Bloco de encoder transformer

Padrao. Multi-head self-attention, MLP, conexoes residuais, pre-LayerNorm.

x = x + MSA(LN(x))
x = x + MLP(LN(x))

MLP is two-layer with GELU: Linear(d -> 4d) -> GELU -> Linear(4d -> d)

O ViT-B/16 empilha 12 desses blocos, cada um com 12 cabecas de atencao, totalizando 86M de parametros.

Por que pre-LN

Os primeiros transformers usavam post-LN (x = LN(x + sublayer(x))) e tinham dificuldade de treinar alem de 6-8 camadas sem warmup. Pre-LN (x = x + sublayer(LN(x))) treina redes mais profundas de forma estavel sem warmup. Todo ViT e todo LLM moderno usam pre-LN.

Trade-off do tamanho de patch

  • Patches 16x16 -> 196 tokens, padrao.
  • Patches 32x32 -> 49 tokens, mais rapido mas resolucao menor.
  • Patches 8x8 -> 784 tokens, mais fino mas o custo O(n^2) da atencao escala mal.

Patches maiores = menos tokens = mais rapido mas menos detalhe espacial. O SwinV2 usa patches 4x4 em janelas hierarquicas.

A receita do DeiT para treinar ViT no ImageNet-1k

O ViT original precisava do JFT-300M para superar CNNs. O DeiT (Touvron et al., 2020) treinou o ViT-B ate 81,8% top-1 no ImageNet-1k sozinho com quatro mudancas:

  1. Augmentation pesada: RandAugment, Mixup, CutMix, Random Erasing.
  2. Stochastic depth (descartar blocos inteiros aleatoriamente durante o treinamento).
  3. Repeated augmentation (mesma imagem amostrada 3 vezes por batch).
  4. Destilacao de um professor CNN (opcional, eleva ainda mais a acuracia).

Toda receita moderna de treinamento de ViT descende do DeiT.

Swin vs ConvNeXt

  • Swin (Liu et al., 2021) — atencao baseada em janela. Cada bloco atende dentro de uma janela local; blocos alternados deslocam a janela para misturar informacao entre janelas. Traz de volta um prior de localidade no estilo CNN mantendo o operador de atencao.
  • ConvNeXt (Liu et al., 2022) — uma CNN redesenhada que iguala as escolhas arquiteturais do Swin (convs depthwise, LayerNorm, GELU, bottleneck invertido). Mostrou que a diferenca nao e "atencao vs convolucao", mas "receita de treinamento moderna + arquitetura".

Em 2026, ConvNeXt-V2 e Swin-V2 sao ambos de nivel de producao; a escolha certa depende da sua stack de inferencia (ConvNeXt compila melhor para borda) e do corpus de pretreinamento.

Pretreinamento MAE

Masked Autoencoder (He et al., 2022): mascare 75% dos patches aleatoriamente, treine o encoder para processar apenas os 25% visiveis, treine um pequeno decoder para reconstruir os patches mascarados a partir da saida do encoder. Apos o pretreinamento, descarte o decoder e faca fine-tune do encoder.

O MAE torna o ViT treinavel no ImageNet-1k sozinho, atinge SOTA e e a receita auto-supervisionada padrao atual.

Construa

Passo 1: Patch embedding

import torch
import torch.nn as nn

class PatchEmbedding(nn.Module):
    def __init__(self, in_channels=3, patch_size=16, dim=192, image_size=64):
        super().__init__()
        assert image_size % patch_size == 0
        self.proj = nn.Conv2d(in_channels, dim, kernel_size=patch_size, stride=patch_size)
        num_patches = (image_size // patch_size) ** 2
        self.num_patches = num_patches

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

Uma conv, um flatten, um transpose. Esse e o passo inteiro de imagem-para-tokens.

Passo 2: Bloco transformer

Pre-LN, multi-head self-attention, MLP com GELU, conexoes residuais.

class Block(nn.Module):
    def __init__(self, dim, num_heads, mlp_ratio=4, dropout=0.0):
        super().__init__()
        self.ln1 = nn.LayerNorm(dim)
        self.attn = nn.MultiheadAttention(dim, num_heads, dropout=dropout, batch_first=True)
        self.ln2 = nn.LayerNorm(dim)
        self.mlp = nn.Sequential(
            nn.Linear(dim, dim * mlp_ratio),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(dim * mlp_ratio, dim),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        a, _ = self.attn(self.ln1(x), self.ln1(x), self.ln1(x), need_weights=False)
        x = x + a
        x = x + self.mlp(self.ln2(x))
        return x

O nn.MultiheadAttention cuida da divisao em cabecas, do produto escalar escalonado e da projecao de saida. batch_first=True para que os shapes sejam (N, seq, dim).

Passo 3: O ViT

class ViT(nn.Module):
    def __init__(self, image_size=64, patch_size=16, in_channels=3,
                 num_classes=10, dim=192, depth=6, num_heads=3, mlp_ratio=4):
        super().__init__()
        self.patch = PatchEmbedding(in_channels, patch_size, dim, image_size)
        num_patches = self.patch.num_patches
        self.cls_token = nn.Parameter(torch.zeros(1, 1, dim))
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, dim))
        self.blocks = nn.ModuleList([
            Block(dim, num_heads, mlp_ratio) for _ in range(depth)
        ])
        self.ln = nn.LayerNorm(dim)
        self.head = nn.Linear(dim, num_classes)
        nn.init.trunc_normal_(self.pos_embed, std=0.02)
        nn.init.trunc_normal_(self.cls_token, std=0.02)

    def forward(self, x):
        x = self.patch(x)
        cls = self.cls_token.expand(x.size(0), -1, -1)
        x = torch.cat([cls, x], dim=1)
        x = x + self.pos_embed
        for blk in self.blocks:
            x = blk(x)
        x = self.ln(x[:, 0])
        return self.head(x)

vit = ViT(image_size=64, patch_size=16, num_classes=10, dim=192, depth=6, num_heads=3)
x = torch.randn(2, 3, 64, 64)
print(f"output: {vit(x).shape}")
print(f"params: {sum(p.numel() for p in vit.parameters()):,}")

Cerca de 2,8M de parametros — um ViT minusculo tratavel em CPU. O ViT-B real tem 86M; mesma definicao de classe com dim=768, depth=12, num_heads=12.

Passo 4: Sanity check — inferencia de imagem unica

logits = vit(torch.randn(1, 3, 64, 64))
print(f"logits: {logits}")
print(f"probs:  {logits.softmax(-1)}")

Deve rodar sem erro. As probabilidades somam 1.

Use

O timm traz cada variante de ViT com pesos pre-treinados no ImageNet. Uma linha:

import timm

model = timm.create_model("vit_base_patch16_224", pretrained=True, num_classes=10)

O timm e o padrao de producao para vision transformers em 2026. Suporta ViT, DeiT, Swin, Swin-V2, ConvNeXt, ConvNeXt-V2, MaxViT, MViT, EfficientFormer e dezenas de outros sob a mesma API.

Para trabalho multimodal (imagem + texto), o transformers traz CLIP, SigLIP, BLIP-2, LLaVA. O encoder de imagem em todos esses e uma variante de ViT.

Entregue

Esta licao produz:

  • outputs/prompt-vit-vs-cnn-picker.md — um prompt que escolhe entre um ViT, um ConvNeXt ou um Swin com base no tamanho do dataset, no compute e na stack de inferencia.
  • outputs/skill-vit-patch-and-pos-embed-inspector.md — uma skill que verifica se os shapes de patch embedding e positional embedding de um ViT correspondem ao comprimento de sequencia esperado pelo modelo, capturando os bugs de portabilidade mais comuns.

Exercicios

  1. (Facil) Imprima os shapes de cada tensor intermediario para um forward pass pelo ViT minusculo acima. Confirme: input (N, 3, 64, 64) -> patches (N, 16, 192) -> com CLS (N, 17, 192) -> input do classificador (N, 192) -> output (N, num_classes).
  2. (Medio) Faca fine-tune de um ViT-S/16 pre-treinado do timm no dataset CIFAR sintetico da Licao 4. Compare com o fine-tune de um ResNet-18 nos mesmos dados. Reporte o tempo de treinamento e a acuracia final.
  3. (Dificil) Implemente o pretreinamento MAE para o ViT minusculo: mascare 75% dos patches, treine o encoder + um pequeno decoder para reconstruir os patches mascarados. Avalie a acuracia de linear-probe nos dados sinteticos antes e depois do pretreinamento.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
Patch embedding "A primeira conv" Uma conv com tamanho de kernel = stride = tamanho do patch; transforma a imagem em uma grade de token embeddings
Class token "[CLS]" Um vetor aprendido adicionado no inicio da sequencia de tokens; sua saida final e a representacao global da imagem
Positional embedding "Pos aprendido" Um vetor aprendido somado a cada token para que o transformer saiba de onde veio cada patch
Pre-LN "LayerNorm antes do sublayer" A variante estavel do transformer: x + sublayer(LN(x)) em vez de LN(x + sublayer(x))
Multi-head attention "Atencao paralela" A atencao padrao do transformer dividida em num_heads subespacos independentes, concatenados depois
ViT-B/16 "Base, patch 16" O tamanho canonico: dim=768, depth=12, heads=12, patch_size=16, image=224; ~86M params
DeiT "ViT eficiente em dados" ViT treinado apenas no ImageNet-1k com augmentation forte; provou que datasets grandes de pretreinamento nao sao estritamente necessarios
MAE "Masked autoencoder" Pretreinamento auto-supervisionado: mascare 75% dos patches, reconstrua; a receita dominante de pretreinamento de ViT

Leitura Adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).