Frontend Labs
⋯

13. Calculus II — Gradients, Partial Derivatives, the Chain Rule

Level: Intermediate You'll use this for: the entire mathematical basis of how neural networks learn (backpropagation, Ch. 21).

Partial derivatives

When a function has multiple inputs, ∂f/∂x measures the rate of change with respect to x, holding every other variable fixed. For f(x, y) = x²y + y³:

∂f/∂x = 2xy          (treat y as a constant)
∂f/∂y = x² + 3y²      (treat x as a constant)

Getting there term by term. f(x,y) = x²y + y³ has two additive terms, so differentiate each separately (sum rule, Ch. 12) and add:

∂f/∂x  (treat y as a fixed number, like "5"):
  d/dx (x²y) = y · d/dx(x²) = y · 2x = 2xy    <- power rule on x, y is just a constant multiplier
  d/dx (y³)  = 0                               <- no x in this term at all, so its rate of change w.r.t. x is 0
  sum: ∂f/∂x = 2xy + 0 = 2xy

∂f/∂y  (treat x as a fixed number):
  d/dy (x²y) = x² · d/dy(y) = x² · 1 = x²      <- x² is just a constant multiplier
  d/dy (y³)  = 3y²                              <- power rule on y
  sum: ∂f/∂y = x² + 3y²

Every partial derivative is an ordinary single-variable derivative in disguise — you just freeze every other variable as if it were a plain number first.

The gradient

The gradient ∇f is the vector of all partial derivatives — it points in the direction of steepest ascent of f. Negate it, and you get the direction of steepest descent — the entire idea behind gradient descent (Ch. 21).

import sympy as sp
x, y = sp.symbols('x y')
f = x**2 * y + y**3
gradient = [sp.diff(f, x), sp.diff(f, y)]
print(gradient)   # [2*x*y, x**2 + 3*y**2]

The chain rule (the single most important rule in ML math)

If y = f(g(x)), then dy/dx = f'(g(x)) · g'(x). In words: "the rate of change of the outer function times the rate of change of the inner function." This is the entire mathematical mechanism behind backpropagation — a neural network is nothing but nested functions (layer₁(layer₂(layer₃(...)))), and computing how the final loss changes with respect to an early-layer weight requires applying the chain rule across every layer in between.

# y = (3x + 1)^2  =>  outer: u^2, inner: u = 3x+1
x = sp.symbols('x')
y = (3*x + 1)**2
print(sp.diff(y, x))          # 6*(3*x + 1)  -- sympy applies chain rule automatically

Worked example: chain rule through two tiny "layers"

layer1: h = w1 * x
layer2: y = w2 * h
loss:   L = (y - target)^2

To update w1, you need ∂L/∂w1, computed by chaining backwards:

∂L/∂w1 = ∂L/∂y · ∂y/∂h · ∂h/∂w1

This is precisely backpropagation — computing derivatives backward through a computation graph, one chain-rule link at a time (fully worked out with numbers in Ch. 21).

The gradient, symbol by symbol

∇f(x) = [ ∂f/∂x₁,  ∂f/∂x₂,  ...,  ∂f/∂xₙ ]
Symbol Say it as Meaning
∇ "nabla" or "del" the gradient operator: "take every partial derivative"
∂ "partial" a curly d that warns you other variables exist and are being held fixed
∂f/∂xᵢ "partial f partial x i" the slope of f if you nudge only xᵢ
∇f(x) "grad f at x" a vector with one entry per input, the same shape as x

Example: for f(x,y) = x²y + y³ at the point (1, 2):

∇f = (2xy, x² + 3y²) = (2·1·2, 1 + 3·4) = (4, 13)

The output is about 3× more sensitive to y than to x here. In ML terms, y's weight would get a 3× bigger update.

Why the gradient points uphill (a two-line proof)

The slope in an arbitrary unit direction u is the directional derivative:

D_u f = ∇f · u = ‖∇f‖ · ‖u‖ · cos θ = ‖∇f‖ · cos θ

(This uses the geometric dot product from Ch. 10, with ‖u‖ = 1.) cos θ is largest (= 1) when θ = 0, that is, when u points exactly along ∇f. So the gradient is the direction of steepest ascent, and its length is that steepest slope. -∇f is steepest descent. Perpendicular to ∇f (cos θ = 0) the slope is zero, which is the direction of a contour line on a map.

Example: at (1, 2) with ∇f = (4, 13), moving in direction u = (0.6, 0.8) gives slope 4·0.6 + 13·0.8 = 12.8. Moving along the gradient itself gives ‖∇f‖ = √(16 + 169) ≈ 13.6, which is steeper, as the proof promised.

The Jacobian: derivatives of vector-valued functions

