Contents
, where is a unit vector () and is differentiable at
Deep Breakdown
What it tells me (Situation Awareness): The directional derivative answers "if I move away from this point in this particular direction, how fast does the function change?" Partial derivatives are the special case where the direction is a coordinate axis ( gives ). For a differentiable function, the whole limit collapses to one dot product, , which immediately explains why the gradient is the direction of steepest ascent (), why is steepest descent (), and why moving along a level curve changes nothing (). It also tells me that any direction with a negative dot product with the gradient is a descent direction, which is all that SGD, momentum and Adam need to make progress. In DA/DS: sensitivity analysis of a model output to a combined change of inputs (raise price 3% while raising ad spend 4%), checking whether a proposed step reduces the loss (the Armijo line-search condition uses ), Jacobian-vector products in forward-mode autodiff (which compute exactly without forming the gradient), finite-difference gradient checks along random directions, and explainability methods that perturb an input along a chosen direction.
Flowchart
Real-world use
Where it is used
Line search in gradient-based optimizers (sufficient-decrease test ); forward-mode automatic differentiation (JVPs); gradient checking in deep-learning code; what-if analysis in pricing and marketing-mix models; adversarial-example analysis (how fast does a classifier's score move along a perturbation); response-surface methodology in experimentation.
Problem it solves
Partial derivatives only tell me what happens when one input moves while everything else is frozen. Real decisions move several levers at once, and optimizers move along arbitrary directions. I need the rate of change along any direction, not just along the axes.
How it solves the problem
Linearise near : . The coefficient of is the directional derivative. So once the gradient is known, the rate along every direction is a single dot product — no new limits, no extra function evaluations.
Example in a business
An e-commerce team models weekly revenue (in $k) as $R(p, a) = p^2 + 3papa(1, 2)$. Marketing proposes a joint move in the direction $(3, 4)\mathbf{u} = (0.6, 0.8)$. The gradient is $\nabla R = (2p + 3a,\ 3p) = (8, 3)$, so $D_{\mathbf{u}} R = 8(0.6) + 3(0.8) = 7.2$: about $7.2k of extra revenue per unit of distance travelled in that direction. The steepest possible direction would give , so the proposal captures of the best local improvement.
The formula, piece by piece
- : a scalar function (loss, revenue, score)
- : the current point where I evaluate the rate of change
- : the direction of travel, a unit vector; normalising makes the answer "change per unit distance"
- : a small step length along
- : the rise over run along the line through in direction
- : shrink the step to get the instantaneous rate
- : the gradient, vector of partial derivatives
- : the shortcut valid for differentiable ; equals , with the angle between gradient and direction
- Bounds: (Cauchy–Schwarz)
The formula in words
"The directional derivative is how steeply the function climbs if I walk from this point in direction ; for a smooth function it is just the gradient projected onto that direction." Tiny example: at , direction . Unit direction ; gradient ; .
Critical Concepts
- Normalise the direction: using instead of gives , which is 5 times larger only because . Some texts define the derivative along an unnormalised vector (the "Gateaux" or JVP form); know which convention a library uses.
- Steepest ascent follows: is attained at with value . This is the formal reason gradient descent steps along .
- Descent direction: any with decreases for a small enough step. Momentum, Adam and Newton steps are not along , but they are (usually) descent directions.
- Differentiability matters: the dot-product shortcut needs differentiable. For at the origin, directional derivatives exist in every direction () but are not linear in , so no gradient exists. ReLU networks hit such kinks; frameworks pick a subgradient.
- Second order: the curvature along is (Hessian). Negative values along some direction at a stationary point reveal a saddle or maximum.
- Forward-mode autodiff: one forward pass with a tangent vector returns exactly — cheap when there are few directions, versus reverse mode which returns the whole gradient.
Step-by-step calculation (tiny example)
Compute the rate of change of at in the direction .
Step 1: Normalise the direction , so .
Step 2: Partial derivatives ; . Gradient .
Step 3: Dot product .
Step 4: Compare with the steepest slope ; . The chosen direction is about away from the steepest-ascent direction .
Step 5: Check with a finite difference With : .
Code examples
Beginner example (Python)
import numpy as np
def f(p):
x, y = p
return x**2 + 3 * x * y
def grad_f(p):
x, y = p
return np.array([2 * x + 3 * y, 3 * x])
x0 = np.array([1.0, 2.0])
d = np.array([3.0, 4.0])
u = d / np.linalg.norm(d)
analytic = grad_f(x0) @ u
h = 1e-6
numeric = (f(x0 + h * u) - f(x0)) / h
print(f"unit direction u = {u}") # [0.6 0.8]
print(f"gradient = {grad_f(x0)}") # [8. 3.]
print(f"D_u f (analytic) = {analytic:.4f}") # 7.2000
print(f"D_u f (finite diff)= {numeric:.4f}") # 7.2000
print(f"max slope ||grad|| = {np.linalg.norm(grad_f(x0)):.4f}") # 8.5440
angle = np.degrees(np.arccos(analytic / np.linalg.norm(grad_f(x0))))
print(f"angle to steepest ascent = {angle:.1f} deg") # 32.6 degBeginner example (R)
f <- function(p) p[1]^2 + 3 * p[1] * p[2]
grad_f <- function(p) c(2 * p[1] + 3 * p[2], 3 * p[1])
x0 <- c(1, 2)
d <- c(3, 4)
u <- d / sqrt(sum(d^2))
analytic <- sum(grad_f(x0) * u)
h <- 1e-6
numeric <- (f(x0 + h * u) - f(x0)) / h
cat(sprintf("D_u f analytic = %.4f, finite difference = %.4f\n", analytic, numeric))
cat(sprintf("steepest slope = %.4f\n", sqrt(sum(grad_f(x0)^2))))Professional example (Python)
import numpy as np
rng = np.random.default_rng(0)
# Logistic-regression loss on synthetic churn data; use directional derivatives
# (a) to gradient-check the analytic gradient along random directions and
# (b) to run an Armijo backtracking line search along a candidate direction.
n, p = 500, 5
X = rng.normal(size=(n, p))
w_true = np.array([1.5, -2.0, 0.5, 0.0, 1.0])
y = (rng.random(n) < 1 / (1 + np.exp(-X @ w_true))).astype(float)
def loss(w):
z = X @ w
return float(np.mean(np.logaddexp(0.0, z) - y * z))
def grad(w):
prob = 1 / (1 + np.exp(-(X @ w)))
return X.T @ (prob - y) / n
w = np.zeros(p)
g = grad(w)
# (a) gradient check: compare grad . u with central differences along 5 random u
h = 1e-5
max_err = 0.0
for _ in range(5):
u = rng.normal(size=p)
u /= np.linalg.norm(u)
fd = (loss(w + h * u) - loss(w - h * u)) / (2 * h)
max_err = max(max_err, abs(fd - g @ u))
print(f"max |finite diff - grad.u| over 5 directions: {max_err:.2e}")
# (b) Armijo backtracking: accept t when f(w + t d) <= f(w) + c t grad.d
d = -g # steepest descent direction
slope = g @ d # directional derivative along d (negative)
print(f"directional derivative along -grad: {slope:.4f} (negative = descent)")
t, c = 1.0, 1e-4
while loss(w + t * d) > loss(w) + c * t * slope:
t *= 0.5
print(f"accepted step t = {t}, loss {loss(w):.4f} -> {loss(w + t * d):.4f}")
# a direction orthogonal to the gradient changes the loss only at second order
v = rng.normal(size=p)
v -= (v @ g) / (g @ g) * g
v /= np.linalg.norm(v)
print(f"directional derivative along a tangent direction: {g @ v:.2e}")Source: domain knowledge