Matrix calculus & the chain rule
Fix a layout convention, derive the gradients of the linear, quadratic, and affine forms, and compose them into a matrix-level chain rule.
Neural networks are compositions of differentiable maps. Computing their gradients by hand requires differentiating expressions that involve matrices, not just scalars. The key is to fix a layout convention once, then derive the gradient of each building block systematically.
Layout convention: numerator layout
When a scalar depends on a vector , the gradient is a column vector of partial derivatives:
When a vector depends on , the Jacobian is the matrix
Row of is the gradient of with respect to (transposed). This numerator layout (also called "denominator layout" in some texts; conventions differ) makes the chain rule stack Jacobians in the natural left-to-right multiplication order: [1].
Gradient of the linear form
Let .
Gradient of the quadratic form
Let with symmetric. Write index-by-index:
The partial derivative with respect to :
where the last step uses . So[2]
Jacobian of the affine map
Let with , .
So the Jacobian is .
The intuition is direct: differentiating an affine map with respect to its input recovers its linear part.
Chain rule for matrix expressions
If and , the chain rule gives
For a scalar loss and :
The transpose appears because the gradient is a column vector: the chain rule produces a Jacobian transpose times the upstream gradient.
Backprop as repeated chain rule
Training a network means nudging its weights to lower the loss, and gradient descent needs the gradient of the loss with respect to every weight matrix. Backpropagation is the procedure that produces all of those gradients in a single backward sweep. Instead of re-differentiating the whole network once per weight, it applies the chain rule from the previous section one operation at a time, reusing work as it goes.
Take a two-layer network with a scalar loss , where is a nonlinearity (such as ) applied to each entry. The forward pass computes the output and stores the intermediate quantities; the backward pass walks the same operations in reverse, carrying one gradient backward through each[3].
Forward pass. Computed left to right, saving each intermediate for the backward pass:
With , , and , the shapes are and .
Backward pass. Write for the gradient of the loss with respect to an intermediate . Read as the upstream gradient at : how much the loss changes per unit change in , with everything earlier in the forward pass held fixed. Each step takes the of one quantity and pushes it one operation backward, using a rule already derived above.
Step 1, start at the output. comes from differentiating the loss directly. For a squared error , for example, . Nothing is upstream of the output yet, so this is the seed of the whole sweep.
Step 2, through the linear layer . This is the affine map whose Jacobian is . The chain-rule result gives
Step 3, through the nonlinearity . Because acts entrywise, depends only on , so the Jacobian is diagonal with entries . Multiplying a vector by a diagonal matrix is the same as scaling it entrywise, which gives
where is the elementwise (Hadamard) product and means applied to each entry of .
Step 4, into the weights . Here the target is the gradient with respect to itself, not its input. Since , the weight enters only through with coefficient , so . Collecting all entries is exactly an outer product:
Every step is a Jacobian transpose acting on the gradient handed down from the step above (or, for the weights, an outer product with the saved input). Chaining them from the output backward is backpropagation, and the intermediates saved on the forward pass (, , ) are precisely what each step consumes.
Check it on arrays
import numpy as np
# Verify gradients with finite differences
def finite_diff(f, x, eps=1e-5):
grad = np.zeros_like(x)
for i in range(len(x)):
x_plus = x.copy(); x_plus[i] += eps
x_minus = x.copy(); x_minus[i] -= eps
grad[i] = (f(x_plus) - f(x_minus)) / (2 * eps)
return grad
x = np.array([1.0, 2.0, 3.0])
A = np.array([[2.0, 1.0, 0.0],
[1.0, 3.0, 1.0],
[0.0, 1.0, 4.0]]) # symmetric
# Quadratic form: f(x) = x^T A x, grad = 2Ax
f_quad = lambda x: x @ A @ x
grad_analytic = 2 * A @ x
grad_fd = finite_diff(f_quad, x)
print("quadratic form gradient error:", np.max(np.abs(grad_analytic - grad_fd))) # ~1e-9
# Affine map composed with squared L2: L = ||Ax - b||^2, grad = 2 A^T (Ax - b)
b = np.array([1.0, 0.0, -1.0])
f_ls = lambda x: np.sum((A @ x - b)**2)
grad_ls_analytic = 2 * A.T @ (A @ x - b)
grad_ls_fd = finite_diff(f_ls, x)
print("least-squares gradient error:", np.max(np.abs(grad_ls_analytic - grad_ls_fd))) # ~1e-9
# Backprop: two-layer net z = W2 @ tanh(W1 @ x), squared-error loss L = 0.5 * ||z - y||^2
rng = np.random.default_rng(0)
W1 = rng.standard_normal((4, 3)) # m x n
W2 = rng.standard_normal((2, 4)) # k x m
y = np.array([0.5, -0.5])
def loss(W1, W2):
z = W2 @ np.tanh(W1 @ x)
return 0.5 * np.sum((z - y)**2)
# forward pass, saving intermediates
h = W1 @ x
a = np.tanh(h)
z = W2 @ a
# backward pass (the four steps above)
delta_z = z - y # step 1: dL/dz for squared error
delta_a = W2.T @ delta_z # step 2: through the linear layer
delta_h = delta_a * (1 - a**2) # step 3: tanh'(h) = 1 - tanh(h)^2, applied entrywise
grad_W1 = np.outer(delta_h, x) # step 4: outer product into the weights
# finite-difference check on every entry of W1
grad_W1_fd = np.zeros_like(W1)
for i in range(W1.shape[0]):
for j in range(W1.shape[1]):
Wp = W1.copy(); Wp[i, j] += 1e-5
Wm = W1.copy(); Wm[i, j] -= 1e-5
grad_W1_fd[i, j] = (loss(Wp, W2) - loss(Wm, W2)) / (2e-5)
print("backprop dW1 error:", np.max(np.abs(grad_W1 - grad_W1_fd))) # ~1e-9Finite differences confirm the analytic gradients, including the backpropagated , to near-float precision.
Exercises
Exercise 1. Compute for with symmetric, using the linear and quadratic gradients derived above.
Solution
Step 1. The gradient is linear, so differentiate each term separately: .
Step 2. Substitute the two results from the lesson, and (the latter using ):
Setting this to zero gives the stationary point when is invertible, the standard solution of an unconstrained quadratic.
Exercise 2. For not symmetric, derive from the index expansion and show the result is . Confirm it reduces to when is symmetric.
Solution
Step 1. Expand and differentiate with respect to . The variable appears in the first index () and the second ():
Step 2. Stack over to get the gradient vector:
Step 3. If , then , recovering . Only the symmetric part of ever affects a quadratic form, which is why the symmetry assumption is harmless.
Exercise 3 (code). Verify the chain-rule result for with , by checking the analytic gradient against finite differences.
Solution
import numpy as np
rng = np.random.default_rng(0)
A = rng.normal(size=(5, 3))
b = rng.normal(size=5)
x = rng.normal(size=3)
def L(x):
y = A @ x + b
return 0.5 * y @ y
# Analytic: dL/dy = y, and dL/dx = A^T (dL/dy).
y = A @ x + b
grad_analytic = A.T @ y
# Finite differences over each coordinate of x.
eps = 1e-6
grad_fd = np.zeros_like(x)
for i in range(len(x)):
xp = x.copy(); xp[i] += eps
xm = x.copy(); xm[i] -= eps
grad_fd[i] = (L(xp) - L(xm)) / (2 * eps)
assert np.allclose(grad_analytic, grad_fd, atol=1e-6)
print("max gradient error:", float(np.max(np.abs(grad_analytic - grad_fd))))The Jacobian transpose carries the upstream gradient back to the input, matching finite differences to near-float precision.
The final lesson generalizes this calculus to higher-order arrays (tensors) and the Einstein summation notation that batch computations in deep learning rely on.
References
- [1]Petersen, K. B., & Pedersen, M. S. (2012). The Matrix Cookbook. Technical University of Denmark. A reference for layout conventions and matrix-derivative identities.↩
- [2]Magnus, J. R., & Neudecker, H. (2019). Matrix Differential Calculus with Applications in Statistics and Econometrics (3rd ed.). Wiley. doi:10.1002/9781119541219. A rigorous treatment of vector and matrix differentiation.↩
- [3]Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, ch. 6 (Deep Feedforward Networks). MIT Press. Backpropagation as repeated application of the chain rule.↩