We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← CNNs from Scratch step 14 of 15
Train ViT Classifier End-to-End
Implement train_vit_step: classify an image batch, compute gradients by hand, and take one SGD step. Train every supplied parameter, including the CLS token and position embeddings. Return the updated parameters as one flat tensor; do not return the logits or loss.
Inputs
- x: (N, C, H, W), a batch of images.
- y: (N,), class IDs in [0, K). They arrive as floats; cast them to integers for indexing.
- w_proj: (CPP, D), the patch projection.
- cls_token: (D,), the shared classification token.
- pos_embed: (M + 1, D), where M = (H/P)*(W/P) is the number of patches.
- blocks_weights: (L, 6, D, D), containing L encoder blocks.
- w_head: (D, K), the classifier matrix; K is the number of classes.
- num_heads: a positive integer dividing D.
- lr: a finite, nonnegative learning rate.
N, C, H, W, P, D and K are positive; L may be zero. P divides H and W. Infer P from w_proj.shape[0] = CPP, or from the image area and patch count. All shapes are consistent. The floating-point tensors are finite and share a dtype and device. Leave all inputs unchanged.
Forward pass
Extract non-overlapping square patches, visiting patch rows then columns. Within a patch, flatten by channel, row within patch, then column within patch. This gives shape (N, M, CPP). Project with w_proj, prepend cls_token to each image's sequence, and add pos_embed. Position 0 belongs to CLS; positions 1 through M follow the patch order.
Each block contains [w_q, w_k, w_v, w_o, w_mlp1, w_mlp2], in that order. All six matrices have shape (D, D), so the feed-forward width is D here. Apply the L blocks in order:
h = LN(seq)
q, k, v = h @ w_q, h @ w_k, h @ w_v
z = seq + multi_head_attention(q, k, v) @ w_o
seq = z + GELU(LN(z) @ w_mlp1) @ w_mlp2
LN normalizes each token over its D features using population variance: (token - mean) / sqrt(variance + 1e-5). It has no learned scale or bias. For attention, split features into consecutive heads of width D/num_heads. Scale scores by sqrt(D/num_heads), apply softmax over key positions, then concatenate head outputs in their original order. Every token attends to all tokens in its own image, with no causal mask.
Use the tanh GELU approximation:
GELU(u) = 0.5 * u * (1 + tanh(sqrt(2/pi) * (u + 0.044715 * u**3)))
cls_repr = seq[:, 0, :]
logits = cls_repr @ w_head
This simplified classifier has no biases, dropout or final normalization. Build normalization, attention, softmax and GELU from basic tensor operations, without packaged layer helpers. Subtract each score row's maximum before exponentiating to compute stable softmax.
Loss and backward pass
Use cross-entropy averaged over the N examples. Do not divide by the number of classes or tokens. Start with:
loss = -sum(log_softmax(logits)[i, y[i]] for i in range(N)) / N
dlogits = (softmax(logits) - one_hot(y, K)) / N
dw_head = cls_repr.T @ dlogits
dcls_repr = dlogits @ w_head.T
Create a zero gradient for the final sequence and put dcls_repr at position 0. Then visit blocks in reverse order. Within each block, differentiate the feed-forward branch first, followed by attention. A residual addition sends the incoming gradient along both paths; add the two contributions when they meet again at the branch input.
For a shared matrix product B = A @ W, flatten A and its incoming gradient G = dB into rows before accumulating the weight gradient:
A_rows = A.reshape(-1, A.shape[-1])
G_rows = G.reshape(-1, G.shape[-1])
dW = A_rows.T @ G_rows
dA = G @ W.T
For example, dw_mlp2 uses the cached GELU output as A and the incoming feed-forward gradient as G. Multiplication by w_mlp2.T belongs to the activation gradient dA, not to dw_mlp2.
After all blocks, dseq is the gradient at the initial token sequence. Sum over the batch for position embeddings and the shared CLS token:
dpos_embed = sum(dseq, axis=0)
dcls_token = sum(dseq[:, 0, :], axis=0)
dpatches = dseq[:, 1:, :]
dw_proj = patches_raw.reshape(-1, C*P*P).T @ dpatches.reshape(-1, D)
Cache the forward values needed for these derivatives. Use manual chain-rule calculations throughout; do not use backward(), automatic differentiation, jax.grad, or optimizer helpers in either framework.
Update and return
Compute every gradient using the original parameters, then apply new_parameter = parameter - lr * gradient. Flatten each updated parameter in row-major order and concatenate in this order:
[w_proj_flat, cls_token_flat, pos_embed_flat, blocks_weights_flat, w_head_flat]
Preserve dtype and device. When lr=0, the result is the original parameters concatenated in this order. With no encoder blocks, logits depend only on cls_token, pos_embed[0] and w_head, so patch-projection weights and the remaining position embeddings stay unchanged.
Understand the ideas behind this exercise. The Problem tab contains the exact implementation requirements.
Turn image patches into tokens
A Vision Transformer divides an image into patches, flattens each patch, and
projects it into a feature vector called a token. For image height H, width
W, and non-overlapping square patches of side P, the number of patches is
(H/P) * (W/P) when both dimensions are divisible by P.
For an 8-by-8 image with 4-by-4 patches, there are four patch tokens. With
three color channels, each patch contains 4 * 4 * 3 = 48 values before
projection. Preserve the required pixel, channel, and patch order: obtaining the
right final shape does not prove that each token contains the right pixels.
Position embeddings identify where the patches came from. Some models also prepend a learned class token, making the example above a five-token sequence. Attention allows this token to gather information from the patch tokens. A classifier can then use its final representation; other architectures pool the patch tokens instead.
Transformer blocks combine attention with feed-forward layers. Follow the task's normalization placement and classification head, since these choices change the model.
Mix images and labels with the same coefficient
Mixup forms a training image as
$$\tilde x=\lambda x_a+(1-\lambda)x_b.$$
It combines the two label distributions with the same weights. At lambda = 0.25,
the image and target each contain one quarter of example a and three quarters
of example b. For this one model prediction, ordinary cross-entropy with the
mixed target equals the weighted sum of the losses against the two original
targets.
During manual backpropagation, shared class-token and position-embedding parameters receive contributions from every example. Add those contributions, including any scaling already required by the batch loss. Remember the extra class-token position when checking sequence lengths.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
PyTorch reference solution
Sign in to track your attempts toward unlocking the reference solution.