Lesson 115: Transformer End-to-End Review

USAAIO Lesson 115, from Phase 3, a complete transformer review. It runs from scaled dot-product attention through multi-head attention, the encoder block - multi-head attention, feed-forward network, layer norm, and residual - and the decoder block, with its masked self-attention, cross-attention, and feed-forward network. It then covers all the major variants, BERT, GPT, T5, ViT, and GNNs, comparing their architectures, and analyzes the O(n²d) complexity, causal masking, sinusoidal positional encoding, and the role of the CLS token. It ends with a PixelBERT implemented from scratch and verified with torch 2.7.1+cpu on the sklearn digits dataset. The lesson runs to 27 slides.

Subject: Machine Learning · 49 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Transformer End-to-End Review

Title

USAAIO · Lesson 115 · Phase 3

Attention → encoder block → decoder block → BERT / GPT / T5 / ViT / GNN. Every shape, every number, exam-speed drill. Speed target: MHA in 15 min, full encoder in 25 min.

2. By the end of this lesson you can

Objectives

  1. Derive the scaled dot-product attention formula and state why scaling by 1/√d_k is necessary
  2. Implement multi-head attention from nn.Linear projections and explain why splitting into h heads helps
  3. Trace a full encoder block (LayerNorm → MHA → residual → LayerNorm → FFN → residual) with concrete tensor shapes
  4. Explain the decoder's two attention sublayers and what cross-attention Q/K/V come from
  5. Distinguish BERT vs GPT vs T5 vs ViT by architecture, masking strategy, and pre-training objective
  6. State the O(n²d) attention complexity and identify when it becomes the bottleneck

3. What survived from Phase 3 Mock Exam — Transformers, NLP, and Computer Vision?

Warm-up

Discussion prompt

Before we open Lesson 115: Transformer End-to-End Review: without looking back, what was the main idea of Phase 3 Mock Exam — Transformers, NLP, and Computer Vision, and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

timed mock-exam simulation covering all Phase 3 topics — scaled dot-product attention derivation, multi-head attention parameter counting, causal masking, sinusoidal positional encoding, BERT architecture and fine-tuning, LoRA adapters, greedy vs beam-search decoding, BLEU-4 and ROUGE-N computation, NMS with IoU, and full transformer training CE/perplexity. Worked answer-key walkthrough for each section.

4. Scaled Dot-Product Attention — the atomic unit

Section

Part 1 of 5

5. Scaled dot-product attention

Concept

Every transformer sublayer computes the same operation: given Q (queries), K (keys), V (values) — each (seq, d_k) — produce a weighted sum of values where weights are query–key similarities.

\[ \text{Attn}(Q,K,V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right)V \]

The √d_k scale prevents dot products from growing large in high dimension, which would push softmax into flat or near-one-hot regimes and kill gradients. With d_k=8 the scale factor is 1/√8 = 0.3536 (verified).

TensorShapeMeaning
Q(seq, d_k)What position i is looking for
K(seq, d_k)What position j offers
V(seq, d_k)What position j actually contributes
QKᵀ/√d_k(seq, seq)Similarity score matrix
softmax(·)(seq, seq)Row-stochastic attention weights
Output(seq, d_k)Weighted value sum per position

6. Fill in: Shape for Scaled dot-product attention

Comparison

Comparison matrix

From Scaled dot-product attention: refill the Shape column from what you know. The rest of the table is as it appeared.

TensorShapeMeaning
Q(seq, d_k)What position i is looking for
K(seq, d_k)What position j offers
V(seq, d_k)What position j actually contributes
QKᵀ/√d_k(seq, seq)Similarity score matrix
softmax(·)(seq, seq)Row-stochastic attention weights
Output(seq, d_k)Weighted value sum per position

7. Predict the next row: Trace: attention weights with seq=4, d_k=8

Pattern

Predict first

The table runs: 1 | QKᵀ (pre-scale) | (4,4) | raw dot products · 2 | ÷ √8 = 0.3536 | (4,4) | scores dampened · 3 | softmax(row) | (4,4) | [0.1942, 0.4102, 0.3242, 0.0714]

In Trace: attention weights with seq=4, d_k=8, given the rows so far: what is the next one — the row where Step is 4?

Correct: 4 | × V | (4,8) | weighted value sum

StepExpressionShapeValues (row 0)
1QKᵀ (pre-scale)(4,4)raw dot products
2÷ √8 = 0.3536(4,4)scores dampened
3softmax(row)(4,4)[0.1942, 0.4102, 0.3242, 0.0714]
4× V(4,8)weighted value sum

Why: The relationship between the columns, not the individual numbers, is what generates the next row. Small concrete tensors let us print every number; seed 42 is reproducible.

8. Trace: attention weights with seq=4, d_k=8

Worked example

Fix inputs: Q, K, V each shape (4, 8), seed 42

