EasyViT

Position Embedding

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

Easy

Problem

Add one learned position vector to every token position. The supplied position embedding has a batch dimension of one, so the same position vectors are broadcast across every sequence.

z_{b,n,d} = x_{b,n,d} + e_{n,d}.

Here, b indexes the batch, n indexes token position, d indexes the embedding coordinate, x is patches, and e is pos_embed. Return the result as a float64 NumPy array with the same shape as patches.

Theory

Position embeddings inject spatial location information into the token sequence of a Vision Transformer (ViT). Because self-attention is permutation-invariant -- it produces the same output regardless of token order -- the model has no inherent way to distinguish patches from different locations. Position embeddings, a core component in Dosovitskiy et al. (2020), solve this by adding a learnable vector to each token so the model can reason about spatial origin.


What It Is

A position embedding in ViT is a learnable parameter matrix E_{\text{pos}} \in \mathbb{R}^{1 \times N \times D} that is added element-wise to the sequence of patch embeddings (plus the CLS token) before the sequence enters the Transformer encoder. Each row of E_{\text{pos}} corresponds to one position in the sequence and holds a D-dimensional vector encoding "this is position i". Because the matrix is learned during training rather than computed from a fixed formula, the model discovers whatever positional representation is most useful for the task.

The critical detail is what N represents. An image of resolution H \times W is divided into non-overlapping patches of size P \times P, yielding \frac{H}{P} \times \frac{W}{P} patch tokens. A CLS token is prepended, so N = \frac{H}{P} \times \frac{W}{P} + 1. The position embedding must cover all N positions -- one for the CLS token and one for every patch.

The shape (1, N, D) means the position embedding is shared across every image in a batch. The leading dimension of 1 allows broadcasting to replicate the same positional information across the batch dimension $$, so a batch of shape (B, N, D) receives identical position vectors regardless of batch size.


Key Equations

The position embedding is applied in a single addition immediately after patch embedding and CLS token prepending:

z_0 = x_{\text{patches}} + E_{\text{pos}}

where:

The addition broadcasts E_{\text{pos}} from (1, N, D) to (B, N, D). For every image b, position i, and feature j:

z_0[b, i, j] = x_{\text{patches}}[b, i, j] + E_{\text{pos}}[0, i, j]

This is pure element-wise addition -- no concatenation, no multiplication. Position information is blended directly into the same representation space as the content information.


Why Position Embeddings Are Necessary

Self-attention computes weighted sums over all tokens. The attention weight between token i and token j depends only on their content vectors q_i and k_j, not on their positions. If all patch embeddings were randomly shuffled, the attention outputs would be the shuffled version of the original outputs -- the model produces the same prediction for any permutation of patches.

This property is called permutation invariance:

\text{Attention}(\pi(X)) = \pi(\text{Attention}(X))

For vision, this means the model cannot distinguish an intact image from one with randomly rearranged patches. Spatial structure carries essential information -- objects have shape, parts have relative positions, edges are continuous -- and without position encoding all of this is lost.

Adding E_{\text{pos}} breaks permutation invariance. After the addition, token i carries x_i + E_{\text{pos}}[i] and token j carries x_j + E_{\text{pos}}[j]. Even if x_i = x_j (two identical patches), their representations differ because E_{\text{pos}}[i] \neq E_{\text{pos}}[j]. The attention mechanism can now differentiate based on spatial origin.


1D vs 2D Position Embeddings

Patches originate from a 2D grid, so a natural question is whether position embeddings should encode 2D coordinates (row, column) rather than a flat 1D index. Dosovitskiy et al. (2020) explicitly tested both.

1D position embeddings assign a single learnable vector to each position in the flattened sequence. A 4 \times 4 grid produces positions 0, 1, 2, \ldots, 15 (plus CLS). No explicit row or column encoding exists -- the model must learn 2D structure from data.

2D position embeddings use two separate embedding tables: one for row index and one for column index. Each patch at grid position (r, c) receives the sum of row embedding r and column embedding c. This explicitly encodes the 2D grid and reduces unique embeddings from \frac{H}{P} \times \frac{W}{P} to \frac{H}{P} + \frac{W}{P}.

The paper's finding: 1D and 2D position embeddings perform nearly identically. No significant accuracy difference was observed across model sizes and datasets. The authors chose 1D for simplicity -- a single parameter matrix versus two tables with a combining step.

This result makes sense in hindsight. Learned 1D embeddings implicitly recover 2D structure during training. When visualized, nearby patches in the 2D grid have similar embedding vectors and a clear row-column pattern emerges, despite the flat parameterization.


Learned vs Fixed Position Encodings

Different Transformer architectures handle position information differently. ViT's learned additive embeddings are one point in a broader design space.

Sinusoidal (fixed) encoding from the original Transformer (Vaswani et al., 2017) uses deterministic sine and cosine functions:

PE(pos, 2i) = \sin\!\left(\frac{pos}{10000^{2i/d}}\right), \quad PE(pos, 2i+1) = \cos\!\left(\frac{pos}{10000^{2i/d}}\right)

