Chapter 0 · Section 0.2 · 70 min

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, ff is differentiable at xx if there is a number f′(x)f'(x) such that

f(x+h)  =  f(x)+f′(x) h+o(h),f(x + h) \;=\; f(x) + f'(x)\,h + o(h),

where o(h)o(h) means an error that shrinks faster than hh itself (its ratio to hh goes to 0). In plain words, near xx, the change in ff is the slope times the change in input, up to an error that becomes negligible for small steps.

Take f(x)=x2f(x) = x^2 at x=3x = 3. Expanding, (3+h)2=9+6h+h2(3 + h)^2 = 9 + 6h + h^2. The linear part is 6h6h, so f′(3)=6f'(3) = 6, and the error is exactly h2h^2, which is tiny compared with hh when hh is small. The cell checks that the error divided by h2h^2 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→Rf: \mathbb{R}^n \to \mathbb{R}, like a loss as a function of a parameter vector. Its derivative at x\mathbf{x} is a linear map from a small step h∈Rn\mathbf{h} \in \mathbb{R}^n to a change in ff, and every linear map from vectors to numbers is a dot product with some fixed vector. That vector is the gradient,

f(x+h)  =  f(x)+∇f(x)⊤h+o(∥h∥),∇f(x)=(∂f∂x1,…,∂f∂xn) ⁣⊤∈Rn.f(\mathbf{x} + \mathbf{h}) \;=\; f(\mathbf{x}) + \nabla f(\mathbf{x})^\top \mathbf{h} + o(\lVert \mathbf{h} \rVert), \qquad \nabla f(\mathbf{x}) = \left(\frac{\partial f}{\partial x_1}, \dots, \frac{\partial f}{\partial x_n}\right)^{\!\top} \in \mathbb{R}^n .

In plain words, the gradient lists how much ff 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 ϵ\epsilon in a unit direction u\mathbf{u} changes ff by about ϵ ∇f⊤u\epsilon\, \nabla f^\top \mathbf{u}. By the Cauchy–Schwarz inequality, ∇f⊤u≤∥∇f∥∥u∥=∥∇f∥\nabla f^\top \mathbf{u} \le \lVert \nabla f \rVert \lVert \mathbf{u} \rVert = \lVert \nabla f \rVert, with equality exactly when u=∇f/∥∇f∥\mathbf{u} = \nabla f / \lVert \nabla f \rVert. So among all directions, the gradient direction increases ff fastest, and −∇f-\nabla 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\mathcal{L} is a scalar loss and W∈Rm×n\mathbf{W} \in \mathbb{R}^{m \times n}, then ∂L/∂W\partial \mathcal{L} / \partial \mathbf{W} is the m×nm \times n matrix of partial derivatives ∂L/∂Wij\partial \mathcal{L} / \partial W_{ij}, and the first-order change is a sum over all entries,

L(W+H)≈L(W)+∑i,j∂L∂WijHij  =  L(W)+⟨∂L∂W,H⟩,\mathcal{L}(\mathbf{W} + \mathbf{H}) \approx \mathcal{L}(\mathbf{W}) + \sum_{i,j} \frac{\partial \mathcal{L}}{\partial W_{ij}} H_{ij} \;=\; \mathcal{L}(\mathbf{W}) + \Big\langle \frac{\partial \mathcal{L}}{\partial \mathbf{W}}, \mathbf{H} \Big\rangle ,

