Skip to content
Skip to lesson
Back to Topics
LearnTransformer Deep DivesVision Transformers and Image Encoders
👁️HardMultimodal Models

Vision Transformers and Image Encoders

A screenshot can show both a failed deploy and a tiny request ID. Patch size decides which of those the encoder can still see.

42 min read
Learning path
Step 98 of 199 in the full curriculum

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Imagine an automated monitoring bot posting a screenshot into your on-call incident channel. Across the top banner, bold red letters display "Deploy failed: Database connection timed out." Tucked into the bottom-right corner, in tiny 9-pixel gray text, sits a trace identifier: req_7f3a. Right beside it rests a blue Retry button.

Your multimodal incident responder has to accomplish two different jobs. First, it needs semantic scene understanding to recognize that the modal represents a critical deployment error. Second, it needs fine-grained character recognition to copy the exact identifier req_7f3a for an automated query. An image encoder might succeed on the error classification while copying req_7f3a wrong, even while a connected language model replies with grammatical confidence.

You already saw how self-attention routes information across a sequence of tokens. A uses transformer encoder blocks and changes where the sequence originates: non-overlapping image patches replace text .[1]

Our running input is a 224 x 224 pixel crop of that incident notification modal. If you treated every RGB pixel as one sequence token, that crop would produce 224 x 224 = 50,176 tokens. Full self-attention would evaluate over 2.5 billion query-key pairs per head in each layer. Patching reduces that work. A fused attention kernel can avoid storing the full score matrix, but still has to evaluate the dense pairs.

Keep three quantities separate: how many patches the crop yields, how many raw channel values each patch holds, and how wide its projected representation becomes. The worked examples use synthetic arrays so each change of shape stays visible.

A 4 by 4 toy image highlights its top-left 2 by 2 patch. Raster order flattens that patch to [0, 1, 4, 5], and the same learned E maps each four-value patch to width D. The encoder receives a class token followed by four projected patch tokens, each with its own position vector.
The highlighted 2×2 patch yields [0, 1, 4, 5] in raster order. The shared 4×D matrix E maps it to width D; each token adds the position vector for its slot. The first patch p₀ uses slot 1, while [CLS] occupies slot 0.

From pixels to patches

Start with a concrete spatial calculation. If each visual token spans a square patch of 16 x 16 pixels, how many tokens come out of our 224 x 224 input crop?

22416=14 patches per side14×14=196 image patches\begin{aligned} \frac{224}{16} &= 14 \text{ patches per side}\\ 14 \times 14 &= 196 \text{ image patches} \end{aligned}

Each square patch contains 16×16×3=76816 \times 16 \times 3 = 768 RGB channel values. That is the patch width before the model applies any learned parameters. In a real checkpoint, follow its image processor's resize, crop, RGB conversion, and normalization rules first. Here, "raw patch width" means the number of input values, not a requirement to feed unnormalized byte intensities.

ViT flattens each patch into a one-dimensional vector xp∈RP2C\mathbf{x}_p \in \mathbb{R}^{P^2 C} and multiplies it by a learned linear projection matrix E∈R(P2C)×D\mathbf{E} \in \mathbb{R}^{(P^2 C) \times D} to reach the model hidden dimension DD (referred to as d_model). In the standard ViT-Base architecture, D=768D = 768. The matching number between 768 raw pixel values and 768 hidden units is a pure numerical coincidence: ViT-Large uses D=1024D = 1024, mapping 768 raw pixel numbers to a 1024-dimensional . Keep those two dimensions distinct in your head.[1]

Patch width PP governs two separate budgets simultaneously. When you shrink patch width from 16 x 16 down to 8 x 8, the token count on that same 224 x 224 image leaps from 196 to 784. That quadruples the sequence length and multiplies dense pairwise attention comparisons by sixteen times, before counting any summary tokens.

For an arbitrary image of resolution H×WH \times W with CC color channels and square patch width PP, assuming PP divides HH and WW:

Npatch=HPWPDraw=P2CZpatch∈RNpatch×D\begin{aligned} N_{\text{patch}} &= \frac{H}{P}\frac{W}{P}\\ D_{\text{raw}} &= P^2 C\\ \mathbf{Z}_{\text{patch}} &\in \mathbb{R}^{N_{\text{patch}} \times D} \end{aligned}

Here, NpatchN_{\text{patch}} is the token count, DrawD_{\text{raw}} is each flattened patch's input width, and Zpatch\mathbf{Z}_{\text{patch}} is the matrix of projected tokens. Its columns use the hidden width DD defined above.

Changing PP alters the token count and the raw patch dimension, whereas changing d_model modifies the latent projection width. These are encoder design comparisons: an existing checkpoint's patch weights expect a particular P2CP^2C input width. Switching its patch size requires a supported adaptation method and typically further training; changing one preprocessing number is insufficient.

Once projected, the receives a sequence rather than a spatial grid:

text
[patch_1, patch_2, ..., patch_196]

This sequence matches the shape contracts of standard language encoders. The model doesn't process the image as a 2D matrix of pixels. It operates on an ordered list of , enabling self-attention to let any visual patch exchange information with every other visual patch.

Why flatten and project? A linear layer maps raw RGB intensities into a latent representation that transformer blocks can refine. Its parameters are shared across every spatial location: the exact same patch projection runs on the top-left banner as on the bottom-right button, just like a language model uses a single token-embedding across all sequence positions.

There's a clean implementation equivalent. A 2D convolution with kernel size PP, PP, no padding, and DD output filters reads non-overlapping patches in a single operation. Each output filter corresponds to one projection dimension. A Conv2d(3, 768, kernel_size=16, stride=16) produces the exact same numerical outputs as manual flattening followed by matrix multiplication, provided weight ordering and bias match.[2]

Verify the shape transformation with a toy array. A 32 x 32 RGB image with 8 x 8 patches produces 16 patches of raw width 192, which project to width 256.

from-pixels-to-patches.py
import numpy as np

def extract_patches(image: np.ndarray, patch_size: int = 16) -> np.ndarray:
    """Split HxWxC image into (N_patches, patch_size*patch_size*C) flat patches."""
    if image.ndim != 3 or type(patch_size) is not int or patch_size <= 0:
        raise ValueError("expected HxWxC and a positive patch size")
    h, w, c = image.shape
    if min(h, w, c) == 0 or h % patch_size or w % patch_size:
        raise ValueError("image must contain complete, nonempty patches")
    patches = image.reshape(
        h // patch_size, patch_size,
        w // patch_size, patch_size, c
    ).swapaxes(1, 2).reshape(-1, patch_size * patch_size * c)
    return patches

def patch_embed(patches: np.ndarray, proj_weight: np.ndarray) -> np.ndarray:
    """Project (N, patch_width) patches with shared (d_model, patch_width) weights."""
    return patches @ proj_weight.T