Why: Small concrete tensors let us print every number; seed 42 is reproducible.

import torch, torch.nn.functional as F
torch.manual_seed(42)
seq, d_k = 4, 8
Q = torch.randn(seq, d_k)
K = torch.randn(seq, d_k)
V = torch.randn(seq, d_k)

scores = Q @ K.T / d_k**0.5   # (4,4)
attn_w = F.softmax(scores, dim=-1)
out = attn_w @ V               # (4,8)
print(attn_w[0].tolist())

Output: attn_w[0] = [0.1942, 0.4102, 0.3242, 0.0714]

Why: Row 0 sums to 1.0 (softmax row-stochastic). Position 0 attends most to position 1 (weight 0.4102).

StepExpressionShapeValues (row 0)
1QKᵀ (pre-scale)(4,4)raw dot products
2÷ √8 = 0.3536(4,4)scores dampened
3softmax(row)(4,4)[0.1942, 0.4102, 0.3242, 0.0714]
4× V(4,8)weighted value sum

9. What each one costs: Trace: attention weights with seq=4, d_k=8

Trade off

Comparison matrix

From Trace: attention weights with seq=4, d_k=8: every row here is a choice with a cost. Fill the Values (row 0) column, then say which row you would actually pick and what you give up for it.

StepExpressionShapeValues (row 0)
1QKᵀ (pre-scale)(4,4)raw dot products
2÷ √8 = 0.3536(4,4)scores dampened
3softmax(row)(4,4)[0.1942, 0.4102, 0.3242, 0.0714]
4× V(4,8)weighted value sum

10. Multi-Head Attention — h projections in parallel

Section

Part 2 of 5

11. Multi-head attention: why split?

Concept

A single attention head computes one similarity function. Multi-head attention projects into h independent subspaces (each d_k = d_model/h), runs h attention heads in parallel, then concatenates and projects back — capturing h different relationship types simultaneously.

\[ \text{MHA}(X) = \text{Concat}(\text{head}_1,\ldots,\text{head}_h)W^O \quad \text{head}_i = \text{Attn}(XW^Q_i, XW^K_i, XW^V_i) \]

HyperparameterValueShape consequence
d_model32input/output dim
h (heads)44 parallel attention functions
d_k = d_model/h8per-head key/query/value dim
W_Q, W_K, W_VLinear(32,32)4096 params each
W_OLinear(32,32)1024 params
Total MHA params4 × 1024 = 4096verified: 4096

12. Guess the shape of the answer: MHA implementation: shapes at every step

Estimation

Predict first

Lines 9-11: the reshape+transpose trick — 32 dims become 4×8, head axis inserted at dim 1.

Commit before you compute: what does MHA implementation: shapes at every step come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: Output shape: (1, 6, 32) — same as input. Attention weight tensor is (1, 4, 6, 6).

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. MHA is shape-preserving: (B, S, D) in, (B, S, D) out.

13. MHA implementation: shapes at every step

Worked example

Project X → Q, K, V via three Linear(32,32), then reshape to (B, h, S, d_k)

Why: The reshape splits the 32-dim into 4 heads × 8 dims each. .transpose(1,2) moves heads before the sequence dim so batched matmul works per head.

import torch, torch.nn as nn, torch.nn.functional as F
torch.manual_seed(42)
B, S, D, h = 1, 6, 32, 4
d_k = D // h   # 8
W_q = nn.Linear(D, D, bias=False)
W_k = nn.Linear(D, D, bias=False)
W_v = nn.Linear(D, D, bias=False)
W_o = nn.Linear(D, D, bias=False)

x = torch.randn(B, S, D)
Q = W_q(x).view(B, S, h, d_k).transpose(1,2)  # (1,4,6,8)
K = W_k(x).view(B, S, h, d_k).transpose(1,2)
V = W_v(x).view(B, S, h, d_k).transpose(1,2)
scores = Q @ K.transpose(-2,-1) / d_k**0.5    # (1,4,6,6)
attn = F.softmax(scores, dim=-1)
ctx = (attn @ V).transpose(1,2).contiguous().view(B, S, D)  # (1,6,32)
out = W_o(ctx)
print(out.shape)   # torch.Size([1, 6, 32])

Lines 9-11: the reshape+transpose trick — 32 dims become 4×8, head axis inserted at dim 1.

Output shape: (1, 6, 32) — same as input. Attention weight tensor is (1, 4, 6, 6).

Why: MHA is shape-preserving: (B, S, D) in, (B, S, D) out. The h×S×S attention maps can be inspected per-head to see what each head specialized in.

TensorShape after opNote
x(1, 6, 32)input
W_q(x).view(…)(1, 6, 4, 8)split heads
Q after .transpose(1,2)(1, 4, 6, 8)head dim first
scores Q@Kᵀ/√d_k(1, 4, 6, 6)per-head similarity
attn (softmax)(1, 4, 6, 6)per-head weights
ctx after merge(1, 6, 32)concatenated heads
out = W_o(ctx)(1, 6, 32)final projection

