Deep Learning — Interactive Concept Lab
The deep-learning companion lab — training mechanics, CNNs, sequence models, transformers, modern LLMs, generative models, and deep RL. Click, drag, and animate each idea to see how it works. Foundations (neurons, activations, backprop, convolution, attention, embeddings, RNNs) live in the ML Concept Lab. Press Esc for this menu.
Vanishing & exploding gradients
Backprop multiplies gradients layer by layer on the way back. If each factor is a little below 1, the product shrinks toward zero over many layers — early layers stop learning. A little above 1 and it explodes. This single fact is why naive deep nets refused to train for decades.
Per-layer factor
Sigmoid / tanh path
The product
The fixes
This is why we use ReLU (derivative 1 when active, not 0.25 like sigmoid), careful initialization, residual/skip connections, and normalization — each keeps that per-layer factor near 1 so the signal survives the trip back.
Computational graph & autodiff
Every model is a graph of simple operations. The forward pass flows values up to the loss; reverse-mode autodiff (backprop) flows gradients back down, applying the chain rule at each node — computing every parameter's gradient in a single sweep. This is the engine inside every framework.
Inputs
Forward pass
The graph
Why reverse mode
One backward sweep yields the gradient of one output with respect to all inputs — exactly what training needs (one loss, millions of parameters). Forward-mode would cost one sweep per parameter.
Weight initialization
Where you start matters. Initialize every weight to the same value and all neurons in a layer learn identically forever — symmetry never breaks. Initialize too large or too small and activations explode or vanish through the layers. Xavier and He initialization pick the variance that keeps the signal stable.
Initialization scheme
He / Xavier
The variance rule
What the bars show
Each bar is the spread (std) of activations at that layer. Flat across depth = healthy signal. Collapsing to zero = vanishing; growing without bound = exploding/saturating. Zeros init makes every neuron identical — the network can never differentiate them.
Activation zoo
Non-linearities are what let a deep net bend into complex shapes — without them, stacked layers collapse into one linear map. But each activation has a personality: sigmoid/tanh saturate (flat tails → tiny gradients), ReLU is cheap but can "die," and smooth variants like GELU and Swish power modern nets. Compare curves and their derivatives.
Activation
ReLU
Reading the plot
Solid = the activation φ(x). Dashed = its derivative φ′(x) — the multiplier backprop sees. Where the derivative is ~0 (flat tails, ReLU's left half), gradient stops flowing.
Picking one
ReLU is the cheap default but "dies" when stuck negative (zero gradient). LeakyReLU keeps a small slope. GELU/Swish are smooth and tend to train a touch better in transformers and deep CNNs. Sigmoid/tanh now mostly live in gates and outputs, not hidden layers.
Learning-rate schedules
The learning rate is the single most important knob — and the best value changes over training. Big steps early to make fast progress, small steps late to settle into a minimum. Schedules automate this: step decay, exponential, cosine, and the warmup that stabilizes the volatile first steps of large models.
Schedule
Cosine annealing
Why it helps
Too-high a constant rate bounces around the minimum forever; too-low crawls. Decaying lets you have both. Warmup — ramping up over the first few hundred steps — prevents the huge, destabilizing updates that big batches and transformers suffer at initialization.
Double descent
Classical statistics says test error follows a U: too simple underfits, too complex overfits. Modern over-parameterized nets break this — push capacity past the point of exactly fitting the data (the interpolation threshold) and test error, after spiking, falls a second time. It's why enormous models generalize at all.
Model capacity
Under-parameterized
Readings
The three zones
Under-parameterized: the classical U — test error dips then rises. Interpolation threshold: the model just barely fits every point; test error peaks here. Over-parameterized: among the many zero-train-error solutions, gradient descent prefers smooth ones, so test error descends again.
Gradient checking
Backprop is easy to get subtly wrong. Gradient checking is the safety net: compare your analytic gradient against a brute-force numerical one from finite differences. If they agree to ~7 decimal places, your code is right; if not, you've caught a bug. Shrink ε and watch the secant collapse onto the true tangent.
Controls
Correct backprop
The check
The ε sweet spot
Drag ε very small and the relative error rises again — floating-point cancellation swamps the answer. Too large and the finite-difference approximation is inaccurate. Around 1e-5 to 1e-7 is the safe zone.
Gradient clipping
Some loss landscapes have cliffs — regions where the gradient is enormous. One such step can hurl your parameters far away and blow up training. Clipping caps the gradient's norm: keep the direction, bound the length. Watch the ball ride the descent, then toggle clipping off and see it get flung off the cliff.
Controls
Clipping ON
Clip by norm
Mini-batch size
Each step estimates the gradient from a batch of examples. A tiny batch is cheap but noisy — the path jitters. A huge batch is smooth and accurate but expensive, with fewer updates per epoch. The noise isn't all bad: it helps escape sharp minima. Slide the batch size and watch the descent path change character.
Batch size
Mini-batch
The trade-off
Universal approximation
A neural network with a single hidden layer can approximate any continuous function — given enough neurons. Each ReLU unit contributes one kink; sum enough of them and you can trace any curve. Add hidden neurons and watch the network's piecewise approximation close in on the target.
Hidden neurons
Too few neurons
The theorem
For any continuous f on a bounded domain and any ε, there exists a one-hidden-layer net whose output stays within ε of f everywhere.
The forward pass
A neural network is a chain of simple steps: each layer multiplies its inputs by a weight matrix, adds a bias, and applies a non-linearity. Data flows left to right, layer by layer, until it becomes a prediction. Step through a 3→4→4→2 network and watch real numbers propagate through every unit.
Walk the layers
Input layer
This layer computes
General form: a(l) = φ(W(l)a(l−1) + b(l)), where φ is the activation.
Why stacking works
A single linear layer can only draw a straight boundary; the non-linearity between layers is what lets composition build curves, corners, and complex shapes. Remove the activations and the whole stack collapses back into one linear map — depth would buy nothing.
Backpropagation
Backprop is how a network assigns blame. After the forward pass computes the loss, it sweeps backward, using the chain rule to find exactly how much each weight contributed to the error — every gradient in a single pass. Step through a tiny 2→2→1 sigmoid network with real numbers and watch the error flow back.
Step through it
Start — the input
The four equations
It is just the chain rule
For a first-layer weight, ∂L/∂W¹ = ∂L/∂a² · ∂a²/∂z² · ∂z²/∂a¹ · ∂a¹/∂z¹ · ∂z¹/∂W¹. Every factor is local and cheap. Backprop’s trick is to compute the shared prefix once (the δ terms) and reuse it for every weight — turning an exponential blow-up into one linear sweep.
Gradient descent
Learning is gradient descent: compute the slope of the loss, then take a step in the opposite (downhill) direction. The gradient points toward steepest ascent, so we subtract it. The learning rate sets the step size — too small and you crawl, too large and you bounce out. Watch the trajectory on an elliptical bowl.
Controls
Mini-batch gradient descent
The update rule
Stability needs η < 2/λmax; below 1/λmax the descent is monotone.
Optimizers
Plain gradient descent struggles in a ravine — a valley that's steep across and shallow along. It zig-zags down the walls and crawls along the floor. Optimizers fix this: momentum builds speed, RMSProp adapts each parameter's step, and Adam combines both. Switch optimizer and watch the path; the faint trails are the others, for comparison.
Optimizer
Adam
Update rule
g = ∇L · μ,ρ,β are decay rates · ε guards the divide
Why it matters
Real loss surfaces are badly conditioned — some directions far steeper than others. Adapting the step per-direction (RMSProp/Adam) or carrying momentum turns a slow zig-zag into a fast glide, which is why Adam is the default starting point for most deep nets.
Dropout
During training, dropout randomly switches off a fraction of neurons on every forward pass. Each pass trains a different thinned network, so no neuron can rely on any specific other — they can't co-adapt. It's like training a huge ensemble of sub-networks that share weights. At test time all neurons are on, scaled to compensate. Watch the active sub-network change each step.
Controls
Training
The rule
Why it generalizes
Forcing the network to work with random subsets stops neurons from forming fragile, co-dependent detectors. The result behaves like averaging exponentially many sub-networks — a powerful, almost free regularizer. Inverted dropout divides by (1−p) during training so the test-time pass needs no rescaling.
Batch normalization
Batch norm steadies training by normalizing each layer's pre-activations across the mini-batch — subtract the batch mean, divide by the batch std — then a learnable scale (γ) and shift (β) let the network undo or reshape that normalization if it helps. Step through the transformation and watch the distribution snap to zero-mean, unit-variance, then move where γ and β want it.
Stage
Raw activations
The transform
Early stopping
Train long enough and training loss keeps falling — but validation loss bottoms out and then climbs as the model starts memorizing noise. Early stopping watches the validation curve, keeps the best checkpoint, and halts once it hasn't improved for patience epochs. Press play and watch the curves diverge.
Controls
How it works
Checkpoints
The convolution operation
A convolution slides a small kernel across the image; at each position it computes a dot product between the kernel and the patch beneath it, producing one number in the output feature map. The same kernel is reused everywhere — so the layer learns a feature once and detects it anywhere. Pick a kernel and watch it sweep.
Kernel (3×3)
Vertical-edge detector
One output value
Teal = positive response, pink = negative. The running Σ on the canvas is the value being written into the highlighted output cell.
Why convolution, not dense?
A dense layer would need a separate weight for every pixel-to-pixel link — millions of them. Convolution shares one tiny kernel across all positions: far fewer parameters, and translation-equivariant (a feature is detected wherever it appears). This weight-sharing is the core idea that makes vision nets feasible.
Padding & stride
Two knobs control how a convolution moves and how big its output is. Stride is how far the kernel jumps each step; padding adds a border of zeros so the kernel can reach the edges. Together they set the output dimensions — and whether the map keeps its size or shrinks.
Stride
Padding
Valid padding
Output size
N = input, K = kernel = 3, P = padding, S = stride.
Stride as downsampling
Stride 1 visits every location; stride 2 skips every other one, roughly halving each output dimension — a cheap way to downsample inside the convolution instead of adding a separate pooling layer (common in modern architectures).
Pooling
Pooling shrinks a feature map by summarizing each small window with a single number — usually the maximum. It cuts computation, enlarges the effective receptive field of later layers, and grants a little translation invariance: a feature shifting by one pixel often lands in the same pooled cell. Watch a 2×2 window stride across a 6×6 map.
Pooling type
Max pooling
This layer
In max pooling the brightest cell in each window (highlighted) is the one that survives.
The trade-off
Pooling makes the network cheaper and more robust to small shifts, but it is deliberately lossy — it discards where within the window the feature was. Some modern nets drop pooling entirely and downsample with strided convolutions instead.
Receptive field
A single neuron deep in a CNN doesn't see the whole image — only a patch of it, its receptive field. Each stacked conv layer widens that patch, so deep units integrate information from a large region while early ones stay local. Add layers and watch the cone of dependence grow back to the input.
Depth & kernel
3×3 kernels
Receptive field
Growing it faster
Stride and pooling multiply the field instead of adding to it, and dilated convolutions skip pixels to expand it without extra parameters. That's how a network reaches a field covering the whole image within a manageable number of layers.
Residual & skip connections
Stacking dozens of plain layers paradoxically makes a net harder to train — gradients vanish on the long trip back. A residual block adds a shortcut: out = F(x) + x. The layers only learn the residual, and the identity skip gives gradients a clear path home. This one idea (ResNet) unlocked networks hundreds of layers deep.
Skip connection
With skip connection
The block
F(x) is the conv→ReLU→conv path; the +x is the identity skip. If F learns nothing useful, the block falls back to passing x through unchanged — so adding depth can never hurt.
What filters learn
Train a CNN and a hierarchy emerges on its own. Early layers learn generic edge and color detectors; middle layers combine them into textures and motifs; deeper layers assemble parts; the last layers respond to whole objects. Step through the depths to see the abstraction climb (patterns shown are illustrative).
Layer depth
Layer 1 — edges & colors
Why this matters
Because early features are generic, a network trained on millions of images can be reused for a new task — keep the early layers, retrain only the last few. That's transfer learning, and it's why you rarely train a vision model from scratch.
Architecture timeline
CNNs didn't arrive fully formed — they evolved. From LeNet reading digits in 1998, to AlexNet shocking ImageNet in 2012, through VGG's depth, Inception's efficiency, and ResNet's skip connections, each milestone added one key idea and drove the error rate down. Tap a model to see what it contributed.
Milestone
ResNet (2015)
At a glance
Transposed convolution
To go the other way — make a feature map bigger (for segmentation masks, image generation, decoders) — we use transposed convolution. Instead of collapsing a patch to one value, each input pixel stamps a learned kernel into a region of the output; overlapping stamps add up. Watch the small grid expand into a large one.
Stride
Stride 2
Output size
Dilated convolution
Want a wider view without more weights or losing resolution? Dilated (atrous) convolution spreads the kernel's taps apart, leaving gaps. The same nine weights now cover a much larger region — the receptive field grows exponentially with stacked dilation while the parameter count stays fixed. Change the dilation rate and watch the field expand.
Dilation rate
Dilation 2
Effective field
Depthwise separable convolution
A standard convolution mixes space and channels in one expensive step. Depthwise separable convolution factorizes it: first filter each channel spatially (depthwise), then mix channels with a 1×1 convolution (pointwise). The result is the engine of mobile-friendly nets like MobileNet — often ~8–9× cheaper for nearly the same accuracy.
Convolution type
Depthwise separable
Cost (multiply-adds / pixel)
Image augmentation
The cheapest regularizer in vision: transform each training image in label-preserving ways so the model sees endless variety and stops memorizing. Flips, crops, rotations, color jitter, and cutout all teach invariances — a flipped cat is still a cat. Pick a transform and watch it animate.
Augmentation
Horizontal flip
Why it works
Each transform encodes an invariance you want the model to have. It effectively multiplies your dataset for free and is one of the most reliable ways to cut overfitting — but every transform must keep the label correct, which is domain-specific.
RNN unrolling & BPTT
A recurrent network reads a sequence one step at a time, carrying a hidden state forward as memory. "Unrolling" draws the same cell repeated across time — the same weights at every step. Training runs backprop through that unrolled chain (BPTT), and that's where the trouble starts: gradients from distant steps fade away. Toggle the forward and backward passes.
Pass
Forward pass
The recurrence
Wₓ and Wₕ are the same at every step. The output is yₜ = Wᵧ hₜ. One fixed-size state must summarize the entire past.
LSTM & GRU gates
Plain RNNs forget quickly because their gradient decays. Gated cells fix this with a nearly-linear memory path that gates only nudge: the LSTM has forget, input, and output gates plus a cell-state highway; the GRU is a leaner two-gate version. Slide the gate value to see how long a memory survives.
Cell type
LSTM
Gates are sigmoids
Each gate outputs values between 0 (block) and 1 (pass). Multiplying the memory by a gate near 1 preserves it; near 0 erases it.
Word2Vec (skip-gram)
Before transformers, this is how machines learned word meaning: predict a word's neighbours from the word itself. Words appearing in similar contexts drift to similar vectors, and relationships become directions you can do arithmetic on — king − man + woman ≈ queen. Switch views to see training, the resulting space, and analogies.
View
Skip-gram training
Why it mattered
Dense embeddings replaced sparse one-hot vectors and gave models a notion of similarity for free. The same idea — learn vectors so context is predictable — underlies the token embeddings at the input of every modern language model.
Bidirectional RNN
A plain RNN only knows the past. But meaning often depends on what comes after — "the bank was steep" only resolves once you read "steep". A bidirectional RNN runs two passes, left-to-right and right-to-left, and concatenates them, so every word is encoded with full context. Toggle between forward-only and bidirectional.
Direction
Bidirectional
Combined state
The forward and backward hidden states are concatenated, doubling the representation size and giving each position both directions of context.
Sequence-to-sequence
Translation and summarization map one sequence to another of different length. The classic design: an encoder RNN reads the whole input into a single fixed context vector, and a decoder RNN generates the output from it, one token at a time. It works — until the input gets long and that one vector can't hold everything. Try a long sentence.
Input length
Short input
The bottleneck
Every output token is generated from the same fixed-size c. The longer the input, the more information that single vector must throw away.
Attention (Bahdanau)
Attention removes the single-vector bottleneck: instead of compressing everything into one context, the decoder looks back at all encoder states and takes a weighted average, with weights it computes fresh at each step. It learns to align each output word with the input words that matter — and this mechanism is the seed of the entire transformer.
View
Live attention weights
Weighted context
The weights αᵢ sum to 1 and depend on the current decoder state, so the context vector changes for every output token.
Beam search
When a model generates text it must choose tokens one by one. Greedy decoding always grabs the single most likely next token — but the best sequence may start with a less likely word. Beam search hedges: it keeps the top-k partial sequences alive at each step, scoring them by total probability. Widen the beam and watch it find a better path.
Beam width
Beam width 2
Scoring
BLEU score
How do you grade a machine translation automatically? BLEU compares the candidate to a reference by counting overlapping n-grams (1- to 4-word chunks), with two safeguards: clipping stops repetition gaming, and a brevity penalty punishes too-short output. Pick a candidate to see how each design choice rewards or destroys the score.
Candidate
Good translation
Components
Self-attention (Q · K · V)
This is the engine of the transformer. Every token looks at every other token and rewrites itself as a relevance-weighted blend of them. It does this with three learned projections — Query, Key, Value — a dot-product score, a scale, a softmax, and a weighted sum. Step through the whole machine with real numbers, and switch which token is doing the attending.
Attending from
The input sequence
The whole thing in one line
Q = E·WQ, K = E·WK, V = E·WV — three learned projections of the same input E.
Why it changed everything
Unlike an RNN, every position is computed in parallel and any token can reach any other in one step — no vanishing gradient over distance. Watch out: comparing every token to every other is O(n²) in sequence length, which is why long-context efficiency is a major research area.
Multi-head attention
One attention pattern is limiting — a word may relate to others in several ways at once (its subject, its article, the verb it belongs to). Multi-head attention runs several attention computations in parallel, each with its own Q/K/V projections, so each head can specialize. Their outputs are concatenated and mixed by a final projection. Tap a head to see what it focuses on.
Head
Head 1 — previous-token
Combining heads
Each head works in a smaller subspace (dmodel/h), so total compute matches one full-size attention.
Positional encoding
Attention treats its input as a set — shuffle the tokens and the output just shuffles too. But "dog bites man" ≠ "man bites dog", so order must be injected. The original transformer adds sinusoidal encodings: each position gets a vector of sines and cosines at geometrically-spaced frequencies, a unique fingerprint the model can read. Explore the pattern.
View
Heatmap of encodings
The formula
The transformer block
Stack the pieces and you get the unit that repeats N times to form a transformer. An encoder block is two sub-layers — multi-head self-attention and a feed-forward network — each wrapped in a residual connection and layer norm. A decoder block adds masked self-attention and cross-attention. Watch a representation flow through, residuals and all.
Block type
Encoder block
Each sub-layer
The x + is the residual skip; the feed-forward is two linear layers with an activation between. This wrapper is what makes deep stacks trainable.
Layer normalization
Transformers normalize differently from CNNs. Layer norm rescales each token's feature vector on its own — across features, not across the batch. That makes it independent of batch size and indifferent to sequence length, which is exactly what attention needs. Compare the two normalization axes side by side.
Normalize over
Layer norm (per token)
Per group
Same formula as batch norm — only the axis the statistics are computed over changes. Layer norm: each row (token). Batch norm: each column (feature).
Causal masking
A model that generates text left-to-right must never see the future — when predicting word 3 it can only use words 1–2. Causal masking enforces this inside attention: before the softmax, every score where a token would attend to a later token is set to −∞, so its weight becomes exactly zero. Toggle the mask and watch the attention matrix change shape.
Masking
Masked (causal)
How it's done
softmax(−∞) = 0, so future positions get zero attention weight. GPT-style decoders use this; BERT-style encoders don't.
Tokenization (BPE)
A model doesn't see characters or words — it sees tokens. Byte-pair encoding starts from characters and repeatedly merges the most frequent adjacent pair, so common words become single tokens while rare ones split into reusable subwords. This covers any text with a fixed vocabulary and never hits an unknown word. See how different inputs break apart.
Example
Common words → whole tokens
Token count
Cost and context limits are measured in tokens, not words — and the split follows merge frequency, not grammar.
Generating text, end-to-end
Here is the whole transformer, working on real text. A prompt goes in, and the model predicts the next token by running every stage you've seen — tokenize, embed, add position, masked attention through N layers, project to logits, softmax, sample — then appends the token and does it all again. Press Write and watch it generate one token at a time, or step through a single prediction in detail.
Run the model
Input context
Next-token probability
Low T → near-greedy and repetitive; high T → more varied but riskier. This single loop, repeated, is all an LLM does.
Encoder–decoder, end-to-end
The original transformer translates. Its two halves work together: the encoder reads the whole source sentence at once and builds a context-rich representation; the decoder then generates the translation word by word, and at each step cross-attention lets it look back at the encoded source — aligning each output word with the input words it comes from. Run it on "le chat noir" and watch the alignment, including the adjective reorder.
Run the translation
Source input
Cross-attention
The decoder's query comes from the target; the keys and values come from the encoder. That's the only place the two halves meet.
Why two halves
The encoder is bidirectional (it may see the whole source), while the decoder is masked (it can't see future target words). Cross-attention is the bridge — and dropping the encoder entirely gives you a decoder-only model like GPT.
Transformer, the mathematics
The visual demos show what flows where; this one shows the linear algebra. We carry a tiny but real example — 3 tokens, d=4, 2 heads — through every stage of the forward pass, tracking the exact matrix operation, the resulting shape, and the derivation behind each design choice (why √dₖ, why residuals, how the loss factorizes). Step through encoding, then decoding, to output.
Walk the forward pass
Equations
Shapes
Backpropagation, the mathematics
Backprop is just the chain rule applied with bookkeeping. On a small network — 2 inputs → 3 hidden → 1 output, with a squared-error loss — we do one forward pass, then derive the four equations that carry the error backwards and turn it into a gradient for every weight. Watch the signal go forward, then the error come back.
Walk the algorithm
Equations
Shapes
Optimizers, the mathematics
Every optimizer is a rule for turning a gradient into a parameter step. Starting from plain gradient descent, we build up through SGD, momentum, RMSProp, and Adam — deriving each addition (a velocity, a per-parameter scale, a bias correction) and what problem it solves. The contour shows the step each rule would take.
Build up the optimizer
Update rule
What it adds
Convolution, the mathematics
A convolution layer is a small, shared linear map slid across a grid. Here we derive the operation itself, the output-size formula, the parameter count (and why sharing makes it tiny), the equivalent matrix multiply, how the receptive field grows, and the surprisingly elegant fact that its backward pass is also a convolution. Step through with a 5×5 input and a 3×3 kernel.
Walk the math
Equations
Numbers
Sampling strategies
Once the model gives next-token probabilities, something has to pick a token — and that choice shapes the whole output's character. Greedy is safe but dull; temperature dials randomness; top-k and top-p (nucleus) trim the unreliable tail in different ways. Here is one fixed distribution; switch strategies and see which tokens survive and get sampled.
Strategy
Greedy
Pick from the distribution
Top-k keeps the k highest; top-p keeps the smallest set summing to ≥ p; both renormalize before sampling.
KV cache
Generating each new token requires attending to every previous token — which needs their keys and values. But those never change once computed. The KV cache stores them, so each step recomputes K and V for only the one new token instead of the whole sequence. Toggle the cache and watch the wasted work appear or vanish as the text grows.
Cache
With KV cache
Cost & memory
Per-step compute: O(n) with the cache vs O(n²) without. The cache's size grows linearly with the sequence — eventually the memory bottleneck.
Context window
A model can only attend to a fixed maximum number of tokens — its context window. Feed it more and the earliest tokens fall outside and are simply not seen. And because attention compares every token to every other, the cost grows with the square of the window, which is why long context is expensive. Drag the window size to see both effects.
View
What fits
Attention cost
Scaling laws
One of the most striking facts in deep learning: test loss falls as a smooth power law in model size, dataset size, and compute — a straight line over many orders of magnitude on a log-log plot. That predictability is why labs invest in scale. But returns diminish, there's an irreducible floor, and for a fixed compute budget, size and data must be balanced.
Scale by
Scaling parameters
The power law
E is the irreducible loss; α the scaling exponent. Compute-optimal: C ≈ 6·N·D, balance N and D.
In-context learning
A large model can pick up a brand-new task from a few examples placed in its prompt — with no weight updates, no training, nothing saved. It infers the pattern on the fly. Here the rule is "map a word to its letter count," which the model has to discover from the examples alone. Add shots and watch it go from guessing to confident.
Examples in the prompt
Few-shot (in-context)
No weights change
The pattern is inferred from the prompt at inference time; the weights are frozen. This ability emerges mainly in large models.
RLHF
A pretrained model predicts likely text, not helpful text. Reinforcement Learning from Human Feedback aligns it in three stages: supervised fine-tuning on demonstrations, a reward model trained from human preference comparisons, then RL that optimizes the policy against that reward while a KL penalty keeps it close to where it started. Step through the pipeline.
Stage
1 · Supervised fine-tuning
RL objective
Push toward high reward R, but the KL term keeps the policy π near the SFT model so it stays fluent and doesn't hack the reward.
Mixture-of-experts
Instead of one big feed-forward network, a mixture-of-experts layer has many expert networks and a router that sends each token to only a few of them — say the top 2 of 8. The model holds an enormous number of parameters but activates only a small fraction per token, buying capacity without paying full compute. The catch is keeping the experts evenly used.
View
Top-k routing
Sparse mixture
Active per token: 2 of 6 experts. Total parameters scale with the expert count, but compute scales with k only.
Quantization
Weights are usually stored as 32- or 16-bit floats, but you can map them to a small set of low-precision integer levels — INT8 or INT4 — to shrink memory and speed up inference. Fewer bits means a coarser grid and more rounding error. Switch precision and watch the quantization levels spread apart and the reconstruction error grow.
Precision
INT8 — 8-bit integers
Quantize → dequantize
s is the scale, z the zero-point. Levels = 2bits; coarser grid → larger error.
Transfer learning
Rarely do you train from scratch. A model pretrained on a huge dataset has already learned general features — edges, textures, shapes — in its early layers, and only its later layers are task-specific. Transfer learning reuses that backbone and adapts the top to a new task with far less data. Choose how much to freeze versus fine-tune.
Approach
Feature extraction
What changes
Early layers are general and transfer across tasks; later layers are specific and need adapting.
LoRA
Fine-tuning every weight of a large model is expensive to train and store. LoRA's insight: the change a task induces is mostly low-rank, so freeze the original weight W and learn only a thin update ΔW = B·A through a rank-r bottleneck. You train a tiny fraction of the parameters, and can merge or swap the adapter freely. Drag the rank to see the savings.
View
Low-rank update
The decomposition
Knowledge distillation
To get a small, fast model that still works well, train it to mimic a large "teacher." The key is what it mimics: not just the hard correct label, but the teacher's full softened probability distribution — which encodes "dark knowledge" about how classes relate. Compare hard labels with soft targets, and soften with temperature.
Student learns from
Soft targets (distillation)
Loss
Catastrophic forgetting
Train a network on task A, then move it to task B, and something alarming happens: as it learns B, its accuracy on A collapses. Gradient descent freely overwrites the weights A relied on, because nothing is testing A anymore. This is the central obstacle to continual learning — and the reason mitigations like rehearsal and EWC exist. Watch A's accuracy after the task switch.
Strategy
Naive sequential (forgets)
Protecting old knowledge
Fi measures how important weight i was for A; the penalty resists changing it.
Multi-task learning
Instead of one model per task, train a single shared backbone with task-specific heads on several tasks at once. Each task becomes extra signal and regularization for the others, improving data efficiency. The risk is the flip side: when tasks pull the shared weights in opposing directions, they conflict and one can get worse.
View
Shared backbone
Combined loss
Curriculum learning
Like a school syllabus, present training examples in a meaningful order — easy first, then progressively harder — rather than all shuffled together. Clean early gradients build good representations that make the hard examples learnable faster and to a better optimum. Compare a curriculum against a random shuffle on the same task.
Ordering
Curriculum (easy → hard)
The idea
Helps most on hard or noisy tasks; needs a sensible difficulty measure.
Autoencoder
An autoencoder learns to copy its input through a narrow bottleneck: an encoder squeezes the input down to a small latent code, and a decoder rebuilds it. Forced through the bottleneck, the network must keep only what matters — a learned, nonlinear compression. Shrink the bottleneck and watch the reconstruction degrade; corrupt the input and watch it learn to clean.
Mode
Compression (bottleneck)
Objective
A smaller latent = more compression = higher reconstruction error.
Variational autoencoder
A plain autoencoder's latent space is a bag of unrelated points — you can't sample from it. A VAE fixes this by encoding to a distribution (a mean and spread), sampling a code from it, and regularizing those distributions toward a standard normal prior. The result is a smooth, continuous latent space you can sample and interpolate — a true generative model.
View
Encode to a distribution
Reparameterize + regularize
GAN
A generative adversarial network pits two networks against each other: a generator turns noise into fakes, and a discriminator tries to tell fakes from real data. Training is a game — the generator gets better at fooling, the discriminator at detecting — until the fakes are indistinguishable and the discriminator is reduced to a coin flip. Watch the two distributions converge.
View
The two players
The minimax game
Diffusion models
Diffusion turns generation into denoising. A fixed forward process slowly adds Gaussian noise to a real sample until it's pure static; a learned reverse process starts from static and removes noise step by step until a coherent sample emerges. Train the network to predict the noise at each step, and you can generate from nothing. This is how modern image generators work.
Process
Forward (add noise)
Noising & denoising
The reverse model predicts ε at each step and subtracts a little of it.
Contrastive learning
How do you learn good representations without labels? Make two random augmentations of the same image — a positive pair — and train the encoder so their embeddings land close, while embeddings of different images are pushed apart. The model has to capture what's essential and invariant. This self-supervised recipe underpins much of modern representation learning.
Force
Positive pair (pull together)
InfoNCE
Pull the positive in, push all negatives out — no labels required.
Latent interpolation
A good generative model's latent space is smooth and meaningful, not a lookup table. Walk a straight line between two latent codes, decode each step, and the output morphs continuously through plausible in-betweens — proof the space is continuous. Directions can even carry meaning, so you can do arithmetic on concepts. Drag through the morph.
View
Interpolate (morph)
Walking the latent
Bellman & value functions
Reinforcement learning rests on one recursive idea: the value of a state is the best reward you can get now plus the discounted value of wherever you land next. That's the Bellman equation. Apply it repeatedly — value iteration — and value flows outward from the goal across a gridworld. The discount γ controls how far the reward's influence reaches.
View
Value iteration
The Bellman equation
Exploration (ε-greedy)
An agent that always picks the best-known option may never discover a better one; an agent that only explores never cashes in. ε-greedy is the simplest balance: with probability ε take a random action, otherwise take the best you know. Watch a multi-armed bandit's value estimates form, and how the exploration rate changes which arm it settles on.
Regime
Balanced
The rule
Deep Q-networks
Tabular value methods break when states are images or huge. A DQN approximates Q(s,a) with a neural net — but naive training is unstable, so two tricks make it work: experience replay (store transitions, train on random minibatches to break correlation) and a target network (a frozen copy that provides stable learning targets). Together they turned Q-learning into something that plays Atari.
Trick
Experience replay
TD loss
Q⁻ is the frozen target network; the squared TD error is minimized.
Policy gradient
Value methods learn what states are worth and act greedily. Policy gradients skip that and adjust the policy itself: push up the probability of actions that led to high return, push down the rest. It optimizes exactly what you care about and handles continuous actions — but raw returns make the gradient noisy, so a baseline is used to calm it. Watch the policy concentrate on the best action.
Method
With a baseline
The gradient
Scale each action's log-prob gradient by how much better than baseline b its return was.
Actor-critic
Actor-critic fuses the two families. An actor (the policy) chooses actions; a critic (a value function) judges them. The critic's estimate replaces the noisy Monte-Carlo return that makes plain policy gradients shaky, so the actor learns from a low-variance advantage signal and can update every single step. It's the backbone of modern RL algorithms like A2C, PPO, and SAC.
View
Actor & critic
Advantage
Actor pushes by ∇log π · A; critic trains V toward r + γV(s′).
Neural Networks, from scratch
Most explanations stop at the diagram. Here we build a tiny but complete network, feed it a real dataset, and turn every crank by hand — the forward pass, the loss, the gradients, the weight update — with the actual numbers shown at each step. Then we train it live and watch it solve a problem no straight line can. By the end you'll have seen not just what a neural network is, but exactly what happens inside one.
What a neural network actually is
Strip away the mystique and a neural network is just a function with adjustable knobs. You feed in numbers, it multiplies them by weights, bends the result through a nonlinear squashing function, and passes that to the next layer. "Learning" means nudging the weights so the output gets closer to the answer you wanted.
Neuron
Takes a weighted sum of its inputs, adds a bias, then applies a nonlinearity: a = σ(w·x + b).
Layer
Many neurons in parallel, each with its own weights — together they transform one vector into another.
Depth
Stacking layers lets the network compose simple features into complex ones. The nonlinearity is what makes depth worth having.
Learning
Measure error with a loss, compute how each weight affected it (gradient), and step every weight downhill.
Five expressions are the whole story
Everything below is built from these. A neuron computes a weighted sum, squashes it with an activation, the network chains layers into one function, a loss scores the output, and gradient descent improves the weights.
The squashing function is the key trick: without it, stacking layers would just give another linear function. The sigmoid σ maps any number into (0, 1) — handy for a probability — while its cousin tanh maps into (−1, 1) and is common in hidden layers. Both are smooth, so we can take derivatives and run gradient descent.
A real problem: XOR
We'll teach the network exclusive-or: output 1 when exactly one input is on. It's tiny — four examples — but famous, because no single straight line can separate the 1s from the 0s. That's exactly why we need a hidden layer.
| x₁ | x₂ | target y |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
2 inputs → 2 hidden → 1 output
The smallest network that can solve XOR: two inputs, a hidden layer of two tanh neurons, and one sigmoid output that reads as the probability of class 1. That's nine numbers to learn — six weights and three biases.
Compute one prediction, by hand
Let's run the input x = [1, 0] (whose target is 1) through a network with some example weights, doing the actual arithmetic. Step through it:
Backpropagation, by hand
The prediction was 0.80 but should be 1. Backprop answers one question for every weight: which way, and how much, should I move you to reduce the loss? It applies the chain rule from the output backwards. Same example, real numbers:
Stepping downhill
Backprop hands us the gradient — the direction in which the loss increases. Gradient descent does the obvious thing: take a step the opposite way, sized by the learning rate η, and repeat. Here's that idea on a single weight — the curve is the loss, the dot is the weight, the amber arrow is the downhill direction. Roll it, and watch η decide everything.
Watch the boundary form
Now the payoff. Hit Train and the network runs that exact loop — forward, loss, backprop, update — over all four points, thousands of times. The background colour is the network's output across the whole input plane; watch it bend until it carves the XOR pattern, and the loss curve fall. Reset re-randomises the weights so you can see it solve it again from a fresh start.
What the number means, and where this goes
The output 0.80 isn't arbitrary — because of the sigmoid it's a probability: the network's confidence that the answer is class 1. Threshold at 0.5 to decide. Everything you just saw scales directly: real networks use the same weighted-sum-then-nonlinearity neuron, the same loss-and-gradient-descent loop, just with millions of weights, many layers, and specialised structure.
Vision
Swap dense layers for convolutions and the same machinery recognises objects in images.
Language
Stack attention layers and it predicts the next token — the basis of modern LLMs.
Control
Let the output choose actions and train on reward, and it learns to play games or steer robots.
The constant
Weighted sums, nonlinearities, a loss, and gradient descent — the four ideas never change.
What you saw
A neuron is a weighted sum plus a squash. A network chains them into a function with adjustable weights. A dataset and a loss define "right." The forward pass produces a prediction from real numbers; backprop assigns blame to each weight via the chain rule; gradient descent nudges them all downhill. Repeat, and a network with just two hidden neurons learns a pattern no line can split.
Transformers, from scratch
The transformer powers every modern large language model, and its one essential idea — attention — sounds abstract until you watch the arithmetic. So we'll take the three-word sequence "the cat sat", give each word a tiny vector, and turn the full self-attention crank by hand: project to queries/keys/values, score every pair, softmax into attention weights, and blend the values — with every number on screen. Then you'll explore attention interactively and watch the model predict the next word.
Every word looks at every other word
Older sequence models read words one at a time, passing a memory along. Transformers throw that out: every token looks at all the others at once and decides which are relevant. That mechanism is self-attention. It's parallel (fast to train), it handles long-range links directly, and stacked up it becomes GPT.
Token
Each word becomes a vector (an embedding) plus a marker of its position.
Query · Key · Value
Each token emits a query ("what I'm looking for"), a key ("what I offer"), and a value ("what I'll pass on").
Attention
Match queries to keys → weights; blend everyone's values by those weights. That's it.
Depth
Stack many attention+MLP blocks and the representations get richer at every layer.
A three-word sequence
Our input is "the cat sat" — three tokens at positions 0, 1, 2. We'll use an embedding dimension of just 4, and a single attention head that projects down to 2 dimensions, so every vector and dot-product is small enough to read.
| position | token | id |
|---|---|---|
| 0 | the | 0 |
| 1 | cat | 1 |
| 2 | sat | 2 |
Words become vectors
A lookup table maps each token id to a learned vector. Similar words get similar vectors; here each word is a point in 4-dimensional space. These are the raw material everything else operates on.
Telling the model the order
Attention sees the tokens as a set — it has no built-in sense of order. So we add a positional encoding: a fixed pattern of sines and cosines, different for each position, that stamps "where" onto each vector. The input to attention is x = embedding + position.
Three projections of each token
Three learned matrices turn every token vector x into a query q = x·WQ, a key k = x·WK, and a value v = x·WV. Here they map the 4-dim vectors down to 2-dim. Done for all tokens at once, that's just three matrix multiplies.
Compute one token's context, by hand
Let's compute attention for the query token "cat" (position 1): how much should it draw from "the", "cat", and "sat", and what's the result? Every number is real — step through it.
Explore who attends to whom
Every token runs that same computation, giving a full attention matrix: row = the querying token, column = how much weight it puts on each other token (each row sums to 1). Click a row (or pick a query) to see its weights and the blended output. Toggle the causal mask to forbid looking at future tokens — the rule language models use when predicting left-to-right.
Several attentions at once
One head captures one kind of relationship. Real transformers run several heads in parallel — each with its own WQ, WK, WV projecting into a small subspace — then concatenate the results and mix them with one more matrix. Different heads learn different patterns (one tracks syntax, another nearby words, and so on).
Let's actually do it with two heads for the last token "sat" — the one we'll predict from — combining them with the output matrix WO:
Attention is half of it
A transformer block wraps attention with two more ideas: a residual connection (add the input back, so information and gradients flow freely) and layer normalization (keep activations well-scaled), then a small per-token feed-forward network (MLP) with its own residual + norm. That whole block is what gets stacked.
Now the arithmetic, continuing with "sat" — every step of the block with real numbers. (LayerNorm here uses scale 1, shift 0 for clarity; real ones learn those two parameters.)
From the last token to a word
To predict what follows "the cat sat", take the processed vector at the last position, map it to one score (logit) per vocabulary word, and softmax into probabilities — using the block's actual output vector for "sat" that we computed just above. The temperature controls how sharp the choice is.
Every formula you just used
Now that you've seen each step with numbers, here are the equations behind them — essentially the whole transformer in nine lines.
And the shape of the data at every stage — the journey of "the cat sat" from words to a probability over the next word:
| stage | shape | what it is |
|---|---|---|
| tokens | 3 | the, cat, sat |
| embeddings | 3 × 4 | a vector per word |
| + positional encoding | 3 × 4 | order stamped in |
| Q, K, V | 3 × 2 each | query / key / value |
| attention weights | 3 × 3 | who attends to whom |
| head output | 3 × 2 | context per token |
| multi-head output | 3 × 4 | heads combined by W_O |
| block output | 3 × 4 | after residual + norm + FFN |
| last token vector | 4 | "sat", processed |
| logits → probabilities | 6 | → "mat" |
The loss, the gradient, and training
So far every weight was fixed. Training adjusts them. We score the prediction with cross-entropy loss — how surprised the model is by the correct next word — then nudge the weights to reduce it, exactly the gradient-descent loop from the neural-network dive.
The elegant part is the gradient at the output. For softmax followed by cross-entropy it collapses to one of the simplest expressions in all of deep learning:
That gradient flows backward through WU, the block, attention, and the embeddings by the chain rule (backpropagation), and every matrix takes a small step downhill: W ← W − η·∂L/∂W. Below we train just the final output layer on this one example — pick a target word and watch the model learn to predict it.
From this to GPT
Everything you just traced — embed, add position, attend, MLP — is one block. Stack dozens to a hundred of them, widen the vectors from 4 to thousands, grow the vocabulary, and train on a large corpus, and the same machinery becomes a large language model.
Wider
dmodel grows from 4 to thousands; many heads instead of one.
Deeper
Dozens–hundreds of identical blocks stacked.
Bigger vocab
Tens of thousands of subword tokens, not three words.
Same idea
Query·key attention, softmax, value blend, residual+norm+MLP — unchanged.
What you saw
Words became vectors; positions were stamped in; each token produced a query, key, and value. Scores compared queries to keys, softmax turned them into attention weights, and each token's output was a weighted blend of values — its context. Multi-head, residual, norm, and an MLP complete the block; the last token's vector becomes a probability over the next word. That is a transformer.
Convolutional networks, from scratch
A dense network treats an image as a flat list of pixels and forgets that nearby pixels belong together. A convolutional network keeps the 2-D layout and slides a tiny filter across it, looking for the same local pattern everywhere. We'll take a small image, convolve a 3×3 filter over it by hand — every multiply-and-add on screen — see edges light up, add ReLU and pooling, classify the image, and finally train a small CNN live and watch its filters grow into edge detectors.
Slide a small filter everywhere
Instead of one giant weight per pixel, a CNN learns a small filter (here 3×3) and applies the same filter at every location. That's two big wins: far fewer weights, and a pattern learned in one corner is recognized everywhere else.
Filter (kernel)
A small grid of weights that detects one local pattern — an edge, a corner, a texture.
Convolution
Slide the filter over the image; at each spot, multiply overlapping values and sum → one output number.
Feature map
All those outputs form a new image showing where the pattern was found.
Hierarchy
Stack layers: edges → textures → parts → objects, each built from the last.
A tiny image
Our image is 6×6 grayscale — dark on the left, bright on the right, so there's a single vertical edge down the middle. Pixel values are 0 (dark) to 1 (bright). The job of a good filter is to find that edge.
One output pixel, by hand
Place the 3×3 filter over a patch of the image, multiply each filter weight by the pixel beneath it, and add it all up — that single number is one pixel of the feature map. Step through it at the edge:
Now watch the filter slide across the whole image, filling in the feature map one pixel at a time:
Different filters, different patterns
The filter's weights decide what it finds. Swap them and the same image reveals different things — a vertical-edge detector lights up the middle seam; a horizontal-edge detector sees nothing here; a blur smooths; a sharpen exaggerates. Pick one and watch the feature map change.
Colour images and stacks of filters
Real images aren't one grid — a colour image has three channels (red, green, blue). So a filter isn't 3×3, it's 3×3×3: one slice per input channel. You convolve each slice with its channel and add the three results into a single feature-map pixel. And a layer has many filters, each making its own map — so the output is itself a stack whose depth equals the number of filters, ready for the next layer to convolve again.
How big is the output?
A 3×3 filter on a 6×6 image, moved one pixel at a time (stride 1) with no border added (valid padding), can sit in 4×4 positions — so the feature map is 4×4. Larger stride skips positions and shrinks the output; adding a border (same padding) keeps it the same size.
For us: (6 − 3 + 0)/1 + 1 = 4. Each output pixel summarizes a 3×3 patch — its receptive field. Stack layers and that field grows, so deep neurons "see" more of the image.
Keep the strong signals, shrink the rest
Two cheap steps follow convolution. ReLU zeroes negative responses (keep evidence "for" a pattern, drop evidence "against"). Max pooling then slides a 2×2 window and keeps only the largest value in each — shrinking the map, keeping the strongest activations, and making the result robust to small shifts.
Edges → corners → objects
One layer finds edges. Stack a second whose filters convolve the feature maps of the first, and it can combine a vertical and a horizontal edge that meet into a corner detector; a third assembles corners into shapes, and so on. Each deeper layer also sees a wider patch of the original image — a growing receptive field — so early layers handle fine detail while deep layers reason about whole objects. This edges→parts→objects hierarchy is learned, not designed.
From pixels to a label
Put it together: convolve with two filters (a vertical- and a horizontal-edge detector), ReLU, pool each map down to a single strongest response, and feed those two numbers through a softmax to choose a class — "vertical" vs "horizontal". For our image the vertical detector fires hard and the horizontal one barely, so the answer is clear.
Watch the filters become edge detectors
We hand-picked those filters. A real CNN learns them. Starting from random 3×3 weights, we train a tiny CNN (two filters → ReLU → pool → softmax) to tell vertical-edge images from horizontal ones, using the same cross-entropy loss and gradient descent as the other dives. Backprop sends the error to whichever pixel patch won the max-pool. Hit Train and watch the random filters sharpen into edge detectors while accuracy climbs.
First, the gradient for one image — worked by hand. It's the same predicted-minus-target rule, sent backward through the softmax, the max-pool, and the convolution:
Now let the network run that loop automatically over all the images:
Every formula in one place
Convolution is one weighted sum repeated across positions; everything else you've met before.
| stage | shape | what it is |
|---|---|---|
| image | 6 × 6 | pixels |
| conv (2 filters, 3×3) | 2 × 4 × 4 | feature maps |
| ReLU | 2 × 4 × 4 | negatives → 0 |
| max pool (global) | 2 | strongest response each |
| softmax | 2 | → vertical / horizontal |
What you saw
A filter is a small grid of weights; convolution slides it everywhere and records where the pattern matched; ReLU keeps the positive evidence; pooling shrinks and stabilises; a final softmax turns the strongest responses into a label. Train with the same loss-and-gradient loop and the filters discover edges on their own. Stack many such layers and a CNN builds from edges up to whole objects.
Sequence models, from scratch
Images have a fixed shape; language, audio and time-series don't — they arrive one step at a time and the meaning depends on order and on what came before. A recurrent neural network handles this with a trick: it processes one element at a time while keeping a hidden state — a small memory vector — that it carries forward and updates at every step. We'll unroll one through time, watch the memory evolve as you change the input, do the forward pass by hand, and train it by backprop-through-time to remember.
A network with a memory
A feed-forward net maps a fixed input to an output and forgets everything between calls. An RNN adds a loop: at each step it combines the new input with its previous hidden state to produce a new hidden state. The same weights are reused at every step, so it can handle sequences of any length and a pattern learned at one position works at any other.
Hidden state
A small vector of numbers — the network's running memory of the sequence so far.
Recurrence
hₜ depends on the input xₜ and the previous state hₜ₋₁ — a loop through time.
Weight sharing
One set of weights is applied at every timestep, so length doesn't matter.
Unrolling
Lay the loop out across time and it becomes a deep net — one layer per step.
A task that needs memory
To prove the memory does something, we pick a task it can't fake: read a sequence of bits, and at the very end output the first bit you saw. The answer is decided at step 1 but only asked for at the end, so the network has no choice but to carry it the whole way in its hidden state.
Examples: 1011 → 1 · 0010 → 0 · 1000 → 1. The last three bits are distractions; only the first matters.
Watch the memory evolve
Here's a small RNN (2-dimensional hidden state) unrolled across a 5-step sequence. Each cell takes the input bit and the previous state, and produces a new state (shown as two bars, teal positive / purple negative). Toggle the bits and watch how the state — and the final read-out — change. Notice the first bit's long reach.
One step, reused forever
The entire recurrent layer is a single equation applied at each step, plus a read-out at the end.
Wₓ mixes in the new input, Wₕ carries the old state forward, tanh keeps the state bounded in (−1, 1). The same Wₓ, Wₕ, b act at every timestep — that's weight sharing. The final state h_T is squeezed through one more layer into a probability.
Unroll a real sequence
Take the sequence [1, 0, 1] with fixed weights and step through it — each hidden state feeds the next, and the last one decides the answer.
Teaching it to remember
Unrolled, the RNN is just a deep network that happens to reuse its weights — so we train it the same way: forward pass, cross-entropy loss, then backpropagate the error backwards through every timestep (this is backprop-through-time) and step the shared weights downhill. We train on all 16 four-bit sequences, each labelled by its first bit. Hit Train and watch accuracy climb from a coin-flip to perfect recall.
First, the gradient by hand. Backprop-through-time pushes the error from the loss back through every step — and you can watch the signal shrink on the way, the vanishing gradient in real numbers:
Now let the network run that update automatically over all 16 sequences:
Why long memories are hard
Backprop-through-time multiplies a gradient by roughly the same factor at every step it travels back — the recurrent weight times tanh's slope. If that factor is below 1, the gradient shrinks geometrically: after many steps it's effectively zero, so early inputs get almost no learning signal. (Above 1 it explodes instead.) This is why a plain RNN struggles to connect events far apart in time. Drag the per-step factor and watch the signal that survives back to the first step.
A protected path for memory
The Long Short-Term Memory cell adds a second state — the cell state C, a conveyor belt running straight through time. Three gates (each a little sigmoid that outputs values in 0–1) decide what happens to it: the forget gate erases part of the old memory, the input gate writes new candidate information, and the output gate chooses how much of the memory to reveal as this step's hidden state.
Step through one update with real numbers:
What you saw
A recurrent net carries a hidden-state memory and updates it with one shared set of weights at every step; unrolled through time it's a deep network trained by backprop-through-time. That memory lets it recall the first bit long after seeing it — but pushing a gradient back through many steps makes it shrink toward zero, the vanishing-gradient problem, so plain RNNs struggle with long range. The LSTM fixes this with a protected, additive cell state and three gates that learn what to keep, write, and reveal — remembering across hundreds of steps.
Generative models, from scratch
The networks so far recognise — image to label, sequence to answer. A generative model instead learns the shape of the data well enough to make new examples. The simplest way in is the autoencoder: squeeze each data point through a narrow bottleneck into a tiny latent code, then expand it back. To reconstruct well through such a tight pipe, the network is forced to discover the data's underlying structure. Decode a latent you choose and you've generated something new. We'll do the squeeze by hand, train one live and watch its decoded shape mould itself to the data, then upgrade it to a variational autoencoder you can sample from.
Compress, then recreate
An autoencoder is two networks back to back. The encoder maps a data point down to a few latent numbers; the decoder maps those numbers back up to a full data point. Train it to make the output match the input, and the latent code becomes a compact summary — a coordinate in a learned "map" of the data. New points on that map decode to new, plausible data.
Bottleneck
A layer far narrower than the input — it can't copy, so it must compress.
Latent code
The few numbers in the bottleneck: the data point's coordinates in the learned map.
Reconstruction
The decoder's attempt to rebuild the input from the code; the loss is how far off it is.
Generation
Pick any latent code, decode it, and out comes a brand-new sample.
Two dimensions, one hidden factor
Our dataset is a cloud of 2-D points lying along a gentle arc. Even though each point has an x and a y, they really vary along a single direction — position on the arc. That hidden 1-D factor is exactly what a good autoencoder with a one-number latent should discover.
Down to one number, back to two
Here the encoder takes the 2-D point through a small hidden layer to a single latent number z; the decoder takes z through another hidden layer back to a 2-D reconstruction. The whole thing is trained to minimise the squared distance between input and output.
One point through the bottleneck
Take the point x = [0.80, 0.60] and push it through a tiny autoencoder (two hidden units, one latent). Watch two numbers collapse to one and expand back to two.
Watch the shape fit the data
Now train the full autoencoder by gradient descent on the reconstruction loss. The line drawn here is the decoded manifold — everything the decoder produces as the latent z sweeps across its range. Start random and it's a tangled stub; as training proceeds it stretches and bends until it threads right through the data cloud. That curve is the 1-D structure the network has discovered.
First, the gradient by hand — continuing the point from chapter 4. The reconstruction error flows back through the decoder, funnels into the single latent, and on into the encoder:
Now let it run that update over all 30 points and watch the manifold settle onto the data:
Generate by choosing a code
Once trained, the decoder is a generator: hand it a latent number and it produces a data point. Drag the slider to move along the latent axis and watch the generated point glide along the learned arc — including spots between the original samples. That's the model inventing plausible new data.
A latent you can sample
To generate from scratch we need to know which latents are valid. A variational autoencoder arranges for the latents to fill a standard bell curve. Its encoder outputs not one z but a mean μ and spread σ; we sample z from that little Gaussian, and a KL term in the loss gently pulls every (μ, σ) toward (0, 1). Sampling is made differentiable by the reparameterization trick.
Why bother? A plain autoencoder scatters its codes wherever is convenient, leaving stretches of the latent axis that decode to nothing — so you can't just pick a random z. Here are the trained autoencoder's actual codes versus the bell the VAE packs them into (train in chapter 5 first to see them spread):
That KL term isn't arbitrary — it's the exact closed form of the gap between the encoder's Gaussian 𝒩(μ, σ²) and the prior 𝒩(0, 1). Here is the integral behind it:
And the reparameterization that lets us sample z while keeping the gradient flowing:
Watch the codes fill the bell
Now train a real VAE. Same decoder as before, but the encoder outputs a mean μ and spread σ for every point; we sample z = μ + σ·ε, reconstruct, and add the KL term to the loss. Hit Train and watch two things at once: the blue manifold settles onto the data (reconstruction), while below, the codes slide inward to fill the standard bell (the KL term doing its job). The amber points are decoded from z drawn out of pure 𝒩(0,1) noise — once the bell is filled, they land cleanly on the data.
First, the two gradients that make a VAE different from a plain autoencoder — the path through the reparameterized sample and the KL term — worked by hand (continuing μ = 0.30, log σ² = −1.20, ε = 0.70 from the previous chapter):
Those are exactly the updates the trainer applies every step. Run it and watch them accumulate — reconstruction sharpening the fit, KL shaping the codes:
And the codes themselves — each point's encoded mean μ — drifting from wherever they started into the shape of the target bell:
What you saw
An autoencoder squeezes data through a bottleneck and learns to rebuild it, and in doing so discovers the data's hidden structure — the decoded manifold that threads through the cloud. Decoding a chosen latent generates new data; the VAE makes that latent space samplable with a probabilistic encoder, the reparameterization trick, and a KL term. From recognition to creation, it's the same machinery — forward pass, a loss, gradient descent — pointed at a new goal.
Reinforcement learning, from scratch
Every other dive learned from labelled targets — here is the input, here is the right answer. Reinforcement learning has no answer key. An agent moves through an environment and all it ever gets back is a number: good, bad, or nothing. No one says which action was correct, and the reward for a good move may arrive only many steps later. We'll teach an agent to cross a small gridworld with Q-learning: write the update by hand, then watch a map of values and a set of policy arrows form on the grid as it works out the route entirely on its own.
Learning from reward, not answers
The loop is simple: the agent sees a state, picks an action, and the environment hands back a reward and the next state. Repeat. The agent's job is to choose actions that maximise total reward over time — which forces it to think ahead, since a small reward now can be worth less than reaching a big one later.
State & action
Where the agent is, and the moves available from there.
Reward
A single number after each move. The only feedback there is.
Policy π
The agent's rule for choosing an action in each state.
Return
Total future reward — the thing the agent actually wants to maximise.
A goal, a pit, and two walls
The environment is a 5×5 grid. The agent starts bottom-left (S) and wants the goal top-right (reward +1). One cell is a pit (reward −1) that ends the episode badly; two cells are walls it can't enter. Every ordinary step costs a small −0.02, so wandering is mildly punished. From any cell it can move up, right, down or left; bumping a wall or the edge just leaves it in place. An episode ends at the goal or the pit.
Valuing the future
To choose well the agent must value the future, not just the next reward. The return from a moment is the sum of all rewards after it, with later ones discounted by γ (here 0.9) so nearer reward counts for more:
Worth doing once with numbers — it's exactly why the agent ends up preferring short routes:
It can't know the return ahead of time, so it estimates it. The action-value Q(s, a) is the return the agent expects if it takes action a in state s and then keeps acting well. Given Q, the policy is just: in each state, take the highest-valued action.
One estimate built on the next
Q-learning learns those values straight from experience, one step at a time. After taking action a in state s, landing in s′ with reward r, it nudges Q(s, a) toward a fresher estimate — the reward just seen plus the discounted value of the best action available next:
The bracket is the temporal-difference error δ: the gap between the old estimate and the new one. α (here 0.5) sets how big a correction each experience makes. The trick is that Q(s, a) is improved using Q(s′, ·) — an estimate leaning on another estimate. That bootstrapping is exactly what lets value flow backward from the goal.
Value seeps back from the goal
Two updates with real numbers (γ = 0.9, α = 0.5) show how a cell with no value at all becomes valuable — purely because of what it leads to.
Watch the map and the arrows form
Now let it learn by trial and error. Each cell is tinted by its value (green good, red bad), the arrow is the current best action there, and the amber dot is the agent. Hit Train: value spreads outward from the goal, the arrows organise into a route that skirts the pit, and the return-per-episode curve climbs from negative (falling in the pit, wandering) toward the reward.
Why the agent rolls the dice
If the agent always took its current best guess it would exploit what it knows but never explore — and might lock onto a mediocre route, never discovering a better one. ε-greedy fixes this: with probability ε it ignores its values and tries a random move. Below are the four action-values for one state; greedy always picks the tallest bar, but ε of the time the agent gambles instead.
What you saw
With no labels and only a reward signal, the agent learned to cross the grid. The return discounts future reward by γ; the action-value Q(s, a) estimates that return; the Q-learning update drags each value toward the reward plus the best next value, so value propagates backward from the goal one step at a time; and ε-greedy keeps the agent exploring instead of settling early. The greedy policy — follow the arrows — is what falls out at the end.
Optimization & training, from scratch
Every dive so far ended with "...and then gradient descent." This one opens that box. Training is a walk downhill on a loss surface, but the way you walk decides everything: too big a step explodes, too small crawls, and a naive step zig-zags helplessly down a narrow valley. We'll take a step by hand, see the learning rate make or break it, schedule it over training, race three optimizers down an awkward landscape, escape the saddle points and plateaus that fill real loss surfaces, keep deep signals alive with good initialization and normalization, and then meet the other half of training — making the model generalise instead of just memorise, with early stopping as the simplest tool of all.
Walking downhill, carefully
Backprop gives the gradient — the uphill direction of the loss. Training just steps the opposite way, over and over. Simple in principle, but four choices decide whether it works at all: which update rule, how big a step, how much data per step, and how to stop the model overfitting.
The optimizer
The rule that turns a gradient into a parameter update — SGD, momentum, Adam.
Learning rate
How far to step each time. The single most important knob in all of training.
Batch size
How many examples each step is based on — trading gradient noise for speed.
Generalisation
Fitting the signal, not the noise, so the model works on data it never saw.
The gradient points the wrong way
Picture the loss as a surface over the parameters; the rings below are its contours, the centre is the minimum. At any point the gradient ∇L points uphill — the steepest way to increase loss. So we step along −∇L, scaled by the learning rate η. That's the whole of gradient descent.
Concretely, this bowl is L = ½(2x² + y²), so ∇L = (2x, y). At the marked point (1.7, 1.25) that’s (3.4, 1.25) — the red arrow — and the orange step heads exactly opposite.
How far is just right?
Take the simplest loss, f(x) = x², and walk it down with real numbers:
The step size η is everything. Drag it below: too small and the ball barely moves; around the sweet spot it drops straight in; push it past the edge and the steps overshoot harder each time — the loss explodes.
Remember where you were going
Plain descent treats every step independently, so in a narrow valley it bounces between the walls and barely crawls along the floor. Momentum fixes this by accumulating a velocity — a running average of past gradients. Consistent directions build up speed; alternating ones cancel out. It's a ball with inertia rolling downhill instead of a dot teleporting.
A learning rate per parameter
One global η is a compromise: directions that need big steps and ones that need small steps get the same. Adam gives each parameter its own effective rate. It keeps a running mean of the gradient (m, like momentum) and a running mean of its square (v, the typical magnitude), then steps by the mean divided by the root of the square — so steep directions are damped and flat ones amplified.
Three optimizers, one awkward valley
Here is the classic test: a loss that's steep in one direction and shallow in the other (the contours are stretched ellipses; here L = ½(18x² + y²), eighteen times more curved across the valley than along it, so no single step size suits both). All three start at the same corner. Watch SGD zig-zag and crawl, momentum build speed along the valley floor, and Adam rescale each axis and drive almost straight to the minimum.
Change the step as you go
η doesn't have to be a fixed number. Early in training you want big steps to cover ground; later you want small ones to settle into the minimum instead of rattling around it. A schedule lowers η over training — most commonly a cosine decay, often with a short warmup at the start so the first steps don't explode while Adam's running averages are still cold.
Why a constant rate leaves a floor: near the minimum a noisy step behaves like x ← (1 − 2η)x − η·ξ, where ξ is the gradient noise (variance s²). The variance settles where it stops shrinking — Var(x) = η²s² ∕ (1 − (1−2η)²) ≈ η s² ∕ 4 for small η — so the expected loss E[x²] is proportional to η. Halve the rate and you roughly halve the floor; drive η → 0 on a schedule and the floor follows it down. With s = 0.7 and η = 0.34 here, the constant floor sits near 0.34·0.49 ∕ 4 ≈ 0.042, while the cosine tail drives η toward 0 and keeps dropping past it.
The bottom panel shows the payoff (log scale): a high constant rate (dashed) drops the loss fast but then bounces on a noise floor it can't get under — each step's randomness is proportional to η. Decaying η lowers that floor as you go, so the loss keeps falling. Pick a schedule:
The real obstacle isn't local minima
The bowl in chapter 2 was a convenient lie. Real loss surfaces are non-convex and high-dimensional, and there the trouble is rarely a bad local minimum — it's saddle points and flat plateaus, regions where the gradient nearly vanishes and plain gradient descent stalls. Below, three optimizers start on the flat shoulder near x = 0 (slope ≈ 0) of a double-well. Watch plain GD inch along the plateau while momentum (carrying velocity) and SGD noise break free and drop into a minimum far sooner.
Which critical point is the trap? The second derivative f″(x) = 3x² − 2 is −2 at x = 0 (a maximum along this slice — the saddle/plateau) and +4 at x = ±√2 ≈ ±1.414 (the two real minima). The hazard isn't the minima; it's the flat top. A gradient step there is f′(0.02) = 0.02³ − 2·0.02 = −0.040, so with η = 0.03 plain GD moves just −η·f′ = +0.0012 per step — a crawl. Down on the wall at x = 1, f′ = 1 − 2 = −1, a step 25× larger. Momentum accumulates those tiny pushes into real velocity; noise jolts x off the ridge directly — both clear the plateau while GD inches.
Why the loss curve is fuzzy
A real gradient is averaged over a batch of examples. The full dataset gives the exact gradient but is slow; a single example (pure stochastic) is fast but noisy, since one example is a rough estimate of the whole. Noise scales as 1∕√batch. Slide the batch size and watch the training curve go from jagged to smooth — small batches are noisier per step, yet cheaper and their noise can even help escape sharp, brittle minima.
Why the square root? The batch gradient is an average of b per-example gradients, and averaging b independent estimates divides their variance by b (a mean of b draws has variance σ²∕b). The standard deviation — the wobble you actually see — is the square root of that, so it falls as 1∕√b. Going from batch 1 to 100 cuts the noise only 10×, which is why ever-larger batches give diminishing returns.
Starting the weights at the right scale
Before training even begins, the initial weight scale decides whether a signal survives a deep stack. Each layer multiplies the typical activation (and gradient) magnitude by roughly a constant factor. Below 1, the signal vanishes layer by layer; above 1 it explodes; right at 1 it holds steady. Slide the per-layer gain and watch the magnitude across 24 layers (log scale):
Each layer scales the typical magnitude by a factor g (set by the weight variance and the fan-in), so down a stack of L the magnitude is the product g·g·…·g = g^L — exponential in depth. That's unforgiving: at L = 24, g = 1.40 gives 1.40²⁴ ≈ 3.2 × 10³ (exploding), g = 0.60 gives 0.60²⁴ ≈ 4.7 × 10⁻⁶ (vanished), and only g = 1.00 holds at exactly 1. He initialization sets Var(W) = 2∕fan-in precisely so each layer's output variance matches its input — pinning g at ≈ 1 so the signal neither dies nor blows up.
This is exactly what Xavier and He initialization solve: they set the weight variance from the layer's fan-in (He uses 2∕fan-in for ReLU) so the gain lands at ≈ 1 and signals propagate intact. And for spikes that still slip through — one huge gradient that would blow up a step — gradient clipping rescales it to a ceiling: g ← g · min(1, c ∕ ‖g‖), capping the norm at c without changing its direction.
Keeping each layer's inputs well-behaved
Even with good initialization, the distribution of each layer's inputs drifts and stretches as the layers below it update — so every layer is forever chasing a moving target. Normalization (batch norm, layer norm) fixes this by standardizing each layer's pre-activations back to zero mean and unit variance, then letting the model rescale them with two learnable parameters γ and β. Toggle it and watch the per-layer distributions:
Worked example: a unit emits activations [2, 4, 6, 8] across a batch. Mean μ = 5; variance σ² = ((−3)² + (−1)² + 1² + 3²) ∕ 4 = 5. Normalize: x̂ = (x − 5) ∕ √5 ≈ [−1.34, −0.45, 0.45, 1.34] — now zero mean, unit variance. Then the learnable γ, β rescale: with γ = 2, β = 1 the output is y = 2x̂ + 1 ≈ [−1.68, 0.11, 1.89, 3.68]. The model can still represent any mean and spread it wants — it just no longer has to chase a drifting input to do it.
Off, the bells march sideways and spread layer by layer; on, every layer sees a clean, standardized input regardless of what's happening below it. That stability lets you train deeper networks at higher learning rates — it both speeds convergence and smooths the loss landscape.
Fitting the signal, not the noise
A model with enough capacity can drive training error to zero by memorising every point — including the noise — and then fails on anything new. Below, a polynomial is fit to noisy samples of a smooth curve. Raise the degree and the fit wiggles through every training point while the held-out (validation) error blows up: overfitting. Then raise weight decay (λ, an L2 penalty on the coefficients) and watch the curve smooth back out — generalisation restored.
That shrink factor isn’t arbitrary: differentiating the penalty gives ∂(λ‖θ‖²)∕∂θ = 2λθ, which adds −η·2λθ to every update. So each step first pulls the weights a little toward zero — by the factor (1 − 2ηλ) — then applies the data gradient. Big weights are taxed, so the fit can’t lean too hard on any one coefficient.
Quit while you're ahead
The simplest regularizer of all is to stop at the right moment. Training loss falls forever — the model keeps fitting the training set harder — but validation loss traces a U: it improves while the model learns real structure, bottoms out, then climbs as the model starts memorising noise. The best model is the one at that bottom. Drag the stopping epoch:
There's nothing to differentiate here — it's a rule, not a formula — but the bookkeeping matters. You track the best validation loss seen so far and the epoch it occurred; each epoch that fails to beat it increments a counter, and when the counter reaches the patience (say 10 epochs) you halt and roll back to that checkpoint. Here validation bottoms out at epoch 22 (val ≈ 0.256); by epoch 48 it has climbed to ≈ 0.564 while training loss kept falling — that widening gap is the overfitting, and the epoch-22 weights are what you actually ship.
What you saw
Training is gradient descent plus a stack of choices. The learning rate sets the step — too large explodes, too small crawls — and a schedule shrinks it over time to settle cleanly. Momentum carries velocity through narrow valleys and off plateaus; Adam gives every parameter its own rate; and on an ill-conditioned surface both leave plain SGD far behind. Batch size trades gradient noise for speed, and that same noise helps escape the saddle points that fill high-dimensional landscapes. Good initialization and normalization keep signals from vanishing or exploding through deep stacks. And capacity without restraint memorises noise — so weight decay, dropout, and early stopping keep the model general. The same machinery as every other dive — a loss and its gradient — but tuned to actually converge.
Diffusion models, from scratch
Every image, video and audio generator you have heard of runs on the same surprising idea: learn to destroy data with noise in a fixed, known way, then train a network to run that destruction backwards. Start from pure static and denoise, step by step, until structure appears. We will take a ring of 2-D points, melt it into a Gaussian blob, derive the one identity that makes training cheap, see why the network only ever has to predict the noise it was given, and then watch a cloud of random points reorganise itself back into the ring — by hand and live.
Learn to undo noise
There are two processes. The forward process is fixed and dumb: it just adds a little Gaussian noise, over and over, until the data is indistinguishable from static. The reverse process is the model: a network that, given a noisy point and a noise level, predicts what noise was added — which is enough to take one step back toward clean data. Run the reverse step from fresh noise enough times and you have generated a brand-new sample.
Forward q
Add noise on a fixed schedule. No parameters, nothing to learn.
Reverse pθ
The network. Predicts the noise, so we can step back toward data.
Objective
Just predict the added noise ε. A plain regression loss.
Sampling
Start from N(0, 1), denoise T times, watch a sample form.
A ring we can watch
To see the process we use a 2-D toy: 90 points arranged in a ring. It is clearly not a Gaussian — there is a hole in the middle and mass at a fixed radius — so when noise destroys that structure, and when the model rebuilds it, both are obvious to the eye. Everything below is the exact algorithm a real image model uses; only the dimensionality is tiny.
Melting the data into noise
One forward step scales the point down a touch and adds a little Gaussian noise, with the amount set by a schedule βt:
The √(1−βt) shrink keeps the variance from growing without bound; the +βt injects fresh noise. Drag t from 0 to the end and watch the ring dissolve into an isotropic blob — the same blob no matter what the data was:
Jump to any noise level in one shot
Composing many small Gaussian steps is itself one big Gaussian step. Define the surviving signal after t steps as the running product ᾱt = ∏s≤t(1−βs). Then you can skip the whole loop and sample xt directly from x0:
Why it collapses into one step: a single step is xt = √αt·xt−1 + √(1−αt)·ε. Substitute xt−1 = √αt−1·xt−2 + √(1−αt−1)·ε′ and the two independent noise terms add:
Independent zero-mean Gaussians merge in quadrature — variances add — so two steps become one: xt = √(αtαt−1)·xt−2 + √(1 − αtαt−1)·ε̃. Induct down to x₀ and the surviving-signal coefficient is just the running product ᾱt = ∏ αs — the closed form above. With α₁ = 0.9, α₂ = 0.8 that gives signal √(0.9·0.8) = √0.72 = 0.849 and noise √(1−0.72) = √0.28 = 0.529 — the very numbers from the by-hand steps below.
That single identity is the engine of the whole method: training never runs the chain, it just picks a random t, draws one ε, and jumps. Below, the green curve √ᾱt is how much signal survives and the violet √(1−ᾱt) is how much noise has taken over — they cross when the data is half gone.
Two steps, then the shortcut
Worth doing once with real numbers on a single 1-D point, so the closed form stops looking like magic:
Just predict the noise
Here is the part that makes diffusion practical. Because xt = √ᾱt x0 + √(1−ᾱt) ε, the clean point and the noise are interchangeable — recover either and you have the other. So instead of predicting the clean image, the network εθ is trained to predict the noise that was added, with a plain mean-squared error:
This is not an ad-hoc trick: the full variational bound on −log p(x₀) unrolls into one denoising term per step, and after the Gaussian algebra each term reduces to exactly this — predict the ε that produced xt. Dropping the per-step weights (which barely matters in practice) leaves the clean MSE above.
It also has a beautiful interpretation. The score of the noisy distribution — the direction in which data becomes more likely — is ∇x log q(xt) = − ε ∕ √(1−ᾱt). So predicting the noise is, up to that scale factor, the same as learning the score: the network learns which way to push a point to make it look more like real data. That is literally the arrows in the next panel.
Which way is “more like data”
At a fixed noise level, evaluate the (perfectly trained) denoiser everywhere on the plane and draw −εθ, the direction it would nudge each point. Every arrow points back toward the ring: outside it pushes in, inside it pushes out, and on the ring the push vanishes. That field is the learned score. Slide the noise level — at high noise the field is broad and vague, at low noise it sharpens onto the exact ring:
Watch the field be learned
The arrows above came from the perfect denoiser. A real network has to learn that field from data, using exactly the loss from chapter 6: draw a point, noise it to a random level t, and nudge the weights so the predicted ε matches the true ε. Here a small time-conditioned model does precisely that. It starts predicting zero — so its loss is just the variance of the noise, ≈ 2 — and as it trains the arrows swing from random to pointing home. Choose a noise level to inspect and press Train:
The loss falls fast then flattens on a floor well above zero — that floor is real, not a bug. Many different (x₀, ε) pairs collide on the same noisy xt, so the noise is only partly recoverable; the best possible prediction is the average ε consistent with xt. That average is exactly the score direction, which is why the learned arrows line up with the ideal field from the previous panel even though the loss never reaches 0.
One step back toward the data
Where does that formula come from? Reverse the chain. The reverse of a single step is intractable in general, but if you also condition on the clean point x₀ it becomes a tractable Gaussian — Bayes on the forward step times the two closed forms:
At sampling time we don’t know x₀ — but εθ hands us an estimate of it: x̂0 = (xt − √(1−ᾱt)·εθ) ∕ √ᾱt. Substitute that x̂0 into μ̃t and the coefficients collapse into the compact update below; the σt z term (with σt² = β̃t) puts back exactly the randomness the posterior says belongs there. So the sampling rule isn’t a heuristic — it’s the posterior mean of the true reverse step with x₀ replaced by the model’s guess:
Watch a sample form
Now the payoff. Seventy points start as pure Gaussian noise (t = 39). Each frame applies the reverse step above — predict the noise with the optimal denoiser, nudge toward the clean estimate, add a shrinking dash of randomness — and the cloud walks itself down to t = 0, settling onto the ring it has never been shown directly. This is generation: structure out of static, one denoising step at a time.
From a ring to an image model
Scale this up and almost nothing changes conceptually. The 2-D point becomes a full image (or a compressed latent of one, as in Stable Diffusion, which is cheaper); the toy denoiser becomes a large U-Net or transformer; and the schedule runs over hundreds or thousands of steps (with faster samplers like DDIM cutting that to a few dozen). To make it follow a prompt, the network is also fed a text embedding — conditioning — and classifier-free guidance sharpens prompt adherence by extrapolating between the conditioned and unconditioned predictions.
The network is trained on both the prompt c and an empty prompt ∅ (the conditioning is randomly dropped during training). At sampling you push past the conditioned prediction, away from the unconditioned one; the guidance weight w (typically 5–15) trades diversity for prompt fidelity. And to cut hundreds of steps to a few dozen, deterministic samplers like DDIM drop the random σt z and take larger exact steps along the same path — xt−1 = √ᾱt−1·x̂0 + √(1−ᾱt−1)·εθ, reusing the same clean-point estimate — same trained model, far fewer evaluations.
The reason diffusion overtook GANs: training is a stable regression instead of a fragile two-player game, it covers the full data distribution instead of collapsing onto a few modes, and the iterative sampler trades compute for quality on demand. The cost is exactly that — many network evaluations per sample — which is what the fast-sampler research keeps chipping away at.
What you saw
A diffusion model is a fixed forward process that drowns data in Gaussian noise, plus a network trained to undo one step of it. The closed form xt = √ᾱt x₀ + √(1−ᾱt) ε lets training jump to any noise level in one shot; the network just predicts that ε, which doubles as learning the score — the direction toward more-likely data. You trained a small denoiser from scratch and watched its vector field sharpen toward that score; the reverse step you stepped by hand is the exact posterior mean with x₀ swapped for the model’s guess. Sampling reverses the chain from pure noise, and you watched 70 points do exactly that, reforming a ring they were never directly shown. Same loss-and-gradient machinery as every other dive, pointed at a strange and powerful target.
LLM training & alignment, from scratch
The Transformers dive built the engine. This one drives it through the whole factory: from raw text to a model that predicts the next token, to one that follows instructions, to one that has been shaped by human preferences into something helpful — and finally to the sampling knobs that decide what actually comes out. Every stage is the same loss-and-gradient machinery you have seen, pointed at a new target. We will tokenise by hand, train a real next-token network live until it speaks in grammatical sentences, and derive the reward model and the DPO loss line by line.
Four stages, one model
A modern assistant is built in stages. First pretraining: predict the next token on a vast pile of text until the model has absorbed grammar, facts and reasoning patterns. That gives a base model — a brilliant autocomplete that does not yet answer questions. Then fine-tuning and alignment turn that autocomplete into something that follows instructions and prefers helpful, honest answers. Finally decoding chooses, token by token, what to actually emit.
1. Pretrain
Next-token prediction on huge text. Learns language itself.
2. SFT
Imitate curated instruction → answer demonstrations.
3. Align
Reshape with human preferences: reward model, RLHF, DPO.
4. Decode
Sample the next token — temperature, top-k, top-p.
Text becomes a sequence of integers
A model never sees letters — it sees token IDs. Splitting on whole words makes the vocabulary explode and chokes on anything unseen; splitting on single characters makes sequences painfully long. Byte-pair encoding (BPE) finds the middle: start from characters and repeatedly merge the most frequent adjacent pair into a new token, so common chunks like "ing" or "the" become single tokens while rare words still decompose into pieces. Build a tiny BPE vocabulary by hand:
Predict the next token
Pretraining is one task repeated trillions of times: given the tokens so far, put a probability on every token in the vocabulary for what comes next, via a softmax over the model's output logits. Training maximises the probability of the token that actually came next — equivalently, it minimises the cross-entropy, the negative log-probability assigned to the truth:
What does one training example look like? A single sequence is shredded into many next-token problems at once. The tokens "the cat sat ." become four overlapping predictions — given "the" predict "cat", given "the cat" predict "sat", given "the cat sat" predict "." — all scored in a single forward pass. The model's input is the token sequence; its target is the very same sequence shifted left by one position. (This trick, teacher forcing, is what lets a whole document be trained on in parallel rather than one token at a time.)
Perplexity is the friendlier number: it is roughly "how many tokens the model is effectively choosing between." A model that has learned nothing is as confused as the vocabulary is big; a perfect model on a deterministic next token has perplexity 1. Watch the arithmetic:
And the gradient that does the work is famously clean. Differentiating the softmax cross-entropy with respect to the logits gives, for every token i,
— the predicted probability minus the one-hot truth. In the example above (p = [0.50, 0.20, 0.15, 0.10, 0.05], true = "sat"), the gradient on the logits is [0.50−1, 0.20, 0.15, 0.10, 0.05] = [−0.50, 0.20, 0.15, 0.10, 0.05]: one descent step lowers every wrong token's logit and raises "sat"'s, in a single stroke. (At initialisation the model is uniform, p = 1∕V, so L = log V and perplexity = V — exactly the vocabulary size, which is where the live demo below starts.) This is the precise update the trainer runs.
Watch a model learn to talk
Here is a real next-token network — a small two-layer model that sees the previous two tokens and predicts the next, trained by exactly the cross-entropy above on a stream of simple sentences (det → noun → verb → optional adverb → period). It starts with random weights, so its perplexity is near the vocabulary size and its samples are noise. Press Train and watch the perplexity fall and the generated sentences snap into grammar:
Why bigger and more is reliably better
One of the most consequential empirical findings in the field: test loss falls as a smooth power law in compute, model size and data over many orders of magnitude. Plotted on log axes it is nearly a straight line, bending toward an irreducible floor (the entropy of language itself). This predictability is why labs invest in scale — the return is forecastable. Slide the compute budget:
The exponent α is small, so each step down in loss costs roughly an order of magnitude more compute — but it keeps paying out. The Chinchilla result refined this: for a fixed compute budget, model size and training tokens should grow together (roughly 20 tokens per parameter); most early large models were badly under-trained on too little data for their size.
From autocomplete to assistant
A base model continues text; it does not answer questions, because the internet rarely follows a question immediately with a clean answer. Supervised fine-tuning (SFT) fixes this with the same next-token objective on a curated dataset of instruction → response demonstrations. The one twist: the loss is computed only on the response tokens — the prompt is masked out, so the model learns to produce good answers, not to predict the user's questions.
A few tens of thousands of high-quality demonstrations are enough to convert a base model into a usable instruction-follower — the knowledge is already in there from pretraining; SFT just teaches it the format of being helpful. Quality matters far more than quantity here: a small clean dataset beats a large noisy one, because the model is imitating every example faithfully, mistakes included.
Learning what people prefer
SFT can only imitate demonstrations, and writing the perfect answer is hard — but comparing two answers is easy. So we collect preferences: show people two responses to a prompt and record which they prefer. The Bradley–Terry model turns those comparisons into a numeric reward by assuming the probability of preferring one response rises smoothly with its reward advantage:
A network rφ is trained to output a scalar reward per response so that preferred answers score higher. Work one example:
Optimise reward — but stay on a leash
With a reward model in hand, alignment becomes an optimisation: adjust the policy (the LLM) to produce higher-reward responses. But maximising a learned reward without limit invites reward hacking — the model finds degenerate text that fools the imperfect reward model. The fix is a KL penalty that keeps the policy close to the SFT reference it started from:
The leash is the KL divergence, a measure of how far the policy has moved from the reference: KL(π ‖ πref) = Σy π(y)·log[π(y) ∕ πref(y)] — zero when the two match, growing as they diverge. For a number: moving from a reference [0.40, 0.35, 0.25] to a policy [0.70, 0.20, 0.10] costs 0.70·log(0.70∕0.40) + 0.20·log(0.20∕0.35) + 0.10·log(0.10∕0.25) ≈ 0.39 − 0.11 − 0.09 = 0.19 nats. That is exactly the quantity the alignment panel two chapters down reports as you slide β.
This objective can be solved exactly, which is the key to everything that follows. Maximise Σ π(y)r(y) − β Σ π(y)·log[π(y)∕πref(y)] subject to Σ π(y) = 1; setting the derivative of the Lagrangian to zero gives r(y) − β(log[π(y)∕πref(y)] + 1) + λ = 0, so log[π(y)∕πref(y)] = r(y)∕β + const, i.e.
— the reference distribution tilted toward reward (a Gibbs/Boltzmann form), with Z renormalising. This is the policy chapter 10 plots, and the exact identity DPO exploits next: we already know what the optimum looks like, so there is no need to actually run the RL loop to reach it.
Classic RLHF optimises this with a policy-gradient method (PPO): sample responses, score them with the reward model, and nudge the policy toward the high-reward ones while the KL term pulls back any token whose probability has drifted too far from the reference. β sets the leash length: small β chases reward hard (and risks hacking and gibberish), large β barely moves from the SFT model. It works well but is finicky — four models in memory, an unstable RL loop, careful tuning.
Skip the RL — the policy is its own reward model
Direct Preference Optimization makes a sharp observation: the RLHF objective above has a known closed-form solution, and you can rearrange it to train directly on preferences with a plain classification loss — no reward model, no sampling, no RL loop. Walk the derivation:
The bracketed log-ratio is the implicit reward: the policy scores its own responses. DPO is stable, needs only the policy and a frozen reference, and has become the default for open models. RLHF can still edge ahead when the reward model generalises well beyond the preference data, but DPO gets most of the benefit for a fraction of the complexity.
The reward–vs–reference tradeoff
Both RLHF and DPO are solving the same problem from chapter 8, and it has an exact optimal policy: tilt the reference distribution toward reward, tempered by β.
Below, an SFT reference assigns fairly flat probabilities to three candidate answers, and the reward model scores the helpful one highest. Slide β to see alignment's central knob: small β concentrates all mass on the top-reward answer (aggressive, risks over-optimising); large β leaves the policy almost unchanged from the reference. The readouts track how far the aligned policy has drifted (KL) and its expected reward:
Turning probabilities into words
Training is over; now the model emits text one token at a time, and how you sample from its distribution changes everything. Temperature T rescales the logits before the softmax — T < 1 sharpens toward the top token (focused, repetitive), T > 1 flattens (diverse, riskier). Top-k keeps only the k most likely tokens; top-p (nucleus) keeps the smallest set whose probabilities sum to p, then renormalises. Reshape a real next-token distribution:
Concretely, take three tokens with logits [2.0, 1.0, 0.0]. At T = 1 the softmax is [0.67, 0.24, 0.09]. Cool to T = 0.5 and the logit gaps double → [0.87, 0.12, 0.02], nearly greedy. Heat to T = 2 and they halve → [0.51, 0.31, 0.19], nearly uniform. Temperature is a single dial running from "always the top token" (T → 0) to "ignore the model entirely" (T → ∞).
How it really gets built
The frontier recipe is pretrain → SFT → preference-tune, with endless refinement at each stage. Preference data is expensive, so RLAIF and Constitutional AI use a strong model (guided by a written set of principles) to generate the comparisons instead of humans. Alignment is increasingly done with DPO or its variants for stability, sometimes followed by a round of RLHF for the last increment of quality. And none of it replaces pretraining — alignment reshapes how the model uses what it knows, but the knowledge, the reasoning, and the ceiling all come from the next-token objective at scale.
A useful mental model: pretraining grows the capability, SFT teaches the format, preference-tuning sets the manners, and decoding picks the words. Every one of those is the same gradient on the same kind of loss — the surprise of the whole field is how far that single idea, scaled, actually goes.
What you saw
An assistant is a next-token predictor that has been progressively shaped. Pretraining minimises cross-entropy on raw text — you watched a small one learn grammar from nothing and its perplexity collapse — and scaling laws say that loss keeps falling predictably with size and data. SFT re-runs the same objective on instruction demonstrations, masked to the response. Then preferences take over: a Bradley–Terry reward model turns comparisons into scores, RLHF maximises that reward under a KL leash, and DPO — derived straight from that objective — drops the RL entirely by letting the policy be its own reward model. Finally, temperature and nucleus sampling decide what the trained distribution actually says. Same loss, same gradient, four times over.
Self-supervised learning, from scratch
Labels are expensive; raw data is everywhere. Self-supervised learning turns that around — it manufactures its own supervision out of the structure already present in unlabelled data, learning representations so good that a tiny labelled dataset (or none at all) suffices downstream. Two big ideas do most of the work: hide part of the input and predict it, or pull together two views of the same thing while pushing apart everything else. We will derive the contrastive loss line by line, watch clusters self-organise from nothing but "these two came from the same source," and then prove the representations are useful with a linear probe.
Supervision from the data itself
A supervised model needs a human-labelled target for every example. A self-supervised model invents the target from the input: delete a word and the word itself is the label; take two random crops of one photo and "they match" is the label. No annotation required — so you can train on the entire internet. The goal is not the pretext task itself but the representation it forces the network to build: a feature space where similar things land nearby, which then transfers to real tasks.
Masked prediction
Hide part of the input, predict it. BERT, MAE.
Contrastive
Same source → close; different → far. SimCLR, CLIP.
Representation
The real prize: a transferable feature space.
Transfer
Freeze features, train a tiny head — or go zero-shot.
The pretrain-then-probe bet
The wager of self-supervision: if a network must solve a hard enough pretext task on huge unlabelled data, it has to discover the underlying structure of the data to succeed — and that structure is exactly what downstream tasks need. So you pretrain once, expensively, on a pretext task, then freeze the features and attach a small task-specific head trained on whatever little labelled data you have. The representation does the heavy lifting; the head just reads it out.
This is the engine behind modern foundation models: BERT and GPT pretrain on text, MAE and DINO on images, CLIP on image–text pairs, wav2vec on audio. In every case the labelled fine-tuning is a thin layer on top of representations learned without labels.
Hide it, then predict it
The simplest pretext task: corrupt the input and reconstruct it. BERT masks ~15% of the tokens in a sentence and predicts each from the words on both sides; MAE masks ~75% of the patches in an image and reconstructs them. The loss is the same cross-entropy from the LLM dive — but applied only at the masked positions, and the context is bidirectional, which makes these models superb at understanding (not generation).
There is a subtlety in the data: of the 15% of tokens BERT selects, it replaces 80% with [MASK], 10% with a random token, and leaves 10% unchanged. The reason is that [MASK] never appears at fine-tuning or inference time, so a model trained to expect it everywhere would be mismatched; the random and unchanged cases force it to build a useful representation of every token, not just the masked slots.
Pull together, push apart
Contrastive learning needs no reconstruction at all. Make two augmented views of one example (crop, recolour, blur) — they form a positive pair that should map to nearby points. Every other example in the batch is a negative that should be pushed away. Measure closeness by cosine similarity — the dot product of the unit-normalised embeddings, sim(a, b) = (a·b) ∕ (‖a‖‖b‖) = â·b̂ ∈ [−1, 1] — and the InfoNCE loss is just a softmax cross-entropy whose "correct class" is the positive:
Minimising it means making the anchor far more similar to its positive than to any negative. Work the arithmetic:
And the gradient shows why it works. Writing mk = softmaxk(sim(a,k)∕τ) for the weight the loss puts on each candidate, the update on the anchor is
— move toward the positive a+, and away from the m-weighted average of all candidates. Because mk is largest for whichever negatives are already closest, the push lands hardest on the "hard negatives." In the worked example, m = [0.965, 0.029, 0.007] is dominated by the positive itself, so a+ − Σ mk·k ≈ 0: an already-aligned anchor barely moves. In the misaligned case (m on the positive only 0.33) that term is large, so the pull toward a+ is strong — the force self-limits exactly as alignment improves.
The name is literal: InfoNCE is a lower bound on the mutual information between the two views, and the bound is capped by log(number of candidates). More negatives ⇒ a tighter bound ⇒ better representations — which is precisely why SimCLR trains with very large batches and MoCo keeps a running queue of thousands of negatives.
Clusters from nothing but "same source"
Below are 30 samples — six augmented views of each of five source items — each with a 2-D embedding on the unit circle (a real encoder would compute these from raw input; here they are free parameters so we can watch the forces directly). The only signal is which views came from the same source; no labels, no class names. Press Train and watch InfoNCE pull each source's six views together and shove the five groups apart until clean, evenly-spaced clusters appear:
Two knobs, one failure mode
The temperature τ controls how harshly negatives are penalised. Small τ sharpens the softmax so only the very hardest negatives matter (sharper clusters, but touchy training); large τ treats all negatives gently. With τ = 0.2, similarities [0.9, 0.2, −0.1] become logits [4.5, 1.0, −0.5] and a near-one-hot weighting; at τ = 1 the same similarities barely separate.
The two forces from the last chapter can be written down exactly. Asymptotically the contrastive loss decomposes into an alignment term (positives should coincide) and a uniformity term (features should spread evenly over the sphere):
InfoNCE optimises both at once. The lurking danger is collapse: if the encoder maps everything to the same point, alignment is trivially perfect and the loss looks great — but uniformity is destroyed and the representation is useless.
Negatives are what prevent collapse — pushing different examples apart makes the all-same-point solution costly. Methods without explicit negatives (BYOL, DINO) avoid collapse by other means: a momentum "teacher" network, a stop-gradient, or centering — so the student can't just copy a constant.
Contrastive across two modalities
The same loss links different kinds of data. CLIP takes a batch of N (image, caption) pairs, embeds images with one encoder and captions with another, and builds the N×N matrix of image–text similarities. The matched pairs sit on the diagonal; the contrastive objective pushes the diagonal up and everything off-diagonal down — InfoNCE applied symmetrically, once across rows (each image finds its caption) and once down columns (each caption finds its image):
Toggle between an untrained and a trained encoder and watch the diagonal light up:
Once trained, CLIP classifies zero-shot: to label a new image, embed it, embed candidate captions like "a photo of a cat / dog / car," and pick the highest similarity — no task-specific training at all. The supervision was never labels; it was 400 million image–caption pairs scraped from the web, each its own positive.
Proving the features are useful
How do we know the representation is any good? The standard test is a linear probe: freeze the encoder, train only a single linear classifier on top with a little labelled data, and measure accuracy. If a mere linear boundary now separates the classes, the encoder has already done the hard nonlinear work. Below, two classes form concentric rings in the raw input — no straight line can split them — but in the learned representation they become trivially separable:
Same points, same labels — only the representation changed. A linear probe on the raw coordinates is stuck at chance because the classes are concentric; on the learned features it is near-perfect. That gap is the entire value proposition of self-supervised pretraining: it reshapes the data so that the easy models downstream actually work.
The quiet engine of foundation models
Self-supervision is how nearly every large model is born. Text models pretrain by next-token or masked prediction; vision models use masked autoencoding (MAE) or contrastive/self-distillation (SimCLR, MoCo, BYOL, DINO); CLIP and its successors bridge vision and language; speech uses masked prediction on audio. The labelled stage that follows — fine-tuning, instruction tuning, alignment — is comparatively tiny, because the representation already encodes the structure of the world the data came from.
The throughline across this whole lab: the loss keeps changing — squared error, cross-entropy, InfoNCE, preference loss — but the machinery never does. Define a differentiable objective that only a good representation can minimise, then follow the gradient. Self-supervision's trick is simply finding such objectives that need no human labels at all.
What you saw
Self-supervised learning builds targets out of unlabelled data. Masked prediction hides part of the input and reconstructs it with the familiar cross-entropy. Contrastive learning needs no reconstruction: the InfoNCE loss — a softmax cross-entropy whose correct class is a positive pair — pulls two views of one source together and pushes all others apart, and you watched five clean clusters self-organise from that signal alone. Temperature tunes how hard negatives bite, and negatives (or clever substitutes) prevent the everything-collapses-to-a-point failure. CLIP runs the same loss across images and text for zero-shot classification, and a frozen-feature linear probe proves the representations actually transfer.
Graph neural networks, from scratch
Images are grids and text is a sequence, but molecules, social networks, road maps, knowledge graphs and recommendation systems are graphs — nodes joined by edges, with no fixed order and no fixed shape. A graph network learns on them with one deceptively simple operation repeated in every layer: each node looks at its neighbours, aggregates their messages, and updates itself. We will run that update by hand with real numbers, watch information spread hop by hop, label a whole network from four seed nodes, and see why stacking too many layers quietly destroys everything.
Learning on nodes and edges
A graph is a set of nodes (each with a feature vector) connected by edges. The catch that breaks ordinary networks: there is no canonical ordering of nodes and every node can have a different number of neighbours, so you cannot just flatten it into a fixed-size vector. The fix is message passing — a local rule, applied identically at every node, that is invariant to how you happen to number things.
Nodes & edges
Entities and their relationships. Features live on the nodes.
Message passing
Each node aggregates its neighbours, then updates itself.
Permutation invariance
Re-label the nodes and nothing changes. Aggregation must not care about order.
Three tasks
Classify a node, predict an edge, or label a whole graph.
An adjacency matrix and a feature matrix
Two arrays describe everything. The feature matrix X stacks one feature vector per node. The adjacency matrix A records the edges: Aij = 1 if node i connects to node j. Here is a small graph and its adjacency — symmetric, because these edges are undirected:
The one operation everything is built from
Every graph-network layer is the same two-step local rule, run at every node simultaneously: aggregate the neighbours' current vectors with some order-independent function (sum, mean or max), then update the node by combining that aggregate with its own vector through a small learned transform:
The aggregation must be permutation-invariant — a sum does not care what order the neighbours arrive in, and that is exactly what a graph needs. The learned transform (a weight matrix W and bias, shared across all nodes) is the only thing with parameters, which is why one trained network handles graphs of any size or shape.
Formally this makes the layer permutation-equivariant: relabel the nodes by any permutation P and the output is just the same vectors relabelled — GNN(PX, PAPᵀ) = P·GNN(X, A). Nothing depends on the arbitrary numbering, which is exactly the property a graph demands.
Averaging a neighbourhood
Take the graph above with a single number on each node and update one of them, so the aggregate-then-transform stops being abstract:
Doing it for all nodes at once
Written as matrices, a whole layer is one line. The graph convolutional network (GCN) uses a particular aggregation: add self-loops (so a node keeps its own features) and normalise by degree symmetrically, so high-degree hubs do not drown everything out:
Here A + I adds the self-loop, D̃ is the degree matrix of A + I, and the D̃−1/2(·)D̃−1/2 sandwich weights the edge between nodes i and j by 1∕√(di dj) — gentle on hub nodes. (The by-hand step above used a plain mean for clarity; the symmetric normalisation is the same idea with degree-aware weights.) Multiplying by  mixes each node with its neighbours; multiplying by W transforms the features; σ is a nonlinearity like ReLU.
A concrete entry: suppose node i has 3 neighbours and node j has 2. With self-loops their degrees are d̃i = 4 and d̃j = 3, so the weight Âij = 1∕√(d̃i·d̃j) = 1∕√12 = 0.289, while node i keeps 1∕d̃i = 0.25 of itself. A degree-50 hub contributes only 1∕√(51·d̃j) to each neighbour — automatically tiny — which is the whole point of the symmetric normalisation: popular nodes do not shout over everyone.
Each layer is one hop further
One layer lets a node see its immediate neighbours. Stack a second and those neighbours have already absorbed their neighbours — so two layers reach two hops out, three layers reach three, and so on. The number of layers is literally the radius of the receptive field. Pick how many layers and watch the reach of the highlighted node grow:
Labelling a whole graph from a few seeds
The classic graph task is semi-supervised node classification: you know the labels of only a handful of nodes and must infer the rest from the graph structure. Below, a network of two loosely-connected communities has just four labelled seeds (two per colour, ringed). A two-layer GCN is trained on those four alone — every other node starts unlabelled (grey). Press Train and watch labels propagate along the edges until the whole graph is correctly split:
Only the four seeds appear in the loss — yet the gradient trains a classifier for all fourteen. The reason is the two  multiplications: a seed's prediction depends on its 2-hop neighbourhood and on the shared W, so backprop pushes signal into W from the seeds' surroundings, and because W is reused at every node, what it learns there applies everywhere. This is transductive learning: the unlabelled nodes' features and edges are used in the forward pass (they shape the seeds' predictions); only their labels are withheld. The gradient at the output is the familiar p − y, non-zero only on seed rows, then flowed back through  and W.
Not all neighbours matter equally
A GCN weights every neighbour by a fixed function of degree. The graph attention network (GAT) learns the weights instead: it scores each neighbour, softmaxes the scores into attention weights over the neighbourhood, and aggregates accordingly — the same attention idea as transformers, restricted to the graph's edges.
The score concatenates the transformed anchor and neighbour, dots them with a learned vector a, and passes through a LeakyReLU; softmax over the neighbourhood normalises the scores to weights that sum to 1. In practice GAT runs several independent attention heads in parallel and concatenates their outputs — exactly the multi-head trick from transformers — so the node can attend to neighbours in several different ways at once.
Why you cannot just stack more layers
If depth grows the receptive field, why not use fifty layers? Because each layer also averages neighbours, and repeated averaging is diffusion: push it far enough and every node's features blur into the same value, losing all the distinctions the network needed. This is over-smoothing. Slide the depth and watch six initially-distinct node features collapse together:
Why a line and not random noise? Â is an averaging operator whose largest eigenvalue is 1, with eigenvector proportional to √(degree). Repeated multiplication ÂL shrinks every other eigen-component toward zero, so ÂLX converges onto that one direction: every node's feature becomes the same vector, scaled only by √(dv). Whatever distinguished the nodes is gone — all that survives is a degree pattern.
This is the central tension in graph nets: you need enough layers to reach the relevant neighbourhood, but too many wash out the signal. Most GCNs use just 2–3 layers. Remedies — residual/skip connections, jumping-knowledge, normalisation, or attention that can learn to ignore neighbours — let a few architectures go deeper, but shallow remains the norm.
Three tasks, many flavours
Message passing supports all three graph tasks. Node-level: classify each node (the demo above). Edge-level: predict whether two nodes should be linked — the engine of recommendation and knowledge-graph completion. Graph-level: pool all node vectors into one — a permutation-invariant readout, hG = Σv hv (or mean/max) — and classify the whole graph, which is how models predict a molecule's properties from its atoms and bonds. (The sum is what keeps the graph label independent of node ordering.) The popular variants differ mainly in the aggregator: GCN (degree-normalised mean), GraphSAGE (sample-and-aggregate for huge graphs), GAT (attention), and GIN (a sum-based form proven as expressive as the classical graph-isomorphism test).
The throughline of this lab holds here too: the loss and gradient are unchanged from every other dive. What is special to graphs is only the layer — an aggregation that respects the irregular, orderless structure of the data — and from that single idea comes drug discovery, traffic forecasting, fraud rings, and recommendation at billion-edge scale.
What you saw
A graph network learns by message passing: every node aggregates its neighbours with a permutation-invariant function and updates through a shared learned transform. You did one GCN layer by hand (aggregate the neighbourhood, transform, activate), saw the whole-graph matrix form  H W, and watched depth extend the receptive field one hop per layer. A two-layer GCN coloured an entire network from four seeds; graph attention learned which neighbours matter; and over-smoothing showed why too much depth collapses every node into the same blur. Same loss, same gradient — a new kind of layer for a new kind of data.
GANs, from scratch
Most networks learn by being told the right answer. A generative adversarial network learns by being caught. One network, the generator, turns random noise into fake samples; a second, the discriminator, tries to tell those fakes from real data. Each makes the other better: the forger improves until its counterfeits pass, the detective sharpens until it can spot them — a game that, at equilibrium, forces the generator to reproduce the data distribution exactly. We will write the game down, derive its optimum, and watch a real generator paint a target shape out of pure noise.
A forger and a detective
Picture a counterfeiter and a bank inspector locked in escalation. The counterfeiter (generator) prints fake notes from random ink; the inspector (discriminator) learns the tells and rejects them; the counterfeiter adapts, the inspector re-learns, and round after round the fakes get better. When the inspector can do no better than a coin flip, the counterfeits are indistinguishable from real money — and that is exactly the generator we wanted.
Generator
Noise z → fake sample. Never sees real data directly.
Discriminator
Sample → probability it is real. A learned critic.
Adversarial
One's gain is the other's loss. A minimax game.
Equilibrium
Fakes match data; the critic is reduced to guessing.
One value function, two opposite goals
The discriminator D outputs a probability that its input is real. The generator G maps noise z to a sample G(z). They share a single value function — the discriminator tries to maximise it, the generator to minimise it:
The first term rewards D for scoring real data near 1; the second rewards it for scoring fakes near 0. G only touches the second term — and wants the opposite, to make D(G(z)) near 1. Put numbers on it:
The best critic, in closed form
Freeze the generator. What is the best possible discriminator? Because the value function is an average over x, we can maximise it pointwise — and the answer is beautifully simple: at each x, D should report the fraction of probability mass there that is real.
Below, the real data is a fixed bell curve and the generator's distribution is another you can slide. The white curve is the optimal discriminator D* = pdata∕(pdata+pg). Drag the generator onto the data and watch D* flatten to 0.5 everywhere — the critic can no longer tell them apart:
The game is a divergence
Substitute that optimal D* back into the value function and the algebra collapses into something profound — the generator is minimising the Jensen–Shannon divergence between the fake and real distributions:
JSD is a symmetric, always-finite cousin of the KL divergence: JSD(p ‖ q) = ½ KL(p ‖ m) + ½ KL(q ‖ m) where m = (p + q)∕2 is their average. It is zero only when the two distributions are identical and positive otherwise. So the generator's objective is to drive pg all the way onto pdata. At that global optimum pg = pdata, the divergence is zero and D*(x) = ½ everywhere — the detective is reduced to a coin flip.
Why exactly −log4 at the bottom? With D* = ½ every term inside V becomes log ½, so V = Edata[log ½] + Efake[log ½] = 2·log ½ = −log 4 ≈ −1.386. That is the theoretical promise of a GAN: the unique equilibrium of the game is the true data distribution.
Painting a distribution from noise
Here are two real networks playing the game. The generator (a small MLP) maps 2-D Gaussian noise to points on the plane; the discriminator (another MLP) learns to separate them from the target — a ring. The shaded background is the discriminator's current verdict (green = "looks real," dark = "looks fake"). Press Train and watch the fake cloud, scattered at first, migrate until it covers the ring and the background washes to grey:
The saturating gradient, and the fix
The theory is clean; the training is treacherous. Look at the generator's original term, log(1 − D(G(z))). Early on the generator is terrible, the discriminator rejects its fakes with confidence (D ≈ 0), and exactly there the gradient of log(1 − D) is nearly flat — the generator gets almost no signal precisely when it needs it most. This is the saturating gradient.
The standard fix flips the objective: instead of minimising "the chance D catches me," the generator maximises "the chance D is fooled," log D(G(z)). The optimum is identical, but the gradient is strong exactly when D is winning — so the generator keeps learning even while being beaten. (The live demo uses this non-saturating loss.) Even so, the two-player dynamics can oscillate, stall, or diverge rather than settle; balancing the pair is the central art of training GANs.
Underneath, it is all the familiar gradient. The discriminator minimises a binary cross-entropy, whose gradient on its output logit is the same D − label that trained every classifier in this lab: D − 1 on a real sample (push the score up), D on a fake (push it down). The generator has no labels and never touches a real point — it learns purely by backpropagating through the frozen discriminator: ∂loss∕∂(G's weights) = ∂loss∕∂D · ∂D∕∂(G output) · ∂(G output)∕∂(weights). "Move your samples wherever D thinks looks more real."
The saturating problem lives in that first factor. Under the original loss log(1 − D), the gradient on a fake's logit works out to −D — proportional to D itself, so when the discriminator confidently rejects (D ≈ 0) the signal nearly vanishes. The non-saturating loss −log D instead gives D − 1, proportional to (1 − D), which is largest exactly when D ≈ 0:
Numerically, when the generator is losing badly at D = 0.1: the saturating push is just 0.1 (feeble, right when G most needs to learn), while the non-saturating push is 0.9 (strong). At D = 0.9 they swap — but by then the generator is already winning. Same optimum, far healthier gradient when it counts.
The failure that haunts every GAN
The most infamous failure: the generator discovers one output that reliably fools the discriminator and produces only that, ignoring the rest of the data. Fakes look fine individually but have no variety — a face generator that draws the same face, a digit model stuck on a single "7." Here the target has eight separate clusters; toggle between a healthy generator that covers them all and a collapsed one that has piled all its mass onto a few:
Collapse happens because the generator is only ever asked to fool the discriminator on the current batch, not to match the full diversity of the data — covering one mode perfectly can be a stable trap. Remedies include showing the discriminator several samples at once (minibatch discrimination), unrolling its updates, or changing the distance the game minimises altogether.
Wasserstein and beyond
Jensen–Shannon divergence has a flaw: when the fake and real distributions barely overlap — common early in training — it saturates and its gradient vanishes, starving the generator. The Wasserstein GAN replaces it with the earth-mover (Wasserstein) distance, the minimal "cost" to transport one distribution onto the other, which gives a smooth, informative gradient even when the distributions are far apart:
The discriminator becomes a "critic" that scores realness on an unbounded scale rather than outputting a probability, kept well-behaved by constraining it to be 1-Lipschitz (its output cannot change faster than its input) — the condition that the Kantorovich–Rubinstein duality needs to turn the Wasserstein distance into a maximisation over critics. Weight clipping (as in the live demo above) and the gradient penalty are two ways to enforce that bound. WGANs train far more stably and rarely mode-collapse, which is why the technique became standard.
What GANs built, and what came after
GANs powered a decade of generative leaps: DCGAN made them work with convolutions, StyleGAN produced photorealistic faces with controllable style, conditional and image-to-image variants (pix2pix, CycleGAN) enabled sketch-to-photo and style transfer. Their signature strengths are sharp samples and fast, single-pass generation. The trade-off is the training difficulty you have just seen — instability and mode collapse — which is precisely why diffusion models (their own dive in this lab) have overtaken GANs for many image tasks: they are slower to sample but far more stable to train and cover the full data distribution by construction.
The throughline of the lab holds once more: a differentiable loss and a gradient. What is singular about the GAN is that the loss is not fixed — it is another network, learned in lockstep, so the target moves as the generator chases it. That moving target is both the source of their remarkable sample quality and the reason they are so hard to train.
What you saw
A GAN is a two-player game: a generator turning noise into samples and a discriminator judging them, sharing one value function that one maximises and the other minimises. You scored the game by hand, derived the optimal discriminator D* = pdata∕(pdata+pg) and the result that the generator is minimising Jensen–Shannon divergence — zero only when the fakes match the data and the critic is reduced to a coin flip. You watched a real generator reconstruct a ring from noise using nothing but the discriminator's gradient, met the saturating-gradient trap and its non-saturating fix, saw mode collapse, and met the Wasserstein distance that tames it. A loss that is itself a learned, adversarial network — same gradient, a moving target.