Derivatives, gradients, and the chain rule in matrix form
Training a neural network means nudging millions or billions of numbers so that a loss goes down. To know which way to nudge each number, you need the derivative of the loss with respect to it. Frameworks compute those derivatives for you, so why derive anything by hand?
Three reasons. Interviews at every lab ask you to ("derive the gradient of softmax cross-entropy" is close to universal). Every custom kernel, every fused operation, and every new layer you write at work needs a correct backward pass, and the only way to know it is correct is to understand what it should be. And the gradients themselves carry intuition. The gradient of cross-entropy with respect to the logits is "predicted minus true", and once you see that, you understand why classifiers train the way they do.
All of it rests on one idea, that a derivative is the best linear approximation to a function. This section builds from that idea to the matrix gradients of a linear layer and of softmax cross-entropy, and ends with the tool that checks any gradient you will ever write.
The derivative is the best linear approximation
Zoom in on a smooth curve far enough and it looks like a straight line. The derivative is the slope of that line. Precisely, f is differentiable at x if there is a number f′(x) such that
f(x+h)=f(x)+f′(x)h+o(h),
where o(h) means an error that shrinks faster than h itself (its ratio to h goes to 0). In plain words, near x, the change in f is the slope times the change in input, up to an error that becomes negligible for small steps.
Take f(x)=x2 at x=3. Expanding, (3+h)2=9+6h+h2. The linear part is 6h, so f′(3)=6, and the error is exactly h2, which is tiny compared with h when h is small. The cell checks that the error divided by h2 stays at 1.
Python · cell 1
That framing, "the linear map that best approximates the function nearby", is the one that generalizes. When the input is a vector or a matrix, the derivative is no longer a number. It is a linear map, and the whole of matrix calculus is about writing that map down and composing such maps.
Gradients, for functions that return one number
Let f:Rn→R, like a loss as a function of a parameter vector. Its derivative at x is a linear map from a small step h∈Rn to a change in f, and every linear map from vectors to numbers is a dot product with some fixed vector. That vector is the gradient,
In plain words, the gradient lists how much f changes per unit step along each coordinate, and the change for any small step is the dot product of the gradient with that step.
The gradient points uphill, as steeply as possible. Here is why. A step of length ϵ in a unit direction u changes f by about ϵ∇f⊤u. By the Cauchy–Schwarz inequality, ∇f⊤u≤∥∇f∥∥u∥=∥∇f∥, with equality exactly when u=∇f/∥∇f∥. So among all directions, the gradient direction increases f fastest, and −∇f decreases it fastest. That is the entire justification for gradient descent, which §0.6 develops.
The same idea works when the input is a matrix. If L is a scalar loss and W∈Rm×n, then ∂L/∂W is the m×n matrix of partial derivatives ∂L/∂Wij, and the first-order change is a sum over all entries,
L(W+H)≈L(W)+i,j∑∂Wij∂LHij=L(W)+⟨∂W∂L,H⟩,
where ⟨A,B⟩=∑ijAijBij=tr(A⊤B) is the matrix inner product. We will use one convention throughout the book. The gradient of a scalar with respect to any tensor has the same shape as that tensor. That is what every framework does, and it is what makes the update W←W−η∂L/∂W well-typed, with η the learning rate.
Jacobians, for functions that return vectors
Now let f:Rn→Rm, like a layer that maps an input vector to an output vector. Its derivative is a linear map from Rn to Rm, which is a matrix, the JacobianJf(x)∈Rm×n, with entries
(Jf)ij=∂xj∂fi,f(x+h)≈f(x)+Jf(x)h.
In plain words, row i of the Jacobian is the gradient of output i, and multiplying the Jacobian by a small input step predicts the output step.
Two Jacobians come up constantly.
A linear mapf(x)=Ax has Jf=A everywhere. A linear function is its own best linear approximation.
An elementwise functionf(x)=ϕ(x), applying a scalar function ϕ such as tanh or ReLU to each entry, has a diagonal Jacobian diag(ϕ′(x1),…,ϕ′(xn)), because output i depends only on input i.
The chain rule is matrix multiplication
Compose two functions, y=f(x) with f:Rn→Rm, and z=g(y) with g:Rm→Rk. Near x, a step h moves y by about Jfh, and that moves z by about Jg(Jfh). The composition of two linear maps is the product of their matrices, so
Jg∘f(x)=k×mJg(f(x))m×nJf(x).
In plain words, the derivative of a composition is the product of the derivatives, evaluated at the right points. The shapes chain like dominoes.
A neural network is a long composition, L=ℓ(fL(⋯f2(f1(x)))), ending in a scalar loss. Its gradient with respect to the input is a product of Jacobians, and since the final one (from ℓ) is a 1×m row, we can transpose the whole product into column form,
∇xL=Jf1⊤Jf2⊤⋯JfL⊤∇yLL,
where yL is the network's output. Evaluate it from the right. Start with the gradient of the loss, a vector. Multiply by JfL⊤ to get another vector, then by JfL−1⊤, and so on. At every step you multiply a transposed Jacobian by a vector, which is called a vector-Jacobian product (VJP). You never form a full Jacobian, and you never multiply two Jacobians together. That right-to-left sweep is backpropagation, and §0.3 builds an engine that does it automatically.
For the elementwise case the VJP is especially simple. With y=ϕ(x) and upstream gradient g=∇yL, the diagonal Jacobian gives ∇xL=g⊙ϕ′(x), an elementwise product.
Warm-up
Four gradients every interviewer knows
Implement the gradients with respect to x (or X) of four scalar functions, each returning an array of the same shape as the variable.
grad_linear(a, x) for f(x)=a⊤x
grad_quadratic(A, x) for f(x)=x⊤Ax, where A is square but not necessarily symmetric
Hidden tests, hints, and AI help, from $1.99 a month.
The gradient of a linear layer
This is the most important gradient in deep learning, because nearly all the parameters and nearly all the compute of a Transformer are in linear layers. A batch of N inputs, stacked as rows of X∈RN×din, goes through
Y=XW+b,W∈Rdin×dout,b∈Rdout,Y∈RN×dout,
where the bias row b is broadcast to every row. Some loss L is computed from Y, and backpropagation hands us the upstream gradient G=∂L/∂Y∈RN×dout. We want the three gradients
∂W∂L=X⊤G,∂X∂L=GW⊤,∂b∂L=n=1∑NGn,:
In plain words, the weight gradient correlates inputs with output errors, the input gradient sends the errors back through the transposed weights, and the bias gradient adds up the errors over the batch.
Let's derive the weight gradient in index notation, the most reliable method there is. Entry (n,j) of the output is Ynj=∑iXniWij+bj. The weight Wij affects every row n of column j, with ∂Ynj/∂Wij=Xni, and nothing else. Summing over every place it appears,
The sum over n is the multivariate chain rule. A variable that influences several outputs collects the gradient from all of them. The input gradient works the same way. Xni affects Ynj for every j, with ∂Ynj/∂Xni=Wij, so ∂L/∂Xni=∑jGnjWij=(GW⊤)ni. And bj adds into Ynj for every n with coefficient 1, so its gradient is ∑nGnj.
∂ The same result with differentials, a method that scales to harder cases
For the hard gradients (LayerNorm, attention), index notation gets messy. The differential method is cleaner. Write the first-order change of the loss as dL=⟨G,dY⟩, which is the definition of G. Then express dY through the differentials of the inputs, and rearrange each term into the form ⟨something,dW⟩. Whatever multiplies dW is the gradient.
Here dY=dXW+XdW+1db⊤, where 1∈RN is a column of ones. The tool for rearranging is the cyclic property of the trace, tr(ABC)=tr(CAB), together with ⟨A,B⟩=tr(A⊤B).
For the weight, ⟨G,XdW⟩=tr(G⊤XdW)=tr((X⊤G)⊤dW)=⟨X⊤G,dW⟩.
For the input, ⟨G,dXW⟩=tr(G⊤dXW)=tr(WG⊤dX)=⟨GW⊤,dX⟩.
For the bias, ⟨G,1db⊤⟩=1⊤Gdb=⟨G⊤1,db⟩, and G⊤1 is the column sums.
Notice what the weight gradient costs. X⊤G is a matmul of the same size as the forward XW, and so is GW⊤. So the backward pass of a linear layer costs two forward passes. That is where the rule of thumb "training costs about 6 FLOPs per parameter per token" comes from (2 for the forward multiply-add, 4 for the two backward matmuls), which you will use to budget compute in the scaling-law chapter. It holds up at real scale. GPT-3 has 1.75×1011 parameters and was trained on 3×1011 tokens, so the rule predicts 6×1.75×1011×3×1011≈3.15×1023 floating-point operations, and the paper reports 3.14×1023 (Brown et al., 2020).
Let's verify all three formulas numerically, using the loss L=∑njGnjYnj, whose gradient with respect to Y is exactly G by construction.
Python · cell 2
Core
Backward pass of a linear layer, for any batch shape
Implement linear_backward(X, W, b, dY) returning (dX, dW, db) for Y=XW+b. The input X may have any number of leading axes, for example (N, d_in) or (B, T, d_in) for a batch of sequences, and dY has the matching shape (..., d_out). dW must have shape (d_in, d_out) and db shape (d_out,), summing over all leading axes.
Hidden tests, hints, and AI help, from $1.99 a month.
The softmax Jacobian
Softmax turns a vector of scores (logits) z∈RK into a probability distribution over K classes,
pi=softmax(z)i=∑k=1Kezkezi.
Every output depends on every input, because they share the denominator, so its Jacobian is full. Differentiate pi with respect to zj with the quotient rule, writing Z=∑kezk and noting ∂Z/∂zj=ezj,
where δij is 1 if i=j and 0 otherwise. In plain words, raising your own logit raises your own probability in proportion to pi(1−pi), and raising someone else's logit lowers yours in proportion to pipj.
Three facts about this matrix come up in interviews. It is symmetric. Its rows sum to zero, since ∑j(piδij−pipj)=pi−pi∑jpj=0, so J1=0. That says adding the same constant to every logit changes nothing, the shift invariance we exploit for numerical stability later in this section, and it means the Jacobian has rank at most K−1. And because J is symmetric with u⊤Ju=∑ipiui2−(∑ipiui)2, which is the variance of u under p, it is positive semidefinite. (Rank, null spaces, and positive semidefinite matrices are developed properly in §0.10. Here you only need the definitions just used.)
The VJP never needs the K×K matrix. For an upstream gradient g=∇pL,
∇zL=J⊤g=p⊙g−p(p⊤g)=p⊙(g−(p⊤g)1).
In plain words, subtract the probability-weighted average of the upstream gradient, then scale by the probabilities. This costs O(K) instead of O(K2), and in attention, where softmax runs over every row of a T×T score matrix, that difference is everything. FlashAttention's backward pass is built on exactly this identity (§12.3).
Python · cell 3
Interview
Softmax backward without the Jacobian
Implement softmax_backward(P, G) where each row of P (shape (N, K)) is a softmax output and G is the upstream gradient with respect to P, same shape. Return the gradient with respect to the logits, shape (N, K), row by row, in O(NK) time and memory. Also implement softmax_jacobian(p) for a single probability vector p of shape (K,), returning the (K, K) Jacobian (the tests use it to check you).
Hidden tests, hints, and AI help, from $1.99 a month.
Softmax plus cross-entropy gives p minus y
Now put a loss on top. For a classifier with true class y, the cross-entropy loss is the negative log-probability of the true class (§0.5 explains why this is the loss). Write the target as a one-hot vector y=ey, so
L=−k=1∑Kyklogpk=−logpy.
We could chain the two VJPs. The gradient of L with respect to p is gk=−yk/pk, and then p⊙(g−(p⊤g)1) gives, entry by entry, pj(−yj/pj)−pj∑kpk(−yk/pk)=−yj+pj∑kyk=pj−yj. Everything cancels into one of the most important results in machine learning,
∇zL=p−y
In plain words, the gradient on each logit is the predicted probability minus the true probability. It pushes the true class's logit up by 1−py and every other logit down by its own probability.
There is an even shorter route, which also shows how to compute the loss stably. Substitute the softmax into the loss,
L=−log∑kezkezy=−zy+logk∑ezk.
The derivative of −zy with respect to zj is −δjy, and the derivative of the log-sum-exp is ezj/∑kezk=pj. So ∂L/∂zj=pj−δjy, the same answer in one line. You will meet this result again and again. Word2vec's softmax gradient is the same "expected minus observed" (§1.5), and so is the output layer of every language model.
Several things make this gradient special.
It never saturates. Each entry lies in [−1,1], and it is large exactly when the prediction is badly wrong. Compare a sigmoid output trained with the half squared error 21(σ(z)−y)2, whose gradient (σ(z)−y)σ′(z) carries an extra factor σ′(z)=σ(z)(1−σ(z)). At z=−5 with target 1, σ(z)=0.0067, and the cross-entropy gradient is −0.993 while the squared-error gradient is −0.0066, about 150 times smaller. The model is confidently wrong and squared error barely notices.
It works for any target distribution. The derivation never used that y is one-hot, only that its entries sum to 1. So the same p−y holds for the soft targets of label smoothing (§0.5) and knowledge distillation.
It averages over a batch. With a mean over N examples, L=N1∑nLn, the gradient on the logit matrix is (P−Y)/N. Forgetting the 1/N is a classic bug that silently multiplies your learning rate by the batch size.
Computing it without overflow
The formula L=−zy+log∑kezk is right, but computed literally it fails on ordinary logits. In float32, ez overflows to infinity once z passes about 88.7, so a logit of 100 makes the sum infinite, the loss infinite, and the softmax ∞/∞, which is NaN. Very negative logits fail the other way, since every ezk underflows to 0 and the log returns −∞.
The fix is the shift invariance of the softmax, made exact. Let m=maxkzk and factor em out of the sum,
logk∑ezk=log(emk∑ezk−m)=m+logk∑ezk−m.
In plain words, subtract the largest logit before exponentiating and add it back after the log. Every exponent is now at most 0, so nothing overflows, and the largest term is exactly e0=1, so the sum is at least 1 and its log is safe. The log-probabilities follow as logpk=zk−(m+log∑jezj−m), and they never pass through a probability that could round to 0.
Python · cell 4
This quantity, log∑kezk, is called the log-sum-exp, and §0.4 studies it further (it is a smooth maximum, it handles masked rows that are entirely −∞, and it carries over to probabilities of whole sequences). For now the shift is all you need.
This is why every framework offers a fused "cross-entropy from logits" function and why you should always use it. It computes the loss through the stable log-sum-exp (never through log of a softmax that may have underflowed to 0), and its backward pass is the clean p−y.
Interview
Stable softmax cross-entropy, forward and backward
Implement softmax_cross_entropy(logits, y) where logits has shape (N, K) and y holds integer class labels of shape (N,). Return (loss, dlogits), where loss is the mean cross-entropy over the batch (a Python float) and dlogits is its gradient with respect to logits. It must be numerically stable for logits as large as 104 and must not use any loops.
Hidden tests, hints, and AI help, from $1.99 a month.
Checking any gradient with finite differences
You now have a handful of hand-derived gradients, and you will write many more. How do you know they are right? Compare them with a numerical estimate built straight from the definition of the derivative. This is called gradient checking, and it is the single most valuable debugging tool for anyone writing backward passes.
The obvious estimate is the forward difference, (f(x+h)−f(x))/h. Taylor's theorem tells us how accurate it is,
So its error shrinks only in proportion to h. The central difference does much better. Subtract the expansion of f(x−h), in which the odd powers of h flip sign. The f and f′′ terms cancel, and
2hf(x+h)−f(x−h)=f′(x)+6h2f′′′(x)+O(h4).
In plain words, the symmetric difference cancels the curvature term, so its error shrinks like h2. Halve the step and the error drops fourfold.
That suggests using a tiny h, but floating point fights back. Each evaluation of f carries a relative rounding error of about εmach, the machine epsilon (2.2×10−16 in float64, 1.2×10−7 in float32). The numerator subtracts two nearly equal numbers, so its absolute error is about εmach∣f∣, and dividing by h amplifies it to εmach∣f∣/h. For the central difference the total error is roughly
E(h)≈6h2∣f′′′∣+hεmach∣f∣,
which is smallest near h⋆≈εmach1/3, about 6×10−6 in float64, giving errors around 10−11. For the forward difference the same argument gives h⋆≈εmach1/2≈1.5×10−8 and errors around 10−8. Go smaller than h⋆ and the error grows. The widget shows the characteristic V shape on a log-log plot. Switch to float32 to see why gradient checks must run in float64.
Why does a smaller finite-difference step eventually make the gradient worse?
Error of the forward difference (f(x+h) - f(x))/h and the central difference (f(x+h) - f(x-h))/2h against the true derivative, on log scales. Slide h and switch the precision.
Loading interactive figure…
For f=sin at x=1, the true derivative is cos1. With h=10−5, the forward difference is off by 4.2×10−6 and the central difference by about 10−11. With h=10−12, both are off by more than 10−5, because rounding has taken over. In float32 the central difference is typically off by 10−6 to 10−5 even at its best steps, around h≈10−3 to 10−2, and the error jumps around from one h to the next. That is too crude to catch subtle bugs.
How to read the result
Comparing an analytic gradient a with a numerical one n needs a relative error, because gradients can be 10−6 or 106 in scale. A robust choice is
rel(a,n)=max(∥a∥,∥n∥,τ)∥a−n∥,
with a tiny floor τ (say 10−12) so two zero gradients do not give 0/0. In float64 with central differences, a correct gradient typically gives 10−7 or less. Around 10−4 means a subtle bug or a kink nearby, and 10−2 or worse means the gradient is wrong. (The CS231n notes linked below give similar thresholds.)
Core
A gradient checker you can trust
Implement numerical_grad(f, x, h=1e-5) that returns the central-difference gradient of the scalar function f at the array x (any shape), and rel_error(a, b) as defined above, with τ=10−12. numerical_grad must leave x unchanged when it returns, even though it perturbs entries along the way.
Hidden tests, hints, and AI help, from $1.99 a month.
In PyTorch
PyTorch can compute full Jacobians when you need them (rarely, for small functions), and it ships a gradient checker that does exactly what you just built, in float64.
Goodfellow, Bengio & Courville (2016). Deep Learning. Section 6.5 states back-propagation and the chain rule carefully.
Brown et al. (2020). Language Models are Few-Shot Learners. GPT-3, whose reported training compute matches the 6 FLOPs per parameter per token rule.
Nocedal & Wright (2006). Numerical Optimization. Chapter 8 is the rigorous treatment of finite-difference error and optimal step sizes.
Watch next
🎬 Visualizing the chain rule and product rule | Chapter 4, Essence of calculus · 3Blue1BrownThe chain rule as nudges flowing through a composition, the picture behind backprop.🎬 Backpropagation calculus | Deep Learning Chapter 4 · 3Blue1BrownThe chain rule applied to a tiny network, one partial derivative at a time.🎬 Building makemore Part 4: Becoming a Backprop Ninja · Andrej KarpathyKarpathy derives every tensor gradient of an MLP by hand, including cross-entropy and BatchNorm.