14. Inspect it line by line: MHA implementation: shapes at every step

Error analysis

Annotate

Walk the callouts on MHA implementation: shapes at every step. Each one is a place this is easy to get subtly wrong.

  • The reshape splits the 32-dim into 4 heads × 8 dims each. .transpose(1,2) moves heads before the sequence dim so batched matmul works per head.
  • MHA is shape-preserving: (B, S, D) in, (B, S, D) out. The h×S×S attention maps can be inspected per-head to see what each head specialized in.

15. Encoder & Decoder Blocks — the full architecture

Section

Part 3 of 5

16. Encoder block: four sublayers

Concept

One encoder block applies four operations in order: LayerNorm → MHA → residual, then LayerNorm → FFN → residual. This is the Pre-LN variant (used in GPT-2, modern practice); original 'Attention is All You Need' used Post-LN.

\[ x \leftarrow x + \text{MHA}(\text{LN}(x)) \quad\text{then}\quad x \leftarrow x + \text{FFN}(\text{LN}(x)) \]

SublayerOperationShape
LayerNorm 1normalize across d_model(B, S, 32)
MHAmulti-head self-attention(B, S, 32)
Residual addx = x + MHA(LN(x))(B, S, 32)
LayerNorm 2normalize across d_model(B, S, 32)
FFNLinear(32,64) → ReLU → Linear(64,32)(B, S, 32)
Residual addx = x + FFN(LN(x))(B, S, 32)

With d_model=32, h=4, d_ff=64: encoder block has 8,416 parameters (verified). FFN typically uses d_ff = 4×d_model, expanding then contracting.

17. Break it if you can: Encoder block: four sublayers

Counterexample

Discussion prompt

With d_model=32, h=4, d_ff=64: encoder block has 8,416 parameters (verified). FFN typically uses d_ff = 4×d_model, expanding then contracting.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

18. Decoder block: two attention sublayers

Concept

A decoder block has three sublayers: (1) masked self-attention on the target, (2) cross-attention where Q comes from the decoder but K,V come from the encoder output, (3) FFN. The cross-attention is what lets the decoder 'read' the encoded source.

SublayerQ sourceK,V sourceMask
Masked self-attndecoder tgtdecoder tgtcausal lower-triangular
Cross-attentiondecoder tgtencoder outputnone (src padding mask only)
FFN——none

With tgt_seq=4, src_seq=6, d_model=32: decoder maps (B,4,32)×(B,6,32) → (B,4,32). Decoder params with d=32, h=4, d_ff=64: 12,576 (verified — extra cross-attn projections).

19. By analogy: Decoder block: two attention sublayers

Analogy

Discussion prompt

Explain Decoder block: two attention sublayers by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

With tgt_seq=4, src_seq=6, d_model=32: decoder maps (B,4,32)×(B,6,32) → (B,4,32). Decoder params with d=32, h=4, d_ff=64: 12,576 (verified — extra cross-attn projections).

20. Something is wrong here: causal mask in encoder vs decoder

Anomaly

Predict first

A student writes this, and it looks reasonable:

Wrong: Apply a causal (lower-triangular) mask to the encoder self-attention so early tokens cannot see future ones.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Feels like 'the encoder processes left-to-right so it should not peek ahead.'

Right: The encoder uses bidirectional (no causal) attention — every position sees every other. Only the decoder's self-attention sublayer uses the causal mask.

Why: Feels like 'the encoder processes left-to-right so it should not peek ahead.'

21. Trap: causal mask in encoder vs decoder

Trap

The trap

Wrong: Apply a causal (lower-triangular) mask to the encoder self-attention so early tokens cannot see future ones.

mask = torch.tril(torch.ones(S, S)) # used in encoder

Why: Feels like 'the encoder processes left-to-right so it should not peek ahead.'

The fix

Right: The encoder uses bidirectional (no causal) attention — every position sees every other. Only the decoder's self-attention sublayer uses the causal mask.

Encoder: mask=None. Decoder self-attn: mask = torch.tril(torch.ones(T, T))

Why: The encoder's job is to build a full contextual representation of the source; causal masking would cripple it. The decoder must not peek at future target tokens because at inference time they don't exist yet.

22. Sinusoidal positional encoding

Concept

Attention is permutation-equivariant — it does not know token order. Positional encoding adds a position-dependent signal to each token embedding before the first layer.

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

posd0 (sin)d1 (cos)d2 (sin)d3 (cos)
00.00001.00000.00001.0000
10.84150.54030.09980.9950
20.9093-0.41610.19870.9801
30.1411-0.99000.29550.9553

