Matrix Calculus: Gradients, Jacobians & Backprop
The chain rule in matrix form — which is precisely what backpropagation is.
On this page
New to derivatives? A derivative just measures "if I nudge this input a tiny bit, how much does the output change, and in which direction?" A partial derivative is the same question asked about one input at a time, while every other input is frozen in place — "if I only nudge x (not y), how does the output move?" That's the entire prerequisite for this lesson; nothing more advanced is assumed.
Beginner: the gradient of a function with respect to a vector is just a vector holding every one of those partial derivatives — one number per input, all collected together. The gradient always points in the direction of steepest increase, which is exactly why gradient descent moves in the opposite direction.
Intermediate: the Jacobian generalizes this to functions that output a whole vector, not just a single number: it's a matrix where row i is the gradient of output i with respect to every input. The Hessian (section 1.16) is the Jacobian of the gradient itself — the matrix of all second derivatives.
Advanced: the chain rule, written in matrix form, says the Jacobian of a composition of functions is the product of their individual Jacobians. This one sentence is, precisely and without exaggeration, what backpropagation is: applying the chain rule layer by layer through a neural network, where "layer by layer" means "Jacobian by Jacobian."
This is exactly the gradient used in the descent animation above and in every derivation of linear/ridge regression's closed-form solution.
Write the quadratic form out entirely in components:
Differentiate with respect to one single coordinate xₖ, using the ordinary product rule on every term that contains xₖ. A term Aᵢⱼxᵢxⱼ contains xₖ either when i=k (contributing Akjxj) or when j=k (contributing Aikxi) — both cases can happen at once only when i=j=k, but the sum below already handles that correctly:
Collecting this across every k gives the full gradient vector, ∇f = Ax + Aᵀx = (A+Aᵀ)x. When A is symmetric (A = Aᵀ, section 1.7's expert note), this simplifies immediately to 2Ax.
Where this is used: setting this gradient to zero and solving is exactly how the closed-form solution to linear and ridge regression (sections 1.6, 1.9) is derived — the "normal equation" is nothing more than this identity, solved for x.
Drag the starting point, then press Descend — each step moves opposite the gradient 2Ax. Notice the zig-zag: that's the cost of a poorly conditioned Hessian.
"Gradient checking" — comparing an analytic gradient formula against a numeric finite-difference approximation — is a standard debugging technique whenever you implement backpropagation by hand.
#include <cstdio>
#include <vector>
using Matrix = std::vector<std::vector<double>>;
using Vector = std::vector<double>;
Matrix A = {{2., 0.}, {0., 5.}};
Vector matvec(const Matrix& A, const Vector& x) {
Vector out(A.size(), 0.0);
for (size_t i = 0; i < A.size(); ++i)
for (size_t j = 0; j < x.size(); ++j) out[i] += A[i][j] * x[j];
return out;
}
double dot(const Vector& a, const Vector& b) {
double s = 0.0;
for (size_t i = 0; i < a.size(); ++i) s += a[i] * b[i];
return s;
}
double f(const Vector& x) {
return dot(x, matvec(A, x));
}
Vector analyticGrad(const Vector& x) {
Vector Ax = matvec(A, x);
for (double& v : Ax) v *= 2.0; // the identity above, since A is symmetric
return Ax;
}
Vector numericGrad(const Vector& x, double eps = 1e-6) {
Vector g(x.size(), 0.0);
for (size_t i = 0; i < x.size(); ++i) {
Vector xPlus = x, xMinus = x;
xPlus[i] += eps;
xMinus[i] -= eps;
g[i] = (f(xPlus) - f(xMinus)) / (2 * eps);
}
return g;
}
int main() {
Vector x = {1.5, -0.7};
Vector ag = analyticGrad(x);
Vector ng = numericGrad(x);
std::printf("analytic: [%.6f, %.6f]\n", ag[0], ag[1]);
std::printf("numeric: [%.6f, %.6f]\n", ng[0], ng[1]); // should match closely
return 0;
}- Every deep learning framework's autograd engine (PyTorch's
autograd, TensorFlow'sGradientTape) is a system for automatically applying the chain rule of Jacobians through an arbitrary computation graph. - Normalizing flows in generative modeling need the Jacobian determinant (section 1.12) of each transformation to correctly track how probability density changes — they're specifically engineered so that Jacobian is triangular and cheap to compute (section 1.35).
- Second-order optimizers (Newton's method, L-BFGS) use the Hessian or an approximation of it to converge faster than plain gradient descent near a solution — in practice via Hessian-vector products (section 1.32) rather than the full matrix.
- Using
∇(xᵀAx) = 2Axwhen A isn't symmetric — the correct general form is(A + Aᵀ)x, which only simplifies to2Axwhen A = Aᵀ. - Forming a full Jacobian matrix explicitly when only a Jacobian-vector product is needed — for a network with millions of parameters, the full Jacobian would be far too large to fit in memory; real autograd never materializes it.
Going deeper
Reverse-mode automatic differentiation (what backpropagation actually is) computes gradients by propagating Jacobian-vector products backward through the computation graph, without ever forming any full Jacobian matrix — this is precisely why it's efficient enough to train networks with billions of parameters.
At the master level: forward-mode automatic differentiation computes the same chain rule in the opposite order (Jacobian-vector products propagated forward) — it's more efficient when a function has few inputs and many outputs, the mirror image of the typical neural-network case (many inputs/parameters, one scalar loss output), which is exactly why reverse-mode dominates deep learning specifically.