At a glance
Key takeaways
- 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 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 · Chart
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 · Chart
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 4 · 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 5 · Chart
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 · Chart
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 10 · 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 11 · Chart
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 c8d5c21, so the two always agree: the explanation, the code that builds it and the tests that prove it.