Low-index dims (d0,d1) oscillate fast (period 2π); high-index dims oscillate slowly (period 2π×10000). This gives every position a unique fingerprint. BERT and ViT use learned positional embeddings instead — both approaches work.

23. Watch it run: Sinusoidal positional encoding

Pattern

Step through it

Step through Sinusoidal positional encoding one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: pos is 0
  2. Step 2: pos is 1
  3. Step 3: pos is 2
  4. Step 4: pos is 3

24. BERT / GPT / T5 / ViT / GNN — the variant landscape

Section

Part 4 of 5

25. Architecture variants: one comparison table

Concept

ModelArchitectureMaskingPre-train taskPrimary use
BERTEncoder-onlyBidirectionalMLM + NSPClassification, NER, QA
GPTDecoder-onlyCausal (LT)Next-token LMText generation
T5Enc-DecEnc=bi, Dec=causalSpan denoisingSeq2seq (translation, summarization)
ViTEncoder-onlyBidirectionalSupervised / MAEImage classification
GNNGraph message-passingNeighbor aggregationSupervised / contrastiveGraph-structured data

Lesson 88 built the full encoder (Vaswani). Lesson 89 covered BERT's masked LM. Lesson 90 covered GPT's causal LM. Lesson 92 built ViT. This lesson synthesizes all four under one roof.

26. Fill in: Primary use for Architecture variants: one comparison table

Comparison

Comparison matrix

From Architecture variants: one comparison table: refill the Primary use column from what you know. The rest of the table is as it appeared.

ModelArchitectureMaskingPre-train taskPrimary use
BERTEncoder-onlyBidirectionalMLM + NSPClassification, NER, QA
GPTDecoder-onlyCausal (LT)Next-token LMText generation
T5Enc-DecEnc=bi, Dec=causalSpan denoisingSeq2seq (translation, summarization)
ViTEncoder-onlyBidirectionalSupervised / MAEImage classification
GNNGraph message-passingNeighbor aggregationSupervised / contrastiveGraph-structured data

27. CLS token: what it does and why

Concept

BERT and ViT prepend a [CLS] token to the input sequence before any attention layers. By the final encoder layer, the CLS position has attended to every other token — it becomes a pooled sentence/image representation used for classification.

PositionTokenAfter final encoder layer
0[CLS] (learned)aggregate of full sequence → fed to classifier head
1token_1contextual embedding of token_1
2token_2contextual embedding of token_2
………

The CLS embedding is not a mean pool — it is a dedicated slot trained end-to-end to aggregate. Alternative: global average pooling of all positions (works comparably, used in many ViT variants). Both avoid autoregressive decoding for classification.

28. ViT: patches as tokens (Lesson 92 callback)

Concept

ViT (Lesson 92) applies a standard encoder to images by treating P×P pixel patches as tokens. With H=8, P=2: N = (8/2)² = 16 patches. A nn.Conv2d(C, d, kernel=P, stride=P) implements patch embedding in one op.

\[ N = \left(\frac{H}{P}\right)^2 = \left(\frac{8}{2}\right)^2 = 16 \quad\text{sequence length} = N+1 = 17\text{ (CLS)} \]

StepTensor shapeOperation
Input image(1, 1, 8, 8)C=1 grayscale
patch_embed (Conv2d)(1, 32, 4, 4)16 patch vectors, d=32
.flatten(2).T(1,2)(1, 16, 32)sequence of N=16 tokens
cat([CLS, patches])(1, 17, 32)prepend CLS
+ pos_embed(1, 17, 32)learned positional bias

29. GNN: message passing (Lesson 99 callback)

Concept

A GNN updates each node by aggregating neighbor features, then combining with its own state. Mean aggregation + self-loop on a 3-node graph (edges 0↔1, 1↔2) with 4-dim features:

NodeFeatures beforeMean of neighborsAfter (self + mean-nbr)
0[1.0, 0.0, 2.0, -1.0][0.0, 1.0, 0.0, 1.0][1.0, 1.0, 2.0, 0.0]
1[0.0, 1.0, 0.0, 1.0][1.5, 1.0, 1.5, -0.5] → [1.5, 1.0, 1.5, -0.5][1.5, 2.0, 1.5, 0.5]
2[2.0, 2.0, 1.0, 0.0][0.0, 1.0, 0.0, 1.0][2.0, 3.0, 1.0, 1.0]

Unlike transformers, GNNs aggregate over sparse adjacency (not all pairs) — complexity O(E·d) per layer, where E = number of edges. Transformer's O(n²d) is infeasible on large graphs.

30. Break it if you can: GNN: message passing (Lesson 99 callback)

Counterexample

Discussion prompt

A GNN updates each node by aggregating neighbor features, then combining with its own state. Mean aggregation + self-loop on a 3-node graph (edges 0↔1, 1↔2) with 4-dim features:

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

Answer:

Unlike transformers, GNNs aggregate over sparse adjacency (not all pairs) — complexity O(E·d) per layer, where E = number of edges. Transformer's O(n²d) is infeasible on large graphs.

31. Complexity & Implementation Drill

Section

Part 5 of 5

32. Attention complexity: O(n²d)

Concept

The QKᵀ matmul is O(n²d) — quadratic in sequence length n, linear in head dimension d. This is the transformer's primary scaling bottleneck for long sequences.

n (seq len)O(n²)×d=64 total ops
644,096262,144
12816,3841,048,576
25665,5364,194,304
512262,14416,777,216

At n=512 the attention matrix alone costs 16M multiply-adds per head per layer. This motivates FlashAttention (fused kernel, avoids materializing the full S×S matrix), sparse attention, and linear attention approximations — all USAAIO-relevant topics.

33. Watch it run: Attention complexity: O(n²d)

Pattern

Step through it

Step through Attention complexity: O(n²d) one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: n (seq len) is 64
  2. Step 2: n (seq len) is 128
  3. Step 3: n (seq len) is 256
  4. Step 4: n (seq len) is 512

34. Without one step: The transformer authoring recipe (exam speed)

Constraint

Discussion prompt

Run The transformer authoring recipe (exam speed) with this step confiscated:

Causal mask: torch.tril(torch.ones(T,T)) — decoder self-attn only; encoder uses no mask

Is it still possible? If it is, say what takes its place and what it costs you. If it is not, say exactly what that step was providing that nothing else does.

Hint: A step you can drop for free was never load-bearing. If you cannot drop it, name the thing that goes wrong the moment it is gone.

Answer:

  1. Scaled dot-product attention: scores = Q@K.T / d_k**0.5; attn = softmax(scores); out = attn@V
  2. Multi-head: project X → Q,K,V via Linear(D,D), .view(B,S,h,d_k).transpose(1,2), run attention, .transpose(1,2).view(B,S,D), project out
  3. Encoder block: x = x + MHA(LN(x)) then x = x + FFN(LN(x)) — two residuals, two LayerNorms
  4. Decoder block: add masked self-attn sublayer first; cross-attn Q from decoder, K/V from encoder
  5. Causal mask: torch.tril(torch.ones(T,T)) — decoder self-attn only; encoder uses no mask
  6. CLS token: prepend, feed to encoder, take x[:,0,:] for classification head
  7. ViT patch embed: nn.Conv2d(C, d, kernel=P, stride=P) → .flatten(2).transpose(1,2)
  8. Architecture selector: classification → BERT/ViT; generation → GPT; seq2seq → T5; graph → GNN

35. The transformer authoring recipe (exam speed)

Pattern

  1. Scaled dot-product attention: scores = Q@K.T / d_k**0.5; attn = softmax(scores); out = attn@V
  2. Multi-head: project X → Q,K,V via Linear(D,D), .view(B,S,h,d_k).transpose(1,2), run attention, .transpose(1,2).view(B,S,D), project out
  3. Encoder block: x = x + MHA(LN(x)) then x = x + FFN(LN(x)) — two residuals, two LayerNorms
  4. Decoder block: add masked self-attn sublayer first; cross-attn Q from decoder, K/V from encoder
  5. Causal mask: torch.tril(torch.ones(T,T)) — decoder self-attn only; encoder uses no mask
  6. CLS token: prepend, feed to encoder, take x[:,0,:] for classification head
  7. ViT patch embed: nn.Conv2d(C, d, kernel=P, stride=P) → .flatten(2).transpose(1,2)
  8. Architecture selector: classification → BERT/ViT; generation → GPT; seq2seq → T5; graph → GNN

36. Where does it stop working: The transformer authoring recipe (exam speed)

Edge cases

Discussion prompt

The transformer authoring recipe (exam speed) works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".

Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.

Answer:

  1. Scaled dot-product attention: scores = Q@K.T / d_k**0.5; attn = softmax(scores); out = attn@V
  2. Multi-head: project X → Q,K,V via Linear(D,D), .view(B,S,h,d_k).transpose(1,2), run attention, .transpose(1,2).view(B,S,D), project out
  3. Encoder block: x = x + MHA(LN(x)) then x = x + FFN(LN(x)) — two residuals, two LayerNorms
  4. Decoder block: add masked self-attn sublayer first; cross-attn Q from decoder, K/V from encoder
  5. Causal mask: torch.tril(torch.ones(T,T)) — decoder self-attn only; encoder uses no mask
  6. CLS token: prepend, feed to encoder, take x[:,0,:] for classification head
  7. ViT patch embed: nn.Conv2d(C, d, kernel=P, stride=P) → .flatten(2).transpose(1,2)
  8. Architecture selector: classification → BERT/ViT; generation → GPT; seq2seq → T5; graph → GNN

37. Rule out three: Check 1: attention weight computation