rng = np.random.default_rng(0)
numbered = np.arange(16).reshape(4, 4, 1)
print("Raster patch rows:", extract_patches(numbered, 2).tolist())
toy_image = rng.normal(size=(32, 32, 3)).astype(np.float32)
patches = extract_patches(toy_image, patch_size=8)
print("Patches shape:", patches.shape)

d_model = 256
proj = rng.normal(size=(d_model, 192)).astype(np.float32) * 0.02
embedded = patch_embed(patches, proj)
print("Embedded tokens shape:", embedded.shape)
Output
Raster patch rows: [[0, 1, 4, 5], [2, 3, 6, 7], [8, 9, 12, 13], [10, 11, 14, 15]]
Patches shape: (16, 192)
Embedded tokens shape: (16, 256)

Now compare manual patch flattening against an actual PyTorch convolution layer. Notice tensor conventions: NumPy uses channels-last (HWC) arrays, while PyTorch uses channels-first (BCHW) tensors. A raw shape match won't catch transposed channel dimensions, so verify numerical equality across all tokens.

verify-convolutional-patch-embedding.py
import torch

torch.manual_seed(3)
image = torch.arange(2 * 3 * 4 * 6, dtype=torch.float64).reshape(2, 3, 4, 6)
patch_size = 2
d_model = 5
conv = torch.nn.Conv2d(3, d_model, patch_size, stride=patch_size).double()

n, channels, height, width = image.shape
grid_h, grid_w = height // patch_size, width // patch_size
patches = (
    image.reshape(n, channels, grid_h, patch_size, grid_w, patch_size)
    .permute(0, 2, 4, 1, 3, 5)
    .reshape(n, grid_h * grid_w, channels * patch_size * patch_size)
)
tokens = patches @ conv.weight.reshape(d_model, -1).T + conv.bias
convolved = conv(image).flatten(2).transpose(1, 2)
torch.testing.assert_close(tokens, convolved)
wrong_order = patches.reshape(n, grid_h * grid_w, channels, 4)
wrong_order = wrong_order.transpose(2, 3).reshape_as(patches)
wrong_tokens = wrong_order @ conv.weight.reshape(d_model, -1).T + conv.bias
print("Token shape:", tuple(tokens.shape))
print("All convolution tokens match:", torch.allclose(tokens, convolved))
print("Wrong channel order matches:", torch.allclose(wrong_tokens, convolved))
Output
Token shape: (2, 6, 5)
All convolution tokens match: True
Wrong channel order matches: False

Convolutional patch projection isn't a different model; it's the exact same linear mapping executed through standard 2D convolution primitives.

Flattening complete patches is lossless: reshape them back with the same ordering to recover the input. A projection can lose information whenever its matrix lacks full input rank, even if DD equals or exceeds the raw patch width. A visual token is not necessarily an averaged blur. Smaller patches give attention finer spatial granularity, which can help with localized details; evaluate character accuracy on the target screenshots.

Look at token counts and dense query-key pairs on a fixed 224 x 224 image. These counts exclude class and register tokens:

Patch sizeTokens for 224 x 224 imageRaw RGB values per patchAttention pairs per head
32 x 32493,0722,401
16 x 1619676838,416
14 x 1425658865,536
8 x 8784192614,656
For a 224 by 224 image, patch widths 32, 16, 14, and 8 yield patch grids of 7 by 7, 14 by 14, 16 by 16, and 28 by 28. The 14 by 14 and 28 by 28 token grids are distinct from the drawn 7 by 7 and 28 by 28 attention-score block grids. Each score block contains 784 query-key pairs. Total dense pairs per head are 2,401, 38,416, 65,536, and 614,656; halving patch width quadruples tokens and multiplies pair count by sixteen.
Each matrix tile represents 28 query positions by 28 key positions, or 784 scores. The 16-patch grid forms 7×7 tiles; the 8-patch grid forms 28×28 tiles and 16× the pairs. Counts exclude auxiliary tokens; projections and MLPs add work.

At fixed head count and feature width, the all-to-all attention products cost O(N2)=O((HWP2)2)=O(H2W2P4)O(N^2) = O\left(\left(\frac{HW}{P^2}\right)^2\right) = O\left(\frac{H^2 W^2}{P^4}\right) work. The fourth-power term explains the steep increase. Materializing scores also needs quadratic space; fused attention avoids that intermediate, and windowed attention changes which pairs are evaluated.

Calculate the exact query-key pairs across settings:

patch-size-attention-cost.py
def patch_tokens(image_size: int, patch_size: int) -> int:
    if any(type(value) is not int or value <= 0 for value in (image_size, patch_size)):
        raise ValueError("image and patch sizes must be positive integers")
    if image_size % patch_size:
        raise ValueError("image must split into complete patches")
    patches_per_side = image_size // patch_size
    return patches_per_side**2

def attention_pairs(tokens: int) -> int:
    return tokens**2

pixel_tokens = 224 * 224
tokens_16 = patch_tokens(224, 16)
tokens_14 = patch_tokens(224, 14)
tokens_8 = patch_tokens(224, 8)