When a function takes a vector in and gives a vector out, as every neural network layer does, its derivative is a matrix:

       [ ∂y₁/∂x₁   ∂y₁/∂x₂   ...  ∂y₁/∂xₙ ]
J  =   [ ∂y₂/∂x₁   ∂y₂/∂x₂   ...  ∂y₂/∂xₙ ]        (m outputs × n inputs)
       [   ...                             ]
       [ ∂yₘ/∂x₁   ...            ∂yₘ/∂xₙ ]

Row i is the gradient of output i. The multivariable chain rule becomes matrix multiplication of Jacobians:

if  y = f(x)  and  L = g(y),  then   ∂L/∂x = Jᵀ · ∂L/∂y

So backpropagation is a sequence of matrix-vector products, running the forward pass's matrices in reverse, transposed. That's why a backward pass costs about 2× a forward pass and runs on the same GPU matrix units.

Backprop through a linear layer, by hand

For y = W·x (Ch. 10's layer, bias left out for clarity) with loss L, and writing δ = ∂L/∂y (the "error signal" arriving from above), the two gradients you need are:

∂L/∂W = δ · xᵀ          (an outer product: same shape as W)
∂L/∂x = Wᵀ · δ          (sent further back to the previous layer)
Symbol Meaning
δ "delta", how much the loss would change per unit change of each output
δ · xᵀ column × row = a matrix. Entry [i][j] = δᵢ · xⱼ: "error at output i × activity of input j"
Wᵀ · δ the same weights, transposed, carry the blame backwards

Same W and x as Ch. 10, target t = (0, 2), loss L = ½‖y - t‖²:

x = (1, 2)       W = [0.5  -1  ]       y = W·x = (-1.5, 2.5)
                     [2     0.25]

L = ½·((-1.5-0)² + (2.5-2)²) = ½·(2.25 + 0.25) = 1.25
δ = y - t = (-1.5, 0.5)                                    (derivative of ½(y-t)²)

∂L/∂W = δ·xᵀ = [-1.5] · [1  2] = [-1.5  -3.0]
               [ 0.5]            [ 0.5   1.0]

∂L/∂x = Wᵀ·δ = [0.5   2  ] · [-1.5] = [ 0.25 ]
               [-1    0.25]   [ 0.5]   [ 1.625]

The weight connecting input 2 (the larger input, 2) to output 1 (the larger error, -1.5) gets the biggest gradient, -3.0. Learning is assigning blame in proportion to activity × error. That's the mathematical form of Hebb's "neurons that fire together wire together". loss.backward() computes exactly these two products for every layer.

The most elegant gradient in deep learning: softmax + cross-entropy

Classifiers (and every LLM's next-token prediction) turn raw scores z ("logits") into probabilities with softmax, then score them with cross-entropy against the true class c:

pᵢ = e^(zᵢ) / Σⱼ e^(zⱼ)                L = -ln(p_c)
Symbol Meaning
zᵢ logit for class i, any real number
e^(zᵢ) exponentiate to make it positive
Σⱼ e^(zⱼ) normalizer, so the pᵢ sum to 1
p_c the probability the model gave the correct class
-ln(p_c) 0 if p_c = 1, and grows without bound as p_c → 0

Substitute and simplify: L = -z_c + ln Σⱼ e^(zⱼ). Differentiate with respect to zᵢ:

∂L/∂zᵢ = -[i = c] + e^(zᵢ)/Σⱼ e^(zⱼ)  =  pᵢ - yᵢ

where yᵢ is 1 for the true class and 0 otherwise (one-hot). After all those exponentials and logs, the gradient is just "predicted minus actual".

Example: logits z = (2.0, 1.0, 0.1), true class 0:

p = softmax(z) = (0.659, 0.242, 0.099)
L = -ln(0.659) = 0.417
∂L/∂z = p - y = (0.659-1, 0.242, 0.099) = (-0.341, 0.242, 0.099)

Push the correct logit up, push the wrong ones down, each in proportion to how wrong it was. Every token of every LLM's pre-training uses this gradient, trillions of times over.

The Hessian: curvature in many dimensions

The matrix of all second partial derivatives:

H[i][j] = ∂²f / ∂xᵢ ∂xⱼ

It's symmetric (the order of differentiation doesn't matter for smooth f, which is Schwarz's theorem), so by the spectral theorem (Ch. 11) its eigenvalues are real. They give the second-derivative test in n dimensions. At a point where ∇f = 0:

Eigenvalues of H Point is Shape
all > 0 local minimum a bowl
all < 0 local maximum a dome
mixed signs saddle point a horse saddle or mountain pass

Examples:

f(x,y) = x² + xy + y²   ->   H = [2 1]   eigenvalues 1, 3  (Ch. 11!)  ->  minimum at (0,0)
                                 [1 2]
f(x,y) = x² - y²        ->   H = [2  0]  eigenvalues 2, -2            ->  saddle at (0,0)
                                 [0 -2]

Why this matters for deep learning: in a model with a billion parameters, a critical point is a minimum only if all billion eigenvalues are positive. If signs were coin flips, that would be absurdly unlikely, so most points where the gradient vanishes are saddles, not bad local minima (Dauphin et al., 2014). Plain gradient descent slows to a crawl near saddles, and momentum (Ch. 21) helps carry it through. The Hessian's largest eigenvalue also sets the maximum stable learning rate, η < 2/λ_max, which Ch. 21 derives.

Putting it together, the multivariable Taylor expansion (Ch. 12, in n dimensions) is the model behind every optimizer:

f(x + Δ) ≈ f(x)  +  ∇f(x)ᵀ·Δ  +  ½ · Δᵀ·H·Δ
           value     slope         curvature

Gradient descent uses the first two terms. Newton's method and its approximations (L-BFGS, K-FAC, Shampoo) use all three.

Networking in the wild: splitting traffic by marginal delay

Two backends: a fast one (μ₁ = 100 req/s) and a slow one (μ₂ = 50 req/s). Total traffic λ = 90 req/s. Send x to the fast one and 90 - x to the slow one. By Little's Law and the M/M/1 formula (Ch. 25), the average number of requests in the system is:

N(x) = x/(μ₁ - x) + (λ - x)/(μ₂ - (λ - x))         average delay = N / λ

Set the derivative to zero. d/dx [x/(μ - x)] = μ/(μ - x)², so the optimum balances marginal delays, not loads:

μ₁/(μ₁ - x)² = μ₂/(μ₂ - λ + x)²    ->   x* ≈ 64.9 req/s to the fast server
Policy Split (fast / slow) Avg delay
Round-robin (equal) 45 / 45 109 ms, since the slow server is at 90% load
Proportional to capacity 60 / 30 33.3 ms
Gradient-optimal 64.9 / 25.1 31.7 ms

Proportional-to-capacity looks "obviously fair", but the derivative says to overload the fast server slightly, because its queue grows more slowly. This is Gallager's 1977 minimum-delay routing: at the optimum, every path in use has equal marginal delay ∂D/∂xᵢ. It's the same "equalize the derivatives" condition as the fleet-sizing example in Ch. 12, and the same idea behind least-loaded and "power of two choices" load balancing in Envoy and NGINX.

from scipy.optimize import minimize_scalar
mu1, mu2, lam = 100, 50, 90
N = lambda x: x/(mu1-x) + (lam-x)/(mu2-(lam-x))
best = minimize_scalar(N, bounds=(40.01, 89.99), method="bounded")
print(best.x, 1000 * N(best.x) / lam)    # ~64.85 req/s, ~31.7 ms

Real-life engineering ties

  • AI: this chapter is the prerequisite for understanding backpropagation and automatic differentiation (autograd/.backward() in PyTorch) rather than treating it as a magic black box.
  • SWE: understanding sensitivity analysis — "how much does output change if this input changes slightly" — for tuning any system with multiple interacting parameters.
  • Network/Security: less direct, but the same "how sensitive is the output to a small input change" question underlies stability analysis of control loops (Ch. 29) like TCP congestion control and rate limiters.

More real-life examples:

  • Options pricing in finance defines "the Greeks" (Delta, Gamma, Vega) as partial derivatives of an option's price with respect to the underlying stock price, volatility, and time — the same partial-derivative machinery as this chapter, applied to markets instead of models.
  • PyTorch's and TensorFlow's autograd engines build a computation graph of every tensor operation and apply the chain rule automatically, backward through the graph, to compute every gradient needed for training — this chapter is literally what .backward() does under the hood (Ch. 21 walks through it by hand).
  • Distributed-systems performance engineers ask chain-rule-flavored sensitivity questions constantly: "if the auth service gets 10ms slower, how much slower is the end-to-end checkout request" — the answer depends on how many chained calls sit between the two, exactly like nested functions in the chain rule.
  • Chemical and biological process control systems use partial derivatives of a target variable (e.g., reactor temperature) with respect to multiple inputs (flow rate, pressure) to decide which knob to turn — the same gradient idea used to train a neural net (Ch. 21), applied to a physical process.

Common mistake

Forgetting to multiply by the inner derivative — computing only f'(g(x)) and stopping, instead of f'(g(x)) · g'(x). This single dropped term is the most common manual backpropagation error.


Generated from real-life-maths-guide.md · 8 Oct 2026 16:36