There are dozens of optimization algorithms with intimidating names — conjugate gradient, BFGS, Adam, trust region. Underneath, every one of them is doing the same thing: standing somewhere, deciding which way to go, deciding how far to go, and stepping. Everything else is bookkeeping.
You start with a guess. You repeatedly add an increment. You hope to land at the bottom.
\(d_k\) is the direction — which way to go.
\(\alpha_k\) is the step length — how far to go.
Name any method, and all you are naming is a particular way of filling those two slots.
Imagine walking down a foggy hillside. You can feel the slope under your feet, but you can't see the valley. Two questions, every step: which way is downhill? and how big a step do I dare take? Take too small a step and you'll be there all day. Take too big a step and you'll fly over the valley and land higher than you started.
Every optimization algorithm ever written is an answer to those two questions.
Written as code, every method on this page is the same loop with three blanks to fill in. Learn this shape once and each algorithm below becomes a variation you can read at a glance:
x = x0; init HISTORY for k = 0,1,2,...: g = grad(x) # the information you have if |g| < tol: break # flat ground — you're done d = DIRECTION(g, HISTORY) # ── SLOT A : which way a = LENGTH(f, x, d, g) # ── SLOT B : how far x = x + a*d # ── THE MASTER EQUATION HISTORY = UPDATE(...) # ── SLOT C : what to remember
Once you see the skeleton, the interesting question stops being "which algorithm is best" and becomes "who decides the step length — me, or the method?" That single question splits the entire field cleanly in two, and predicts which methods will blow up on which problems before you run a single line of code.
Everything here is unconstrained: minimise \(f(x)\) over all of \(\mathbb{R}^n\), with no side conditions. The target is characterised by \(\nabla f = 0\) — find flat ground. Add constraints and the optimum typically sits on a boundary where \(\nabla f \neq 0\), the condition becomes KKT rather than a vanishing gradient, and you need a different toolkit. That's a separate page — though most constrained methods end up calling the machinery below as their inner solver.
Direction rules vary a lot in appearance and surprisingly little in consequence. The step length \(\alpha_k\) is the real fork in the road — and for any given method you are in exactly one of three situations. You are either forced to compute the step, forced to fix it, or free to choose.
| Method | Must compute the step | Free choice | Must fix |
|---|---|---|---|
| Batch Gradient Descent | ✓ | ||
| SGD | ✓ | ||
| Mini-batch GD | ✓ | ||
| Momentum | ✓ | ||
| Nesterov | ✓ | ||
| Adam | ✓ | ||
| RMSprop | ✓ | ||
| Fletcher–Reeves CG | ✓ | ||
| Polak–Ribière CG | ✓ | ||
| BFGS / L-BFGS | ✓ | ||
| Newton / Trust region | ✓ |
The two outer columns carry no ambiguity — there is no decision to make. But they are determined for different reasons, and the difference matters:
Conjugate gradient is not allowed to use a fixed step. Its construction rests on the previous step having exactly minimised \(f\) along its direction, which forces \(g_{k+1}^\top d_k = 0\). That orthogonality is precisely what makes the next direction conjugate. Feed CG an arbitrary fixed step and the directions stop being conjugate, the convergence theory collapses, and you are left with a worse version of momentum.
Same for BFGS: it needs the Wolfe curvature condition \(s_k^\top y_k > 0\) to keep its Hessian approximation positive-definite. This never changes, in any setting.
SGD and Adam cannot line-search because they never see the true \(f\) — only a noisy estimate from one mini-batch. A line search on a noisy objective minimises the noise realisation, not the function. So the step length has to come from outside the data.
But this one is conditional. Run the same methods on a closed-form function where \(f\) is exact, and the constraint evaporates — they slide straight into the free-choice column. Nothing in the algebra forbids it.
And that is no accident. It is the only family whose direction rule builds no model. Every other family constructs something — conjugacy, a secant approximation, a quadratic model, a sampling distribution — whose validity depends on how far you actually moved. The step length gets absorbed into the method and stops being yours to set.
A naming quirk hides this. In numerical optimization, "steepest descent" means \(d=-\nabla f\) with a line search; in machine learning, "gradient descent" means \(d=-\nabla f\) with a fixed \(\eta\). Same direction rule, different step policy, different name.
When you fix \(\eta\) yourself, there is still a hard limit you must respect. Linearising \(x_{k+1}=x_k-\eta\nabla f(x_k)\) near a minimiser gives \(e_{k+1}=(I-\eta H)e_k\), so the iteration converges only if the spectral radius stays below one:
Above that bound it diverges — guaranteed, every time. The catch is that \(L\) is a global property of a function you only ever see one point at a time, so a fixed \(\eta\) is fundamentally a guess. And nothing warns you: no error, no exception, just silently worse numbers. On a landscape whose curvature varies by two orders of magnitude, no single \(\eta\) is simultaneously stable in the steep parts and useful in the flat ones.
A backtracking line search is nothing more than measuring that bound locally, every step, instead of guessing it globally once. Try a step; if the function didn't actually drop enough, halve it and try again. That's the whole idea, and it's five lines of code.
So the real distinction isn't "line search versus no line search." It's one global guess made before the run versus a fresh local measurement at every point.
Here it is once, so the pseudocode below stays short:
ARMIJO_BACKTRACK(f, x, d, g, alpha0=1.0, c1=1e-4, rho=0.5): alpha = alpha0 while f(x + alpha*d) > f(x) + c1*alpha*(g @ d): # ◄── INNER LOOP alpha = rho * alpha # halve and retry return alpha # Wolfe adds a second test — |grad(x+a*d) @ d| <= c2*|g @ d| — # which is the version CG and BFGS specifically require.
Master equation, then pseudocode, in the same three slots every time. Watch slot B change — and watch the loop count change with it.
A: d = -g # batch GD # v = beta*v - eta*g ; d = v/eta # momentum # s = rho*s + (1-rho)*g**2 # RMSprop / Adam # d = -g/(sqrt(s)+eps) B: a = eta # FIXED — or line-search, your call C: keep v and/or s # O(n) state, or nothing at all
1 LOOP One gradient per iteration, \(O(n)\) memory, and exactly one number you must supply. Two sub-variants worth naming, because they behave very differently:
A: beta = (g @ g) / (g_old @ g_old) # Fletcher-Reeves # beta = (g @ (g-g_old)) / (g_old @ g_old) # Polak-Ribiere beta = max(beta, 0) # PR+ safeguard if k % n == 0: beta = 0 # restart every n iterations d = -g + beta*d_old B: a = WOLFE(f, x, d, g) # COMPUTED — required, not optional C: g_old = g ; d_old = d
2 LOOPS \(O(n)\) memory, and — this is the trade — zero hyperparameters. Nothing to tune, because nothing was left to you.
Both build \(d_k = -g_k + \beta\,d_{k-1}\). The difference is that momentum's \(\beta\) is a number somebody picked (0.9, usually), and no theorem asserts anything about it. CG's \(\beta_k\) is derived, under an assumption about the previous step. That is the whole distinction between using memory and depending on it — and it is exactly why one sits in the free column and the other doesn't.
A: d = -B @ g B: a = WOLFE(f, x, d, g) # COMPUTED — required C: s = a*d ; y = grad(x_new) - g if y @ s > 0: # curvature condition r = 1/(y @ s) B = (I - r*outer(s,y)) @ B @ (I - r*outer(y,s)) + r*outer(s,s)
2 LOOPS
Memory is \(O(n^2)\) — L-BFGS drops that to \(O(mn)\) by storing only the
last \(m\) pairs \((s,y)\) instead of the whole matrix. That is why L-BFGS, not anything in
family 1, is the default workhorse in scipy.optimize.minimize
for deterministic smooth problems.
On a quadratic with exact line search, BFGS and CG generate identical iterates. Different mechanisms — one builds an explicit matrix from gradient differences, the other never forms a matrix at all — arriving at the same place. CG is in effect the matrix-free limit of quasi-Newton.
# ── damped Newton ── A: H = hess(x) d = solve(H + tau*I, -g) # tau > 0 if H not positive-definite B: a = ARMIJO(f, x, d, g) # COMPUTED (a → 1 near the solution) C: — # H rebuilt from scratch each step # ── trust region: the skeleton BREAKS here ── A+B: p = SOLVE_SUBPROBLEM(g, hess(x), Delta) # direction AND length, jointly rho = (f(x) - f(x+p)) / -(g@p + 0.5*p@H@p) x = x + p if rho > 0.1 else x # the step can be REJECTED C: Delta = 0.25*Delta if rho < 0.25 Delta = min(2*Delta, Dmax) if rho > 0.75 and |p| == Delta
2 LOOPS Uses second derivatives, so \(O(n^2)\) memory and an \(O(n^3)\) linear solve per iteration. Pure Newton with \(\alpha=1\) is technically single-loop — but it only converges locally, and from a poor start it diverges. Anything you would actually ship is damped.
Instead of picking a way and then a distance, it fixes a radius and solves for both at once inside that ball. \(\Delta_k\) is not a hyperparameter — it is updated every iteration from the agreement ratio \(\rho_k\) between actual and predicted reduction. And uniquely, it can reject a step entirely: a line search always returns some \(\alpha>0\), but a trust region can say "that model was wrong, shrink the ball, try again."
Line search and trust region are the two globalization strategies in numerical optimization. Neither one fixes the step. Both compute it.
m = x0; sigma = sigma0; C = I; p_sigma = 0; p_c = 0 for k = 1..N: for i = 1..lam: # ◄── INNER LOOP (population, not step size) x[i] = m + sigma * chol(C) @ randn(n) fit[i] = f(x[i]) # no gradient anywhere sort x by fit m_old = m m = sum(w[i]*x[i] for i in 1..mu) # weighted mean of the best mu sigma = sigma * exp((c_s/d_s)*(|p_sigma|/E_norm - 1)) # step size adapts itself C = (1-c1-cmu)*C + c1*outer(p_c,p_c) + cmu*rank_mu_update
2 LOOPS But the inner loop is over a population, not a step size. There is no \(d_k\), no \(\alpha_k\), no single iterate at all — the state is \((m,\sigma,C)\) and what moves is a distribution, not a point. That is the honest signal that global search is a different kind of object, not another row in the same table.
Families 1–4 are all local methods: they follow the slope from wherever they happen to be, and they stop at the first flat spot. On a landscape with many minima, none of them has any mechanism for leaving the basin it started in — no amount of cleverness in \(d_k\) changes that. Family 5 gives up gradients entirely and buys global reach with function evaluations instead.
| Family | Loops | Uses | Memory | Cost per iteration | Step length | Tuning |
|---|---|---|---|---|---|---|
| Gradient | 1 | \(g\) | \(O(n)\) | 1 gradient | fixed, or free | \(\eta\) — required |
| Conjugate gradient | 2 | \(g\) | \(O(n)\) | 1 gradient + line search | computed | none |
| Quasi-Newton | 2 | \(g\) | \(O(n^2)\) / \(O(mn)\) | 1 gradient + search + \(n^2\) | computed | none |
| Newton / trust region | 2 | \(g, H\) | \(O(n^2)\) | gradient + Hessian + \(n^3\) solve | computed | none |
| Global (CMA-ES) | 2 | \(f\) only | \(O(n^2)\) | \(\lambda \approx 4+3\ln n\) evaluations | self-adapting | none |
One loop buys you a hyperparameter you must guess.
Two loops buy you a method that guesses nothing.
And going down the table you pay progressively more per iteration — a gradient, then a gradient plus a search, then curvature, then a whole population — in exchange for needing fewer iterations, or for being able to handle a landscape the rows above cannot.
"Gradient descent versus conjugate gradient" is a common mental model, and it is too small. Nearly every method above is really
and the families are just choices of the matrix \(B_k\), crossed with choices of \(\alpha_k\). Two axes, not one list:
| \(B_k\) | Gives you | What it costs |
|---|---|---|
| \(I\) | Gradient descent | nothing — no curvature model at all |
| \(\mathrm{diag}\!\left(1/\sqrt{s_k}\right)\) | RMSprop, Adam | \(O(n)\) state, estimated from data you already had |
| built recursively, never stored | Conjugate gradient | a mandatory line search |
| \(\approx H^{-1}\), from gradient differences | BFGS, L-BFGS | \(O(n^2)\) memory + a mandatory line search |
| \(H^{-1}\) exactly | Newton | second derivatives + an \(O(n^3)\) solve |
CG wins where the problem is conditioning and exact \(f\) is cheap. Adaptive methods win where the problem is noise and scale. Nothing here wins on multimodality — that needs family 5.
Same \(d_k=-g_k+\beta d_{k-1}\) shape. The gap is that \(\beta\) is assumed in one and derived in the other — the same gap as \(\eta\) versus \(\alpha_k\), one level up.
Dividing by \(\sqrt{s_k}\) buys robustness to gradient scale. It buys precisely nothing against a landscape with many minima — a bounded step simply cannot leave its basin.
How much curvature information can you afford to build? And how much are you willing to pay to measure the step? Every method is one answer to each.
The families differ less in cleverness than in what they can afford to compute.
Solving for the step length means evaluating the objective exactly, many times per iteration. That is free on a closed-form function and impossible on a stochastic objective averaged over millions of samples. The gradient family's fixed constants are not naïveté — they are what remains when you cannot afford to solve for the step.
Which is why any ranking of these methods inverts the moment function evaluations stop being free — and why the entire deep learning field runs on the family with the weakest guarantees.
Start with gradient descent, and start with it fixed. It is the only single-loop method, so the entire algorithm is the master equation and nothing else:
for k in range(N): x -= eta * grad(x)
Write conjugate gradient instead and you need a line-search subroutine, which is more code than the method itself — plus Wolfe conditions, plus bracketing and zoom. You end up debugging the scaffolding rather than seeing the idea.
The thing you switched off is the thing that makes it robust. So the first behaviour you meet will be divergence, and it will look like the method failing when it is actually your \(\eta\). Beginners routinely conclude "gradient descent doesn't work" when what they have really learned is "I picked one number wrong."
Start with \(\eta\) deliberately small, confirm it descends, then raise it until it blows up. The blow-up point is \(2/L\) — finding it by hand teaches you more than the successful run does.
A trajectory plot cannot tell you whether you are improving; a column of numbers can. Monotone decrease means it is working, whatever the path looks like.
Halve the step until \(f(x+\alpha d) \le f(x) + 10^{-4}\alpha\,g^\top d\). Now you can toggle between fixed and computed and watch the difference directly — the whole point of this page, in one flag.