print(pixel_tokens, tokens_16, tokens_14, tokens_8)
print("pixel vs P=16 pair ratio:", attention_pairs(pixel_tokens) // attention_pairs(tokens_16))
print("P=8 vs P=16 pair ratio:", round(attention_pairs(tokens_8) / attention_pairs(tokens_16), 1))
print("16-patch count correct:", tokens_16 == 196)
print("8-patch count correct:", tokens_8 == 784)
Output
50176 196 256 784
pixel vs P=16 pair ratio: 65536
P=8 vs P=16 pair ratio: 16.0
16-patch count correct: True
8-patch count correct: True

Pixel-level attention generates 65,536 times more attention comparisons than 16 x 16 patching on this single image crop. Grouping pixels into patches is what makes transformer-scale attention computationally feasible on images.

Patching cuts the dense attention budget. The patches still need a record of where they sat, or two layouts of the same tiles can look the same.

Position information

Two screenshots can contain the same patches arranged differently. A model that sees only an unordered bag cannot distinguish those arrangements from token order, even if patch contents offer clues about the likely scene.

Without position information or an order-dependent mask, deterministic is permutation-equivariant: shuffling tokens shuffles the output representations the same way. Mean pooling or attention with a fixed query is then invariant to that shuffle. This is the setting below, with dropout disabled. In a class-token encoder, keep the class token fixed and permute only patches: its summary remains unchanged.

Original ViT adds a learned one-dimensional position embedding table Epos∈R(N+1)×D\mathbf{E}_{\text{pos}} \in \mathbb{R}^{(N+1) \times D}.[1] Using zero-based patch coordinates, row 0, column 1 is patch index 1. With the class token at sequence slot 0, that patch occupies sequence slot 2 and receives Epos[2]E_{\text{pos}}[2].

Before passing tokens into the encoder, ViT also prepends a learnable classification token ([CLS]), designated xclass∈RD\mathbf{x}_{\text{class}} \in \mathbb{R}^D. Its output state after the final layer becomes the aggregate image representation:

z0=[xclass;xp1E;xp2E;… ;xpNE]+Epos\mathbf{z}_0 = [\mathbf{x}_{\text{class}}; \mathbf{x}_p^1\mathbf{E}; \mathbf{x}_p^2\mathbf{E}; \dots; \mathbf{x}_p^N\mathbf{E}] + \mathbf{E}_{\text{pos}}

The resulting input sequence z0\mathbf{z}_0 has length N+1N + 1 and width DD.

Test how adding position embeddings breaks permutation symmetry. In this experiment, two patch vectors swap locations. Without position embeddings, attention against a fixed query yields an identical summary. With position embeddings added, the two arrangements produce distinct representations:

position-makes-layout-visible.py
import numpy as np

def attend(query: np.ndarray, tokens: np.ndarray) -> np.ndarray:
    scores = tokens @ query / np.sqrt(tokens.shape[1])
    scores = scores - scores.max()
    weights = np.exp(scores) / np.exp(scores).sum()
    return weights @ tokens

query = np.array([1.0, 0.0])
patches = np.array([[2.0, 0.1], [0.2, 1.5]])
swapped = patches[::-1]
positions = np.array([[0.0, 0.6], [0.5, 0.0]])

without_position = np.allclose(attend(query, patches), attend(query, swapped))
with_position = np.allclose(
    attend(query, patches + positions),
    attend(query, swapped + positions),
)
print("Same summary after swap without positions:", without_position)
print("Same summary after swap with positions:", with_position)
print("Original positioned summary:", np.round(attend(query, patches + positions), 3).tolist())
print("Swapped positioned summary:", np.round(attend(query, swapped + positions), 3).tolist())
Output
Same summary after swap without positions: True
Same summary after swap with positions: False
Original positioned summary: [1.629, 0.928]
Swapped positioned summary: [2.122, 0.429]

Track the unbatched tensors for ViT-B/16 at 224-pixel resolution:

StageShapeDescription
Input image x\mathbf{x}[3, 224, 224]Preprocessed RGB tensor
Flattened patches xp\mathbf{x}_p[196, 768]196 non-overlapping 16×1616 \times 16 patches
Projected tokens xpE\mathbf{x}_p \mathbf{E}[196, 768]Linear projection into model hidden dimension DD
Encoder input z0\mathbf{z}_0[197, 768]Prepended [CLS] token plus learned position embeddings
Encoded tokens zL\mathbf{z}_L[197, 768]Contextualized representation after LL transformer blocks
A fixed query pools patch vectors A=[2.0,0.1] and B=[0.2,1.5]. Swapping their order leaves the summary [1.606,0.406] when positions are absent. Adding e0=[0.0,0.6] and e1=[0.5,0.0] produces [1.629,0.928] for A then B and [2.122,0.429] for B then A.
With no position vectors or order mask, the fixed query gives the same summary for A,B and B,A. Adding e₀ or e₁ to its slot produces distinct vectors and summaries. A trained model's result depends on its query and patch features.

Resolution scaling and 2D position embeddings

A rigid 1D learned table creates an engineering challenge when input resolution changes. Moving from 224 x 224 to 256 x 256 with 16 x 16 patches expands sequence length from 196 to 256 patch tokens. The pretrained 1D table only has vectors for indices 0 through 196.

Original ViT uses 2D grid interpolation during higher-resolution fine-tuning.[1] Separate the [CLS] position vector, reshape 196 patch-position vectors into a 14×1414 \times 14 grid, resize to 16×1616 \times 16, flatten, and reattach [CLS]. The paper does not specify bicubic interpolation. Google's reference implementation uses scipy.ndimage.zoom(..., order=1), a linear interpolation method.[3] Other implementations use bicubic; match the checkpoint's method and coordinate settings.

Observe the shape mismatch before any interpolation:

position-table-resolution-mismatch.py
import numpy as np

def sequence_length(image_size: int, patch_size: int, cls_token: bool = True) -> int:
    if any(type(value) is not int or value <= 0 for value in (image_size, patch_size)):
        raise ValueError("image and patch sizes must be positive integers")
    if type(cls_token) is not bool or image_size % patch_size:
        raise ValueError("expected a Boolean class-token flag and complete patches")
    patch_count = (image_size // patch_size) ** 2
    return patch_count + int(cls_token)

trained_positions = np.zeros((sequence_length(224, 16), 768))
higher_resolution_tokens = np.zeros((sequence_length(256, 16), 768))

print("trained slots:", trained_positions.shape[0])
print("new slots:", higher_resolution_tokens.shape[0])
print("needs resized position table:", trained_positions.shape != higher_resolution_tokens.shape)
Output
trained slots: 197
new slots: 257
needs resized position table: True

Try the resize itself on a rectangular grid. This synthetic example chooses PyTorch bicubic interpolation with align_corners=False, not Google's original method.[4] The three feature channels are interpolated separately, and the class position is never treated as a grid cell:

resize-patch-position-grid.py
import torch
import torch.nn.functional as F

def resize_patch_positions(positions, old_grid, new_grid):
    """Resize [1, 1+H*W, D] positions while preserving the class slot."""
    dimensions = (*old_grid, *new_grid)
    if len(old_grid) != 2 or len(new_grid) != 2 or any(
        type(value) is not int or value <= 0 for value in dimensions
    ):
        raise ValueError("expected two positive integer grid dimensions each")
    old_h, old_w = old_grid
    if positions.ndim != 3 or positions.shape[0] != 1 or positions.shape[-1] == 0:
        raise ValueError("expected one position table with nonempty feature width")
    if positions.shape[1] != 1 + old_h * old_w:
        raise ValueError("old grid does not match the patch positions")
    cls = positions[:, :1]
    grid = positions[:, 1:].reshape(1, old_h, old_w, -1).permute(0, 3, 1, 2)
    resized = F.interpolate(grid, size=new_grid, mode="bicubic", align_corners=False)
    patches = resized.permute(0, 2, 3, 1).reshape(1, new_grid[0] * new_grid[1], -1)
    return torch.cat((cls, patches), dim=1)

positions = torch.arange(197 * 3, dtype=torch.float64).reshape(1, 197, 3)
resized = resize_patch_positions(positions, (14, 14), (16, 20))
print("Original / resized:", tuple(positions.shape), tuple(resized.shape))
print("Class position preserved:", torch.equal(positions[:, :1], resized[:, :1]))
Output
Original / resized: (1, 197, 3) (1, 321, 3)
Class position preserved: True

Interpolation supplies a correctly shaped table; it does not train the model to read the higher-resolution screenshot. That requires checking the checkpoint's supported resolutions and evaluating its outputs.

Some backbones, including Qwen2.5-VL, use 2D Rotary Position Embeddings (2D-RoPE).[5][6] Rather than adding a vector to each token, they rotate queries and keys using row and column coordinates. One simple construction divides head width dd into two equal groups of paired dimensions: half rotate with row yy, half with column xx. This construction requires dd divisible by four; axis allocation and dimension ordering can differ between implementations.

For fixed unrotated q\mathbf{q} and k\mathbf{k}, their rotated dot product contains the relative row and column offsets. The score still depends on those content vectors, too. Rotary positions avoid resizing a learned absolute table; evaluate token limits and extrapolation quality for the checkpoint. Qwen2.5-VL still resizes height and width to multiples of 28 and merges neighboring patches before its LLM.[5]

Encoder attention

ViT stacks standard transformer encoder blocks: multi-head self-attention and position-wise feed-forward networks, wrapped with residual connections and LayerNorm.

Original ViT uses full bidirectional self-attention without a . Any patch can attend to any other, so the banner patch can use evidence from the Retry button in the first layer. Other vision encoders restrict visibility: Qwen2.5-VL uses local windows in most layers and full attention in four of its 32 vision layers.[5] Bidirectional does not necessarily mean globally unrestricted.

Compare that bidirectional attention with a language decoder causal mask. In vision understanding, later raster patches aren't "future" steps in time; they're spatial neighbors that can provide contextual evidence immediately:

bidirectional-image-attention.py
import numpy as np

def softmax(scores: np.ndarray) -> np.ndarray:
    scores = scores - scores.max(axis=-1, keepdims=True)
    probs = np.exp(scores)
    return probs / probs.sum(axis=-1, keepdims=True)

tokens = np.array([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
scores = tokens @ tokens.T / np.sqrt(tokens.shape[1])
full_weights = softmax(scores)
causal_scores = np.where(np.triu(np.ones_like(scores), k=1) == 1, -np.inf, scores)
causal_weights = softmax(causal_scores)

print("Vision patch 0 can read later patch 1:", full_weights[0, 1] > 0)
print("Causal token 0 can read later token 1:", causal_weights[0, 1] > 0)
print("Vision row sums to one:", np.isclose(full_weights[0].sum(), 1.0))
Output
Vision patch 0 can read later patch 1: True
Causal token 0 can read later token 1: False
Vision row sums to one: True

Full bidirectional attention allows distant screen regions to exchange evidence without waiting for successive local layers.

The ViT architecture in full

A ViT builds image features in these stages:

ViT employs Pre-LN transformer blocks, applying LayerNorm before each sublayer and adding residuals afterward:[1]

  1. Patch embedding: linear projection of flattened patches from RP2C\mathbb{R}^{P^2 C} to RD\mathbb{R}^D

  2. Prepend a learnable [CLS] token xclass∈RD\mathbf{x}_{\text{class}} \in \mathbb{R}^D at sequence slot 0

  3. Add learned sequence position embeddings Epos∈R(N+1)×D\mathbf{E}_{\text{pos}} \in \mathbb{R}^{(N+1) \times D}

  4. Pass through LL blocks with the same architecture and separate parameters. Write multi-head attention as MHA and LayerNorm as LN:

    zℓ′=MHA⁡(LN⁡(zℓ−1))+zℓ−1zℓ=MLP⁡(LN⁡(zℓ′))+zℓ′\begin{aligned} \mathbf{z}'_\ell &= \operatorname{MHA}(\operatorname{LN}(\mathbf{z}_{\ell-1})) + \mathbf{z}_{\ell-1} \\ \mathbf{z}_\ell &= \operatorname{MLP}(\operatorname{LN}(\mathbf{z}'_\ell)) + \mathbf{z}'_\ell \end{aligned}
  5. Apply final LayerNorm

  6. Readout: extract the [CLS] token for image-level tasks, or keep all NN patch tokens for dense vision-language handoff

For ViT-B/16, the model hidden size is D=768D = 768, with L=12L = 12 layers and 12 attention heads. Each head operates on dimension dk=768/12=64d_k = 768 / 12 = 64. Queries and keys have shape [B, 12, 197, 64], forming attention score matrices of shape [B, 12, 197, 197]. The position-wise MLP expands hidden width by 4×4\times to 3072, then projects back to 768.

Inductive biases and pretraining scale

Compare a ViT with a convolutional network such as ResNet.[7] Their different inductive biases influence what training has to learn.

An inductive bias represents the architectural assumptions baked directly into a model's wiring:

  1. Locality: A small convolution kernel connects nearby pixels. Successive layers expand the receptive field; dilation, larger kernels, or global pooling can also connect distant regions. Downsampling is not required for receptive-field growth.
  2. Translation equivariance: On an ideal grid, stride-one convolution commutes with translation: f(T(x))=T(f(x))f(T(x)) = T(f(x)). Finite image boundaries and padding qualify this property, and strided sampling generally preserves it only for shifts aligned with the stride. It is not an exact guarantee for every shifted screenshot.

Original ViT has weaker image-specific biases than a conventional CNN, but still groups neighboring pixels into patches and uses their 2D grid when resizing position embeddings. Full attention permits long-range connections immediately; evaluate whether the model has learned useful spatial reasoning.

In the original paper's training setup, ViTs trained on ImageNet-1K (about 1.3 million images) without strong regularization underperformed comparable ResNets. Larger-scale pretraining on ImageNet-21k or JFT improved transfer.[1] Compare models using the target dataset and training recipe to determine how they perform at smaller scale.

DeiT subsequently trained competitive ViTs using ImageNet alone, with a stronger training recipe and an optional distillation token.[8] ConvNeXt showed competitive convolutional backbones under updated designs and training.[9] Architecture, pretraining, regularization, and task all matter. For a small private screenshot set, compare pretrained encoders and smaller baselines on held-out examples before committing to a large model from scratch.

CLIP and image-text training

A trained ViT produces visual embeddings. To match our screenshot against a query like "deployment error with database timeout", we can train image and text representations in a shared space. This aligns their global readouts, rather than every image patch with every language token.

CLIP (Contrastive Language-Image Pre-training) accomplishes this using contrastive learning across a dual-encoder architecture.[10]

Original CLIP (2021) pairs an image encoder fIf_I with a text encoder fTf_T and trains on 400 million image-text pairs collected from the web.[10] It evaluates both modified ResNet and ViT image encoders. Its text transformer uses a causal mask and reads the end-of-text token; its ViT image encoder uses the class token. Learned projections map those readouts into a common width DsharedD_{\text{shared}}, which can differ from either encoder's hidden width.

Given a training batch of BB paired images and captions:

  1. The vision encoder and its output projection produce BB image vectors ui∈RDsharedu_i \in \mathbb{R}^{D_{\text{shared}}}, then L2-normalize them: u^i=ui/∥ui∥2\hat{u}_i = u_i / \|u_i\|_2.

  2. The text encoder and its output projection produce BB caption vectors vj∈RDsharedv_j \in \mathbb{R}^{D_{\text{shared}}}, then L2-normalize them: v^j=vj/∥vj∥2\hat{v}_j = v_j / \|v_j\|_2.

  3. Compute the B×BB \times B cosine similarity matrix scaled by a learned logit scale parameter s=1/τs = 1/\tau:

    Sij=s (u^i⊤v^j)S_{ij} = s \, (\hat{u}_i^\top \hat{v}_j)
  4. Diagonal entries (SiiS_{ii}) are the supplied positive pairs. The B2−BB^2 - B off-diagonal entries receive negative labels by the batch training convention. False negatives can occur when another caption is semantically compatible with an image.

The training objective is the symmetric InfoNCE loss,[11][10] composed of image-to-text and text-to-image cross-entropy terms:

LI→T=−1B∑i=1Blog⁡exp⁡(Sii)∑j=1Bexp⁡(Sij)\mathcal{L}_{I \to T} = -\frac{1}{B} \sum_{i=1}^B \log \frac{\exp(S_{ii})}{\sum_{j=1}^B \exp(S_{ij})} LT→I=−1B∑j=1Blog⁡exp⁡(Sjj)∑i=1Bexp⁡(Sij)\mathcal{L}_{T \to I} = -\frac{1}{B} \sum_{j=1}^B \log \frac{\exp(S_{jj})}{\sum_{i=1}^B \exp(S_{ij})} LCLIP=12(LI→T+LT→I)\mathcal{L}_{\text{CLIP}} = \frac{1}{2} \left(\mathcal{L}_{I \to T} + \mathcal{L}_{T \to I}\right)

Row softmax trains each image to pick its matching caption among BB options; column softmax trains each caption to pick its matching image.

Suppose two screenshots both show a database timeout and both captions say "deployment failed." The diagonal-only objective treats each cross-pair as negative despite its plausible meaning. Duplicate captions cannot be distinguished by a deterministic text encoder. Batch curation and objectives supporting multiple positives can address this mismatch; increasing batch size alone does not resolve it.

A synthetic 3 by 3 CLIP similarity matrix marks supplied positive pairs on its diagonal. The first image row is normalized across captions, while the first caption column is normalized across images. Their batch-averaged cross-entropies are approximately 0.1548 and 0.1557; their symmetric mean is approximately 0.1553.
The diagonal cells are the supplied positive pairs. Row softmax compares one image with all captions; column softmax compares one caption with all images. Their unrounded cross-entropies average to 0.1553. Vectors and scores are synthetic.

Calculate the symmetric InfoNCE loss on three synthetic normalized pairs:

clip-symmetric-training-loss.py
import numpy as np

images = np.eye(3)
texts = np.array([[1.0, 0.2, 0.0], [0.1, 1.0, 0.2], [0.0, 0.3, 1.0]])
texts /= np.linalg.norm(texts, axis=1, keepdims=True)
logits = 3.0 * images @ texts.T

def matching_loss(scores):
    shifted = scores - scores.max(axis=1, keepdims=True)
    log_probs = shifted - np.log(np.exp(shifted).sum(axis=1, keepdims=True))
    return -np.diag(log_probs).mean()

row_loss = matching_loss(logits)
column_loss = matching_loss(logits.T)
print("Logits:", np.round(logits, 3).tolist())
print("Row / column loss:", round(row_loss, 3), round(column_loss, 3))
print("Symmetric loss:", round((row_loss + column_loss) / 2, 3))
Output
Logits: [[2.942, 0.293, 0.0], [0.588, 2.928, 0.862], [0.0, 0.586, 2.873]]
Row / column loss: 0.155 0.156
Symmetric loss: 0.155

Zero-shot open-vocabulary classification

Because CLIP projects images and natural language into the same embedding space, it performs zero-shot open-vocabulary classification without needing a fixed, closed-set classification head.

To classify our incident screenshot across arbitrary categories:

  1. Formulate prompt templates for candidate classes: "a screenshot of a deploy failed error" versus "a screenshot of a successful deployment banner".
  2. Pass the candidate strings through the text encoder to obtain normalized candidate vectors v^1,…,v^K\hat{\mathbf{v}}_1, \dots, \hat{\mathbf{v}}_K.
  3. Compute the cosine similarity u^⊤v^k\hat{\mathbf{u}}^\top \hat{\mathbf{v}}_k between the image embedding and each candidate text vector.
  4. Select the class with the highest similarity score.

Prompt ensembling can improve zero-shot accuracy, as it did on the original paper's benchmarks.[10] Encode several prompt variants suited to the domain per class, normalize each vector, average within that class, and normalize the average again. A photograph template can mismatch a terminal screenshot, so evaluate templates against the target image types.

The following vectors are invented for the arithmetic; the prompt strings are labels, not inputs to a real CLIP checkpoint:

clip-style-zero-shot-ranking.py
import numpy as np

def normalize(rows: np.ndarray) -> np.ndarray:
    return rows / np.linalg.norm(rows, axis=-1, keepdims=True)

image = normalize(np.array([[0.95, 0.20, 0.05]]))
prompts = ["a deploy failed toast", "a deploy succeeded banner"]
text = normalize(np.array([[0.90, 0.25, 0.05], [0.05, 0.95, 0.10]]))
logit_scale = 5.0

logits = logit_scale * (image @ text.T)[0]
probabilities = np.exp(logits - logits.max())
probabilities = probabilities / probabilities.sum()

print("Ranked prompt:", prompts[int(np.argmax(probabilities))])
print("Candidate probabilities:", np.round(probabilities, 3).tolist())
duplicated = np.exp(np.array([logits[0], logits[0], logits[1]]) - logits.max())
duplicated /= duplicated.sum()
print("After duplicating first candidate:", np.round(duplicated, 3).tolist())
Output
Ranked prompt: a deploy failed toast
Candidate probabilities: [0.976, 0.024]
After duplicating first candidate: [0.494, 0.494, 0.012]

Why did 0.976 become 0.494 without changing the image? Softmax distributes mass among the supplied candidates, so the duplicated prompt splits its share. These are candidate-relative probabilities, not a calibrated probability that a deployment failed. A positive common logit scale changes their sharpness without changing the similarity ranking. Neither ranking nor a sharp softmax verifies req_7f3a.

The ranking favors the deployment-failure label under the embedding model. For the next job, copying the identifier, you need a separate procedure that produces and checks a string: for example, an OCR tool or a generative VLM tested on exact IDs. To generate an explanation conditioned on the image, connect visual features to a language decoder.

From image encoder to VLM

Many generative Vision-Language Models (VLMs) connect a pretrained vision encoder to an autoregressive Large Language Model (LLM).

Original LLaVA (2023) uses a CLIP ViT-L/14 encoder and a learned linear projector.[12] It extracts patch-grid hidden states from a selected encoder layer, rather than CLIP's final projected global vector. For the 224-pixel CLIP checkpoint, there are (224/14)2=256(224/14)^2 = 256 patch features of width 1024; the class token is separate.[13] The LLM width depends on the chosen language checkpoint, so label it dLLMd_{\text{LLM}} rather than assuming 4096.

Three architecture-specific vision pathways. LLaVA v1 with CLIP ViT-L/14 at 224 pixels produces a 16 by 16 grid of 256 patch features, each 1024 wide, and projects them into 256 language-prefix slots; the class token is separate. Flamingo uses a variable NFNet-F6 spatial feature grid, then a Perceiver Resampler creates 64 learned latents for gated language-model cross-attention. Qwen2.5-VL uses its own dynamic-resolution ViT with 1280-wide patch features; for a 224 by 224 example, 256 patches are grouped in adjacent 2 by 2 sets into 64 language-model inputs. Its output width depends on the 3B, 7B, or 72B variant.
Each lane starts from its model’s own encoder output. Original LLaVA uses a 224-pixel CLIP ViT-L/14 crop: 256 patch features of width 1024. LLaVA-1.5 uses a separate 336-pixel encoder, which yields 576 patches. Flamingo uses NFNet-F6 spatial features and resamples them into 64 learned latents for gated cross-attention. For Qwen2.5-VL, the 224×224 worked crop forms 256 patches of width 1280; adjacent 2×2 groups pass through its MLP merger to produce 64 LLM inputs. Qwen’s grid length follows input resolution, and the 3B, 7B, and 72B variants use different LLM widths.

Keep three counts separate: encoder patch states, bridge outputs, and visual slots actually inserted into LLM self-attention. The bridge determines how they relate:

BridgeWhat happens to visual features?Where does the LLM use them?
Token-wise linear or MLP projectorChanges each feature's width while preserving countA prefix-style model inserts them as sequence slots. Original LLaVA uses a linear bridge.
Perceiver ResamplerLearned queries compress a variable feature grid into a fixed number of latentsFlamingo uses 64 latents as memory for added gated cross-attention blocks, not 64 ordinary self-attention prefix slots.[14] Its original encoder is an NFNet CNN.
Spatial feature mergerPacks each neighboring 2×2 group into a wider vector, then projects it; count falls 4×Qwen2.5-VL inserts the merged outputs into its LLM sequence.[5] Packing is a rearrangement; the learned projection can lose information.

A token-wise projector changes each slot's width while preserving the number of input slots. A toy linear bridge from vision width 8 to language width 16 preserves the four input slots:

project-visual-features-into-llm-space.py
import numpy as np

rng = np.random.default_rng(5)
visual_features = rng.normal(size=(1, 4, 8))
projector = rng.normal(size=(8, 16))
llm_visual_inputs = visual_features @ projector
text_token_count = 3

print("Vision features:", visual_features.shape)
print("Projected features:", llm_visual_inputs.shape)
print("Slots before generation:", llm_visual_inputs.shape[1] + text_token_count)
Output
Vision features: (1, 4, 8)
Projected features: (1, 4, 16)
Slots before generation: 7

Token budget economics in VLMs

In a prefix-style VLM, each inserted visual input occupies a slot. Full attention over the prefix has quadratic pairwise work, while retained self-attention KV cache grows linearly with sequence length. One newly decoded token attends to the retained keys with linear lookback work. A cross-attention design such as Flamingo has separate visual memory and a different accounting boundary.

First calculate patch counts for P=14P=14, before any merger:

(224/14)2=256 visual tokens(448/14)2=1,024 visual tokens(896/14)2=4,096 visual tokens\begin{aligned} (224/14)^2 &= 256 \text{ visual tokens}\\ (448/14)^2 &= 1{,}024 \text{ visual tokens}\\ (896/14)^2 &= 4{,}096 \text{ visual tokens} \end{aligned}

If a prefix-style bridge passes every patch through unchanged, four 448×448448 \times 448 images at P=14P=14 consume 4,096 visual slots, before text and any image delimiters. A 2×2 merger instead produces 256 features per image, or 1,024 total. Crops, tiling, and extra global views can increase the input count again.

Predict the storage change when one image grows from 224 to 448 pixels per side. Use a hypothetical 32-layer decoder with eight KV heads, 128-wide keys and values, two bytes per element, and 512 text tokens. Retain all prefix positions, with one patch per visual slot and no auxiliary image tokens:

MKV=2LTHKVdheadbelementM_{\text{KV}} = 2 L T H_{\text{KV}} d_{\text{head}} b_{\text{element}}

Here, MKVM_{\text{KV}} is raw KV storage in bytes, LL is the number of decoder layers, TT is the number of retained text and visual slots, HKVH_{\text{KV}} is the number of KV heads, dheadd_{\text{head}} is each head's width, and belementb_{\text{element}} is bytes per value. The factor of 2 counts keys and values.

visual-prefix-kv-budget.py
layers, kv_heads, head_width, element_bytes = 32, 8, 128, 2
text_tokens = 512

def raw_kv_mib(retained_tokens):
    if type(retained_tokens) is not int or retained_tokens <= 0:
        raise ValueError("retained token count must be a positive integer")
    return 2 * layers * retained_tokens * kv_heads * head_width * element_bytes / 2**20

for image_side in (224, 448, 896):
    visual_slots = (image_side // 14) ** 2
    prefix_slots = text_tokens + visual_slots
    print(image_side, "visual / prefix:", visual_slots, prefix_slots,
          "raw KV MiB:", raw_kv_mib(prefix_slots))

print("Extra raw KV per retained token, MiB:", raw_kv_mib(1))
Output
224 visual / prefix: 256 768 raw KV MiB: 96.0
448 visual / prefix: 1024 1536 raw KV MiB: 192.0
896 visual / prefix: 4096 4608 raw KV MiB: 576.0
Extra raw KV per retained token, MiB: 0.125

Four times the visual slots only doubles the first two total prefix lengths because the 512 text slots stay fixed. The raw cache doubles too; it does not quadruple with that doubled prefix. This is payload accounting for one request, not total GPU memory: it excludes model weights, activations, allocator overhead, and cache quantization metadata. Sliding windows or other retention policies also change the cache count.

Calculate patch-feature counts for a separate P=16P=16 toy encoder:

from-image-encoder-to-vlm.py
def patch_feature_slots(image_size: int, patch_size: int, images: int = 1) -> int:
    if any(type(value) is not int or value <= 0 for value in (image_size, patch_size, images)):
        raise ValueError("sizes and image count must be positive integers")
    if image_size % patch_size:
        raise ValueError("image must split into complete patches")
    per_image = (image_size // patch_size) ** 2
    return images * per_image

base = patch_feature_slots(224, 16, images=1)
two_images = patch_feature_slots(224, 16, images=2)
high_res = patch_feature_slots(448, 16, images=1)

print(base, two_images, high_res)
print("single-image patch features correct:", base == 196)
print("two-image patch features correct:", two_images == 392)
print("high-res patch features correct:", high_res == 784)
Output
196 392 784
single-image patch features correct: True
two-image patch features correct: True
high-res patch features correct: True

Pixel resizing, changing patch granularity, and merging encoded features are different operations. Pixel downsampling can erase character strokes before encoding; patch flattening itself does not. A learned feature merger can lose details later. Locate that boundary before trying to fix the language decoder.

ConcernProduction trade-off
Context budgetPassing 1,024 patch states individually uses 1,024 prefix slots. A merger reduces that count but may lose information. Neither preserves layout automatically; positional handling and training matter.
Detail preservationResizing a 672-pixel-wide screenshot to width 224 with the same aspect ratio shrinks 9-pixel-high text to about 3 pixels. A native 224-pixel crop keeps that text about 9 pixels high. Inspect the processed crop and compare OCR or VLM strings with labeled IDs; cropping adds evidence while labeled checks establish correctness.
Multi-turn chatReuse cached visual embeddings when the image, crop, processor, and encoder are unchanged. That saves vision passes. Reusing the decoder's prefix KV is a separate optimization requiring a matching prefix; an embedding cache alone does not save all LLM work.

Encoder variants change supervision and token budgets

The screenshot can still fail when the encoder was trained for a different target. A new loss or extra register slots changes what the model is asked to learn. Neither change automatically transcribes req_7f3a.

SigLIP: independent pair losses

CLIP normalizes each image's scores over captions and each caption's scores over images. Distributed implementations commonly gather remote embeddings to include cross-device negatives. The choice of communication collective is an implementation decision, not a mathematical requirement of softmax; a denominator can be accumulated across chunks.

SigLIP (Sigmoid Loss for Language-Image Pre-training) replaces matrix-wide softmax normalization with independent binary sigmoid losses on every pair:[15]

zij={+1if i=j−1if i≠jz_{ij} = \begin{cases} +1 & \text{if } i = j \\ -1 & \text{if } i \neq j \end{cases} Lij=log⁡(1+exp⁡(−zij⋅(u^i⊤v^j⋅s+b)))\mathcal{L}_{ij} = \log\left(1 + \exp\left(-z_{ij} \cdot (\hat{u}_i^\top \hat{v}_j \cdot s + b)\right)\right)

Here s>0s>0 is a learned scale and bb is a learned bias. With BB diagonal positives and B(B−1)B(B-1) negatives, the bias helps represent that imbalance. The original paper sums all pair losses and divides by BB, not B2B^2:

LSigLIP=1B∑i=1B∑j=1BLij\mathcal{L}_{\text{SigLIP}} = \frac{1}{B}\sum_{i=1}^B\sum_{j=1}^B\mathcal{L}_{ij}

Independent pair terms make chunked evaluation simpler: a chunk needs no row or column softmax denominator. SigLIP's paper exchanges negative embeddings between neighboring devices using collective permutes.[15] This reduces peak memory and changes communication; it does not decouple the workers or eliminate cross-device traffic. Gradient synchronization also remains. If you discard remote negatives instead, you have changed the training pairs, not obtained the same loss for free.

siglip-pairwise-loss.py
import numpy as np

logits = np.array([[2.0, -1.0], [-0.4, 1.7]])
labels = 2 * np.eye(2) - 1
pair_losses = np.logaddexp(0.0, -labels * logits)

print("Positive losses:", np.round(np.diag(pair_losses), 3).tolist())
print("Negative losses:", np.round(pair_losses[labels < 0], 3).tolist())
print("Mean pair loss:", round(float(pair_losses.mean()), 3))
print("SigLIP batch normalization:", round(float(pair_losses.sum() / len(logits)), 3))
Output
Positive losses: [0.127, 0.168]
Negative losses: [0.313, 0.513]
Mean pair loss: 0.28
SigLIP batch normalization: 0.56

The mean over four pairs is 0.28; the paper's sum divided by two examples is 0.56. Using the pair mean multiplies the objective and its gradients by 1/B1/B relative to that normalization. Weighting or sampling negatives also changes the objective. Independent terms do not mean independent training decisions.

Variable resolution and dense features

Many fixed-resolution checkpoints expect a square input, but the ViT architecture itself does not require square images. Check the processor: stretching a dashboard into a square distorts characters; resizing and center-cropping can instead drop the identifier near an edge.

SigLIP 2 (2025) adds captioning and localization pretraining, followed by self-distillation and masked teacher-feature prediction, to the image-text sigmoid objective.[16] Its NaFlex variant largely preserves aspect ratio while resizing to patch-compatible dimensions under a chosen maximum token budget. It still interpolates a learned position grid, pads shorter sequences, and masks the padding. "Native aspect ratio" does not mean preserving every original pixel or removing context limits.

SigLIP 2 uses multi-head attention pooling for global readouts rather than assuming ViT's class-token readout.[16] The auxiliary captioning decoder serves training and is not included in the released encoder pair. A captioning training objective therefore does not make that checkpoint a generative captioner. Check actual auxiliary slots before adding a class token to your budget.

Padding is a correctness contract, too. A zero-valued padded key still receives positive softmax weight unless masked. Adding more such keys changes the denominator even though they contribute zero values. This synthetic fixed-query readout isolates the effect; it is not a complete SigLIP 2 implementation:

padding-must-not-change-the-image-readout.py
import numpy as np

def readout(tokens, valid):
    if not valid.any():
        raise ValueError("image has no valid patch tokens")
    query = np.array([1.0, 0.0])
    scores = tokens @ query / np.sqrt(tokens.shape[1])
    scores = np.where(valid, scores, -np.inf)
    weights = np.exp(scores - scores.max())
    weights /= weights.sum()
    return weights @ tokens

patches = np.array([[2.0, 0.1], [0.2, 1.5]])
padded = np.concatenate((patches, np.zeros((4, 2))))
baseline = readout(patches, np.ones(2, dtype=bool))
unmasked = readout(padded, np.ones(6, dtype=bool))
masked = readout(padded, np.array([True, True, False, False, False, False]))
assert not np.allclose(unmasked, baseline)
np.testing.assert_allclose(masked, baseline)
print("Unpadded readout:", np.round(baseline, 3).tolist())
print("Padding treated as patches:", np.round(unmasked, 3).tolist())
print("Padding masked:", np.round(masked, 3).tolist())
Output
Unpadded readout: [1.606, 0.406]
Padding treated as patches: [0.913, 0.231]
Padding masked: [1.606, 0.406]

Repeat this check on the same processed image alone and beside a longer image in a batch, with dropout disabled. Preserve its real patch coordinates and inspect masks through every encoder layer and the readout. A masked key does not necessarily zero the output of a padded query; omit padded outputs from pooling, losses, or the LLM bridge as required by the checkpoint. Padding added after patch extraction also differs from real border pixels added before extraction. Reject an image with no valid patches instead of asking softmax to normalize an all-masked row.

DINOv2 demonstrates self-supervised learning without paired text labels.[17] In its main pretraining setup, a student learns image-level representations and predicts teacher outputs for masked patches; the teacher sees those patches unmasked and is updated by an exponential moving average. This predicts feature distributions, not reconstructed RGB pixels. The paper evaluates segmentation and depth using readouts trained on the patch features. A backbone's feature tensor alone is not a depth map or a text-aligned classifier.

DINOv3 (released August 2025) extends this line with Gram anchoring: a teacher reference constrains relationships among patch features to counter the dense-feature degradation observed during long training.[18] Its family includes both ViT and ConvNeXt backbones. For our screenshot, compare exact-ID and layout-task results under the same crop and token budget; evaluate UI transcription directly alongside natural-image dense prediction.

Some checkpoints introduce register tokens.[19] The paper observes high-norm background tokens in several tested ViTs and interprets them as slots repurposed for internal computations. Training with extra learned registers, commonly four in its experiments, removes the observed outliers and smooths feature maps in those models. Check for the effect in each checkpoint and artifact type you use. Registers add encoder slots; whether the LLM receives them depends on the bridge's feature selection.

Trace an encoder boundary

Follow the incident crop from pixels to the language-model prefix before you blame the decoder:

  • Calculate patch count, raw patch width, and projected tensor shapes from input resolution H×W×CH \times W \times C and patch width PP.
  • Account for quadratic self-attention scaling O(H2W2P4)O\left(\frac{H^2 W^2}{P^4}\right) before shrinking patch size to avoid memory exhaustion.
  • Verify the checkpoint's position mechanism: additive tables, rotary queries and keys, or another spatial design.
  • Resize learned patch-position grids with the expected interpolation settings, or use the supported rotary coordinates. Keep class and register slots separate.
  • Distinguish CLIP ranking from verified transcription. Count encoder states, bridge outputs, inserted prefix slots, and retained KV separately.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A 48×32 RGB image uses complete 8×8 patches and hidden width 128. What flat-patch and projected shapes follow?
2.At fixed 16×16 patches and no auxiliaries, a full-attention design changes its input from 64×64 to 128×64. What happens to patch count and patch-pair count?
3.A supplied projection outputs mean of four values and half of (right-column sum − left-column sum). Compare raster patches [1,5,3,7] and [2,5,2,7]. Which output pair follows?
4.A learned patch-position grid changes from 10×14 to 12×18 at the same patch width, with one separate class slot. Which table transformation matches that contract?
5.A Pre-LN block starts with x=[1,3]. Its attention branch returns [2,−1]. After normalizing the first residual, its MLP branch returns [−0.5,0.25]. What final residual follows?
6.Two candidate logits have exponential weights 4 and 1. Copy the first candidate without changing either score. What softmax probabilities result?
7.A SigLIP-style batch has two positive pairs and two negative pairs with supplied logistic losses 0.2,0.4,0.6,0.8. Using the paper’s batch-size normalization, what objective and relation to the pair mean follow?
8.A fixed query gives logit zero to each of two valid values [2,0] and [0,2] and to one added zero-valued padding key. What unmasked and correctly key-masked readouts follow?
9.A hypothetical decoder uses the lesson’s 0.125 MiB raw KV per retained slot. One 336×336 image uses 14×14 patches, plus 256 text slots. What raw cache is needed before and after a 2×2 visual merger, with all positions retained?
10.A student hides a button patch while its EMA teacher sees it unmasked and supplies feature probabilities [0.7,0.2,0.1]. Candidate student outputs are A=[0.7,0.2,0.1] and B=[0.1,0.2,0.7]. Which target match and downstream claim follow?

10 questions remaining.

References

An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale.

Dosovitskiy, A., et al. · 2020 · ICLR 2021

https://arxiv.org/abs/2010.11929

Conv2d

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/stable/generated/torch.nn.Conv2d.html

Vision Transformer reference checkpoint interpolation

Google Research · 2026

https://github.com/google-research/vision_transformer/blob/main/vit_jax/checkpoint.py

torch.nn.functional.interpolate

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/2.14/generated/torch.nn.functional.interpolate.html

Qwen2.5-VL Technical Report

Qwen Team, Alibaba Group · 2025

https://arxiv.org/abs/2502.13923

RoFormer: Enhanced Transformer with Rotary Position Embedding

Su, J., et al. · 2021

https://arxiv.org/abs/2104.09864

Deep Residual Learning for Image Recognition.

He, K., Zhang, X., Ren, S., Sun, J. · 2015 · CVPR 2016

https://arxiv.org/abs/1512.03385

Training Data-Efficient Image Transformers & Distillation through Attention

Touvron, H., Cord, M., Douze, M., Massa, F., Sablayrolles, A., & Jégou, H. · 2021 · ICML 2021

https://arxiv.org/abs/2012.12877

A ConvNet for the 2020s

Liu, Z., Mao, H., Wu, C.-Y., Feichtenhofer, C., Darrell, T., & Xie, S. · 2022

https://arxiv.org/abs/2201.03545

Learning Transferable Visual Models From Natural Language Supervision.

Radford, A., et al. · 2021 · ICML 2021

https://arxiv.org/abs/2103.00020

Representation Learning with Contrastive Predictive Coding.

Oord, A. van den, Li, Y., & Vinyals, O. · 2018

https://arxiv.org/abs/1807.03748

Visual Instruction Tuning.

Liu, H., et al. · 2023 · NeurIPS 2023

https://arxiv.org/abs/2304.08485

CLIP ViT-L/14 checkpoint configuration

OpenAI · 2026

https://huggingface.co/openai/clip-vit-large-patch14/blob/main/config.json

Flamingo: a Visual Language Model for Few-Shot Learning.

Alayrac, J.-B., et al. · 2022 · NeurIPS 2022

https://arxiv.org/abs/2204.14198

Sigmoid Loss for Language Image Pre-training.

Zhai, X., et al. · 2023 · ICCV 2023

https://arxiv.org/abs/2303.15343

SigLIP 2: Multilingual Vision-Language Encoders with Improved Semantic Understanding, Localization, and Dense Features

Tschannen, M., Gritsenko, A., Wang, X., et al. · 2025

https://arxiv.org/abs/2502.14786

DINOv2: Learning Robust Visual Features without Supervision

Oquab, M., Darcet, T., Moutakanni, T., et al. · 2024

https://arxiv.org/abs/2304.07193

DINOv3

Siméoni, O., Vo, H. V., Seitzer, M., et al. · 2025

https://arxiv.org/abs/2508.10104

Vision Transformers Need Registers

Darcet, T., Oquab, M., Mairal, J., & Bojanowski, P. · 2024 · ICLR 2024

https://arxiv.org/abs/2309.16588

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.