Contents
Deep Breakdown
What it tells me (Situation Awareness): This is the foundational definition of the derivative — the limit of the difference quotient as the step size . The difference quotient is the slope of the secant line through and . As , this secant line approaches the tangent line at , and its slope approaches the instantaneous rate of change. All differentiation rules (power, product, chain, etc.) are derived from this definition. In DA/DS: numerical differentiation via finite differences directly implements this formula (with small but nonzero ) to compute gradients when analytic derivatives are unavailable; gradient checking uses this to verify backpropagation implementations; the definition clarifies why neural network gradients must be computed carefully (h cannot actually be 0 in floating-point arithmetic).
Flowchart
Real-world use
Where it is used
Gradient checking in deep learning: verify that autograd output matches finite-difference approximation ; numerical Jacobian computation for non-differentiable operations or when analytic gradient is unavailable; deriving all differentiation rules from scratch; understanding where and why a function is non-differentiable (ReLU at 0, at 0, piecewise functions at breakpoints).
Problem it solves
All derivative rules (power, chain, product, etc.) are derived from this definition. Without it, derivatives are just "magic formulas." With it, one can derive any new rule from scratch, verify numerical gradients, and understand the conditions under which a derivative exists.
How it solves the problem
The difference quotient is a well-defined ratio for any finite . Taking the limit as (if it exists) gives the exact instantaneous slope. This is the "from scratch" definition that requires no prior knowledge of differentiation rules.
Example in a business
A DS implements a custom loss function and wants to verify their manually computed gradient. They evaluate the loss at and for each parameter , with , and compare to the analytic gradient. If the relative error is , the gradient implementation is correct. This is standard gradient checking in ML (e.g., Andrew Ng's deep learning course, PyTorch's gradcheck).
The formula, piece by piece
- : the derivative of at ; also written , ,
- : the limit as approaches 0 (from both sides)
- : the function evaluated at a point to the right of
- : the numerator; the change in over the interval
- : the denominator; the width of the interval
- : the difference quotient; the average rate of change over
- One-sided limits: right derivative uses ; left derivative uses ; both must agree
- Central difference (numerical): ; error vs for forward difference
- Differentiable implies continuous: if exists, then is continuous at ; the converse is false
The formula in words
"The derivative is the limit of rise-over-run as the run shrinks to zero. Take a tiny step to the right, measure how much changes, divide by . Let get infinitely small. The limiting ratio is the instantaneous slope — the tangent line slope at ." Tiny trace: , find from first principles. . As : . Matches power rule: .
Critical Concepts
- Why the limit must exist from both sides: if the left-limit () and right-limit () differ, the function has a "corner" at (e.g., at : left derivative , right derivative ). In ML: ReLU is not differentiable at 0; in practice, subgradients (0 or 1) are used.
- Floating-point vs mathematical : in code, cannot truly approach 0. Choose to balance truncation error ( for forward, for central) and round-off error (floating-point precision for float64). Optimal for central differences: for float64.
- Gradient checking protocol: relative error . Accept if , investigate if .
- Jacobian and Hessian from first principles: the Jacobian uses the same definition component-wise. The Hessian uses the second-order divided difference.
torch.autograd.gradcheckimplements these checks automatically. - Non-differentiable points matter in ML: ReLU at 0, absolute value at 0, max/min operations, and piecewise-linear functions all lack derivatives at breakpoints.
Modern ML frameworks handle these with subgradients; understanding first principles clarifies what the framework is actually computing.
Step-by-step calculation (tiny example)
Derive from first principles.
Step 1: Write the difference quotient. .
Step 2: Expand . .
Step 3: Subtract and simplify. .
Step 4: Take the limit. . Result: . Matches power rule.
Code examples
Beginner example (Python)
import numpy as np
def numerical_derivative_forward(f, x: float, h: float = 1e-5) -> float:
"""Forward difference: [f(x+h) - f(x)] / h. O(h) error."""
return (f(x + h) - f(x)) / h
def numerical_derivative_central(f, x: float, h: float = 1e-5) -> float:
"""Central difference: [f(x+h) - f(x-h)] / (2h). O(h^2) error."""
return (f(x + h) - f(x - h)) / (2 * h) # Example functions and their analytic derivatives
test_functions = [ (lambda x: x**2, lambda x: 2*x, "x^2", 2.0), (lambda x: x**3, lambda x: 3*x**2, "x^3", 1.5), (lambda x: np.sin(x), lambda x: np.cos(x), "sin(x)", 0.5), (lambda x: np.exp(x), lambda x: np.exp(x), "exp(x)", 1.0),
]
print(f"{'Function':>10} {'x':>5} {'Analytic':>12} {'Forward':>12} {'Central':>12} {'Fwd err':>10} {'Cen err':>10}")
for f, f_prime, name, x0 in test_functions:
analytic = f_prime(x0)
fwd = numerical_derivative_forward(f, x0)
cen = numerical_derivative_central(f, x0)
print(f"{name:>10} {x0:>5.2f} {analytic:>12.8f} {fwd:>12.8f} {cen:>12.8f} " f"{abs(analytic-fwd):>10.2e} {abs(analytic-cen):>10.2e}") # Show effect of h choice on accuracy
print("\nEffect of h on central difference accuracy for sin(0.5):")
analytic = np.cos(0.5)
print(f"{'h':>12} {'Error':>15}")
for h in [1e-1, 1e-3, 1e-5, 1e-7, 1e-9, 1e-12, 1e-15]:
cen = numerical_derivative_central(np.sin, 0.5, h)
print(f"{h:>12.0e} {abs(analytic - cen):>15.2e}")Beginner example (R)
# First principles: numerical derivative via central difference
numerical_deriv <- function(f, x, h = 1e-5) (f(x + h) - f(x - h)) / (2 * h) # Test on standard functions
functions <- list( list(f = function(x) x^2, f_prime = function(x) 2*x, name = "x^2"), list(f = function(x) x^3, f_prime = function(x) 3*x^2, name = "x^3"), list(f = function(x) sin(x), f_prime = function(x) cos(x), name = "sin(x)"), list(f = function(x) exp(x), f_prime = function(x) exp(x), name = "exp(x)")
) x0 <- 1.5
cat(sprintf("%-10s %-12s %-12s %-12s\n", "Function", "Analytic", "Central", "Error"))
for (fn in functions) { analytic <- fn$f_prime(x0)
central <- numerical_deriv(fn$f, x0)
cat(sprintf("%-10s %-12.8f %-12.8f %-12.2e\n", fn$name, analytic, central, abs(analytic - central)))
}Professional example (Python)
import numpy as np
np.random.seed(42) # -- Gradient checking: verify analytic gradient of logistic regression loss --
# Standard practice before training: compare autograd/analytic gradient
# to numerical gradient computed from first principles (central differences).
def sigmoid(z: np.ndarray) -> np.ndarray:
"""Numerically stable sigmoid."""
return np.where(z >= 0, 1 / (1 + np.exp(-z)), np.exp(z) / (1 + np.exp(z)))
def log_loss(w: np.ndarray, X: np.ndarray, y: np.ndarray) -> float:
"""Logistic regression log-loss."""
p = sigmoid(X @ w)
p_safe = np.clip(p, 1e-10, 1 - 1e-10)
return float(-np.mean(y * np.log(p_safe) + (1 - y) * np.log(1 - p_safe)))
def log_loss_gradient(w: np.ndarray, X: np.ndarray, y: np.ndarray) -> np.ndarray:
"""Analytic gradient: X^T(p-y)/N."""
p = sigmoid(X @ w)
return X.T @ (p - y) / len(y)
def numerical_gradient( loss_fn, w: np.ndarray, h: float = 1e-5 ) -> np.ndarray:
"""Numerical gradient via central differences (first principles). grad_i = [L(w + h*e_i) - L(w - h*e_i)] / (2h). """
grad = np.zeros_like(w)
for i in range(len(w)):
w_plus = w.copy(); w_plus[i] += h
w_minus = w.copy(); w_minus[i] -= h
grad[i] = (loss_fn(w_plus) - loss_fn(w_minus)) / (2 * h)
return grad
def relative_error(g_analytic: np.ndarray, g_numeric: np.ndarray) -> float:
"""Max relative error between two gradient vectors."""
diff = np.abs(g_analytic - g_numeric)
denom = np.maximum(np.abs(g_analytic), np.abs(g_numeric))
denom = np.maximum(denom, 1e-8)
return float(np.max(diff / denom)) # Generate synthetic classification data
N, D = 100, 5
X = np.random.randn(N, D)
w_true = np.random.randn(D) * 0.5
y = (sigmoid(X @ w_true) > 0.5).astype(float)
w_test = np.random.randn(D) * 0.1
import functools
loss_fn = functools.partial(log_loss, X=X, y=y)
g_analytic = log_loss_gradient(w_test, X, y)
g_numeric = numerical_gradient(loss_fn, w_test, h=1e-5)
rel_err = relative_error(g_analytic, g_numeric)
print("Gradient Check: First Principles vs Analytic (logistic regression)")
print(f"\nw_test = {w_test.round(4)}")
print(f"\n{'Param':>8} {'Analytic':>14} {'Numerical':>14} {'Abs err':>12}")
for i in range(D):
print(f"{'w['+str(i)+']':>8} {g_analytic[i]:>14.8f} {g_numeric[i]:>14.8f} {abs(g_analytic[i]-g_numeric[i]):>12.2e}")
print(f"\nMax relative error: {rel_err:.2e}")
status = "PASSED" if rel_err < 1e-4 else "FAILED"
print(f"Gradient check: {status} (threshold: 1e-4)")
print(f"\nKey: numerical gradient uses [L(w+h*e_i) - L(w-h*e_i)] / (2h) -- first principles definition.")Source: 1300 p. 217