AI Knowledge Tree

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.

⏱ 36 minPrerequisites:Linear maps and matrices

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 ff depends on a vector x∈Rnx \in \mathbb{R}^n, the gradient ∇xf\nabla_x f is a column vector of partial derivatives:

∇xf=∂f∂x=(∂f/∂x1⋮∂f/∂xn)∈Rn.\nabla_x f = \frac{\partial f}{\partial x} = \begin{pmatrix}\partial f/\partial x_1 \\ \vdots \\ \partial f/\partial x_n\end{pmatrix} \in \mathbb{R}^n.

When a vector y∈Rmy \in \mathbb{R}^m depends on x∈Rnx \in \mathbb{R}^n, the Jacobian is the m×nm \times n matrix

J=∂y∂x,Jij=∂yi∂xj.J = \frac{\partial y}{\partial x}, \qquad J_{ij} = \frac{\partial y_i}{\partial x_j}.

Row ii of JJ is the gradient of yiy_i with respect to xx (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: ∂z∂x=∂z∂y∂y∂x\frac{\partial z}{\partial x} = \frac{\partial z}{\partial y}\frac{\partial y}{\partial x}[1].

Gradient of the linear form

Let f(x)=a⊤x=a1x1+⋯+anxnf(x) = a^\top x = a_1 x_1 + \cdots + a_n x_n.

∂f∂xj=aj  ⟹  ∇x(a⊤x)=a.\frac{\partial f}{\partial x_j} = a_j \implies \nabla_x(a^\top x) = a.

Gradient of the quadratic form

Let f(x)=x⊤Axf(x) = x^\top A x with AA symmetric. Write index-by-index:

f=∑i,jaijxixj.f = \sum_{i,j} a_{ij} x_i x_j.

The partial derivative with respect to xkx_k:

∂f∂xk=∑jakjxj+∑iaikxi=(Ax)k+(A⊤x)k=2(Ax)k,\frac{\partial f}{\partial x_k} = \sum_j a_{kj} x_j + \sum_i a_{ik} x_i = (Ax)_k + (A^\top x)_k = 2(Ax)_k,

where the last step uses A=A⊤A = A^\top. So[2]

∇x(x⊤Ax)=2Ax.\nabla_x(x^\top A x) = 2Ax.

Jacobian of the affine map

Let y=Ax+by = Ax + b with A∈Rm×nA \in \mathbb{R}^{m \times n}, b∈Rmb \in \mathbb{R}^m.

yi=∑jaijxj+bi  ⟹  ∂yi∂xj=aij.y_i = \sum_j a_{ij} x_j + b_i \implies \frac{\partial y_i}{\partial x_j} = a_{ij}.

So the Jacobian is ∂y∂x=A\frac{\partial y}{\partial x} = A.

The intuition is direct: differentiating an affine map with respect to its input recovers its linear part.

Chain rule for matrix expressions

If z=h(y)z = h(y) and y=g(x)y = g(x), the chain rule gives

∂z∂x=∂z∂y⋅∂y∂x.\frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \cdot \frac{\partial y}{\partial x}.

For a scalar loss L=ℓ(y)L = \ell(y) and y=Ax+by = Ax + b:

∇xL=(∂y∂x)⊤∇yL=A⊤∇yL.\nabla_x L = \left(\frac{\partial y}{\partial x}\right)^\top \nabla_y L = A^\top \nabla_y L.

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 z=W2σ(W1x)z = W_2 \sigma(W_1 x) with a scalar loss L(z)L(z), where σ\sigma is a nonlinearity (such as tanh⁡\tanh) 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:

h=W1x,a=σ(h) (elementwise),z=W2a,L=L(z).h = W_1 x, \quad a = \sigma(h)\ \text{(elementwise)}, \quad z = W_2 a, \quad L = L(z).

With x∈Rnx \in \mathbb{R}^n, W1∈Rm×nW_1 \in \mathbb{R}^{m \times n}, and W2∈Rk×mW_2 \in \mathbb{R}^{k \times m}, the shapes are h,a∈Rmh, a \in \mathbb{R}^m and z∈Rkz \in \mathbb{R}^k.

Backward pass. Write δu=∇uL\delta_u = \nabla_u L for the gradient of the loss with respect to an intermediate uu. Read δu\delta_u as the upstream gradient at uu: how much the loss changes per unit change in uu, with everything earlier in the forward pass held fixed. Each step takes the δ\delta of one quantity and pushes it one operation backward, using a rule already derived above.

Step 1, start at the output. δz=∇zL\delta_z = \nabla_z L comes from differentiating the loss directly. For a squared error L=12∥z−y∥2L = \tfrac12 \lVert z - y \rVert^2, for example, δz=z−y\delta_z = z - y. Nothing is upstream of the output yet, so this is the seed of the whole sweep.

Step 2, through the linear layer z=W2az = W_2 a. This is the affine map whose Jacobian is W2W_2. The chain-rule result ∇aL=(∂z/∂a)⊤δz\nabla_a L = (\partial z / \partial a)^\top \delta_z gives

δa=W2⊤δz∈Rm.\delta_a = W_2^\top \delta_z \in \mathbb{R}^m.

Step 3, through the nonlinearity a=σ(h)a = \sigma(h). Because σ\sigma acts entrywise, aia_i depends only on hih_i, so the Jacobian ∂a/∂h\partial a / \partial h is diagonal with entries σ′(hi)\sigma'(h_i). Multiplying a vector by a diagonal matrix is the same as scaling it entrywise, which gives

δh=δa⊙σ′(h)∈Rm,\delta_h = \delta_a \odot \sigma'(h) \in \mathbb{R}^m,

where ⊙\odot is the elementwise (Hadamard) product and σ′(h)\sigma'(h) means σ′\sigma' applied to each entry of hh.

Step 4, into the weights h=W1xh = W_1 x. Here the target is the gradient with respect to W1W_1 itself, not its input. Since hi=∑j(W1)ij xjh_i = \sum_j (W_1)_{ij}\, x_j, the weight (W1)ij(W_1)_{ij} enters only through hih_i with coefficient xjx_j, so ∂L/∂(W1)ij=δh,i xj\partial L / \partial (W_1)_{ij} = \delta_{h,i}\, x_j. Collecting all entries (i,j)(i,j) is exactly an outer product:

∇W1L=δh x⊤∈Rm×n.\nabla_{W_1} L = \delta_h\, x^\top \in \mathbb{R}^{m \times n}.

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 (xx, hh, aa) are precisely what each step consumes.

Check it on arrays

python · numpy
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-9

Finite differences confirm the analytic gradients, including the backpropagated ∇W1L\nabla_{W_1} L, to near-float precision.

Exercises

Exercise 1. Compute ∇xf\nabla_x f for f(x)=a⊤x+x⊤Axf(x) = a^\top x + x^\top A x with AA symmetric, using the linear and quadratic gradients derived above.

Solution

Step 1. The gradient is linear, so differentiate each term separately: ∇x(a⊤x+x⊤Ax)=∇x(a⊤x)+∇x(x⊤Ax)\nabla_x(a^\top x + x^\top A x) = \nabla_x(a^\top x) + \nabla_x(x^\top A x).

Step 2. Substitute the two results from the lesson, ∇x(a⊤x)=a\nabla_x(a^\top x) = a and ∇x(x⊤Ax)=2Ax\nabla_x(x^\top A x) = 2Ax (the latter using A=A⊤A = A^\top):

∇xf=a+2Ax.\nabla_x f = a + 2Ax.

Setting this to zero gives the stationary point x=−12A−1ax = -\tfrac12 A^{-1}a when AA is invertible, the standard solution of an unconstrained quadratic.

Exercise 2. For AA not symmetric, derive ∇x(x⊤Ax)\nabla_x(x^\top A x) from the index expansion and show the result is (A+A⊤)x(A + A^\top)x. Confirm it reduces to 2Ax2Ax when AA is symmetric.

Solution

Step 1. Expand f=∑i,jaijxixjf = \sum_{i,j} a_{ij} x_i x_j and differentiate with respect to xkx_k. The variable xkx_k appears in the first index (i=ki = k) and the second (j=kj = k):

∂f∂xk=∑jakjxj+∑iaikxi=(Ax)k+(A⊤x)k.\frac{\partial f}{\partial x_k} = \sum_j a_{kj} x_j + \sum_i a_{ik} x_i = (Ax)_k + (A^\top x)_k.

Step 2. Stack over kk to get the gradient vector:

∇x(x⊤Ax)=Ax+A⊤x=(A+A⊤)x.\nabla_x(x^\top A x) = Ax + A^\top x = (A + A^\top)x.

Step 3. If A=A⊤A = A^\top, then A+A⊤=2AA + A^\top = 2A, recovering 2Ax2Ax. Only the symmetric part of AA ever affects a quadratic form, which is why the symmetry assumption is harmless.

Exercise 3 (code). Verify the chain-rule result ∇xL=A⊤∇yL\nabla_x L = A^\top \nabla_y L for L=12∥y∥2L = \tfrac12\lVert y\rVert^2 with y=Ax+by = Ax + b, by checking the analytic gradient against finite differences.

Solution
python · numpy
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 A⊤A^\top carries the upstream gradient y=∇yLy = \nabla_y L 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. [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. [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. [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.↩