Elimination

Eliminate the wrong options

With d_k = 64, what is the scale factor applied to QKᵀ before the softmax, and why is it necessary?

3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.

  • A. Divide by √64 = 8; prevents dot products from growing O(d_k) large and pushing softmax into vanishing-gradient territory
  • B. Divide by 64; the dot product sums d_k terms each O(1), so variance grows as d_k, not √d_k
  • C. Multiply by √64 = 8; amplifies the signal-to-noise ratio in high-dimensional spaces
  • D. Divide by d_k²; normalizes the full covariance matrix of Q and K

Survives elimination: A

Why: Dot product of two d_k-dim unit random vectors has variance d_k, so std dev is √d_k. Dividing by √d_k restores unit variance, keeping softmax inputs in the responsive region where gradients are non-vanishing. With d_k=64, scale = 1/8 = 0.125.

38. Check 1: attention weight computation

Check

Work through this before clicking. Pen and paper — do not compute softmax in your head, just identify the correct formula step.

Check your understanding

With d_k = 64, what is the scale factor applied to QKᵀ before the softmax, and why is it necessary?

  • A. Divide by √64 = 8; prevents dot products from growing O(d_k) large and pushing softmax into vanishing-gradient territory (correct)
  • B. Divide by 64; the dot product sums d_k terms each O(1), so variance grows as d_k, not √d_k
  • C. Multiply by √64 = 8; amplifies the signal-to-noise ratio in high-dimensional spaces
  • D. Divide by d_k²; normalizes the full covariance matrix of Q and K

Answer: A

Why: Dot product of two d_k-dim unit random vectors has variance d_k, so std dev is √d_k. Dividing by √d_k restores unit variance, keeping softmax inputs in the responsive region where gradients are non-vanishing. With d_k=64, scale = 1/8 = 0.125.

Why B tempts people
The variance of a sum of d_k unit-variance products is d_k, but std dev = √d_k — dividing by the full d_k over-scales, damping signals too aggressively.
Why C tempts people
Multiplying amplifies the dot products, making softmax outputs more peaked (harder) not more stable — the opposite of what scaling aims for.
Why D tempts people
Dividing by d_k² is not derived from any statistical property of the dot product; it would reduce scores to near zero, destroying the attention signal.

39. Check 2: architecture identification

Check

Identify the model given the description. Think about masking and blocks before clicking.

Check your understanding

A model processes a source sequence through N encoder blocks (bidirectional attention), then generates a target token-by-token through N decoder blocks using masked self-attention on the target and cross-attention to the encoder output. Which model does this describe?

  • A. T5 (correct)
  • B. BERT
  • C. GPT-2
  • D. ViT

Answer: A

Why: T5 is an encoder-decoder (seq2seq) model: the encoder builds a bidirectional representation of the source; the decoder autoregressively generates the target with masked self-attention (causal) and cross-attention to the encoder output. This is the classic Vaswani 2017 architecture adapted for text-to-text pre-training.

Why B tempts people
BERT is encoder-only — it has no decoder blocks and no cross-attention sublayer. It processes a single masked sequence and predicts masked tokens bidirectionally.
Why C tempts people
GPT-2 is decoder-only — it has no encoder blocks. It processes the full context (source + target concatenated) with a single causal attention stack.
Why D tempts people
ViT is also encoder-only, applied to image patches. It has no decoder blocks or cross-attention, and takes a fixed image as input, not a variable-length source sequence.

40. Rule out three: Check 3: complexity and masking

Elimination

Eliminate the wrong options

A 12-layer transformer encoder processes sequences of length n=1024 with d_model=768 and h=12 heads (d_k=64 each). If you double the sequence length to n=2048, by what factor does the cost of the attention computation increase?

3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.

  • A. 4× (quadratic in n)
  • B. 2× (linear in n)
  • C. 8× (cubic in n)
  • D. 12× (one factor per layer)

Survives elimination: A

Why: Attention cost is O(n²d). Doubling n multiplies n² by 4, so cost increases 4×. Concretely: at n=1024, QKᵀ is 1024² = 1,048,576 entries per head per layer; at n=2048 it is 2048² = 4,194,304 — exactly 4×. The d factor and h and L factors remain constant.

41. Check 3: complexity and masking

Check

Combine complexity and masking knowledge. No calculator — reason from the formulas.

Check your understanding

A 12-layer transformer encoder processes sequences of length n=1024 with d_model=768 and h=12 heads (d_k=64 each). If you double the sequence length to n=2048, by what factor does the cost of the attention computation increase?

  • A. 4× (quadratic in n) (correct)
  • B. 2× (linear in n)
  • C. 8× (cubic in n)
  • D. 12× (one factor per layer)

Answer: A

