The lesson in one minute
What you'll be able to explain
- A convolution slides a small filter across an image, multiplying and adding at each position; the output map shows where the filter's pattern appears.
- Weight sharing (one filter everywhere) and local receptive fields make CNNs efficient; pooling downsamples; depth builds edges → parts → objects.
- An RNN reads one step at a time, carrying a hidden state; backpropagating through many steps multiplies gradients until they vanish or explode.
- LSTMs add a cell state edited by forget, input and output gates, so information and gradients survive many steps.
- Transformers won through parallel training and one-hop paths between any two tokens; Vision Transformers treat image patches as tokens.
Level 1
The practitioner's guide
In one sentence
Convolutional networks see images by sliding small learned pattern detectors across them, recurrent networks read sequences one step at a time with a running memory, and knowing what each does well and where each breaks tells you when to reach for one, when to reach for a transformer instead, and why images cost what they cost in a multimodal model.
When you need it
Not for a chat product on a hosted model: there the
transformer has already won and this lesson is background. You need it when
you have an image or signal problem of your own to solve: a defect detector
for a production line, a model that must run on a phone or a camera, a time
series or sensor stream, a legacy system built on LSTMs that you now
maintain, or a bill for image inputs that you want to predict. The tell: a
dataset that is not text, or a device that is not a GPU. The number behind
the first choice, from this lesson's conv_params and dense_params: 64
filters of 3 × 3 over a colour image need 1,792 parameters, at any image
size, while a dense layer mapping a 224 × 224 colour image to an output of
the same size needs about 483 billion. Built-in assumptions (patterns are
local, the same pattern matters anywhere) are what make vision affordable
from little data.
Your options
Two families for images, three for sequences, and the transformer that now spans both. From the most specialised to the most general:
| Option | What it does | What it gives you | What it costs | Where it lives |
|---|---|---|---|---|
| A convolutional network (ResNet family) | Slides learned filters over the image, pools, stacks edges into parts into objects | Strong results from modest data and modest hardware; 1,792 parameters for 64 filters | Its locality assumption caps it at the largest scales | Vision libraries and on-device runtimes |
| A vision transformer (ViT) | Cuts the image into patches, treats each as a token, runs ordinary attention | The best accuracy at scale, on the same stack as text; its paper reports better results than the leading CNNs with substantially less training compute | Needs large pretraining data; 196 tokens for a 224 × 224 image at 16-pixel patches | Vision backbones and multimodal models |
| An image sent to a multimodal LLM | Patches become tokens beside your text | No model to train; you ask questions about the picture | Billed per patch: Claude counts one visual token per 28 × 28 block, so a 1000 × 1000 image is 1,296 tokens | Hosted APIs |
| A plain recurrent network | Rewrites one summary vector after every step | The smallest possible state; runs on anything | Forgets: the start's influence is 0.5¹⁰, about a thousandth, ten steps back in this lesson's example | Legacy and tiny embedded models |
| An LSTM or GRU | Keeps a gated cell state that is edited, not rewritten | Memory across hundreds of steps: the gradient stays near 1 after 50 steps where the plain RNN's is around 10⁻¹⁶ | Sequential training, one step per token; superseded for language | Legacy NLP, time series, small sequence models |
| A transformer | Compares every token with every other in one parallel step | Parallel training and a one-hop path between any two tokens | Cost that grows with the square of the length (primer.ml.attention) |
Every modern language model |
| A state-space model (Mamba) | A selective recurrence that trains in parallel and runs in linear time | 5× the inference throughput of a transformer in its paper, with a fixed-size state | Fewer mature models; some hybrids mix it with attention | Long-stream and hybrid models |
How to choose
Start from the data, then the device.
- Images, limited data or a small device: a convolutional network, pretrained if you can get one. Weight sharing and locality mean it learns from less and runs in a fixed budget of parameters.
- Images at scale, or images beside text: a vision transformer or a multimodal model. Count the tokens an image will cost before you build the pipeline.
- Sequences of any kind today: a transformer by default. Keep an LSTM or GRU only for tiny streaming models, or where a legacy system already works.
- Very long streams where throughput matters more than exact recall: a state-space model or a hybrid, measured against a transformer on your data.
- Anything deep, of any family: residual connections. A 34-layer plain network scored worse than an 18-layer one on ImageNet (28.54% against 27.94% top-1 error) until shortcuts took it to 25.03%.
- Whatever you pick, benchmark at your own scale: the crossover between built-in assumptions and raw data is different for every dataset.
What it costs
Parameters, compute, and the sequential steps that nobody can parallelize.
- Parameters. A convolution's cost depends on the filter and the channel counts, never on the image size: 1,792 for the 64 filters above. A dense layer on raw pixels is out of the question at 483 billion.
- Compute. ResNet-152 runs in 11.3 billion operations per image against
VGG-16's 15.3 billion, deeper and cheaper at once (the ResNet companion).
A 224 × 224 image is 196 tokens of 768 numbers to a vision transformer
(
patchify), and attention over those tokens is the same n² as for text. - Sequential steps. An RNN reading 1,000 tokens takes 1,000 steps one after
another, and information from the first token reaches the last through
999 hand-offs; a transformer layer does it in one step and one hop
(
sequential_steps,path_length). That difference is why transformers could train on vastly more data. - Memory. An RNN's state is one vector however long the input; a
transformer keeps keys and values for every token (
primer.ml.inference). That is the trade state-space models revisit.
What breaks
- A summary that forgets the start. Reading "not very good" one word at a time, this lesson's one-number RNN ends at 0.785, strongly positive, because "not" was rewritten away two steps later. Gates or attention are the fixes.
- Gradients that vanish or explode. Each step back multiplies the training signal by a factor: 0.5 gives 0.00098 after ten steps, 1.5 gives 57.7 and diverges. Clip gradients (Pascanu et al., 2012) and use gated cells.
- Depth that makes things worse. Without shortcuts a deeper network can score lower even on its training data; residual connections reverse it and are inside every transformer block.
- A view too narrow for the object. One 3 × 3 layer sees 3 pixels;
conv, pool, conv sees 8 (
receptive_field). A shallow network without pooling never sees a whole object, however many filters it has. - Image bills that scale with pixels. Every patch is a token: on Anthropic's API an image costs ⌈width / 28⌉ × ⌈height / 28⌉ visual tokens, so a 200 × 200 image is 64 tokens and a 1000 × 1000 image is 1,296, about $1.30 per thousand images at $1 per million tokens. Resize before you send.
- Patches with no positions. A vision transformer reads a bag of
patches unless positions are added, exactly as text does
(
primer.ml.positional).
In the wild
AlexNet won ImageNet in 2012 by training a deep CNN on GPUs; VGG showed that stacks of 3 × 3 filters work; ResNet's 152-layer network reached 3.57% top-5 error in an ensemble and put the shortcut connection into every architecture since. Vision transformers took over large-scale vision, and multimodal language models read images the same way, as patch tokens. On the sequence side, the LSTM (Hochreiter and Schmidhuber, 1997) and the GRU (Cho et al., 2014) carried language modelling until attention, and Mamba (Gu and Dao, 2023) reports a 3-billion-parameter model matching transformers twice its size at linear cost in length. The papers are linked at the end of the lesson.
Go deeper
Level 2 runs a 3 × 3 edge filter over a 5 × 5 image by hand, pools a 4 × 4 map, counts receptive fields, cuts an image into patch tokens, reads "not very good" with a one-number RNN, multiplies out the vanishing gradient, pins an LSTM's gates to keep, erase, overwrite and hide a note, and counts the steps that decided the contest. If you only needed to choose a family, you are done.
Level 2
How it works, from scratch
Before transformers, two designs dominated deep learning, and both still matter. Convolutional networks (CNNs) see images by sliding small pattern detectors across them. Recurrent networks (RNNs) read sequences one item at a time, carrying a running memory. Each builds in an assumption about its data (patterns in images are local; sequences unfold in order), and each has a limit that the transformer removed. Knowing both stories explains why modern models look the way they do.
Part A: convolutional networks
Chapter 1
A1. A convolution is a flashlight looking for one pattern
Everyday picture You are in a dark room with a large photograph and a small flashlight. You're looking for one thing, say a place where dark turns to bright from left to right. You sweep the flashlight across the photo, one step at a time, and at every spot you jot down a score: high if the lit patch matches what you're looking for, near zero if it doesn't. When you finish, your notes form a new, smaller picture: a map of where the pattern appears. That map is called a feature map, and the pattern you were looking for, written as a small grid of numbers, is the filter (or kernel).
Tiny worked example A 5×5 image: two dark columns (0) then three bright columns (1), so there is a vertical edge between columns 2 and 3. The filter is a 3×3 "vertical edge" detector: −1 on the left column, 0 in the middle, +1 on the right. It rewards "bright on the right, dark on the left".
image X filter K
0 0 1 1 1 -1 0 1
0 0 1 1 1 -1 0 1
0 0 1 1 1 -1 0 1
0 0 1 1 1
0 0 1 1 1
Put the filter on the top-left 3×3 patch of the image. Multiply each image number by the filter number on top of it, and add all nine products:
patch × filter = products
0 0 1 -1 0 1 0 0 1
0 0 1 -1 0 1 0 0 1 sum = 1 + 1 + 1 = 3
0 0 1 -1 0 1 0 0 1
So the top-left cell of the feature map is 3. Slide one step right: the
patch is 0 1 1 in every row; products 0 0 1 per row; sum again 3
(the edge is still under the flashlight). One more step: the patch is
1 1 1, products −1 0 1, sum 0 (flat bright area, no edge). Doing the
same for every row gives the full 3×3 feature map:
3 3 0
3 3 0
3 3 0
The high numbers sit exactly where the edge is. Run a horizontal edge filter over the same image and every cell is 0: this image has no top-to-bottom change. Each filter answers one question.
Figure 1 · Diagram
flowchart LR
W[Take the next k×k patch<br/>of the image] --> M[Multiply each pixel by the<br/>filter weight on top of it]
M --> S[Add all the products<br/>into one number]
S --> C[Write it into the<br/>feature map]
C --> N{More positions?}
N -->|slide by the stride| W
N -->|no| F[Feature map complete]
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | Shape / range |
|---|---|---|
| the input image; is channel c (red, green or blue) at row a, column b | (channels, height, width) | |
| the filter (kernel): a small grid of learned weights, one slice per channel | (channels, k, k) | |
| the feature-map value at output row i, column j | one number | |
| add up over every channel c and every filter row u and column v | ||
| filter size (3 for a 3×3 filter) | small integer | |
| stride: how many pixels the filter jumps between positions | 1 or 2 | |
| input size along one side | e.g. 5 or 224 | |
| padding: zeros added around each side | 0, 1, 2… | |
| floor: round down to a whole number |
In words: each output number is the sum, over the filter's footprint and all colour channels, of filter weight times the pixel under it; the output has one number per place the filter can stand.
On the worked example: one channel, k = 3, s = 1, p = 0. Y[0, 0] = (−1)·0 + 0·0 + 1·1 for each of the 3 rows = 3. Output size ⌊(5 + 0 − 3)/1⌋ + 1 = 3. For a 224-pixel image, a 7×7 filter, stride 2 and padding 3: ⌊(224 + 6 − 7)/2⌋ + 1 = 112.
Level 3: in Python
# the 5×5 image: one channel, so Σ_c has one term
X = [[0, 0, 1, 1, 1] for _ in range(5)]
# the vertical-edge filter
K = [[-1, 0, 1] for _ in range(3)]
n, k, s, p = 5, 3, 1, 0
# ⌊(n + 2p - k) / s⌋ + 1
out = (n + 2 * p - k) // s + 1
out # → 3
# Σ_u Σ_v K[u, v] X[i s + u, j s + v]
Y = [[sum(K[u][v] * X[i * s + u][j * s + v]
for u in range(k) for v in range(k))
for j in range(out)]
for i in range(out)]
Y # → [[3, 3, 0], [3, 3, 0], [3, 3, 0]]
# 224 pixels, 7×7 filter, stride 2, padding 3
(224 + 2 * 3 - 7) // 2 + 1 # → 112
In code: conv2d is the loop in the diagram, one multiply-and-add per
position, and conv_output_size is the output-size formula.
Chapter 2
A2. Pooling: summarise each neighbourhood by its loudest voice
Everyday picture A manager asks each of four teams for one number: the strongest signal anyone on the team saw. The report is four times shorter and still says where something important happened.
Tiny worked example 2×2 max pooling on a 4×4 map keeps the largest value in each quarter:
1 3 | 0 0
2 4 | 0 1 -> 4 1
----+---- 1 6
0 0 | 5 2
1 0 | 1 6
Figure 2 · Drawn from the lesson's code
One CNN layer on an 8×8 bright square: the vertical-edge feature map is positive down the left side and negative down the right, and 2×2 max pooling keeps the left edge while the negative right edge becomes 0
In code: max_pool2d keeps the largest value in each non-overlapping
block.
Chapter 3
A3. Weight sharing and the receptive field
Everyday picture A rubber stamp: you carve the pattern once and use it everywhere on the page. A CNN uses the same filter at every position, so a cat detector works in any corner of the photo and costs the same number of weights however large the photo is.
Tiny worked example 64 filters of size 3×3 over a colour image need 3·3·3·64 + 64 = 1,792 parameters, for any image size. A fully connected layer mapping a 224×224×3 image to an output of the same size as those 64 feature maps would need 150,528 × 3,211,264 ≈ 483 billion weights.
As layers stack, each neuron sees more of the original image: its receptive field. One 3×3 layer sees 3×3 pixels; two see 5×5; three see 7×7. Pooling between layers speeds this up: conv 3×3, pool 2×2, conv 3×3 already sees 8×8. That's why two stacked 3×3 filters (18 weights, and two nonlinearities) replaced single 5×5 filters (25 weights) in VGG.
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | Shape / range |
|---|---|---|
| layer number, counting from the input | 1, 2, 3… | |
| receptive field after layer ℓ: input pixels (along one side) one neuron sees | ≥ 1 | |
| kernel (filter or pool window) size of layer ℓ | ||
| stride of layer ℓ | ||
| jump: input pixels between neighbouring neurons at layer ℓ | ≥ 1 |
In words: every layer widens the view by (kernel − 1) steps, and a step at depth ℓ is as many input pixels as all earlier strides multiplied together.
On the worked example: conv 3 (r = 1 + 2·1 = 3, j = 1), pool 2 stride 2 (r = 3 + 1·1 = 4, j = 2), conv 3 (r = 4 + 2·2 = 8).
Level 3: in Python
# (k_ℓ, s_ℓ): conv 3, pool 2 with stride 2, conv 3
layers = [(3, 1), (2, 2), (3, 1)]
# r_0 = j_0 = 1
r, j = 1, 1
for k_l, s_l in layers:
# r_ℓ = r_{ℓ-1} + (k_ℓ - 1) j_{ℓ-1}
r = r + (k_l - 1) * j
# j_ℓ = j_{ℓ-1} s_ℓ
j = j * s_l
print(r, j) # → 3 1 4 2 8 2
Figure 3 · Drawn from the lesson's code
Receptive field against depth: plain 3×3 layers widen the view by 2 pixels per layer, while pooling after every second layer makes the jumps double
In code: conv_params and dense_params count the two layers'
parameters, and receptive_field applies the recurrence to a stack of
(kernel, stride) layers.
Chapter 4
A4. From edges to parts to objects
Everyday picture Reading starts with strokes, then letters, then words, then sentences. A CNN's first layer learns strokes (edges and colour blobs), the next combines them into textures and corners, deeper layers into parts (eyes, wheels), and the last into whole objects.
Figure 5 · Diagram
flowchart LR I[Image pixels] --> C1[Conv layer<br/>edges] C1 --> P1[Pool] P1 --> C2[Conv layer<br/>textures, parts] C2 --> P2[Pool] P2 --> C3[Conv layer<br/>whole objects] C3 --> FC[Dense layer] FC --> O[Prediction<br/>cat 94%]
Figure 4 · Drawn from the lesson's code
Four first-layer filters on an L-shaped block: the edge filters fire on their own sides, the diagonal filter fires on every side, and a second-layer product of the two edge maps lights up only at the L's corners
In code: the figure runs each first-layer filter over the L with
conv2d, and the corner detector multiplies two of those feature maps
entry by entry.
Chapter 5
A5. Landmarks, and patches as tokens
- AlexNet (2012) won the ImageNet competition by a wide margin by training a deep CNN on GPUs, which started the deep-learning boom.
- VGG (2014) showed that deep stacks of small 3×3 filters work well.
- ResNet (2015) added residual (skip) connections, letting gradients
bypass layers, and made networks of 100+ layers trainable. The same idea
sits inside every transformer (see
primer.ml.deep_nets). - Vision Transformers (2020) cut an image into patches and treat each patch as a token, then apply ordinary attention.
Everyday picture for patches. Cut a photo into a grid of jigsaw pieces, lay them out in a row, and read them like the words of a sentence.
Tiny worked example A 224×224 colour image cut into 16×16 patches gives (224/16)² = 196 patches, each flattened to 16·16·3 = 768 numbers: 196 tokens of 768 values.
Figure 6 · Diagram
flowchart LR IMG[224×224×3 image] --> CUT[Cut into 16×16 patches<br/>14 × 14 = 196 pieces] CUT --> FLAT[Flatten each patch<br/>768 numbers] FLAT --> PROJ[Linear projection<br/>to model width] PROJ --> POS[Add position<br/>embeddings] POS --> TF[Transformer blocks<br/>attention across patches]
In code: patchify does the cut-and-flatten step, turning an image into
one row of numbers per patch.
Why it matters in practice. CNNs remain efficient and strong for small vision tasks, on-device models and limited data, because their built-in assumptions (locality, weight sharing) mean they learn from less. At large scale, Vision Transformers dominate.
Part B: recurrent networks
Chapter 6
B1. An RNN reads with a one-page summary
Everyday picture You read a book one word at a time, and you are allowed to keep only a single page of notes. After every word you rewrite the page: blend what the page said with the new word. At the end, the page is all you have. That page is the hidden state.
Tiny worked example The sentence "not very good", with each word turned into one number: not = −1, very = 0.5, good = 1. The summary is a single number h, starting at 0. The rule: new h = tanh(0.5 × old h + 1 × word). (tanh squashes any number into the range −1 to 1: tanh(0) = 0, tanh(1) = 0.76, tanh(−1) = −0.76.)
| step | word | 0.5 × old h + word | new h = tanh(…) |
|---|---|---|---|
| 1 | not (−1) | 0.5 × 0 + (−1) = −1 | −0.762 |
| 2 | very (0.5) | 0.5 × (−0.762) + 0.5 = 0.119 | 0.119 |
| 3 | good (1) | 0.5 × 0.119 + 1 = 1.059 | 0.785 |
The final summary is strongly positive: the "not" at the start has been almost washed out, because the page was rewritten twice since. This is the central weakness of RNNs, in three lines of arithmetic.
Figure 7 · Diagram
flowchart LR H0[h0 = 0] --> C1[RNN cell] X1[x1: not] --> C1 C1 --> H1[h1 = −0.762] H1 --> C2[RNN cell<br/>same weights] X2[x2: very] --> C2 C2 --> H2[h2 = 0.119] H2 --> C3[RNN cell<br/>same weights] X3[x3: good] --> C3 C3 --> H3[h3 = 0.785<br/>final summary]
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | Shape / range |
|---|---|---|
| the time step (word position) | 1, 2, 3… | |
| the input at step t (a word's vector) | (inputs,) | |
| the hidden state (the summary page) after step t | (hidden,), each entry in −1…1 | |
| the summary before this word | (hidden,) | |
| recurrent weights: how the old summary feeds the new one | (hidden, hidden) | |
| input weights: how the word feeds the new summary | (hidden, inputs) | |
| bias | (hidden,) | |
| hyperbolic tangent: squashes each number into −1…1 |
In words: the new summary is the squashed sum of the old summary times its weights, the new word times its weights, and a bias.
On the worked example: one-number summary, W_h = 0.5, W_x = 1, b = 0: h₁ = tanh(0.5·0 − 1) = −0.762; h₂ = tanh(0.5·(−0.762) + 0.5) = 0.119; h₃ = tanh(0.5·0.119 + 1) = 0.785.
Level 3: in Python
import math
W_h, W_x, b = 0.5, 1, 0
# not, very, good
x = [-1, 0.5, 1]
# h_0: a blank page
h = 0
for x_t in x:
# h_t = tanh(W_h h_{t-1} + W_x x_t + b)
h = math.tanh(W_h * h + W_x * x_t + b)
print(round(h, 3)) # → -0.762 0.119 0.785
In code: RNNCell holds W_h, W_x and b; RNNCell.step is the formula
once, RNNCell.run applies it along a sequence, and RNNCell.scalar builds
the one-number cell of the worked example.
Chapter 7
B2. Why RNNs forget: the vanishing gradient
Everyday picture A photocopy of a photocopy of a photocopy. Each copy loses a little; after twenty copies the original is unreadable. Training an RNN sends a correction signal backwards through every step, and each step multiplies it by a factor. Factors below 1 fade the signal to nothing (vanishing gradient); factors above 1 blow it up (exploding gradient).
Tiny worked example With a recurrent weight of 0.5 and zero inputs, each step back multiplies the signal by exactly 0.5. Ten steps back: 0.5¹⁰ = 0.00098, about a thousandth. With a weight of 1.5 instead: 1.5¹⁰ = 57.7, and training diverges.
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | Shape / range |
|---|---|---|
| "how much does the final summary change if the starting summary changes a little?" (a derivative, one per pair of entries) | (hidden, hidden) | |
| multiply the factors for every step from 1 to T | ||
| the slope of tanh at step t: 1 near zero, near 0 when tanh saturates | 0…1 | |
| a matrix with these values on the diagonal and zeros elsewhere | (hidden, hidden) | |
| the recurrent weights, as above | (hidden, hidden) |
In words: the influence of the start on the end is the product, over every step, of tanh's slope times the recurrent weights, so it shrinks or grows geometrically with the number of steps.
On the worked example: h stays 0, so every slope is 1, and the product is 0.5 × 0.5 × … (ten times) = 0.00098.
Level 3: in Python
W_h = 0.5
# zero inputs keep every h_t at 0
h = [0.0] * 10
influence = 1
for h_t in h:
# Π over t of tanh's slope times W_h
influence *= (1 - h_t ** 2) * W_h
round(influence, 5) # → 0.00098
# the same product with a weight of 1.5
round(1.5 ** 10, 1) # → 57.7
Figure 8 · Drawn from the lesson's code
Gradient reaching back through time on a log scale: the plain RNN's plunges to about 10⁻¹⁶ after 50 steps while the LSTM's stays near 1
In code: RNNCell.influence_of_start multiplies out the product above
for one sequence, and gradient_through_time measures both lines of the
figure.
Chapter 8
B3. LSTM: a notebook with an eraser, a pen and a highlighter
Everyday picture Instead of rewriting the whole page after every word, keep a notebook (the cell state) and three tools, each controlled by a dial from 0 to 1 that the network sets for itself at every step:
- the eraser (forget gate f): how much of each line to keep;
- the pen (input gate i): how much of the new note to write in;
- the highlighter (output gate o): how much of the notebook to show the outside world right now.
Because the notebook is edited rather than rewritten, information can pass through many steps untouched: eraser off, pen off, and the line survives.
Tiny worked example One-line notebook holding 0.8.
| eraser keeps f | pen writes i | new note g | highlighter o | new notebook c = f·0.8 + i·g | shown h = o·tanh(c) |
|---|---|---|---|---|---|
| 1 | 0 | – | 1 | 0.8 (kept, even after 100 steps) | 0.664 |
| 0 | 0 | – | 1 | 0 (wiped) | 0 |
| 0 | 1 | 0.5 | 1 | 0.5 (overwritten) | tanh(0.5) = 0.462 |
| 1 | 0 | – | 0 | 0.8 (kept) | 0 (hidden) |
Figure 9 · Diagram
flowchart LR CP[notebook c_prev] --> FX((× f<br/>eraser)) FX --> ADD((+)) G[new note g] --> IX((× i<br/>pen)) IX --> ADD ADD --> C[notebook c] C --> T[tanh] T --> OX((× o<br/>highlighter)) OX --> H[shown h] XH[input x and previous h] -.-> FX & IX & OX & G
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | Shape / range |
|---|---|---|
| forget (eraser), input (pen) and output (highlighter) gates | (hidden,), each 0…1 | |
| the candidate note to write | (hidden,), −1…1 | |
| the cell state: the notebook | (hidden,) | |
| the hidden state: what the cell shows | (hidden,) | |
| the input and previous shown state, stacked into one vector | (inputs + hidden,) | |
| learned weights for each gate | (hidden, inputs + hidden) | |
| sigmoid, 1/(1 + e⁻ᶻ): squashes to 0…1, so it works as a dial | ||
| multiply element by element (entry 1 with entry 1, and so on) |
In words: three sigmoid dials and one candidate are computed from the input and the previous state; the notebook keeps f of itself and adds i of the candidate; the cell shows o of the squashed notebook.
On the worked example: third row: f = 0, i = 1, g = 0.5, o = 1, so c = 0·0.8 + 1·0.5 = 0.5 and h = 1·tanh(0.5) = 0.462.
Level 3: in Python
import math
def sigma(z):
# σ: any score becomes a dial between 0 and 1
return 1 / (1 + math.exp(-z))
[round(sigma(z), 3) for z in (-4, 0, 4)] # → [0.018, 0.5, 0.982]
def lstm_step(f, i, g, o, c_prev=0.8):
# c_t = f ⊙ c_{t-1} + i ⊙ g
c = f * c_prev + i * g
# h_t = o ⊙ tanh(c_t)
h = o * math.tanh(c)
return c, h
for f, i, g, o in [(1, 0, 0, 1), (0, 0, 0, 1), (0, 1, 0.5, 1), (1, 0, 0, 0)]:
# the four rows of the table, dials pinned by hand
c, h = lstm_step(f, i, g, o)
print(c, round(h, 3)) # → 0.8 0.664 0.0 0.0 0.5 0.462 0.8 0.0
GRUs simplify this to two dials and no separate notebook: an update gate z chooses between keeping the old state (z = 1) and taking a new candidate (z = 0), and a reset gate decides how much old state feeds that candidate: h = (1 − z) ⊙ n + z ⊙ h_prev, where n is the candidate. Similar performance, fewer parameters.
In code: LSTMCell holds the four gates' stacked weights;
LSTMCell.step computes the dials and updates the notebook,
LSTMCell.run carries it along a sequence, and
LSTMCell.fixed_gates pins the dials to replay the table above;
GRUCell.step is the two-dial version.
Chapter 9
B4. Why transformers won
Everyday picture An RNN is a line of people passing a note: the last person hears about the first only through everyone in between, and nobody can start until the person before them finishes. A transformer is a meeting where everyone can speak to everyone directly, all at once.
Tiny worked example 1,000 tokens. An RNN needs 1,000 steps one after another, and information from token 1 reaches token 1,000 through 999 hand-offs. A transformer layer processes all 1,000 in 1 parallel step, and any token reaches any other in 1 hop of attention.
Figure 11 · Diagram
flowchart LR
subgraph RNN["RNN: one step at a time"]
r1[The] --> r2[cat] --> r3[sat] --> r4[down]
end
subgraph TF["Transformer: all at once"]
t1[The] & t2[cat] & t3[sat] & t4[down] --> A[Attention<br/>every pair compared]
end
primer.ml.attention).In code: sequential_steps and path_length return the two counts in
the worked example for an RNN or a transformer.
State-space models such as Mamba revisit recurrence with a design that trains in parallel and runs in time linear in sequence length. They carry a compressed state like an RNN but avoid its training bottleneck, and some hybrid models mix them with attention.
Figure 10 · Drawn from the lesson's code
The one-number summary after each word of "not very good": negative after "not", near zero after "very", strongly positive after "good", so the negation is lost
Test yourself
9 questions
Answer each one out loud or on paper before you open it. If you can explain it, you know it.
Question 1Q: What does one number in a feature map mean?Think it through, then reveal
A: How strongly the filter's pattern matches the image patch at that position: the sum of filter weights times the pixels under them.
Question 2Q: Why does a CNN need far fewer parameters than a dense layer on images?Think it through, then reveal
A: Weight sharing: one small filter is reused at every position, so the parameter count depends on filter size and number of filters, not on image size.
Question 3Q: What does pooling buy you?Think it through, then reveal
A: Smaller maps (less computation), a faster-growing receptive field, and tolerance to small shifts, since the strongest response in a neighbourhood survives wherever exactly it was.
Question 4Q: Why did VGG use stacks of 3×3 filters instead of larger ones?Think it through, then reveal
A: Two 3×3 layers see a 5×5 region with 18 weights instead of 25, and add an extra nonlinearity between them.
Question 5Q: How does a Vision Transformer turn an image into tokens?Think it through, then reveal
A: It cuts the image into fixed-size patches (e.g. 16×16), flattens each into a vector, projects it to the model width and adds a position embedding; the patches are then processed like words.
Question 6Q: Why do plain RNNs struggle with long-range dependencies?Think it through, then reveal
A: Backpropagation through time multiplies the gradient by the recurrent weights and tanh slopes at every step, so it shrinks geometrically (or explodes) and early inputs stop influencing learning.
Question 7Q: How do LSTM gates fix that?Think it through, then reveal
A: The cell state is updated additively, c = f ⊙ c_prev + i ⊙ g, so with the forget gate near 1 information and gradients flow through many steps almost unchanged.
Question 8Q: Why did transformers replace RNNs?Think it through, then reveal
A: RNNs process tokens sequentially, which prevents parallel training, and force all history through one fixed-size state. Attention connects any two tokens in one step and trains all positions in parallel.
Question 9Q: What did CNNs contribute that still matters?Think it through, then reveal
A: Residual connections (from ResNet), which make very deep networks trainable and are in every transformer block, plus the general lesson that built-in assumptions help when data is limited.
Primary sources
The papers behind this lesson
He, Zhang, Ren & Sun, Deep Residual Learning for Image Recognition (2015): Introduced residual connections, making networks of 100+ layers trainable.
Read the annotated companion →The paper ↗Vaswani et al., Attention Is All You Need (2017): Replaced recurrence with attention, enabling parallel training and one-hop paths between tokens.
Read on rumblr →The paper ↗Krizhevsky, Sutskever & Hinton, ImageNet Classification with Deep Convolutional Neural Networks (NeurIPS 2012). Trained a deep CNN on GPUs and won ImageNet by a wide margin, starting the deep-learning boom.
Simonyan & Zisserman, Very Deep Convolutional Networks for Large-Scale Image Recognition (VGG, 2014): Showed deep stacks of 3×3 filters work well.
The paper ↗Dosovitskiy et al., An Image is Worth 16x16 Words (ViT, 2020): Applied a plain transformer to image patches as tokens.
The paper ↗Hochreiter & Schmidhuber, Long Short-Term Memory (1997): Introduced the gated cell state that carries information across long sequences.
The paper ↗Cho et al., Learning Phrase Representations using RNN Encoder-Decoder (2014): Introduced the GRU.
The paper ↗Pascanu, Mikolov & Bengio, On the difficulty of training Recurrent Neural Networks (2012): Analysed vanishing and exploding gradients and proposed gradient clipping.
The paper ↗Gu & Dao, Mamba: Linear-Time Sequence Modeling with Selective State Spaces (2023): A recurrent-style model that trains in parallel and scales linearly with length.
The paper ↗Researcher's shelf
Further reading
- Stanford CS231n, Convolutional Neural Networks: https://cs231n.github.io/convolutional-networks/
- Christopher Olah, Understanding LSTM Networks: https://colah.github.io/posts/2015-08-Understanding-LSTMs/
- Andrej Karpathy, The Unreasonable Effectiveness of Recurrent Neural Networks: http://karpathy.github.io/2015/05/21/rnn-effectiveness/
- Olah, Mordvintsev & Schubert, Feature Visualization (Distill): https://distill.pub/2017/feature-visualization/
About this lesson. This is the illustrated edition of a lesson from the open-source AI Primer. Its text, figures and numbers are generated from the Primer's source at commit 048aeaa, so the two always agree: the explanation, the code that builds it and the tests that prove it.