where ⟨A,B⟩=∑ijAijBij=tr⁡(A⊤B)\langle \mathbf{A}, \mathbf{B} \rangle = \sum_{ij} A_{ij} B_{ij} = \operatorname{tr}(\mathbf{A}^\top \mathbf{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\mathbf{W} \leftarrow \mathbf{W} - \eta\, \partial\mathcal{L}/\partial\mathbf{W} well-typed, with η\eta the learning rate.

Jacobians, for functions that return vectors

Now let f:Rn→Rmf: \mathbb{R}^n \to \mathbb{R}^m, like a layer that maps an input vector to an output vector. Its derivative is a linear map from Rn\mathbb{R}^n to Rm\mathbb{R}^m, which is a matrix, the Jacobian Jf(x)∈Rm×n\mathbf{J}_f(\mathbf{x}) \in \mathbb{R}^{m \times n}, with entries

(Jf)ij=∂fi∂xj,f(x+h)≈f(x)+Jf(x) h.\big(\mathbf{J}_f\big)_{ij} = \frac{\partial f_i}{\partial x_j}, \qquad f(\mathbf{x} + \mathbf{h}) \approx f(\mathbf{x}) + \mathbf{J}_f(\mathbf{x})\, \mathbf{h} .

In plain words, row ii of the Jacobian is the gradient of output ii, and multiplying the Jacobian by a small input step predicts the output step.

Two Jacobians come up constantly.

  • A linear map f(x)=Axf(\mathbf{x}) = \mathbf{A}\mathbf{x} has Jf=A\mathbf{J}_f = \mathbf{A} everywhere. A linear function is its own best linear approximation.
  • An elementwise function f(x)=ϕ(x)f(\mathbf{x}) = \phi(\mathbf{x}), applying a scalar function ϕ\phi such as tanh or ReLU to each entry, has a diagonal Jacobian diag⁡(ϕ′(x1),…,ϕ′(xn))\operatorname{diag}(\phi'(x_1), \dots, \phi'(x_n)), because output ii depends only on input ii.

The chain rule is matrix multiplication

Compose two functions, y=f(x)\mathbf{y} = f(\mathbf{x}) with f:Rn→Rmf: \mathbb{R}^n \to \mathbb{R}^m, and z=g(y)\mathbf{z} = g(\mathbf{y}) with g:Rm→Rkg: \mathbb{R}^m \to \mathbb{R}^k. Near x\mathbf{x}, a step h\mathbf{h} moves y\mathbf{y} by about Jfh\mathbf{J}_f \mathbf{h}, and that moves z\mathbf{z} by about Jg(Jfh)\mathbf{J}_g (\mathbf{J}_f \mathbf{h}). The composition of two linear maps is the product of their matrices, so

Jg∘f(x)  =  Jg(f(x))⏟k×m  Jf(x)⏟m×n.\mathbf{J}_{g \circ f}(\mathbf{x}) \;=\; \underbrace{\mathbf{J}_g\big(f(\mathbf{x})\big)}_{k \times m}\; \underbrace{\mathbf{J}_f(\mathbf{x})}_{m \times n} .

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))))\mathcal{L} = \ell(f_L(\cdots f_2(f_1(\mathbf{x})))), ending in a scalar loss. Its gradient with respect to the input is a product of Jacobians, and since the final one (from ℓ\ell) is a 1×m1 \times m row, we can transpose the whole product into column form,

∇xL  =  Jf1⊤Jf2⊤⋯JfL⊤ ∇yLL,\nabla_{\mathbf{x}} \mathcal{L} \;=\; \mathbf{J}_{f_1}^\top \mathbf{J}_{f_2}^\top \cdots \mathbf{J}_{f_L}^\top \, \nabla_{\mathbf{y}_L} \mathcal{L} ,

where yL\mathbf{y}_L is the network's output. Evaluate it from the right. Start with the gradient of the loss, a vector. Multiply by JfL⊤\mathbf{J}_{f_L}^\top to get another vector, then by JfL−1⊤\mathbf{J}_{f_{L-1}}^\top, 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)\mathbf{y} = \phi(\mathbf{x}) and upstream gradient g=∇yL\mathbf{g} = \nabla_{\mathbf{y}} \mathcal{L}, the diagonal Jacobian gives ∇xL=g⊙ϕ′(x)\nabla_{\mathbf{x}}\mathcal{L} = \mathbf{g} \odot \phi'(\mathbf{x}), an elementwise product.

Warm-up

Four gradients every interviewer knows

Implement the gradients with respect to x\mathbf{x} (or X\mathbf{X}) of four scalar functions, each returning an array of the same shape as the variable.

  1. grad_linear(a, x) for f(x)=a⊤xf(\mathbf{x}) = \mathbf{a}^\top \mathbf{x}
  2. grad_quadratic(A, x) for f(x)=x⊤Axf(\mathbf{x}) = \mathbf{x}^\top \mathbf{A} \mathbf{x}, where A\mathbf{A} is square but not necessarily symmetric
  3. grad_least_squares(A, x, b) for f(x)=∥Ax−b∥2f(\mathbf{x}) = \lVert \mathbf{A}\mathbf{x} - \mathbf{b} \rVert^2
  4. grad_trace(A, X, B) for f(X)=tr⁡(AXB)f(\mathbf{X}) = \operatorname{tr}(\mathbf{A}\mathbf{X}\mathbf{B})
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 NN inputs, stacked as rows of X∈RN×din\mathbf{X} \in \mathbb{R}^{N \times d_{\text{in}}}, goes through

