Chapter 9: Neural Networks
Build the foundation of modern deep learning from first principles. Understand how perceptrons combine into multi-layer networks, how activation functions enable non-linear learning, how backpropagation efficiently computes gradients, and how optimizers navigate the loss landscape.
- Topics
- 4
- Demos
- 4
- Min read
- 34
Chapter Overview
Neural networks are universal function approximators—with enough neurons, they can learn to represent any continuous function. This remarkable capability, combined with efficient training via backpropagation, sparked the deep learning revolution.
The basic unit is the perceptron: a weighted sum of inputs passed through a non-linear activation function. Individual perceptrons can only learn linear boundaries, but stacking them into layers creates Multi-Layer Perceptrons (MLPs) that learn arbitrarily complex patterns.
The key insight that made training deep networks practical is backpropagation: by applying the chain rule of calculus, we can efficiently compute how the loss changes with respect to every weight in the network. This transforms learning from an intractable search problem into gradient descent optimization.
Modern neural networks wouldn't work without careful engineering: activation functions prevent vanishing gradients, weight initialization ensures healthy signal flow, and adaptive optimizers like Adam automatically tune learning rates per parameter.
Understanding these fundamentals is essential before moving to specialized architectures like CNNs, RNNs, and Transformers. The same principles—forward passes, loss functions, gradients, and optimization—apply throughout deep learning.
This chapter covers:
- Perceptrons & MLPs: From single neurons to deep multi-layer networks with universal approximation capability
- Activation Functions: Non-linear functions that enable learning complex patterns and their properties
- Backpropagation: The algorithm that makes deep learning tractable by efficiently computing gradients
- Optimizers: Methods for navigating loss landscapes, from basic SGD to adaptive algorithms like Adam
Chapter Roadmap
Click any topic to jump in
Perceptrons & MLPs
Single neurons to multi-layer networks — weighted sums, activation functions, and the universal approximation theorem.
Activation and gradient computation
Activation Functions
Non-linear functions that enable learning complex patterns — ReLU, sigmoid, softmax, and the vanishing gradient problem.
Backpropagation
The chain rule applied to neural networks — efficiently computing gradients from output to input for weight updates.
Optimizers
Navigating loss landscapes with SGD, momentum, and Adam — adaptive learning rates and convergence strategies.
Topics
4 topics in this chapter
The previous chapter, Clustering, found structure without labels. This chapter returns to supervised learning with a harder question. Every model so far, from logistic regression to SVMs, needed us to supply the right features or the right kernel. What if the raw inputs are pixels or audio samples, and nobody knows which combinations of them matter? Can a model learn its own features from data? This chapter, Neural Networks, answers yes by stacking many simple units and training them all at once.
This topic builds the basic unit and the first network. We start with the perceptron, a weighted sum plus a threshold, and see both what it can learn (any linearly separable rule, such as AND) and what it cannot (XOR). Stacking perceptrons into a multi-layer perceptron fixes that, and hidden layers learn intermediate features. The topic closes with weight initialization, the detail that decides whether a deep stack trains at all. The next topics cover activation functions, backpropagation, and optimizers.
Definition
A perceptron computes : a weighted sum of inputs plus a bias, passed through an activation function . A multi-layer perceptron (MLP) composes fully connected layers of such units, , so hidden layers compute learned features that the output layer combines into a prediction.
In this topic
- 1Perceptron
- 2Multi-Layer Perceptron
- 3Hidden Layers
- 4Weight Initialization
Perceptron
A perceptron takes inputs , multiplies each by a weight , adds a bias , and passes the sum through an activation . With a step function, it outputs 1 when and 0 otherwise. Geometrically, is a hyperplane with normal vector , so a single perceptron is a linear classifier, like logistic regression from earlier chapters. Rosenblatt's learning rule nudges the weights toward each misclassified point and is guaranteed to converge when the classes are linearly separable. The failure mode is the converse: no single perceptron can learn XOR, whose classes no line separates.
A perceptron computes , which geometrically defines a hyperplane in that separates two classes. The weight vector is the normal to this hyperplane, and the bias shifts it by from the origin. The perceptron convergence theorem guarantees convergence in at most updates, where is the data radius and is the margin — but only if the data is linearly separable.
Perceptron with w=[2, -1], b=0.5, x=[3, 2]. Using step function f(z)=1 if z≥0, else 0. Output?
Multi-Layer Perceptron
A multi-layer perceptron stacks layers: an input layer, one or more hidden layers, and an output layer. Each layer is fully connected to the next, computing , where has one row per unit in layer and one column per unit in layer . A layer with inputs and outputs therefore has weights and biases. Hidden units let the network combine simple decision boundaries into complex ones, which solves XOR. The universal approximation theorem says one wide enough hidden layer can approximate any continuous function, but it says nothing about how many units that takes or how to train them.
An MLP with layers computes the composition , where each . The universal approximation theorem states that a single hidden layer with enough neurons can approximate any continuous function on a compact set to within any , but the number of neurons may need to be exponentially large. Depth helps: Telgarsky (2016) constructs functions computed by networks with layers of constant width that any network with only layers can approximate only with units.
MLP: 4 inputs, hidden layer with 8 neurons, output layer with 3 neurons. How many weights (excluding biases)?
Hidden Layers
Hidden layers learn intermediate representations that are not in the data. Each layer applies an affine map followed by a nonlinearity, so later layers build features out of earlier ones: in an image model, edges, then textures, then parts. Without the nonlinearity, any stack of layers collapses to a single linear map, adding nothing. Depth is efficient: some functions computable by a deep, narrow network need exponentially many units in a shallow one. Yet very deep plain networks are hard to train because gradients shrink or grow through many layers, which is why initialization, ReLU, normalization, and residual connections matter. Width and depth are hyperparameters, chosen on validation data.
Each hidden layer applies an affine transformation followed by a pointwise non-linearity, mapping . The composition of such layers can represent functions with linear regions — exponentially more expressive than a single layer. This is why deeper networks can learn more complex decision boundaries with fewer total parameters.
For classifying MNIST digits (28×28 images, 10 classes), suggest a simple MLP architecture.
Weight Initialization
Weights must start random, because identical weights receive identical gradients and units never differentiate. The scale matters as much as the randomness. Each layer multiplies the signal's variance by roughly , where is the fan-in, the number of inputs to a unit. If that factor is below 1, activations shrink toward zero layer after layer; if it is above 1, they explode. Xavier (Glorot) initialization sets near for tanh and sigmoid layers. He initialization uses for ReLU, doubling the variance because ReLU zeroes half its inputs. Biases usually start at zero.
He initialization draws weights from , a variance of and so a standard deviation of . It comes from requiring the variance of pre-activations to stay constant across layers: , and ReLU zeroes half its inputs, so the factor of 2 compensates. Xavier initialization uses variance (or ) for tanh and sigmoid. Poor scale makes signals vanish or explode exponentially in depth.
Layer has 512 inputs. Using He initialization for ReLU, what's the weight standard deviation?
Theory Exercise
Problem:
A single perceptron cannot learn the XOR function. Why not, and how does adding a hidden layer fix this?
Hints:
- What kind of decision boundary can a single perceptron produce?
- Plot the XOR truth table — are the classes linearly separable?
- What can a hidden layer build that a single neuron cannot?
Coding Exercise
Problem:
Build a small MLP in PyTorch (2→8→1 with ReLU) and train it to learn the XOR function, demonstrating that a hidden layer succeeds where a single linear unit fails.
Hints:
- XOR inputs are the 4 corners [[0,0],[0,1],[1,0],[1,1]] with targets [0,1,1,0]
- Use nn.Sequential(nn.Linear(2,8), nn.ReLU(), nn.Linear(8,1)) and BCEWithLogitsLoss
- Train for a few hundred steps with Adam, then threshold the sigmoid outputs at 0.5
Related Problems on PixelBank
The previous topic, Perceptrons and MLPs, stacked layers of units and claimed the stack could learn features no single layer could. That claim depends entirely on one ingredient. If each unit just passes its weighted sum through unchanged, two layers compute , which is a single linear map, and a hundred layers are no more expressive than one. The MNIST experiment showed exactly this: removing the ReLU dropped accuracy to the level of logistic regression. So what should sit between the layers, and does the choice matter beyond adding curvature?
This topic shows that it matters a great deal, because the activation's derivative multiplies every gradient flowing backward. We start with ReLU, the default for hidden layers, then sigmoid, whose saturation made deep networks hard to train for decades. Softmax turns a vector of scores into class probabilities at the output. The topic closes with Leaky ReLU, a small change that keeps negative inputs from silencing a unit forever. Backpropagation, the next topic, makes the role of these derivatives precise.
Definition
An activation function is a fixed nonlinear function applied elementwise to each unit's weighted sum, . It lets stacked layers represent nonlinear functions, and its derivative scales every gradient passing back through the unit. Hidden layers typically use ReLU or a variant; output layers use sigmoid for binary and softmax for multiclass probabilities.
In this topic
- 1ReLU (Rectified Linear Unit)
- 2Sigmoid
- 3Softmax
- 4Leaky ReLU
ReLU (Rectified Linear Unit)
ReLU outputs : positive inputs pass unchanged and negative inputs become zero. Its derivative is 1 for and 0 for , so gradients pass through active units without shrinking, the property that made training deep networks practical around 2010 to 2012. It is also cheap to compute and produces sparse activations, since many units output exactly zero. A network of ReLUs is a piecewise linear function of its input. The failure mode is the dead neuron: if a unit's weighted sum is negative for every training input, perhaps after one large update pushes its bias down, its gradient is zero everywhere and it never recovers.
ReLU computes with derivative . Each ReLU neuron partitions input space with a hyperplane, and the network as a whole creates a piecewise linear function with up to linear regions for layers. The 'dying ReLU' problem occurs when for all training inputs — the gradient is permanently zero and the neuron cannot recover without external perturbation.
Apply ReLU to inputs [-2, 0, 3, -0.5, 1.5]. What's the gradient for each?
Sigmoid
The sigmoid squashes any real number into (0, 1), so it is the natural output for binary classification, read as a probability, exactly as in logistic regression. Its derivative is , at most 0.25 at and nearly zero once is beyond about 5 in either direction. That saturation is the problem in hidden layers: backpropagation multiplies one such derivative per layer, so gradients shrink at least fourfold per sigmoid layer, the vanishing gradient problem. Sigmoid outputs are also always positive, not zero-centered, which slows optimization. Hidden layers today use ReLU instead.
Sigmoid maps via , with derivative . The maximum derivative is at . Through layers, gradients decay as in the worst case — after 10 layers: . This exponential decay is the vanishing gradient problem that prevented training deep sigmoid networks before residual connections.
Calculate σ(0), σ(5), and σ(-5). What's the gradient σ'(5)?
Softmax
Softmax turns a vector of real scores, the logits , into a probability distribution: exponentiate each score, then divide by the sum of all exponentials. Outputs are positive, sum to 1, and preserve the ordering of the logits. It is the multiclass generalization of the sigmoid and the standard output layer for classification. Paired with cross-entropy loss, the gradient with respect to the logits is simply , predicted minus true probabilities. Two practical notes: adding a constant to all logits changes nothing, so implementations subtract the maximum to avoid overflow; and scaling logits up makes the distribution sharper, which is how temperature works.
Softmax maps logits to a probability simplex: . The Jacobian is , which is a rank- matrix since probabilities are constrained to sum to 1. Combined with cross-entropy loss , the gradient simplifies elegantly to — predicted minus true probabilities.
Logits z = [2, 1, 0] for 3-class problem. Compute softmax probabilities.
Leaky ReLU
,
Leaky ReLU keeps ReLU's behavior for positive inputs but replaces the flat zero for negative inputs with a small slope: with around 0.01. The derivative is 1 for positive inputs and for negative ones, so a unit whose weighted sum is negative still receives a small gradient and can recover, which fixes the dead-neuron problem. Parametric ReLU (PReLU) makes a learned parameter, per layer or per channel. Related variants such as ELU and GELU are smooth near zero. In practice the gains over plain ReLU are usually small, so Leaky ReLU is a fix to try when many units die.
Leaky ReLU computes with , giving gradient for instead of zero: if and otherwise, so no unit is ever completely silenced. Parametric ReLU (PReLU) makes learnable. In He et al. (2015), the learned coefficients were largest in the first convolutional layer (0.681 channel-shared, 0.596 channel-wise) and generally smaller in deeper layers, dropping below 0.1 near the top: early layers keep more negative information, and later layers behave more like plain ReLU.
Leaky ReLU (α=0.01) applied to x=-10. Output and gradient?
Theory Exercise
Problem:
Why must hidden layers use a non-linear activation function? What happens to a deep network if every activation is the identity (linear) function?
Hints:
- Write out the composition of two linear layers
- Can the product of matrices be replaced by a single matrix?
- What expressive power is lost?
Coding Exercise
Problem:
Implement ReLU, sigmoid, and softmax in NumPy and verify key properties: that ReLU's gradient is zero for negative inputs, sigmoid saturates (vanishing gradient) for large |x|, and softmax outputs a valid probability distribution.
Hints:
- ReLU is np.maximum(0, x); its derivative is (x > 0).astype(float)
- Sigmoid is 1/(1+exp(-x)); its derivative is s*(1-s)
- For numerical stability subtract x.max() before exponentiating in softmax
Related Problems on PixelBank
The previous topic, Activation Functions, ended with a warning: the derivative of each activation multiplies every gradient passing backward through it. But how are those gradients computed in the first place? A network like the 784 to 128 to 10 MLP has about 100,000 weights, and modern networks have billions. Training needs the derivative of the loss with respect to every one of them, at every step. Nudging each weight and re-running the network would cost one forward pass per weight, which is hopeless. Is there a way to get all of them at once?
Backpropagation is that way: it computes every gradient for about the cost of two forward passes. This topic starts with the chain rule, which multiplies local derivatives along a path. The forward pass computes and stores every intermediate value. The backward pass then sweeps from the loss to the inputs, reusing those stored values to produce each layer's gradients. The topic closes with the gradient descent update that consumes them, setting up the next topic, Optimizers.
Definition
Backpropagation computes the gradient of a scalar loss with respect to every parameter of a network by applying the chain rule in reverse order. A forward pass evaluates and stores each layer's outputs; a backward pass propagates from the output to the input, producing each layer's weight gradients at a cost comparable to the forward pass.
In this topic
- 1Chain Rule
- 2Forward Pass
- 3Backward Pass
- 4Gradient Descent Update
Chain Rule
The chain rule says that if the loss depends on only through a chain of intermediate values, here and then , its derivative is the product of the local derivatives along that chain, as in the formula above. Each factor is easy: how the loss changes with the prediction, how the prediction changes with the hidden value, and how the hidden value changes with the weight. When a value feeds several later values, the contributions from each path are summed. Backpropagation is this rule organized for efficiency, computing the shared factors near the loss once and reusing them for every weight below. A useful sanity check is a finite-difference estimate.
Backpropagation applies the multivariate chain rule: for a computation graph , we compute where is the Jacobian of . The key efficiency insight is that this product is computed right-to-left (vector-Jacobian products), costing instead of for matrix-Jacobian products. This makes backprop linear in the number of layers.
Simple network: h = w₁x, ŷ = w₂h, L = (y-ŷ)². If x=2, w₁=3, w₂=0.5, y=5. Find ∂L/∂w₁.
Forward Pass
The forward pass evaluates the network layer by layer: each layer computes its pre-activation , applies the activation to get , and passes it on, ending with the loss. During training it also stores these intermediate values, because the backward pass needs them: the gradient for uses , and the activation's derivative depends on . That storage is why training uses far more memory than inference, growing with depth, width, and batch size. Gradient checkpointing stores only some layers and recomputes the rest during the backward pass, trading extra compute for memory.
The forward pass computes for , storing all intermediate activations . Memory cost is where is batch size. Gradient checkpointing trades compute for memory: storing activations only every layers and recomputing the rest during the backward pass cuts activation memory from to at the cost of about one extra forward pass per step (Chen et al., 2016).
2-layer MLP: x=[1,2], W₁=[[1,0],[0,1]], b₁=[0,0], W₂=[1,1], b₂=0. Compute forward pass with ReLU.
Backward Pass
The backward pass starts from the loss and moves toward the input. For a fully connected layer , given the upstream gradient , three quantities follow. The weight gradient is the outer product , the bias gradient is itself, and the gradient for the layer's input is . Passing that input gradient through the previous activation, multiplying elementwise by , gives the next . Each step is a matrix multiply about as costly as the forward one. Repeated multiplication by small activation derivatives or weights is exactly where vanishing and exploding gradients arise.
The backward pass computes recursively, then . Total compute for one forward-backward pass on a batch of size through layers of width is — dominated by matrix multiplications. Backprop computes exact gradients, not approximations, making it fundamentally different from finite-difference methods which cost per parameter.
Output gradient ∂L/∂z₂ = 0.5. Hidden activations h₁=[1,2]. What is ∂L/∂W₂?
Gradient Descent Update
Once backpropagation has produced for every weight, gradient descent moves each weight a small step against its gradient, , because the negative gradient is the direction of steepest local decrease in loss. The learning rate sets the step size. Too small and training crawls; too large and steps overshoot the minimum and the loss diverges. On a quadratic bowl with curvature , the update converges only if . Real networks estimate the gradient on a mini-batch and repeat the update thousands of times, and the next topic improves on this basic rule.
Gradient descent updates where the learning rate must satisfy for convergence on a quadratic, with the largest eigenvalue of the Hessian. For convex losses, GD converges at rate ; for smooth and strongly convex losses, the rate improves to linear convergence. Neural network losses are non-convex, but SGD empirically finds good local minima due to the smoothing effect of mini-batch noise.
Weight w=2.5, gradient ∂L/∂w = 0.4, learning rate α=0.1. What's the new weight?
Theory Exercise
Problem:
Backpropagation computes gradients for all parameters in roughly the same cost as one forward pass. Why is this far more efficient than estimating each gradient with finite differences?
Hints:
- How many forward passes does finite differences need per parameter?
- How does backprop reuse intermediate computations?
- Scale your reasoning to a network with millions of parameters
Coding Exercise
Problem:
Verify PyTorch's autograd by hand: for the simple network h = w1·x, ŷ = w2·h, L = (y - ŷ)², compute ∂L/∂w1 with autograd and confirm it matches the chain-rule value computed manually.
Hints:
- Create w1, w2 as tensors with requires_grad=True
- Call loss.backward() then read w1.grad
- By hand: ∂L/∂w1 = 2(ŷ - y)·w2·x
Related Problems on PixelBank
The previous topic, Backpropagation, ended with the plain gradient descent update: subtract the learning rate times the gradient. In practice that rule struggles. Real loss surfaces have long narrow valleys where the gradient points mostly across the valley, not along it, so steps zigzag. Different weights see gradients of wildly different sizes, so any single learning rate is too large for some and too small for others. Gradients computed on mini-batches are noisy. Can the update rule itself compensate for these problems?
This topic covers the optimizers used in practice. We start with stochastic gradient descent, the mini-batch version of the basic rule, and its noise. Momentum adds a running velocity that cancels zigzags and speeds up consistent directions. Adam combines momentum with a per-parameter step size, which makes it a robust default. The topic closes with learning rate schedules, which change the step size over training, and warmup. Together with initialization and activations, these choices decide whether a network trains in minutes or not at all.
Definition
An optimizer is the rule that turns gradients into parameter updates. Stochastic gradient descent steps against a mini-batch gradient, . Momentum methods step along an exponentially weighted average of past gradients; adaptive methods such as Adam also divide each parameter's step by a running estimate of its gradient magnitude. A learning rate schedule varies over training.
In this topic
- 1SGD (Stochastic Gradient Descent)
- 2Momentum
- 3Adam
- 4Learning Rate Schedules
SGD (Stochastic Gradient Descent)
Stochastic gradient descent applies the basic update using a gradient estimated on a small random mini-batch, often 32 to 512 examples, instead of the full dataset. Each step is cheap, and the estimate is unbiased, so on average it points the right way; its noise shrinks as the batch grows. One pass over the data, an epoch, contains many updates. The weaknesses come from the fixed step: in a narrow valley it oscillates across the steep direction while crawling along the shallow one, and one learning rate must suit every parameter. With a tuned learning rate schedule, plain SGD often generalizes very well, so it remains common for vision models.
Vanilla SGD updates where is the gradient on mini-batch . The stochastic gradient is an unbiased estimator: , but has variance that scales inversely with batch size. Convergence rate for convex objectives is for SGD versus for full-batch GD — the noise slows convergence but enables escape from sharp minima.
Weight w=5, learning rate α=0.1. Gradients over 3 steps: [2, -1, 3]. What is w after 3 updates?
Momentum
,
Momentum keeps a velocity , an exponentially decaying sum of past gradients: each step multiplies the old velocity by , adds the new gradient, and moves the weights by . With , a gradient that keeps pointing the same way builds the velocity up to times its size, so progress along shallow, consistent directions speeds up. Components that flip sign each step largely cancel, which damps the zigzag across a narrow valley. On an ill-conditioned quadratic, well-tuned momentum needs a number of steps that grows with the square root of the condition number rather than the condition number itself. Nesterov momentum evaluates the gradient at the look-ahead point.
Momentum accumulates a velocity vector and updates . The effective step in a consistent gradient direction after steps scales as — for , momentum amplifies consistent gradients by while dampening oscillating ones. On an ill-conditioned quadratic with condition number , momentum reduces the convergence rate from to iterations.
v=0, β=0.9, α=0.1. Gradient is consistently +2 for 3 steps. What happens to velocity?
Adam
Adapts learning rate per parameter using moment estimates
Adam keeps two running averages per parameter: , a momentum-like average of gradients with , and , an average of squared gradients with . Both start at zero, so Adam divides each by one minus to remove the early bias. The update is . Dividing by the root mean square of recent gradients makes the step size roughly for every parameter regardless of gradient scale, and smaller when gradients are noisy. That makes it robust, with a default . AdamW applies weight decay directly instead of through the gradient and is the standard for transformers.
Adam maintains running averages (first moment) and (second moment), with bias correction , . The update normalizes each parameter's step by its gradient magnitude, effectively giving each parameter its own adaptive learning rate of .
Feature A has gradients ~0.001, feature B has gradients ~10. Why does Adam help here?
Learning Rate Schedules
A learning rate schedule changes during training. Large steps early make fast progress; small steps late let the weights settle into a minimum instead of bouncing around it. Step decay divides by a factor, often 10, at fixed epochs. Cosine annealing lowers it smoothly along half a cosine curve, from the maximum to near zero. Warmup ramps up linearly over the first few hundred or thousand steps, because early gradients and Adam's variance estimates are unreliable, and starting at full size can diverge. One-cycle schedules warm up to a large peak, then anneal. The schedule is often worth as much as the choice of optimizer.
Cosine annealing decays the learning rate as , smoothly reducing from to over steps. Warmup linearly ramps from near 0 to over the first steps to stabilize early training, when gradient and second-moment estimates are unreliable. Smith and Topin's 'super-convergence' showed that a single cycle up to a large maximum learning rate and back down can train some networks an order of magnitude faster than standard schedules, partly because large learning rates act as a regularizer.
Initial lr=0.1, step decay: divide by 10 at epochs 30 and 60. What's lr at epoch 50?
Theory Exercise
Problem:
On an ill-conditioned loss surface (a long, narrow ravine), plain SGD oscillates across the steep walls and crawls along the flat valley floor. Explain how momentum fixes this.
Hints:
- What direction do gradients point on the steep walls vs along the valley?
- What happens when you average gradients over consecutive steps?
- Which components reinforce and which cancel?
Coding Exercise
Problem:
Compare plain SGD, SGD with momentum, and Adam on minimizing a simple ill-conditioned quadratic loss f(x,y) = x² + 50y² starting from the same point, and report how many steps each takes to get close to the minimum.
Hints:
- Represent the point as a single tensor with requires_grad=True and define loss = p[0]**2 + 50*p[1]**2
- Build three optimizers: SGD(lr=0.01), SGD(lr=0.01, momentum=0.9), Adam(lr=0.1)
- Count steps until the loss falls below a small threshold like 1e-3