Why: Attention cost is O(n²d). Doubling n multiplies n² by 4, so cost increases 4×. Concretely: at n=1024, QKᵀ is 1024² = 1,048,576 entries per head per layer; at n=2048 it is 2048² = 4,194,304 — exactly 4×. The d factor and h and L factors remain constant.

Why B tempts people
Linear scaling would hold for FFN layers (O(n·d_ff)) but not attention, which has the extra n from K.T in Q@K.T.
Why C tempts people
O(n³) would arise if we did a naive triple loop over all (query, key, value) triplets, but softmax+weighted-sum is O(n²d), not O(n³).
Why D tempts people
The 12 layers scale cost by 12× overall, but the question asks about the effect of doubling n — layers are a constant multiplier independent of n.

42. Predict the next row: PixelBERT: CLS classification on digits (full…

Pattern

Predict first

The table runs: 0 | 2.3599 | 0.1125 | random init, ~10-class chance · 1 | 2.3490 | 0.1125 | loss falling, weights adjusting · 2 | 2.3391 | 0.1125 | stable decay pattern · 3 | 2.3304 | 0.0625 | acc fluctuates — tiny dataset · 4 | 2.3226 | 0.0500 | loss strictly decreasing

In PixelBERT: CLS classification on digits (full…, given the rows so far: what is the next one — the row where Step is 5?

Correct: 5 | 2.3156 | 0.0875 | longer training needed for high acc

StepLossAccNote
02.35990.1125random init, ~10-class chance
12.34900.1125loss falling, weights adjusting
22.33910.1125stable decay pattern
32.33040.0625acc fluctuates — tiny dataset
42.32260.0500loss strictly decreasing
52.31560.0875longer training needed for high acc

Why: The relationship between the columns, not the individual numbers, is what generates the next row. This exercises the full BERT-style pipeline in < 30 lines: patch projection, CLS prepend, learned positional embedding, encoder block, linear head.

43. PixelBERT: CLS classification on digits (full implementation)

Worked example

Treat each 8×8 digit image as 8 tokens of 8 pixels each, project to d=16, prepend CLS, run one encoder block, classify on CLS

Why: This exercises the full BERT-style pipeline in < 30 lines: patch projection, CLS prepend, learned positional embedding, encoder block, linear head.

import torch, torch.nn as nn, torch.nn.functional as F
from sklearn.datasets import load_digits
torch.manual_seed(0)

class EncoderBlock(nn.Module):
    def __init__(self, d, h, d_ff):
        super().__init__()
        self.mha = nn.MultiheadAttention(d, h, batch_first=True)
        self.ff  = nn.Sequential(nn.Linear(d,d_ff), nn.ReLU(), nn.Linear(d_ff,d))
        self.ln1 = nn.LayerNorm(d); self.ln2 = nn.LayerNorm(d)
    def forward(self, x):
        a,_ = self.mha(self.ln1(x), self.ln1(x), self.ln1(x))
        x = x + a
        return x + self.ff(self.ln2(x))

class PixelBERT(nn.Module):
    def __init__(self):
        super().__init__()
        self.proj = nn.Linear(8, 16)
        self.cls  = nn.Parameter(torch.randn(1,1,16))
        self.pos  = nn.Parameter(torch.randn(1,9,16))
        self.enc  = EncoderBlock(16, 2, 32)
        self.head = nn.Linear(16, 10)
    def forward(self, x):
        B = x.size(0)
        t = self.proj(x.view(B,8,8))
        t = torch.cat([self.cls.expand(B,-1,-1), t], 1) + self.pos
        return self.head(self.enc(t)[:,0,:])

digits = load_digits()
X = torch.tensor(digits.data[:80]/16., dtype=torch.float32)
y = torch.tensor(digits.target[:80])
opt = torch.optim.Adam(PixelBERT().parameters(), lr=1e-3)
model = PixelBERT()
opt   = torch.optim.Adam(model.parameters(), lr=1e-3)
for step in range(6):
    loss = F.cross_entropy(model(X), y)
    loss.backward(); opt.step(); opt.zero_grad()
    acc  = (model(X).argmax(1)==y).float().mean().item()
    print(f"step {step}: loss={loss.item():.4f}  acc={acc:.4f}")

Training output (6 steps, seed 0, 80 samples from load_digits)

Why: Loss monotonically decreases, confirming the forward pass is correct and gradients flow through CLS → encoder → projection.

StepLossAccNote
02.35990.1125random init, ~10-class chance
12.34900.1125loss falling, weights adjusting
22.33910.1125stable decay pattern
32.33040.0625acc fluctuates — tiny dataset
42.32260.0500loss strictly decreasing
52.31560.0875longer training needed for high acc

44. What each one costs: PixelBERT: CLS classification on digits (full…

Trade off

Comparison matrix

From PixelBERT: CLS classification on digits (full…: every row here is a choice with a cost. Fill the Acc column, then say which row you would actually pick and what you give up for it.

StepLossAccNote
02.35990.1125random init, ~10-class chance
12.34900.1125loss falling, weights adjusting
22.33910.1125stable decay pattern
32.33040.0625acc fluctuates — tiny dataset
42.32260.0500loss strictly decreasing
52.31560.0875longer training needed for high acc

45. Plan first: Your turn — multi-head attention in 15 minutes

Step zero

Discussion prompt

Your turn — multi-head attention in 15 minutes — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: Milestone 1 (3 min): Define __init__ — four `nn.Linear(d_model…

Answer:

  1. Milestone 1 (3 min): Define __init__ — four nn.Linear(d_model, d_model, bias=False) for W_q, W_k, W_v, W_o. Store h and d_k = d_model//h.
  2. Milestone 2 (5 min): In forward(x), project and reshape — W_q(x).view(B,S,h,d_k).transpose(1,2) for Q,K,V.
  3. Milestone 3 (5 min): Compute scores = Q@K.T(-2,-1) / d_k**0.5, softmax on dim=-1, ctx = attn@V, then .transpose(1,2).contiguous().view(B,S,D), return…
  4. Milestone 4 (2 min): Verify — mha(torch.randn(2,6,32)) should return shape (2,6,32). Count params: 4×32×32 = 4096.

46. Your turn — multi-head attention in 15 minutes

Worked example

Speed drill task: implement MultiHeadAttention from scratch using only nn.Linear, F.softmax, and tensor ops. No nn.MultiheadAttention. Target: working code in 15 minutes, matching the shapes in the trace table below.

Milestone 1 (3 min): Define __init__ — four nn.Linear(d_model, d_model, bias=False) for W_q, W_k, W_v, W_o. Store h and d_k = d_model//h.

Why: All four projections are d_model×d_model — no per-head parameters, the split happens in the reshape.

Milestone 2 (5 min): In forward(x), project and reshape — W_q(x).view(B,S,h,d_k).transpose(1,2) for Q,K,V.

Why: .transpose(1,2) moves h before S so batched matmul Q@K.transpose(-2,-1) operates per-head (dim -2 and -1 are S and d_k).

Milestone 3 (5 min): Compute scores = Q@K.T(-2,-1) / d_k**0.5, softmax on dim=-1, ctx = attn@V, then .transpose(1,2).contiguous().view(B,S,D), return W_o(ctx).

Why: .contiguous() is required before .view() because .transpose() makes the tensor non-contiguous in memory.

Milestone 4 (2 min): Verify — mha(torch.randn(2,6,32)) should return shape (2,6,32). Count params: 4×32×32 = 4096.

Why: Shape-preserving and param count match both confirm correctness before wiring into an encoder block.

MilestoneTargetCheck
1 — init4 Linear(32,32,bias=False)param count = 4096
2 — reshape Qshape (B,h,S,d_k) = (2,4,6,8)transpose(1,2) after view
3 — scoresshape (B,h,S,S) = (2,4,6,6)dim=-1 softmax
4 — outputshape (B,S,D) = (2,6,32).contiguous().view needed

47. Fill in: Check for Your turn — multi-head attention in 15…

Comparison

Comparison matrix

From Your turn — multi-head attention in 15 minutes: refill the Check column from what you know. The rest of the table is as it appeared.

MilestoneTargetCheck
1 — init4 Linear(32,32,bias=False)param count = 4096
2 — reshape Qshape (B,h,S,d_k) = (2,4,6,8)transpose(1,2) after view
3 — scoresshape (B,h,S,S) = (2,4,6,6)dim=-1 softmax
4 — outputshape (B,S,D) = (2,6,32).contiguous().view needed

48. Connect it up: Lesson 115: Transformer End-to-End Review

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — Scaled Dot-Product Attention — the atomic unit · Multi-Head Attention — h projections in parallel · Encoder & Decoder Blocks — the full architecture · BERT / GPT / T5 / ViT / GNN — the variant landscape · Complexity & Implementation Drill. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

49. Lesson 115 recap: transformer end-to-end

Recap

Sources

  1. USAAIO Year-Long Master Lesson Plan, Lesson 115 — Complete Transformer Review — Barron · USAAIO Round 2 Preparation, 2026
  2. Vaswani et al. 'Attention Is All You Need' (NeurIPS 2017) — arXiv:1706.03762
  3. Devlin et al. 'BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding' (NAACL 2019) — arXiv:1810.04805
  4. Dosovitskiy et al. 'An Image is Worth 16x16 Words' (ICLR 2021) — arXiv:2010.11929
  5. All attention math, MHA shapes, causal masks, PE values, GNN aggregation, and PixelBERT training trace verified with torch 2.7.1+cpu and numpy 2.2.6, June 2026 — Real execution, verified

Want this taught 1-on-1? Alexander tutors Machine Learning — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108