Y=XW+b,W∈Rdin×dout,  b∈Rdout,  Y∈RN×dout,\mathbf{Y} = \mathbf{X}\mathbf{W} + \mathbf{b}, \qquad \mathbf{W} \in \mathbb{R}^{d_{\text{in}} \times d_{\text{out}}},\; \mathbf{b} \in \mathbb{R}^{d_{\text{out}}},\; \mathbf{Y} \in \mathbb{R}^{N \times d_{\text{out}}},

where the bias row b\mathbf{b} is broadcast to every row. Some loss L\mathcal{L} is computed from Y\mathbf{Y}, and backpropagation hands us the upstream gradient G=∂L/∂Y∈RN×dout\mathbf{G} = \partial \mathcal{L} / \partial \mathbf{Y} \in \mathbb{R}^{N \times d_{\text{out}}}. We want the three gradients

  ∂L∂W=X⊤G,∂L∂X=GW⊤,∂L∂b=∑n=1NGn,:  \boxed{\;\frac{\partial \mathcal{L}}{\partial \mathbf{W}} = \mathbf{X}^\top \mathbf{G}, \qquad \frac{\partial \mathcal{L}}{\partial \mathbf{X}} = \mathbf{G}\mathbf{W}^\top, \qquad \frac{\partial \mathcal{L}}{\partial \mathbf{b}} = \sum_{n=1}^{N} \mathbf{G}_{n,:}\;}

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)(n, j) of the output is Ynj=∑iXniWij+bjY_{nj} = \sum_i X_{ni} W_{ij} + b_j. The weight WijW_{ij} affects every row nn of column jj, with ∂Ynj/∂Wij=Xni\partial Y_{nj} / \partial W_{ij} = X_{ni}, and nothing else. Summing over every place it appears,

∂L∂Wij=∑n∂L∂Ynj∂Ynj∂Wij=∑nGnjXni=(X⊤G)ij.\frac{\partial \mathcal{L}}{\partial W_{ij}} = \sum_{n} \frac{\partial \mathcal{L}}{\partial Y_{nj}} \frac{\partial Y_{nj}}{\partial W_{ij}} = \sum_n G_{nj} X_{ni} = \big(\mathbf{X}^\top \mathbf{G}\big)_{ij} .

The sum over nn 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. XniX_{ni} affects YnjY_{nj} for every jj, with ∂Ynj/∂Xni=Wij\partial Y_{nj}/\partial X_{ni} = W_{ij}, so ∂L/∂Xni=∑jGnjWij=(GW⊤)ni\partial \mathcal{L} / \partial X_{ni} = \sum_j G_{nj} W_{ij} = (\mathbf{G}\mathbf{W}^\top)_{ni}. And bjb_j adds into YnjY_{nj} for every nn with coefficient 1, so its gradient is ∑nGnj\sum_n G_{nj}.

∂ 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⟩d\mathcal{L} = \langle \mathbf{G}, d\mathbf{Y} \rangle, which is the definition of G\mathbf{G}. Then express dYd\mathbf{Y} through the differentials of the inputs, and rearrange each term into the form ⟨something,dW⟩\langle \text{something}, d\mathbf{W} \rangle. Whatever multiplies dWd\mathbf{W} is the gradient.

Here dY=dX W+X dW+1 db⊤d\mathbf{Y} = d\mathbf{X}\,\mathbf{W} + \mathbf{X}\, d\mathbf{W} + \mathbf{1}\, d\mathbf{b}^\top, where 1∈RN\mathbf{1} \in \mathbb{R}^N is a column of ones. The tool for rearranging is the cyclic property of the trace, tr⁡(ABC)=tr⁡(CAB)\operatorname{tr}(\mathbf{A}\mathbf{B}\mathbf{C}) = \operatorname{tr}(\mathbf{C}\mathbf{A}\mathbf{B}), together with ⟨A,B⟩=tr⁡(A⊤B)\langle \mathbf{A}, \mathbf{B} \rangle = \operatorname{tr}(\mathbf{A}^\top \mathbf{B}).

