Why Gradient Descent Matters
Suppose a model's error is a differentiable function of its parameters. Evaluating many random candidates wastes the information in those derivatives: the gradient tells us how a small change in each coordinate affects the objective. Gradient descent uses that local information to move in a direction that initially lowers the objective, then repeats from the new point.
The basic update is $\mathbf x_{k+1}=\mathbf x_k-\eta\nabla f(\mathbf x_k)$, with positive learning rate $\eta$. The gradient points toward steepest local increase in Euclidean geometry, so its negative is a descent direction when the gradient is nonzero. A finite step can still overshoot; the step-size rule and the objective's curvature matter.
This lesson implements deterministic, full-gradient descent on a diagonal quadratic whose gradient, curvature, minimizers, and exact error recurrence are known. That makes every update independently checkable. We then distinguish guarantees for smooth nonconvex, convex, and strongly convex objectives, and cover cases where a small computed gradient or an unchanged floating-point point needs careful interpretation.
Gradient descent minimizes a differentiable objective by repeatedly subtracting a positive step size times its gradient. A suitable step-size rule can turn a local descent direction into guaranteed objective decrease; reaching a global minimizer requires additional assumptions such as convexity, while finite precision and stopping limits must be reported honestly.
Core Concepts and Terminology
For a differentiable scalar objective $f:\mathbb R^d\to\mathbb R$, the gradient is the vector of partial derivatives:
$$\nabla f(\mathbf x)=\left(\frac{\partial f}{\partial x_1},\ldots,\frac{\partial f}{\partial x_d}\right).$$
The directional derivative along $\mathbf d$ is $\nabla f(\mathbf x)^\mathsf T\mathbf d$. Taking $\mathbf d=-\nabla f(\mathbf x)$ gives $-\lVert\nabla f(\mathbf x)\rVert_2^2\lt0$ when the gradient is nonzero. Among unit Euclidean directions, the steepest descent direction is the normalized negative gradient. Ordinary gradient descent uses the unnormalized gradient, so the displacement length is $\eta\lVert\nabla f(\mathbf x)\rVert_2$, not merely $\eta$.
A gradient is $L$-Lipschitz, or the objective is $L$-smooth, when $\lVert\nabla f(\mathbf x)-\nabla f(\mathbf y)\rVert_2\le L\lVert\mathbf x-\mathbf y\rVert_2$. This bounds changes in slope; it does not mean the function values themselves are $L$-Lipschitz. Convexity makes every stationary point a global minimizer; strong convexity adds a positive lower curvature bound $\mu$ and gives a unique minimizer.
The programs minimize
$$f(\mathbf x)=\frac12\sum_{j=1}^d a_j(x_j-c_j)^2,\qquad a_j\ge0.$$
Its gradient coordinate is $a_j(x_j-c_j)$, its Hessian is diagonal with entries $a_j$, and $L=\max_j a_j$. If every weight is positive, $\mu=\min_j a_j\gt0$ and the unique minimizer is $\mathbf c$. A zero weight creates an unconstrained flat direction: that coordinate need not equal its center at a minimizer. If every weight is zero, the objective is constant and every point minimizes it.
For the worked weights $(2,8)$ and center $(1,-1)$, the function is $(x-1)^2+4(y+1)^2$. Its elliptical contours explain why one learning rate interacts differently with the two directions. The displayed finite run ends before reaching $(1,-1)$; a step limit is not a claim of convergence.
| Term | Worked example | Meaning |
|---|---|---|
| Parameter vector | $\mathbf x_0=(5,-5)$ | Point to improve |
| Objective / loss | $f(\mathbf x_0)=80$ | Scalar to minimize |
| Gradient | $\nabla f(\mathbf x_0)=(8,-32)$ | Local derivatives in coordinate order |
| Learning rate | $\eta=1/8$ | Multiplier used before subtracting the gradient |
| Displacement | $\eta\nabla f=(1,-4)$ | Subtracted vector; the actual movement is its negative |
| Smoothness constant | $L=8$ | Largest diagonal curvature here |
| Strong convexity | $\mu=2$ | Smallest curvature when all weights are positive |
| Condition number | $\kappa=L/\mu=4$ | Curvature imbalance affecting convergence |
| Gradient residual | $\lVert\nabla f\rVert_\infty=\max_j\lvert g_j\rvert$ | Computed stopping quantity, distinct from objective gap |
| Evaluation pair | One joint objective-and-gradient computation | Unit charged to the program's evaluation budget |
| Numerical stall | Proposed vector equals the current vector | Rounding prevented any coordinate change; not a success certificate |
How Gradient Descent Works, Step by Step
The inputs provide a starting vector, center, nonnegative diagonal weights, fixed learning rate, maximum updates $K$, gradient tolerance $\tau$, and budget $B$ of joint objective/gradient evaluation pairs. The solver is unconstrained: numeric input-range checks are not a feasible box and no position projection is performed.
- Validate the family and the step. Check vector shapes, finite data, nonnegative weights, and control ranges. Compute $L=\max_j a_j$. For $L\gt0$, require $0\lt\eta\le1/L$; for $L=0$, the objective is constant and no curvature-based restriction is needed beyond the positive finite rate limit. The implementation compares the rate with the binary64 value of $1/L$.
- Initialize one coherent state. Copy the data. If $B=0$, return
EVAL_LIMITwith no evaluated state. Otherwise evaluate $f$ and its full gradient at the start, count one pair, compute the gradient infinity norm, and record step $0$. - Check stopping rules in order. First return
GRAD_TOLif $\lVert\nabla f\rVert_\infty\le\tau$. Then check the maximum number of updates (STEP_LIMIT), followed by available evaluation pairs (EVAL_LIMIT). This precedence is deliberate: an already-small gradient is reported even with a zero-update request, provided initialization was evaluated. - Propose a simultaneous update. For every coordinate, compute $s_j=\eta g_j$ and then $y_j=x_j-s_j$. Use the same old gradient for the entire vector; do not recompute partial derivatives midway through a coupled objective's update.
- Detect a representational stall. If every proposed value compares equal to its old value, return
STALLEDwithout another evaluation. This can occur for a very small rate even with a large nonzero gradient. - Evaluate the new point. Commit the candidate, recompute objective and full gradient together, increment both pair count and update count, record the new coherent state, and repeat.
The algorithm uses a fixed validated rate rather than testing multiple candidates or running line search. It does not clip gradients, normalize the direction, project into constraints, or replace the analytic gradient with a finite-difference approximation. Those are different policies, discussed later.
flowchart TD
A["Validate quadratic data and learning rate"] --> B{"Evaluation budget is zero?"}
B -->|Yes| Z["Return no evaluated state"]
B -->|No| C["Evaluate objective and gradient; record start"]
C --> D{"Gradient infinity norm within tolerance?"}
D -->|Yes| T["Return GRAD_TOL"]
D -->|No| E{"Update limit reached?"}
E -->|Yes| U["Return STEP_LIMIT"]
E -->|No| F{"No evaluation pair remains?"}
F -->|Yes| V["Return EVAL_LIMIT"]
F -->|No| G["Form all coordinates from the old gradient"]
G --> H{"Every coordinate unchanged?"}
H -->|Yes| W["Return STALLED"]
H -->|No| I["Evaluate candidate, count update, record state"]
I --> D
Worked Example with a Full Trace
Use start $(5,-5)$, center $(1,-1)$, weights $(2,8)$, learning rate $\eta=0.125$, update limit $K=4$, tolerance $\tau=0$, and evaluation-pair budget $B=100$. No randomness or seed is needed.
The objective and gradient are
$$f(x,y)=(x-1)^2+4(y+1)^2,\qquad\nabla f(x,y)=(2(x-1),8(y+1)).$$
Initially $f(5,-5)=16+64=80$ and the gradient is $(8,-32)$. The first displacement is $0.125(8,-32)=(1,-4)$, so the new point is $(5,-5)-(1,-4)=(4,-1)$. Recomputing gives objective $9$, gradient $(6,0)$, and infinity norm $6$.
The following table records every evaluated state. Its displacement column is the vector subtracted to produce the next row, not the change that produced the current row.
| k | Point | Objective | Gradient | Gradient infinity norm | Displacement to next row | Evaluation pairs |
|---|---|---|---|---|---|---|
| $0$ | (5,-5) | $80.000000000000$ | (8,-32) | $32$ | (1,-4) | $1$ |
| $1$ | (4,-1) | $9.000000000000$ | (6,0) | $6$ | (0.75,0) | $2$ |
| $2$ | (3.25,-1) | $5.062500000000$ | (4.5,0) | $4.5$ | (0.5625,0) | $3$ |
| $3$ | (2.6875,-1) | $2.847656250000$ | (3.375,0) | $3.375$ | (0.421875,0) | $4$ |
| $4$ | (2.26562,-1) | $1.601806640625$ | (2.53125,0) | $2.53125$ | not taken: limit | $5$ |
For these weights the exact coordinate error recurrences are
$$x_{k+1}-1=(1-2\eta)(x_k-1)=\tfrac34(x_k-1),$$
$$y_{k+1}+1=(1-8\eta)(y_k+1)=0.$$
Thus $x_k-1=4(3/4)^k$ for every $k\ge0$, while $y_k=-1$ for $k\ge1$. After the first step, objective values multiply by $(3/4)^2=9/16$. These equations independently validate the trace without re-running its update loop.
The final point is $(2.265625,-1)$, with exact objective $1.601806640625$ and gradient $(2.53125,0)$. The objective has decreased, but the nonzero gradient remains above tolerance. Status is STEP_LIMIT: four updates consumed five objective/gradient pairs. All values in this particular trace are exactly representable binary fractions; the printed objective is rounded to eight decimal places.
With budget $B=2$, initialization and the first new point fit, so the same run returns EVAL_LIMIT at $(4,-1)$ with objective $9$. With $B=0$, there is no evaluated point. The flat-direction sample has weights $(0,2)$, start $(7,3)$, and center $(0,1)$: one update reaches $(7,1)$ with objective $0$, correctly leaving the irrelevant first coordinate unchanged.
Trace It by Hand
At point $(3.25,-1)$ the gradient is $(4.5,0)$ and the objective is $5.0625$. With learning rate $1/8$, predict the displacement, next point, new gradient, and objective.
Reveal: subtract $(0.5625,0)$ to obtain $(2.6875,-1)$. Its gradient is $(3.375,0)$ and objective is $1.6875^2=2.84765625$. The objective ratio is $9/16$, matching the squared error contraction factor.
Implementation in Python, C++, and Java
The panels are independent complete programs using the same fixed start and deterministic arithmetic. Use Python $3.10+$, C++17, or Java $17+$. Compile C++ with g++ -std=c++17 -O2 -ffp-contract=off main.cpp -o gd and avoid fast-math transformations when comparing numerical decisions. Save the Java panel as Main.java.
The public solver accepts $1\le d\le32$, finite initial and center coordinates of magnitude at most $10^6$, finite weights in $[0,10^6]$, a finite learning rate in $(0,10^6]$, $0\le K\le2000$, finite $0\le\tau\le10^6$, and integer $0\le B\le10^6$. It also enforces the $1/L$ rate ceiling whenever $L\gt0$. Python rejects booleans as numeric data, and update/budget controls must be actual integers. Inputs must be ordinary finite sequences; Java arrays must be non-null.
One evaluate call computes both objective and gradient. For each coordinate it subtracts the center, squares that difference, multiplies by its weight, accumulates the weighted square, and computes the weighted difference for the gradient. It multiplies the final sum by $1/2$. Each update separately multiplies rate by gradient and subtracts the result. These prescribed binary64 operations avoid hidden changes from fused arithmetic or reassociation during cross-language verification.
Result holds status, has_value, the final point, its objective and gradient, the gradient infinity norm, update and evaluation-pair counts, and copied history states. When $B=0$, has_value=false, arrays and history are empty, and zero numeric fields are dummy values. Invalid inputs raise an exception before optimization. Initialization is still charged when $K=0$; after initialization, gradient tolerance takes precedence over update and budget limits. Every committed point has a freshly computed objective/gradient pair, including the final point.
The programs preserve inputs and snapshot arrays. Their numeric ranges and conservative rate keep this quadratic's intermediate values finite. A computed residual can nevertheless underflow to zero or lose small changes through rounding. Java's BigDecimal helper only formats the exact double with eight-place half-even rounding; optimization itself uses double throughout.
The final sample deliberately requests rate $2^{-1074}$ at $x=10^6$ for $f(x)=x^2/2$. The gradient is $10^6$, but the proposed displacement is too small to change the stored coordinate. STALLED returns the evaluated point with one pair consumed and zero completed updates, distinguishing numerical immobility from a small gradient.
This is a verified solver for the stated diagonal quadratic family. Adapting it to a general objective requires a coherent objective/gradient evaluator, finite-result checks, an appropriate smoothness estimate or line search, and revised assumptions and numeric policies. Knowing the center solves this teaching family directly; iterative optimization is used here to expose and verify the method.
Expected standard output, identical in all three languages:
quadratick=0 x=(5.00000000,-5.00000000) f=80.00000000 grad=(8.00000000,-32.00000000) inf=32.00000000 eval=1k=1 x=(4.00000000,-1.00000000) f=9.00000000 grad=(6.00000000,0.00000000) inf=6.00000000 eval=2k=2 x=(3.25000000,-1.00000000) f=5.06250000 grad=(4.50000000,0.00000000) inf=4.50000000 eval=3k=3 x=(2.68750000,-1.00000000) f=2.84765625 grad=(3.37500000,0.00000000) inf=3.37500000 eval=4k=4 x=(2.26562500,-1.00000000) f=1.60180664 grad=(2.53125000,0.00000000) inf=2.53125000 eval=5status=STEP_LIMIT f=1.60180664 steps=4 eval=5budgetk=0 x=(5.00000000,-5.00000000) f=80.00000000 grad=(8.00000000,-32.00000000) inf=32.00000000 eval=1k=1 x=(4.00000000,-1.00000000) f=9.00000000 grad=(6.00000000,0.00000000) inf=6.00000000 eval=2status=EVAL_LIMIT f=9.00000000 steps=1 eval=2zero-budgetstatus=EVAL_LIMIT f=NONE steps=0 eval=0flat-directionk=0 x=(7.00000000,3.00000000) f=4.00000000 grad=(0.00000000,4.00000000) inf=4.00000000 eval=1k=1 x=(7.00000000,1.00000000) f=0.00000000 grad=(0.00000000,0.00000000) inf=0.00000000 eval=2status=GRAD_TOL f=0.00000000 steps=1 eval=2stalledk=0 x=(1000000.00000000) f=500000000000.00000000 grad=(1000000.00000000) inf=1000000.00000000 eval=1status=STALLED f=500000000000.00000000 steps=0 eval=1
Complexity, Correctness, and Invariants
Let $t$ be committed updates. If initialization is evaluated, the exact joint-evaluation count is $t+1$, with $t\le\min(K,B-1)$. A stalled proposal costs vector work but no additional evaluation pair. For the diagonal evaluator, each state, norm calculation, update, and history copy takes $\Theta(d)$ work. Best case is $\Theta(d)$ when initialization already meets tolerance or no update is affordable; worst case for a positive budget is $O((1+\min(K,B-1))d)$. Actual runtime is $\Theta((t+1)d)$. Average stopping time depends on the starting point, curvature, rate, and tolerance, rather than a universal distribution-free average.
With a general joint evaluator costing $C_{f,g}$, the work is $O((t+1)(C_{f,g}+d))$ plus input validation. Live state needs $O(d)$ space, but these programs retain history, using $\Theta((t+1)d)$ auxiliary words. A least-squares gradient for a dense $n\times d$ design matrix costs $\Theta(nd)$, which is a different evaluator cost from this diagonal example.
The state invariant is simple: every recorded point is paired with its objective, full gradient, residual norm, and exact evaluation count. Copied arrays preserve that association, and each candidate uses the entire previous gradient. A successful numerical status means its documented stopping condition holds; STEP_LIMIT, EVAL_LIMIT, and STALLED do not assert global optimality.
The main mathematical guarantee comes from the descent lemma. For an $L$-smooth objective, in exact real arithmetic,
$$f(\mathbf x-\eta\nabla f(\mathbf x))\le f(\mathbf x)-\eta\left(1-\frac{L\eta}{2}\right)\lVert\nabla f(\mathbf x)\rVert_2^2.$$
For $0\lt\eta\le1/L$, this is at most $f(\mathbf x)-(\eta/2)\lVert\nabla f(\mathbf x)\rVert_2^2$. The inequality follows by applying the smoothness upper quadratic bound to displacement $-\eta\nabla f(\mathbf x)$. It establishes decrease without convexity, but supplies no global-minimizer guarantee for a nonconvex objective.
If $f$ is bounded below by $f_{\inf}$, summing $T$ such steps gives
$$\min_{0\le k\lt T}\lVert\nabla f(\mathbf x_k)\rVert_2^2\le\frac{2(f(\mathbf x_0)-f_{\inf})}{\eta T}.$$
This is an approximate-stationarity bound, not proof of convergence to a local minimum: stationary points can be saddles or local maxima. Convexity adds a global objective-gap result. If a minimizer $\mathbf x^\star$ exists and $\eta=1/L$, then for $T\ge1$,
$$f(\mathbf x_T)-f(\mathbf x^\star)\le\frac{L\lVert\mathbf x_0-\mathbf x^\star\rVert_2^2}{2T}.$$
For an additional $\mu$-strong convexity assumption, $0\lt\eta\le1/L$ gives the standard geometric bound
$$f(\mathbf x_T)-f(\mathbf x^\star)\le(1-\mu\eta)^T\bigl(f(\mathbf x_0)-f(\mathbf x^\star)\bigr).$$
At rate $1/L$, the condition number $\kappa=L/\mu$ controls the resulting $O(\kappa\log(1/\varepsilon))$ relative-gap iteration scale. These are exact-arithmetic statements with stated smoothness/convexity assumptions; finite-precision comparisons with a rounded $1/L$ threshold are not exact certificates for an arbitrary real-valued objective.
For the implemented quadratic we can also prove behavior directly. Each coordinate error satisfies $e_{j,k+1}=(1-\eta a_j)e_{j,k}$. With positive weights and $\eta\le1/L$, the factors lie between $0$ and $1$, so errors contract toward the unique center. With zero weights those coordinates remain fixed, while every positive-weight coordinate converges toward its center; the limit is still a global minimizer. If $L=0$, every point is already optimal. In exact arithmetic with $\mu\gt0$, a reliable residual $\lVert\nabla f\rVert_\infty\le\tau$ also gives objective gap at most $d\tau^2/(2\mu)$; no such conclusion follows from an unreliable underflowed gradient alone.
| Method | Work per ordinary iteration | Live auxiliary storage | Assumptions or guarantee |
|---|---|---|---|
| Full gradient descent | $O(C_{f,g}+d)$ | $O(d)$ excluding history | Smoothness and a suitable step give descent; convexity supports global-gap bounds |
| Mini-batch SGD | Depends on batch size and gradient evaluation | $O(d)$ plus batch state | Uses gradient estimates; needs stochastic assumptions and a step policy |
| Newton's method | Hessian construction plus typically $O(d^3)$ dense solve | Typically $O(d^2)$ | Uses curvature; globalization and Hessian handling matter |
| Coordinate descent | One or several coordinate updates; problem dependent | Usually $O(d)$ plus cached state | Updates coordinates rather than the full old-gradient vector |
| Projected gradient | Gradient work plus projection cost | $O(d)$ plus projection state | For constrained sets; the right stopping measure includes feasibility |
| Differential evolution | $O(N(d+C_f))$ for a generation | $O(Nd)$ excluding history | Uses population differences and objective values without gradients |
| Particle swarm optimization | $O(N(d+C_f))$ for a sweep | $O(Nd)$ excluding history | Uses velocities and memories; finite-run global optimality is not generic |
Every committed state contains a point and its freshly computed objective and full gradient. Under exact arithmetic, $L$-smoothness and $0\lt\eta\le1/L$ imply objective decrease by at least $(\eta/2)\lVert\nabla f\rVert_2^2$; convexity and strong convexity determine what that progress says about global optimization, while numerical stalls remain distinct from convergence.
Variants, Pitfalls, and Optimizations
A descent direction is not a safe finite jump. For $f(x)=(x-1)^2$, the gradient is $2(x-1)$ and the error multiplier is $1-2\eta$. Starting away from the optimum, convergence requires $0\lt\eta\lt1$; at $\eta=1$ the error alternates without shrinking, and for $\eta\gt1$ its magnitude grows. The programs deliberately enforce the more conservative $\eta\le1/L=0.5$ for this function. They reject the diverging rate $1.1$ shown in the diagram.
Check gradient sign, scaling, and coordinate consistency. Adding the gradient performs ascent. Omitting a factor such as $1/2$, changing a sum loss to a mean loss, or using inconsistent regularization changes gradient scale and the appropriate learning rate. For coupled objectives, recomputing derivatives after changing one coordinate gives a different scheme from a simultaneous full-gradient update. Test analytic gradients with directional or central differences, choosing perturbations that avoid overwhelming truncation error and cancellation.
Separate stopping meanings. A small step can result from a tiny learning rate, not a small gradient. A small objective change can reflect flatness, scaling, or roundoff. A zero gradient at $(0,0)$ for $f(x,y)=x^2-y^2$ is a saddle, and a zero gradient at $0$ for $f(x)=-x^2$ is a local maximum. The convex quadratic evaluator avoids those nonconvex examples, but a generic gradient-based solver must not label every stationary point a global optimum.
Improve poor conditioning deliberately. When curvature differs greatly across directions, the largest curvature restricts a shared learning rate while small-curvature directions move slowly. Feature scaling, preconditioning, momentum, Nesterov acceleration, and quasi-Newton methods can change that behavior. Their update rules and guarantees are distinct; faster empirical progress is not a universal guarantee for every objective.
Use line search when an appropriate curvature scale is unknown. Backtracking can shrink a proposed rate until an Armijo sufficient-decrease condition is met. It needs an explicitly counted objective-evaluation budget and finite-value checks. The fixed-rate programs here perform no backtracking and reject rates above their known family-specific bound. A local curvature estimate must not silently be treated as a global smoothness guarantee.
Match the method to constraints and differentiability. Projected gradient adds a projection onto a feasible set, while proximal methods handle suitable nonsmooth terms. At constrained optima the ordinary unconstrained gradient may be nonzero, so the same raw gradient stopping rule can be inappropriate. Nonsmooth losses need their own treatment; supplying a subgradient changes both the algorithm and its analysis.
Distinguish batch, stochastic, and adaptive updates. Full gradient descent computes the exact chosen training objective's gradient. SGD and mini-batch methods use sampled estimates; individual losses need not decrease each step. Adaptive optimizers such as Adam maintain additional state and have their own assumptions. Autodifferentiation supplies derivatives of the implemented computation; it does not repair an incorrectly specified objective, data leakage, or inconsistent reduction.
Evaluate finite precision and modeling validity. Multiplication underflow can make a computed gradient zero; subtraction can leave a coordinate unchanged despite a nonzero mathematical update. Overflow, non-finite scores, and changing objective definitions need explicit handling in a general solver. The University of Utah gradient-descent notes provide a primary mathematical reference for quadratic and smooth/convex convergence analysis. Compare any theorem's hypotheses, norm, and step-size policy with the actual implementation before using its conclusion.
Use the negative full gradient with a step-size policy justified for the objective, and keep the loss and derivative scaling consistent. Small motion, a small computed derivative, or an exhausted budget can have different meanings; distinguish numerical stalls and stationary points from assumption-backed global convergence.
Practice Problems and Real-World Use
Start with a quadratic whose gradient and minimizer you can compute independently. Then progress to least squares and logistic regression, where data scaling, matrix operations, and regularization affect the objective and gradient. Keep optimization quality separate from predictive quality: a lower training objective does not by itself imply better performance on unseen data.
For a reproducible study, record the exact objective, gradient, starting point, learning-rate policy, normalization, stopping norm and tolerance, objective/gradient evaluation counts, arithmetic settings, and final residual. Compare known reference solutions when available. An optimization status should communicate the condition actually observed rather than a stronger claim unsupported by the model or numerical evidence.
| Problem or application | Exercise | Main idea tested | Difficulty |
|---|---|---|---|
| Weighted diagonal quadratic | Reproduce the trace and its closed-form errors | Curvature, simultaneous updates, and exact validation | Easy |
| Flat and stalled directions | Try zero weights, tiny rates, and zero budgets | Nonunique minima and honest numerical statuses | Easy |
| Learning-rate laboratory | Derive the multiplier for a one-dimensional quadratic | Contraction, oscillation, divergence, and safe step bounds | Medium |
| Linear least squares | Derive the residual gradient and compare with a linear-algebra reference | Matrix gradients, scaling, rank deficiency, and residuals | Medium |
| Regularized logistic regression | Differentiate the mean loss plus stated regularizer | Stable loss evaluation, derivative scaling, and validation data | Medium |
| Ill-conditioned quadratic | Compare feature scaling and preconditioning | Condition number and slow directions | Hard |
| Smooth nonconvex benchmark | Track gradient residual and compare starting points | Stationarity versus local/global optimality | Hard |
| Gradient-verification harness | Check random directional derivatives and finite-difference scales | Analytic/autodiff correctness and floating-point error | Advanced |