Skip to content

← CNNs from Scratch step 12 of 15

Medium End-to-End

ViT Encoder Block

Implement vit_block: one Vision Transformer encoder block with multi-head self-attention followed by a feed-forward network. Both stages normalize their input before computing an update, then add that update to the unnormalized input. This order is called pre-layer normalization.

Inputs and output

  • x: (N, T, D), containing N independent sequences of T token vectors.
  • w_q, w_k, w_v, w_o: (D, D), the attention projection matrices.
  • w_mlp1: (D, F) and w_mlp2: (F, D), the feed-forward matrices.
  • num_heads: a positive integer H that divides D. Each head has width D/H.

All dimensions are positive. Tensors share a floating-point dtype and device. Return shape (N, T, D), preserving dtype and device and leaving inputs unchanged. The tokens are already embedded; do not extract patches, add a CLS token or add position embeddings.

Normalize before each stage

Normalize each token over its D features, independently of other tokens and batch examples. Use population variance, dividing by D rather than D-1:

LN(x) = (x - mean) / sqrt(variance + 1e-5)

There are no learned normalization scales or biases.

Multi-head attention

Compute Q = LN(x) @ w_q, K = LN(x) @ w_k and V = LN(x) @ w_v. Split each projected vector into H consecutive chunks of width D/H. For each batch example and head independently:

scores = Q_head @ K_head.T / sqrt(D/H)   # (T, T)
weights = softmax(scores, axis=-1)
head_output = weights @ V_head         # (T, D/H)

Use stable softmax across key positions. Every token can attend to every token in its own sequence, including itself; there is no causal mask. Concatenate the head outputs in their original order to get (N, T, D), then compute attention_output = concatenated @ w_o.

Add the first residual: y = x + attention_output.

Feed-forward stage

Normalize y, not the original x, then compute:

output = y + GELU(LN(y) @ w_mlp1) @ w_mlp2

Use this tanh approximation to GELU elementwise, where ** means exponentiation:

GELU(z) = 0.5 * z * (1 + tanh(sqrt(2/pi) * (z + 0.044715 * z**3)))

There are no projection biases, dropout or final layer normalization. If both stage outputs are zero, the residual paths return x unchanged.

Build normalization, attention and GELU from basic tensor operations in either framework. Do not call packaged layer-normalization, multi-head attention or scaled-dot-product-attention helpers.

This exercise implements a simplified block, not a complete image classifier. See An Image Is Worth 16x16 Words. The visualization below illustrates visibility; use bidirectional attention for this encoder.

Loading visualization…