For the weight, ⟨G,X dW⟩=tr⁡(G⊤X dW)=tr⁡((X⊤G)⊤dW)=⟨X⊤G,dW⟩\langle \mathbf{G}, \mathbf{X}\, d\mathbf{W} \rangle = \operatorname{tr}(\mathbf{G}^\top \mathbf{X}\, d\mathbf{W}) = \operatorname{tr}((\mathbf{X}^\top\mathbf{G})^\top d\mathbf{W}) = \langle \mathbf{X}^\top \mathbf{G}, d\mathbf{W} \rangle.

For the input, ⟨G,dX W⟩=tr⁡(G⊤dX W)=tr⁡(WG⊤dX)=⟨GW⊤,dX⟩\langle \mathbf{G}, d\mathbf{X}\,\mathbf{W} \rangle = \operatorname{tr}(\mathbf{G}^\top d\mathbf{X}\,\mathbf{W}) = \operatorname{tr}(\mathbf{W}\mathbf{G}^\top d\mathbf{X}) = \langle \mathbf{G}\mathbf{W}^\top, d\mathbf{X} \rangle.

For the bias, ⟨G,1 db⊤⟩=1⊤G db=⟨G⊤1,db⟩\langle \mathbf{G}, \mathbf{1}\, d\mathbf{b}^\top \rangle = \mathbf{1}^\top \mathbf{G}\, d\mathbf{b} = \langle \mathbf{G}^\top \mathbf{1}, d\mathbf{b} \rangle, and G⊤1\mathbf{G}^\top\mathbf{1} is the column sums.

Notice what the weight gradient costs. X⊤G\mathbf{X}^\top\mathbf{G} is a matmul of the same size as the forward XW\mathbf{X}\mathbf{W}, and so is GW⊤\mathbf{G}\mathbf{W}^\top. 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×10111.75 \times 10^{11} parameters and was trained on 3×10113 \times 10^{11} tokens, so the rule predicts 6×1.75×1011×3×1011≈3.15×10236 \times 1.75 \times 10^{11} \times 3 \times 10^{11} \approx 3.15 \times 10^{23} floating-point operations, and the paper reports 3.14×10233.14 \times 10^{23} (Brown et al., 2020).

Let's verify all three formulas numerically, using the loss L=∑njGnjYnj\mathcal{L} = \sum_{nj} G_{nj} Y_{nj}, whose gradient with respect to Y\mathbf{Y} is exactly G\mathbf{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\mathbf{Y} = \mathbf{X}\mathbf{W} + \mathbf{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\mathbf{z} \in \mathbb{R}^K into a probability distribution over KK classes,

pi=softmax⁡(z)i=ezi∑k=1Kezk.p_i = \operatorname{softmax}(\mathbf{z})_i = \frac{e^{z_i}}{\sum_{k=1}^{K} e^{z_k}} .

Every output depends on every input, because they share the denominator, so its Jacobian is full. Differentiate pip_i with respect to zjz_j with the quotient rule, writing Z=∑kezkZ = \sum_k e^{z_k} and noting ∂Z/∂zj=ezj\partial Z/\partial z_j = e^{z_j},

∂pi∂zj=δijeziZ−eziezjZ2=piδij−pipj,soJsoftmax=diag⁡(p)−pp⊤,\frac{\partial p_i}{\partial z_j} = \frac{\delta_{ij} e^{z_i} Z - e^{z_i} e^{z_j}}{Z^2} = p_i \delta_{ij} - p_i p_j , \qquad\text{so}\qquad \mathbf{J}_{\text{softmax}} = \operatorname{diag}(\mathbf{p}) - \mathbf{p}\mathbf{p}^\top ,