These require no training and generalize to longer sequences, but embed a fixed inductive bias about position structure that may not be optimal for all tasks.

Learned position embeddings are a parameter matrix updated by backpropagation. ViT uses this approach, initializing with \text{randn} \times 0.02. The downside is no extrapolation: if the model trains with N = 197 positions, it has no embedding for position 197 or beyond.

Rotary Position Embedding (RoPE) (Su et al., 2021) encodes position by rotating query and key vectors so their dot product depends on relative distance m - n. Used in LLaMA, Mistral, and most modern LLMs.

Dosovitskiy et al. (2020) found learned and sinusoidal encodings performed equally for ViT, so they chose the simpler learned approach.


The Broadcasting Mechanism

The addition x_{\text{patches}} + E_{\text{pos}} relies on broadcasting -- dimensions of size 1 are automatically expanded to match the other operand. Dimension by dimension from right to left:

Every image in the batch receives exactly the same position embeddings. This is correct: position is a property of the grid structure, not of individual images. No memory is allocated for the replication -- broadcasting is a view operation, not a copy.


Paper Context

Dosovitskiy et al. (2020) in "An Image is Worth 16x16 Words" state: "Position embeddings are added to the patch embeddings to retain positional information. We use standard learnable 1D position embeddings." The word "standard" is deliberate -- the authors position this as the simplest option, not a novel contribution. The paper's thesis is that a standard Transformer with minimal vision-specific modifications can match convolutional networks given sufficient data.

The paper provides a visualization of the learned position embeddings (Figure 7) revealing that despite 1D parameterization, the embeddings exhibit clear 2D structure: each position has highest cosine similarity with positions from the same row and column of the patch grid. The model autonomously discovers spatial layout during training.

Removing position embeddings entirely dropped accuracy from approximately 79.4% to approximately 64.2% on ImageNet for ViT-B/16 trained on ImageNet-21k -- a roughly 15-point drop confirming that position information is essential, not optional.


Numerical Example (B=2, N=5, D=3)

Consider a batch of 2 images, each with 4 patches and a CLS token (N = 5), embedding dimension D = 3.

Patch embeddings x_{\text{patches}} \in \mathbb{R}^{2 \times 5 \times 3}:

Image 0: [[0.10, 0.20, 0.30],\; [0.40, 0.50, 0.60],\; [0.70, 0.80, 0.90],\; [1.00, 1.10, 1.20],\; [1.30, 1.40, 1.50]]

Image 1: [[0.01, 0.02, 0.03],\; [0.04, 0.05, 0.06],\; [0.07, 0.08, 0.09],\; [0.10, 0.11, 0.12],\; [0.13, 0.14, 0.15]]

Position embedding E_{\text{pos}} \in \mathbb{R}^{1 \times 5 \times 3} (note small magnitudes from \text{randn} \times 0.02):

[[0.02, -0.01, 0.03],\; [0.01, 0.02, -0.02],\; [-0.01, 0.03, 0.01],\; [0.03, -0.02, 0.02],\; [-0.02, 0.01, -0.01]]

Result for image 0:

Result for image 1:

Both images received the same position offsets. The CLS token at position 0 always gets [0.02, -0.01, 0.03], patch 1 always gets [0.01, 0.02, -0.02], and so on. Position information is content-independent.


What Position Embeddings Learn

Although initialized randomly, training shapes position embeddings into a structured representation reflecting 2D image geometry. Dosovitskiy et al. (2020) visualized this by computing cosine similarity between each pair of position embedding vectors, revealing several patterns:

This emergent structure explains why 1D and 2D embeddings produce equivalent results: the 1D parameterization is expressive enough to recover 2D structure on its own.


Pitfalls


Examples

Example 1

Input
patches = [[[1,2],[3,4]]], pos_embed = [[[0.1,0.2],[0.3,0.4]]]
Output
[[[1.1,2.2],[3.3,4.4]]]
Explanation
The position array broadcasts across the batch while preserving token and embedding axes.

Example 2

Input
patches = [[[1,0],[0,1]],[[2,1],[1,2]]], pos_embed = [[[0.5,-0.5],[1,-1]]]
Output
[[[1.5,-0.5],[1,0]],[[2.5,0.5],[2,1]]]

Example 3

Input
patches = [[[0,1,2]]], pos_embed = [[[-1,-2,-3]]]
Output
[[[-1,-1,-1]]]

Hints

  1. NumPy broadcasts the leading size-one dimension across the batch.
  2. Use elementwise addition without reshaping either input.

Requirements

Constraints

Starter Code

import numpy as np

def add_position_embedding(patches: np.ndarray,
                           pos_embed: np.ndarray) -> np.ndarray:
    """
    Returns the float64 tokens after adding position embeddings.
    """
    pass

Test Cases

CaseMatches
Fractional positionspublic
Broadcast across batchpublic
Negative positionspublic