Skip to content

Gradients of Vector-Valued Functions

So far our functions output a single number. But many map vectors to vectors — a neural network layer takes a vector in and puts a vector out. The derivative of such a function is the Jacobian: a matrix collecting every partial derivative. Its determinant tells you how much the function stretches volume — the key to change-of-variables in probability.

A real-life example: a robot arm

A robot arm’s tip position (x,y)(x, y) is a function of its joint angles (θ1,θ2)(\theta_1, \theta_2) — a map from R2\mathbb{R}^2 to R2\mathbb{R}^2. The Jacobian relates a small change in joint angles to the resulting movement of the tip. Engineers use it constantly: to convert desired tip velocities into joint velocities, and to detect “singular” arm configurations where the Jacobian loses rank and the arm locks up.

The Jacobian

For a vector-valued function f:RnRm\mathbf{f} : \mathbb{R}^n \to \mathbb{R}^m,

f(x)=[f1(x)fm(x)],\mathbf{f}(\mathbf{x}) = \begin{bmatrix} f_1(\mathbf{x})\\ \vdots \\ f_m(\mathbf{x}) \end{bmatrix},

the derivative is the Jacobian JJ — an m×nm \times n matrix whose (i,j)(i, j) entry is fi/xj\partial f_i / \partial x_j:

J=dfdx=[f1x1f1xnfmx1fmxn]Rm×n.J = \frac{d\mathbf{f}}{d\mathbf{x}} = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n}\\ \vdots & & \vdots\\ \frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n} \end{bmatrix} \in \mathbb{R}^{m \times n}.

Each row is the gradient of one output component. The gradient of a scalar function (m=1m = 1) is just a Jacobian with one row. A tidy special case: for a linear map f(x)=Ax\mathbf{f}(\mathbf{x}) = A\mathbf{x}, the Jacobian is simply AA itself.

The Jacobian determinant magnifies volume

Here’s the geometric payoff. Apply f\mathbf{f} to a tiny square and it becomes a tiny parallelogram; the ratio of their areas is detJ|\det J|. Watch a unit square get transformed — the Jacobian determinant is the factor by which its area grows or shrinks:

sketch Jacobian determinant = area magnification p5.js
A unit square (blue) mapped by a linear f to a parallelogram (amber). The ratio of the areas equals |det J|. As the transformation morphs, the area ratio and |det J| track each other exactly.

When detJ=0\det J = 0 the parallelogram collapses to a line — the map crushes area, and (as for the robot arm) the configuration is singular.

The chain rule as matrix multiplication

For a composition h=gf\mathbf{h} = \mathbf{g} \circ \mathbf{f}, the Jacobian is the product of the Jacobians:

Jh=JgJf.J_{\mathbf{h}} = J_{\mathbf{g}} \, J_{\mathbf{f}}.

Dimensions chain like matrix multiplication (m×km\times k times k×nk\times n). This clean composition is exactly why deep networks — long chains of vector-valued functions — can be differentiated mechanically.

Change of variables

The Jacobian determinant is the “stretch factor” in the change-of-variables formula for integrals and probability densities. When you transform a random variable, its density gets rescaled by detJ|\det J|. This shows up directly in normalizing flows, where the log-det of the Jacobian is the trainable term that keeps probabilities normalized.

diagram Diagram mermaid

NumPy

jacobian.py
import numpy as np
 
# f(x1, x2) = [x1² x2,  x1 + sin(x2)]   ->  J is 2x2
def f(v):
    x1, x2 = v
    return np.array([x1**2 * x2, x1 + np.sin(x2)])
 
def jacobian(f, v, h=1e-6):
    n = len(v); fx = f(v); m = len(fx)
    J = np.zeros((m, n))
    for j in range(n):                 # perturb each input, one at a time
        vp = v.copy(); vp[j] += h
        J[:, j] = (f(vp) - fx) / h
    return J
 
v = np.array([2.0, 0.0])
J = jacobian(f, v)
print("Jacobian at (2, 0):\n", np.round(J, 4))
print("|det J| (area magnification):", round(abs(np.linalg.det(J)), 4))
 
# For a linear map f(x) = A x, the Jacobian IS A
A = np.array([[1.0, 2.0], [3.0, 4.0]])
print("linear map Jacobian == A:", np.allclose(jacobian(lambda v: A @ v, v), A))
jacobian.py
import numpy as np
 
# f(x1, x2) = [x1² x2,  x1 + sin(x2)]   ->  J is 2x2
def f(v):
    x1, x2 = v
    return np.array([x1**2 * x2, x1 + np.sin(x2)])
 
def jacobian(f, v, h=1e-6):
    n = len(v); fx = f(v); m = len(fx)
    J = np.zeros((m, n))
    for j in range(n):                 # perturb each input, one at a time
        vp = v.copy(); vp[j] += h
        J[:, j] = (f(vp) - fx) / h
    return J
 
v = np.array([2.0, 0.0])
J = jacobian(f, v)
print("Jacobian at (2, 0):\n", np.round(J, 4))
print("|det J| (area magnification):", round(abs(np.linalg.det(J)), 4))
 
# For a linear map f(x) = A x, the Jacobian IS A
A = np.array([[1.0, 2.0], [3.0, 4.0]])
print("linear map Jacobian == A:", np.allclose(jacobian(lambda v: A @ v, v), A))
text
Jacobian at (2, 0):
 [[0.     4.    ]
 [1.     1.    ]]
|det J| (area magnification): 4.0
linear map Jacobian == A: True
text
Jacobian at (2, 0):
 [[0.     4.    ]
 [1.     1.    ]]
|det J| (area magnification): 4.0
linear map Jacobian == A: True

Why this matters for ML

  • Backpropagation multiplies layer Jacobians together — the vector-valued chain rule at scale.
  • Normalizing flows rely on the log-determinant of the Jacobian to compute exact likelihoods.
  • Sensitivity analysis uses the Jacobian to see how outputs respond to input perturbations (adversarial robustness, feature attribution).

🧪 Try It Yourself

Exercise 1 – Build a Jacobian

Exercise 2 – Area magnification

Exercise 3 – Chain rule via Jacobian product

Recap

  • The Jacobian is the m×nm\times n matrix of partial derivatives of a vector-valued function; each row is one output’s gradient.
  • For a linear map f(x)=Ax\mathbf{f}(\mathbf{x}) = A\mathbf{x}, the Jacobian is AA.
  • detJ|\det J| is the volume magnification factor — the heart of change-of-variables.
  • Jacobians multiply under composition — the vector chain rule behind backpropagation.

Next: derivatives with respect to matrices, which become tensors — Gradients of Matrices.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did