where δij\delta_{ij} is 1 if i=ji = j and 0 otherwise. In plain words, raising your own logit raises your own probability in proportion to pi(1−pi)p_i(1 - p_i), and raising someone else's logit lowers yours in proportion to pipjp_i p_j.

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\sum_j (p_i\delta_{ij} - p_i p_j) = p_i - p_i \sum_j p_j = 0, so J1=0\mathbf{J}\mathbf{1} = \mathbf{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−1K - 1. And because J\mathbf{J} is symmetric with u⊤Ju=∑ipiui2−(∑ipiui)2\mathbf{u}^\top \mathbf{J} \mathbf{u} = \sum_i p_i u_i^2 - (\sum_i p_i u_i)^2, which is the variance of u\mathbf{u} under p\mathbf{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×KK \times K matrix. For an upstream gradient g=∇pL\mathbf{g} = \nabla_{\mathbf{p}}\mathcal{L},

∇zL=J⊤g=p⊙g−p (p⊤g)=p⊙(g−(p⊤g) 1).\nabla_{\mathbf{z}}\mathcal{L} = \mathbf{J}^\top \mathbf{g} = \mathbf{p} \odot \mathbf{g} - \mathbf{p}\,(\mathbf{p}^\top\mathbf{g}) = \mathbf{p} \odot \big(\mathbf{g} - (\mathbf{p}^\top \mathbf{g})\,\mathbf{1}\big) .

In plain words, subtract the probability-weighted average of the upstream gradient, then scale by the probabilities. This costs O(K)O(K) instead of O(K2)O(K^2), and in attention, where softmax runs over every row of a T×TT \times 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)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 yy, 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\mathbf{y} = \mathbf{e}_y, so

L=−∑k=1Kyklog⁡pk=−log⁡py.\mathcal{L} = -\sum_{k=1}^K y_k \log p_k = -\log p_y .

We could chain the two VJPs. The gradient of L\mathcal{L} with respect to p\mathbf{p} is gk=−yk/pkg_k = -y_k / p_k, and then p⊙(g−(p⊤g)1)\mathbf{p} \odot (\mathbf{g} - (\mathbf{p}^\top\mathbf{g})\mathbf{1}) gives, entry by entry, pj(−yj/pj)−pj∑kpk(−yk/pk)=−yj+pj∑kyk=pj−yjp_j(-y_j/p_j) - p_j \sum_k p_k(-y_k/p_k) = -y_j + p_j \sum_k y_k = p_j - y_j. Everything cancels into one of the most important results in machine learning,

  ∇zL=p−y  \boxed{\;\nabla_{\mathbf{z}} \mathcal{L} = \mathbf{p} - \mathbf{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−py1 - p_y 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⁡ezy∑kezk=−zy+log⁡∑kezk.\mathcal{L} = -\log \frac{e^{z_y}}{\sum_k e^{z_k}} = -z_y + \log \sum_k e^{z_k} .

The derivative of −zy-z_y with respect to zjz_j is −δjy-\delta_{jy}, and the derivative of the log-sum-exp is ezj/∑kezk=pje^{z_j} / \sum_k e^{z_k} = p_j. So ∂L/∂zj=pj−δjy\partial\mathcal{L}/\partial z_j = p_j - \delta_{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][-1, 1], and it is large exactly when the prediction is badly wrong. Compare a sigmoid output trained with the half squared error 12(σ(z)−y)2\tfrac12(\sigma(z) - y)^2, whose gradient (σ(z)−y) σ′(z)(\sigma(z) - y)\,\sigma'(z) carries an extra factor σ′(z)=σ(z)(1−σ(z))\sigma'(z) = \sigma(z)(1 - \sigma(z)). At z=−5z = -5 with target 1, σ(z)=0.0067\sigma(z) = 0.0067, and the cross-entropy gradient is −0.993-0.993 while the squared-error gradient is −0.0066-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\mathbf{y} is one-hot, only that its entries sum to 1. So the same p−y\mathbf{p} - \mathbf{y} holds for the soft targets of label smoothing (§0.5) and knowledge distillation.
  • It averages over a batch. With a mean over NN examples, L=1N∑nLn\mathcal{L} = \frac1N \sum_n \mathcal{L}_n, the gradient on the logit matrix is (P−Y)/N(\mathbf{P} - \mathbf{Y}) / N. Forgetting the 1/N1/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\mathcal{L} = -z_y + \log\sum_k e^{z_k} is right, but computed literally it fails on ordinary logits. In float32, eze^{z} overflows to infinity once zz passes about 88.7, so a logit of 100 makes the sum infinite, the loss infinite, and the softmax ∞/∞\infty/\infty, which is NaN. Very negative logits fail the other way, since every ezke^{z_k} underflows to 0 and the log returns −∞-\infty.

The fix is the shift invariance of the softmax, made exact. Let m=max⁡kzkm = \max_k z_k and factor eme^m out of the sum,

log⁡∑kezk=log⁡(em∑kezk−m)=m+log⁡∑kezk−m.\log\sum_k e^{z_k} = \log\Big(e^{m}\sum_k e^{z_k - m}\Big) = m + \log\sum_k e^{z_k - 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=1e^0 = 1, so the sum is at least 1 and its log is safe. The log-probabilities follow as log⁡pk=zk−(m+log⁡∑jezj−m)\log p_k = z_k - (m + \log\sum_j e^{z_j - m}), and they never pass through a probability that could round to 0.

Python · cell 4

This quantity, log⁡∑kezk\log\sum_k e^{z_k}, is called the log-sum-exp, and §0.4 studies it further (it is a smooth maximum, it handles masked rows that are entirely −∞-\infty, 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⁡\log of a softmax that may have underflowed to 0), and its backward pass is the clean p−y\mathbf{p} - \mathbf{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 10410^4 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(f(x + h) - f(x))/h. Taylor's theorem tells us how accurate it is,

f(x+h)=f(x)+hf′(x)+h22f′′(x)+h36f′′′(x)+⋯⟹f(x+h)−f(x)h=f′(x)+h2f′′(x)+O(h2).f(x + h) = f(x) + h f'(x) + \tfrac{h^2}{2} f''(x) + \tfrac{h^3}{6} f'''(x) + \cdots \quad\Longrightarrow\quad \frac{f(x+h) - f(x)}{h} = f'(x) + \tfrac{h}{2} f''(x) + O(h^2) .

So its error shrinks only in proportion to hh. The central difference does much better. Subtract the expansion of f(x−h)f(x - h), in which the odd powers of hh flip sign. The ff and f′′f'' terms cancel, and

f(x+h)−f(x−h)2h=f′(x)+h26f′′′(x)+O(h4).\frac{f(x+h) - f(x-h)}{2h} = f'(x) + \tfrac{h^2}{6} f'''(x) + O(h^4) .

In plain words, the symmetric difference cancels the curvature term, so its error shrinks like h2h^2. Halve the step and the error drops fourfold.

That suggests using a tiny hh, but floating point fights back. Each evaluation of ff carries a relative rounding error of about εmach\varepsilon_{\text{mach}}, the machine epsilon (2.2×10−162.2 \times 10^{-16} in float64, 1.2×10−71.2 \times 10^{-7} in float32). The numerator subtracts two nearly equal numbers, so its absolute error is about εmach∣f∣\varepsilon_{\text{mach}} |f|, and dividing by hh amplifies it to εmach∣f∣/h\varepsilon_{\text{mach}} |f| / h. For the central difference the total error is roughly

E(h)≈h26∣f′′′∣+εmach∣f∣h,E(h) \approx \frac{h^2}{6} |f'''| + \frac{\varepsilon_{\text{mach}} |f|}{h},

which is smallest near h⋆≈εmach1/3h^\star \approx \varepsilon_{\text{mach}}^{1/3}, about 6×10−66 \times 10^{-6} in float64, giving errors around 10−1110^{-11}. For the forward difference the same argument gives h⋆≈εmach1/2≈1.5×10−8h^\star \approx \varepsilon_{\text{mach}}^{1/2} \approx 1.5 \times 10^{-8} and errors around 10−810^{-8}. Go smaller than h⋆h^\star 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⁡f = \sin at x=1x = 1, the true derivative is cos⁡1\cos 1. With h=10−5h = 10^{-5}, the forward difference is off by 4.2×10−64.2 \times 10^{-6} and the central difference by about 10−1110^{-11}. With h=10−12h = 10^{-12}, both are off by more than 10−510^{-5}, because rounding has taken over. In float32 the central difference is typically off by 10−610^{-6} to 10−510^{-5} even at its best steps, around h≈10−3h \approx 10^{-3} to 10−210^{-2}, and the error jumps around from one hh to the next. That is too crude to catch subtle bugs.

How to read the result

Comparing an analytic gradient a\mathbf{a} with a numerical one n\mathbf{n} needs a relative error, because gradients can be 10−610^{-6} or 10610^{6} in scale. A robust choice is

rel(a,n)=∥a−n∥max⁡(∥a∥,∥n∥,τ),\text{rel}(\mathbf{a}, \mathbf{n}) = \frac{\lVert \mathbf{a} - \mathbf{n} \rVert}{\max(\lVert \mathbf{a} \rVert, \lVert \mathbf{n} \rVert, \tau)} ,

with a tiny floor τ\tau (say 10−1210^{-12}) so two zero gradients do not give 0/00/0. In float64 with central differences, a correct gradient typically gives 10−710^{-7} or less. Around 10−410^{-4} means a subtle bug or a kink nearby, and 10−210^{-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\tau = 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.

🔥 In PyTorchfor a machine with PyTorch

Read more

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.