5 Neural Networks and Backpropagation
Lecture 4
Based on Lecture 4 of CS231n, Stanford University, Spring 2025.
5.2 What the max is doing
The previous chapter observed that removing the from Equation 4.18 collapses into a single matrix and returns the model to a linear classifier. That is a proof that the non-linearity is necessary. It is not an account of what the non-linearity contributes, and the account turns out to be simple enough to see in one dimension.
Consider a single hidden unit reading a scalar input, computing . Its output is zero for every on one side of the point and rises linearly on the other. The unit is two straight pieces joined at a corner, and the position of that corner is set by —which is to say, by the parameters, which is to say, by training. A weighted sum of such units is again made of straight pieces, with at most corners, each placed where one unit switches on.
This is what the network’s capacity to fit a curve consists of. A two-layer network with a ReLU non-linearity computes a piecewise-linear function, gradient descent adjusts both the slopes of the pieces and the locations of the joints between them, and the number of joints available is bounded by the width of the hidden layer. Fitting a smooth function means approximating it by a polyline whose vertices the optimizer is free to move, which is why width buys accuracy and why the approximation is exact nowhere and adequate everywhere.
In dimensions the picture is the same with the corners replaced by hyperplanes. Each hidden unit divides input space in two along the plane where its pre-activation crosses zero, the units together cut the space into regions, and within any one region the set of active units is fixed, so the network is exactly affine there. The network is a collection of linear models with a learned rule for choosing between them. Depth matters here in a way width does not, because a second hidden layer folds the regions produced by the first, and the number of distinct regions available grows much faster with depth than with width (Montúfar et al., 2014).
One detail of this construction matters later. A piecewise-linear function is not differentiable at its corners, so has no derivative at , and every framework simply picks a value there—PyTorch returns zero. The choice is arbitrary and inconsequential, because the set of inputs landing exactly on a corner has measure zero, but it is worth knowing that it was made rather than derived.
5.3 Choosing an activation
The rectified linear unit, , is the default and deserves to be. It costs one comparison, it does not saturate for positive inputs, and its derivative is exactly one wherever it is active, so a gradient passing through many layers of ReLU is neither amplified nor attenuated by the activation itself. Every alternative below is an attempt to fix one specific complaint about it, and the complaints are worth understanding because the same failure modes recur throughout the course.
The complaint against ReLU is that a unit can die. If a unit’s pre-activation is negative for every example in the training set, its output is zero for all of them, its derivative is zero for all of them, and no gradient reaches its weights ever again. It is not merely inactive but unrecoverable, and a network can lose a substantial fraction of its units this way, typically to a learning rate large enough to push a whole row of off the data in one step. Leaky ReLU replaces the flat half with a shallow slope,
so that a unit with negative pre-activation still passes a small gradient and can be pushed back. The exponential linear unit does the same with a smooth curve that approaches instead of falling away linearly, which also brings the mean output nearer zero.
The smooth variants matter for a reason that only shows up in deeper models. The Gaussian error linear unit multiplies the input by the probability that a standard normal variable falls below it,
giving a function that behaves like ReLU far from the origin but has a continuous derivative and a small negative dip just below zero (Hendrycks and Gimpel, 2016). The sigmoid linear unit, , has the same shape from a different construction. These are the activations used in transformers and in several convolutional families, and the argument for them is empirical rather than principled: the loss surface they produce is easier to descend, and the gain is real but small.
Sigmoid and tanh predate all of these and are now the wrong choice inside a network, for a reason that is quantitative rather than aesthetic. The derivative of is , which attains its maximum of at and decays towards zero in both directions. A gradient crossing a sigmoid is therefore multiplied by at most a quarter even under the most favourable conditions, and by very much less once the unit is anywhere near saturation, so ten such layers attenuate the gradient by a factor of at least before it reaches the first weight matrix. This is the vanishing gradient problem in its simplest form, and it is why networks of this depth were not trainable before rectifiers.
Sigmoid carries a second, subtler defect: its output is always positive. Every input to the next layer is therefore positive, and by the local-gradient rule developed in Section 5.10, that forces every element of the gradient with respect to a row of the next weight matrix to carry the same sign. The row can only move up in all coordinates or down in all coordinates, so a direction requiring one weight to rise and another to fall must be reached by a zigzag—exactly the poorly conditioned descent of Section 4.7, produced here by the activation rather than by the data. Tanh, being , fixes the centring and raises the maximum derivative to one, which makes it strictly better than sigmoid as a hidden activation while leaving the saturation intact.
Both still have a use at the output, where saturation is the point rather than the problem: a sigmoid maps a score to a probability for a binary decision, and tanh maps one to a bounded quantity. Inside the network, the practical advice is what the empirical record supports. Use ReLU unless the architecture family you are working in has an established alternative, in which case use that one, and treat the choice as a hyperparameter to be validated rather than a question with a general answer.
5.4 Depth, width and the forward pass
Evaluating Equation 4.18 is a matrix product, an elementwise maximum, and a second matrix product. Nothing in it is expensive to state and nothing in it is expensive to run, which is the reason this particular model became the default unit of construction rather than any of the more biologically motivated alternatives: it is exactly the operation that a GPU executes fastest. The regularity of a fully connected layer—every input reaching every output, in one dense matrix—is a decision about hardware as much as about modelling.
The forward pass over a batch is worth writing with its shapes attached, because in an implementation the shapes are what you debug. With examples stacked as rows, the input is a matrix of shape , the first weight matrix is , and the hidden activations come out ; the second weight matrix is and the scores are . Every example travels through independently and the batch dimension is carried along untouched, which is why a batch of any size works without changing a line.
Training the whole thing is that forward pass, the gradient of the loss with respect to each weight matrix, and the update from Equation 4.5. In a numerical library it fits in twenty lines with nothing hidden:
import numpy as np
N, D_in, H, D_out = 64, 1000, 100, 10
x, y = np.random.randn(N, D_in), np.random.randn(N, D_out)
w1 = np.random.randn(D_in, H) / np.sqrt(D_in)
w2 = np.random.randn(H, D_out) / np.sqrt(H)
for t in range(2000):
h = np.maximum(0, x.dot(w1)) # forward: hidden activations
y_pred = h.dot(w2) # forward: scores
loss = np.square(y_pred - y).sum() # forward: squared error
grad_y_pred = 2.0 * (y_pred - y) # backward: through the loss
grad_w2 = h.T.dot(grad_y_pred)
grad_h = grad_y_pred.dot(w2.T)
grad_h[h <= 0] = 0 # backward: through the ReLU
grad_w1 = x.T.dot(grad_h)
w1 -= 1e-4 * grad_w1 # gradient descent
w2 -= 1e-4 * grad_w2Two details in that listing are not decoration. The weight matrices are divided by the square root of their input dimension, because drawing them from a unit normal would give a pre-activation with standard deviation and a first loss large enough to diverge at any usable learning rate; initialization is a subject in its own right and this is the crudest form of the fix. And the line grad_h[h <= 0] = 0 is the backward pass of the —a gradient reaches a hidden unit only where that unit was active going forwards. Everything else is the chain rule applied by hand, which is precisely the thing that will not survive the next section.
Run it and the loss falls from to under within two hundred steps and to zero shortly after. That is not a good result; it is the point of the next section. The targets y are random numbers, so there is nothing to learn, and a hundred hidden units have simply memorised sixty-four arbitrary vectors.
5.5 Size is not a regularizer
More units mean more capacity, and more capacity means a decision boundary that can wander further to accommodate individual training points. The shape of the failure is familiar from Section 3.2: a one-nearest-neighbour classifier carves an island around every noisy example, and a wide network does the same thing with a smooth boundary instead of a Voronoi cell. Faced with a model that fits the training set and fails on held-out data, the obvious repair is to build a smaller one.
The advice is to resist it, and the reason is not that smaller networks generalise badly. It is that width and regularization strength are not interchangeable knobs even when they move the same quantity. The penalty weight of Equation 4.1 is continuous, so a validation sweep over it explores capacity finely; the width of a layer is an integer that changes the model’s architecture, its initialization, its memory footprint and the shape of its loss surface all at once, and a sweep over it is a sweep over different training runs rather than different settings of one run. When a change hurts, tells you by how much and in which direction. Width tells you much less, at a much higher cost per sample.
The working procedure that follows from this is to grow the network until it is capable of overfitting, and only then to regularize. Overfitting is evidence that the model has enough capacity to represent the structure in the data, which is a prerequisite for learning it; a model that cannot overfit its training set is not being cautious, it is being inadequate, and no amount of tuning will recover what it cannot represent. Once the model can overfit, is the instrument that decides how much of that capacity is spent on structure and how much on noise.
That instrument has two ends. Too little regularization gives the boundary in the previous paragraph. Too much constrains the weights so severely that the boundary loses the detail it needed, and the model underfits—the classifier is now failing for the opposite reason and the validation curve looks similar from a distance. Neither end is a subtle effect and both are visible in a sweep, which is exactly the argument for sweeping the quantity that is cheap to sweep.
None of this settles how wide to make the network in the first place, and there is no principled answer. The practice is to start from a width that has worked for similar data and a similar problem, and to explore around it. Recognising which established architecture the problem resembles is a more useful skill than any formula for choosing a layer size, and most of the rest of this course is a tour of those architectures.
5.6 The brain analogy and its limits
The vocabulary comes from biology and the resemblance is real but shallow. A biological neuron collects electrical impulses from many upstream cells through its dendrites, integrates them in the cell body, and emits a signal down its axon when the accumulated input is sufficient. Written as arithmetic, that is a weighted sum followed by a threshold, which is one unit of Equation 4.18 with the activation function playing the part of the threshold. The analogy earned the field its name and is a fair description of where the idea came from.
It is a poor description of what either system is. A cortical neuron is not a scalar rate: it has spike timing, dozens of morphological types, dendrites that themselves compute non-linearly, and neuromodulatory state that changes its behaviour wholesale. Nothing in the brain corresponds to a global scalar loss differentiated back through every synapse, and the layered, densely connected regularity of the models in this chapter was chosen because it maps onto a matrix multiply, not because it maps onto cortex. Networks with irregular connectivity have been built and studied; they perform about as well and are much harder to run, which is the honest reason they are not used. Take the analogy as etymology rather than as evidence.
5.7 Why gradients need a system
Return to the four gradient lines in Section 5.4. They are correct, and they were obtained by differentiating a specific composition—squared error, on top of a linear map, on top of a rectifier, on top of a linear map—once, by hand, on paper. Every part of that sentence is a liability.
Change the loss from squared error to the softmax objective of Equation 3.7 and grad_y_pred is wrong and every line below it inherits the error. Insert a third layer and the derivation restarts. Replace the rectifier and the masking line changes shape. The work is proportional to the architecture, it has to be redone whenever any part of the architecture moves, and it is done in the one place where a mistake is hardest to notice—a gradient that is wrong by a constant factor still trains, slowly and to a worse optimum, and looks like a badly chosen learning rate. Chapter 3’s numerical gradient check exists precisely because this failure is silent.
Tedium is the mild version of the objection. The severe one is that hand differentiation does not scale past architectures you can hold in your head. A recurrent model unrolled over a hundred timesteps, or a network with branches that merge, or anything with a memory that is read and written—the differentiation is not tedious for these, it is infeasible, and the field would have stopped at two layers if it had depended on doing it.
The fix is to stop treating the model as one large formula. Write it instead as a graph in which each node performs a single primitive operation—an addition, a multiplication, a maximum, an exponential—and each edge carries a value from the node that produced it to the nodes that consume it. The inputs to the graph are the data and the parameters; the single output is the scalar loss. Nothing about the model has changed; it has only been decomposed to a granularity at which every individual derivative is something you already know.
What makes the decomposition pay is that the chain rule is local. To send a gradient through a node, that node needs the derivative of its own output with respect to its own inputs, and the gradient of the loss with respect to its output. It needs to know nothing whatsoever about the rest of the graph—not what produced its inputs, not what consumes its outputs, not what loss is being minimised. So if every primitive knows how to differentiate itself, the gradient with respect to every parameter in the graph can be assembled by traversing the graph backwards, once. That procedure is backpropagation.
5.8 The chain rule on a graph
The procedure is easiest to trust after watching it work on something small enough to check by hand. Take
evaluated at , , . As a graph it has two nodes: an addition consuming and and producing an intermediate value , and a multiplication consuming and and producing . The forward pass evaluates them in order, giving and , and each node keeps its inputs, because the backward pass will need them.
What is wanted is , and . Two of the four local derivatives are immediate from the addition node, since gives and . The multiplication node gives the other two: gives and . Note what those say—the derivative of a product with respect to one factor is the other factor, which is why a multiplication node has to remember both of its inputs.
Now traverse backwards. The gradient of with respect to itself is . At the multiplication node, the two inputs receive and . At the addition node, the chain rule composes what has arrived from above with what the node contributes locally:
and identically . The results are checkable without any of this machinery: raising from to raises to and lowers to , a change of for an input change of , which is the just computed.
Equation 5.5 is the entire algorithm, and it is worth naming its two factors, because the names are how the rest of this chapter and most framework source code talk about it. The quantity arriving from the node above is the upstream gradient: the derivative of the loss with respect to this node’s output. The derivative the node computes from its own inputs is the local gradient. Their product is the downstream gradient, which the node passes to whatever produced its inputs, where it becomes that node’s upstream gradient in turn.
The recursion terminates because the graph is finite, and it visits each node exactly once, so the whole backward pass costs about what the forward pass costs. This is the property that makes the method viable rather than merely correct. A network with parameters yields all partial derivatives in one traversal, whereas the finite-difference estimate of Section 4.5 would need separate forward passes to get the same information. The asymmetry is not an implementation detail; it is the reason gradient-based learning at this scale exists at all, and it was the point of the paper that introduced the technique to the field (Rumelhart, Hinton and Williams, 1986).
Two things a node emphatically does not need are worth stating, because they are what make the whole scheme composable. It does not need to know what produced its inputs, and it does not need to know what its output feeds into or what loss is eventually computed. It needs its own inputs, its own local derivative, and one number arriving from above.
5.9 A longer example, and a choice about granularity
Take a logistic unit over two inputs,
at , , , , . Decomposed to primitives the graph runs: two multiplications giving and ; an addition giving ; a second addition with giving ; a negation giving ; an exponential giving ; an addition of one giving ; and a reciprocal giving .
The backward pass walks the same chain in reverse, each step multiplying by one local derivative. The reciprocal has local derivative , so the gradient below it is . Adding a constant has local derivative and changes nothing. The exponential has local derivative equal to its own output, , giving . The negation flips the sign to . From there the two additions distribute unchanged to and to both products, and each multiplication hands each of its inputs the value of the other scaled by : and , and likewise and for the second pair.
Eight nodes, eight local derivatives, no step harder than a first-year exercise. But four of those nodes—negate, exponentiate, add one, reciprocate—exist only to compute the logistic function, and the logistic function differentiates in closed form. Writing ,
which at gives —the same number the four-node chain produced, from one multiplication.
So the decomposition into primitives is a choice, not a fact about the function, and the choice is a real engineering trade. A coarse node is faster, because it replaces four kernel launches with one and four cached intermediate tensors with none. It is often numerically better, because a closed form can avoid the cancellations that its own decomposition runs into. And Equation 5.7 has a property worth noticing on its own: the derivative is expressed in terms of the node’s output, so a sigmoid node need not remember its input at all. Against that, every coarse node is a derivative someone has to derive and test by hand, which is the cost the whole method was introduced to avoid. Frameworks resolve this empirically, by writing fused nodes for the operations that are common enough to be worth the effort and leaving everything else composed from primitives.
5.10 Four patterns worth recognising
Because the local derivatives of the common primitives are so simple, a backward pass can usually be read off a graph rather than computed. Four patterns cover most of what appears in practice.
An addition distributes. Since , the upstream gradient passes to both inputs unchanged: a node adding and to make , receiving an upstream gradient of , sends to each input regardless of their values. Addition nodes do not need to cache anything.
A multiplication swaps. Each input receives the upstream gradient scaled by the other input: a node multiplying and to make , receiving upstream , sends to the first and to the second. The consequence is worth holding onto, because it explains a failure mode. If one input to a multiplication is very large, the gradient on the other is scaled by it, so poorly scaled inputs produce poorly scaled gradients everywhere downstream, which is much of why normalizing inputs matters.
A maximum routes. Only the winning input affected the output, so it receives the whole upstream gradient and the others receive zero: with upstream sends along the branch carrying and along the branch carrying . This is the pattern behind grad_h[h <= 0] = 0 in Section 5.4, and behind the dead units of Section 5.3—a unit that always loses the maximum is a branch the gradient never travels down.
A copy adds. When one value is consumed by several nodes, the graph has a branch, and each consumer sends back its own gradient for that value. They are summed: a value used twice, receiving from one consumer and from the other, has total gradient . This follows from the multivariable chain rule—the value influences the loss along every path, and the influences add—and it is not a special case but the general rule, of which the single-consumer situation is the degenerate instance. Everything downstream depends on it: a weight matrix reused across the timesteps of a recurrent network is a copy node with as many consumers as timesteps, and its gradient is the sum over all of them.
5.11 Forward and backward as an interface
The locality established in Section 5.8 is what turns backpropagation from a procedure into a software architecture. If a node needs only its own inputs and one upstream gradient, then a node can be an object with two methods, and a framework can be a library of such objects plus a traversal:
class Multiply:
def forward(self, x, y):
self.x, self.y = x, y # cache what backward will need
return x * y
def backward(self, grad_z):
grad_x = self.y * grad_z # upstream gradient x local gradient
grad_y = self.x * grad_z
return grad_x, grad_yThe cache is the whole of the contract between the two methods, and deciding what belongs in it is the one design decision each operator makes. A multiplication caches both inputs, because each local gradient is the other one. An addition caches nothing. A sigmoid, by Equation 5.7, caches its output. The choice is not cosmetic at scale: activations saved for the backward pass are usually the largest consumer of memory during training, larger than the parameters, and the reason a model that fits in memory for inference may not fit for training.
Nothing in this depends on the node being scalar, and nothing depends on the graph being known in advance. Both facts matter. PyTorch builds the graph as the forward pass runs, recording each operation and its cached tensors as it goes, so control flow in the host language—a loop whose length depends on the data, a branch taken on a value—is captured for free, and the backward pass traverses whatever was actually executed (Paszke et al., 2019). The general technique is reverse-mode automatic differentiation, and it long predates its use in machine learning (Baydin et al., 2018).
Its production operators are the interface above with the loop pushed into a compiled kernel. PyTorch’s sigmoid backward computes grad * output * (1 - output), using the saved output exactly as Equation 5.7 says it can—the fused node from the previous section, in the library, for the reason given there.
5.12 Vectors, and the Jacobian you do not build
Everything so far has been scalar, and the operations in an actual network are not. Three kinds of derivative appear, and keeping them apart is most of the difficulty.
For a scalar function of a scalar, is one number answering one question: if moves a little, how much does move? For a scalar function of a vector , the answer is one number per component of , assembled into a gradient . For a vector function of a vector, and , every output can respond to every input, so the answer is a matrix—the Jacobian , whose entry is .
One asymmetry runs through the whole subject: the loss is always a scalar, and never anything else. It has to be, because gradient descent needs a single quantity to descend and there is no ordering on vectors to descend with respect to. Everything inside the network is a vector or a matrix or a tensor; the last node collapses all of it to one number. So while intermediate nodes have Jacobians, the object propagated backwards is always the gradient of a scalar with respect to something, and therefore always has exactly the shape of the thing it is a gradient of. A gradient with respect to a activation is itself . This is the single most useful check available when writing a backward pass by hand.
With that convention fixed, the vector chain rule reads
whose shapes are , as required. The Jacobian transpose is the local gradient and is the upstream gradient, so Equation 5.8 is Equation 5.5 with the multiplication promoted to a matrix–vector product.
Written that way it looks ruinous, and taken literally it would be. Consider an elementwise ReLU on a vector of four numbers, giving . Its Jacobian is , but because each output depends only on the input in the same position, every off-diagonal entry is zero, and the diagonal holds where the input was positive and where it was not. Applying Equation 5.8 to an upstream gradient of gives .
That result required no matrix. It required looking at the upstream gradient and zeroing the entries where the input was negative, which is exactly the line of code in Section 5.4. Forming the Jacobian would mean allocating numbers to store meaningful ones—for a realistic , sixteen million entries of which four thousand are non-zero—and then spending a matrix–vector product to apply what is a mask. No implementation does this. Equation 5.8 states what the backward pass computes; it does not describe how. Every operator implements the product of its Jacobian with a vector directly, and the Jacobian itself is never a thing that exists in memory.
5.13 Matrices, and the same trick at scale
The operation that actually dominates a network is the one Section 5.4 wrote as a matrix product: with of shape , of shape , and of shape . Here the gap between the formal statement and the implementation becomes absurd rather than merely wasteful.
Take the shapes from a realistic layer, and . The Jacobian relates every one of the entries of to every one of the entries of , so it has entries, which at four bytes each is gibibytes—for one layer, for one minibatch, for a quantity that exists only as an intermediate step. Nothing about the problem is that large: the input is four megabytes and the answer is four megabytes.
The way through is to write down one entry of the result and read the structure off it. Since , the entry influences only row of , and influences with local derivative . Summing its contributions along every path, as the copy rule of Section 5.10 requires,
and that sum over of an quantity against a quantity is one entry of a matrix product with transposed. The same argument for , where influences column of every row, gives the companion result. Together:
Two matrix products, each about the cost of the forward pass, against the gibibytes that the literal reading demanded. The backward pass of a fully connected layer is the same primitive as its forward pass, which is why training costs roughly three times inference rather than thousands of times.
These are the two lines grad_h = grad_y_pred.dot(w2.T) and grad_w2 = h.T.dot(grad_y_pred) from Section 5.4, now derived rather than asserted. And they suggest a practical shortcut that is worth using and worth distrusting: given the upstream gradient and the cached inputs, there is usually only one arrangement of transposes and products whose shapes come out right, so matching shapes will often find the answer. It is an excellent check. It is not a derivation—it cannot distinguish a correct expression from a wrong one that happens to have the same shape, and it is silent about signs. Use it to catch mistakes, then verify against a numerical gradient.
5.14 What this makes possible
Nothing in the rest of this course adds a new principle to the one in Equation 5.5. Convolutional layers, attention, normalization layers, the residual connection, every loss function and every architecture through to diffusion models and vision–language models: each is a different graph assembled from primitives, each primitive supplying a forward rule and a local derivative, and each trained by the same backward traversal and the same optimizers from the previous chapter. The reason the field could move as fast as it did after 2012 is that changing the model stopped requiring changing the mathematics.
What changes from here is which primitives are worth having. A fully connected layer applied to an image ignores everything about images—that a pixel’s neighbours are informative, that a cat is a cat wherever it appears in the frame—and pays for the omission with a weight matrix of parameters for a thirty-two pixel thumbnail. Building those two facts into the layer itself, rather than hoping the optimizer will discover them, gives the convolution, and with it the architectures that made the difference. That is the next chapter.
References
- D. Rumelhart, G. Hinton and R. Williams, “Learning Representations by Back-Propagating Errors,” Nature, 323, 1986. Paper
- V. Nair and G. Hinton, “Rectified Linear Units Improve Restricted Boltzmann Machines,” ICML, 2010. Paper
- X. Glorot, A. Bordes and Y. Bengio, “Deep Sparse Rectifier Neural Networks,” AISTATS, 2011. Proceedings
- A. Maas, A. Hannun and A. Ng, “Rectifier Nonlinearities Improve Neural Network Acoustic Models,” ICML Workshop on Deep Learning for Audio, Speech and Language Processing, 2013.
- G. Montúfar, R. Pascanu, K. Cho and Y. Bengio, “On the Number of Linear Regions of Deep Neural Networks,” NIPS, 2014. arXiv:1402.1869
- D. Hendrycks and K. Gimpel, “Gaussian Error Linear Units (GELUs),” 2016. arXiv:1606.08415
- A. Baydin, B. Pearlmutter, A. Radul and J. Siskind, “Automatic Differentiation in Machine Learning: a Survey,” Journal of Machine Learning Research, 18, 2018. Paper
- A. Paszke et al., “PyTorch: An Imperative Style, High-Performance Deep Learning Library,” NeurIPS, 2019. arXiv:1912.01703