Gradient Descent

How does the learning rate affect gradient descent on a two-parameter quadratic loss?

The idea

Gradient descent moves parameters against the gradient of the loss. In this quadratic bowl, the direction with larger curvature sets the stable step-size limit, so a safe step changes the gentler coordinate only slowly.

Builds on Derivatives.

The picture

Training a model means lowering one number, the loss, by adjusting many others. The figure shrinks that to two: a bowl, L(x,y)=12(x2+20y2)L(x, y) = \tfrac12(x^2 + 20y^2), seen from above as contour lines around a minimum at the center. It is gentle along xx and twenty times steeper along yy, so the valley is long and narrow: the kind of shape that slows real training down.

Downhill. At any point, the gradient points the way the loss rises fastest. Gradient descent steps the other way, by the gradient times a step size η\eta, the learning rate. It needs no map of the bowl, only the slope where it stands. Where the bowl is steep the slope is large, and so is the step.

One multiplier per direction. On this bowl the slope along yy is 20y20y, so a step replaces yy with (1−20η) y(1 - 20\eta)\,y; along xx it replaces xx with (1−η) x(1 - \eta)\,x. Each coordinate is multiplied by its own fixed number, every step, and that number decides its fate. Between 00 and 11, the coordinate shrinks steadily. Between −1-1 and 00, it shrinks but changes sign, jumping to the other side of the valley. Below −1-1, every jump lands further out than the last.

Two speeds. As η\eta grows, the steep multiplier falls twenty times faster than the gentle one, so it is the first to reach −1-1, when 20η=220\eta = 2. That is the edge: no larger step converges. Yet at any step below the edge, the gentle multiplier is still close to 11, so xx shrinks slowly. The steep direction sets how big a step can be; the gentle one sets how long the walk takes.

The gradient itself is taken as given here. For a neural network, backpropagation computes it; gradient descent is what the optimizer does with it.

An interrupted run. The optional continuation example asks a different question: if training stops, what must be saved to take the same next steps? It uses one parameter with momentum, an ordered list of training rows, a tiny random-number generator and a changing learning rate. It is separate from the bowl.

Predict first

The ball starts at (4.4, 2.6)(4.4,\ 2.6) on a bowl twenty times steeper along yy than along xx, and the step size is η=0.08\eta = 0.08. Before you touch the figure: what does the path do?

Optional depth · continuing an interrupted run

What lets us take the same next step?

We stopped after two complete updates. Restoring the parameter puts us at the same point. Taking the same next steps also depends on the history and inputs used by the update.

This separate scalar example adds momentum: keep half the previous gradient history v, then add the new gradient g. The next parameter is θ − η(v/2 + g). Here v is accumulated gradient history, not the parameter displacement.

Updates 1 and 2 completeSave hereNext: updates 3 and 4

Start with “Restore all state”, which matches, then try “Reset only momentum”. Each choice starts from the same checkpoint; choices do not accumulate.

Saved after update 2

Parameter θ
5/64
Momentum v
15/16
Next row
r3 · target 2
Next rate η
1/8

Generator state s = 15; next row cursor c = 2; next rate index j = 2. Cursors start at zero. Seed 1 was the initial generator state.

Your continuation · update 3

Row used
r3 · target 2
Applied rate η
1/8
Carried momentum v/2
15/32
Next parameter θ
117/512

g = 5/64 − (2) − (-1/4) = -107/64. v next = 15/32 + (-107/64) = -77/64.

Both next updates match the uninterrupted trace. The next row, perturbation, rate and momentum all continue from the saved checkpoint.

Read the small update recipe

Cycle through targets 1, −1, 2 in rows r1, r2, r3. Start θ = 0, v = 0 and s = 1. Use rate 1/4 for updates 1 and 2, then 1/8. Every quantity is dimensionless.

  1. Read the next row and rate. Advance the generator: s next = (5s + 1) mod 16.
  2. Use perturbation ξ = +1/4 if s next is below 8, otherwise −1/4. For loss ½(θ − target − ξ)², the gradient is g = θ − target − ξ.
  3. Set v next = v/2 + g, then θ next = θ − η v next. Advance both cursors before saving.

For example, 5 × 15 + 1 = 76 has remainder 12 when divided by 16, so the next perturbation is −1/4. This tiny generator makes its position in the stream easy to inspect; it is not a source of independent random noise. Because the target and the perturbation change from one update to the next, and momentum carries earlier gradients forward, the step-size bound for the fixed bowl above does not apply here, and the loss need not fall.

Compare every field of the next two updates

“Different” compares exact values, not rounded displays. A parameter or loss can agree for a while even though the saved state differs.

Update 3: unchanged reference beside your choice
QuantityUninterruptedYour continuationComparison
Parameter θ before5/645/64Same
Momentum v before15/1615/16Same
Row and targetr3 · target 2r3 · target 2Same
Generator s → s next15 → 1215 → 12Same
Perturbation ξ-1/4-1/4Same
Gradient g-107/64-107/64Same
Carried momentum v/215/3215/32Same
Next momentum v next-77/64-77/64Same
Applied rate η1/81/8Same
Next parameter θ next117/512117/512Same
Row cursor c → c next2 → 02 → 0Same
Rate index j → j next2 → 32 → 3Same
Completed updates2 → 32 → 3Same
Update 4: unchanged reference beside your choice
QuantityUninterruptedYour continuationComparison
Parameter θ before117/512117/512Same
Momentum v before-77/64-77/64Same
Row and targetr1 · target 1r1 · target 1Same
Generator s → s next12 → 1312 → 13Same
Perturbation ξ-1/4-1/4Same
Gradient g-267/512-267/512Same
Carried momentum v/2-77/128-77/128Same
Next momentum v next-575/512-575/512Same
Applied rate η1/81/8Same
Next parameter θ next1511/40961511/4096Same
Row cursor c → c next0 → 10 → 1Same
Rate index j → j next3 → 43 → 4Same
Completed updates3 → 43 → 4Same
Must every field be saved separately?

No. In this recipe, save θ, v, the number of completed updates and the initial seed, together with the exact ordered data and update code. After two updates, row cursor c = 2 mod 3 = 2, rate index j = 2, and two generator advances from seed 1 give 6 then 15. Choose “Rebuild positions from progress” to check this continuation.

Progress works because this recipe fixes the number and order of calls. Extra calls whose count cannot be recovered would require the current generator state or a sufficient call history. A known extra-call pattern could still be reconstructed. Resetting the initial seed alone supplies neither of these.

The combined weights-and-seed reset deliberately does not reconstruct from progress. Its labels “update 3” and “update 4” tell us where we are comparing; those labels do not repair its row, stream or rate position.

What does this check establish?

It repeats two updates of this exact recipe, with the same ordered data and the same checkpoint. The Python code uses exact fractions, and every value in the browser’s default four-update example is a binary fraction that floating point stores exactly. The check compares every saved quantity exactly; rounding a display differently would not make two different values equal.

To make a real training result repeatable, keep the actual code and configuration, the ordered inputs, the environment and the comparison criterion—not just a file name, seed, version label or final loss. A hash of the source identifies the code’s bytes; it does not show that they were run.

The site’s Fieldwork example trains with full-batch Adam at a fixed rate and makes no random draws during updates. Continuing in the same browser tab restores the parameters, both Adam moment estimates, the step count, the configuration and the ordered data from the last saved checkpoint, which may be earlier than the moment of interruption. Its downloadable package rebuilds finished runs from their configuration; it does not include a checkpoint from midway through a run.

This calculation is not evidence of scientific replication, recovery across machines or identical results on different devices. Read the practical limits on reproducibility.

A worked calculation that runs in this page; no model is trained and nothing is sent elsewhere.

The mathematics

Let L:Rp→RL:\mathbb{R}^p\to\mathbb{R} be a loss function and let θt∈Rp\theta_t\in\mathbb{R}^p be the current parameter vector. Under the standard Euclidean inner product, the gradient

∇θL(θt)\nabla_\theta L(\theta_t)

points in the direction of steepest local increase. Gradient descent uses the update

θt+1=θt−η∇θL(θt),\theta_{t+1} = \theta_t - \eta \nabla_\theta L(\theta_t),

where η>0\eta > 0 is the learning rate. Cauchy (1847) proposed this method of steepest descent for solving systems of equations.

If LL is differentiable near θ\theta and ∇L(θ)≠0\nabla L(\theta)\neq 0, the first-order Taylor approximation says

L(θ−η∇L(θ))≈L(θ)−η∥∇L(θ)∥2.L(\theta-\eta\nabla L(\theta)) \approx L(\theta)-\eta\|\nabla L(\theta)\|^2.

That is why stepping against the gradient lowers the loss, for a small enough step. The learning rate says how far to trust this local approximation: too small and progress is slow, too large and the step jumps across the valley or even raises the loss. How large is too large depends on curvature.

At a stationary point, ∇L(θ)=0\nabla L(\theta)=0, so this first-order decrease argument disappears. In a nonconvex loss, such a point might be a minimum, a saddle, or a maximum.

For the quadratic used below,

L(θ)=12θTAθ,L(\theta)=\frac12\theta^{\mathsf T}A\theta,

with AA symmetric positive definite, the gradient is ∇L(θ)=Aθ\nabla L(\theta)=A\theta. In an eigen-direction of AA with curvature λi\lambda_i, the update becomes

zi,t+1=(1−ηλi)zi,t.z_{i,t+1}=(1-\eta\lambda_i)z_{i,t}.

For convergence to zero from every starting point, fixed-step descent needs ∣1−ηλi∣<1|1-\eta\lambda_i|<1 for every direction, so

0<η<2λmax⁡.0<\eta<\frac{2}{\lambda_{\max}}.

At equality, the steepest direction's factor is −1-1 and that coordinate alternates without decaying. Above the bound it grows. Below it, a positive factor shrinks without changing sign, a zero factor lands in one step, and a negative factor alternates while shrinking.

On the figure's bowl, with η=0.1\eta = 0.1 exactly at the edge and start (4.4,2.6)(4.4, 2.6), the recurrence gives xt=4.4(0.9)tx_t=4.4(0.9)^t and yt=2.6(−1)ty_t=2.6(-1)^t. So Lt=67.6+9.68(0.9)2tL_t=67.6+9.68(0.9)^{2t} falls at every step, toward 67.667.6 rather than zero. A falling loss alone does not show convergence to the minimum.

In deep learning, the exact gradient over the whole dataset is usually too expensive. For a training set of nn examples, define the empirical risk

Lemp(θ)=1n∑i=1nℓ(fθ(xi),yi).L_{\mathrm{emp}}(\theta)=\frac1n\sum_{i=1}^n \ell(f_\theta(x_i),y_i).

For a mini-batch BtB_t, we instead compute

gt=∇θ(1∣Bt∣∑i∈Btℓ(fθ(xi),yi)).g_t = \nabla_\theta\left(\frac{1}{|B_t|}\sum_{i\in B_t}\ell(f_\theta(x_i),y_i)\right).

With uniform sampling, gtg_t is an unbiased estimate of ∇Lemp(θt)\nabla L_{\mathrm{emp}}(\theta_t). Using it turns gradient descent into stochastic gradient descent. The update has the same form, but the direction is noisy:

θt+1=θt−ηgt.\theta_{t+1} = \theta_t - \eta g_t.

Replay math

For the continuation example, all quantities are dimensionless scalars. At the end of update tt, retain parameter θt\theta_t, momentum vtv_t, generator state sts_t, next-row cursor ctc_t, and next-rate index jtj_t. Cursors start at zero. The ordered targets are (y0,y1,y2)=(1,−1,2)(y_0,y_1,y_2)=(1,-1,2), with row labels r1, r2, r3. Initially (θ0,v0,s0,c0,j0)=(0,0,1,0,0)(\theta_0,v_0,s_0,c_0,j_0)=(0,0,1,0,0).

Before each update, read its row and rate. Advance the generator once, then derive a perturbation from its new state:

st+1=(5st+1) mod 16,ξt={1/4st+1<8,−1/4st+1≥8,ηjt={1/4jt<2,1/8jt≥2.s_{t+1}=(5s_t+1)\bmod 16,\qquad \xi_t=\begin{cases}1/4&s_{t+1}<8,\\-1/4&s_{t+1}\ge8,\end{cases}\qquad \eta_{j_t}=\begin{cases}1/4&j_t<2,\\1/8&j_t\ge2.\end{cases}

The update-specific loss is Lt(θ)=12(θ−yct−ξt)2L_t(\theta)=\tfrac12(\theta-y_{c_t}-\xi_t)^2. Different updates can have different effective targets; this is not the fixed quadratic above. Differentiating, then carrying half the previous gradient history, gives

gt=θt−yct−ξt,vt+1=12vt+gt,θt+1=θt−ηjtvt+1.g_t=\theta_t-y_{c_t}-\xi_t,\qquad v_{t+1}=\tfrac12v_t+g_t,\qquad \theta_{t+1}=\theta_t-\eta_{j_t}v_{t+1}.

Only after these calculations, set ct+1=(ct+1) mod 3c_{t+1}=(c_t+1)\bmod3 and jt+1=jt+1j_{t+1}=j_t+1, and increment the completed-update count. Save after the whole update, not between the parameter and cursor changes. After update 2, the checkpoint is (5/64,15/16,15,2,2)(5/64,15/16,15,2,2), with completed count 2.

For update 3, read target 2 and rate 1/81/8. The generator advances from 15 to 12, giving perturbation −1/4-1/4. Thus g2=−107/64g_2=-107/64, the momentum is v3=15/32−107/64=−77/64v_3=15/32-107/64=-77/64, and θ3=5/64−(1/8)(−77/64)=117/512\theta_3=5/64-(1/8)(-77/64)=117/512. Resetting only momentum leaves the same gradient but gives θ3=147/512\theta_3=147/512. The current parameter alone does not determine the next step.

For this recipe, saved progress nn reconstructs c=n mod 3c=n\bmod3, j=nj=n and the generator state after exactly nn advances from its seed. Restoring θ\theta and vv while reconstructing these positions is sufficient. If extra calls occurred whose number or order is not recoverable, progress alone is no longer sufficient. Saving each field separately is one method, not a universal rule.

This generator is a small deterministic rule chosen for the example, not independent noise or uniform mini-batch sampling, and an exact continuation here says nothing about convergence or about restarting training spread over many machines. The PyTorch checkpoint tutorial explains why resuming real training requires the optimizer's state as well as the parameters, and PyTorch's reproducibility notes explain why the same seed need not give identical results on different hardware or software versions.

Try it

First a worked example, then one to finish, then problems on your own. Check your answer before you look at the solution. Answers typed here are not kept when you leave.

  1. 1 · Worked example

    One step by hand

    The figure's bowl is L(x,y)=12(x2+20y2)L(x, y) = \tfrac12(x^2 + 20y^2). Start at (4.4, 2.6)(4.4,\ 2.6) with η=0.08\eta = 0.08 and take one step.

    1. Slope. ∇L=(x, 20y)=(4.4, 52)\nabla L = (x,\ 20y) = (4.4,\ 52). The slope along yy is almost twelve times the slope along xx.

    Say the next step to yourself first.
  2. 2 · Finish it

    Three steps along the steep direction

    Stay at η=0.08\eta = 0.08 and follow only yy, starting from 2.62.6. Every step multiplies it by the same number. What is yy after three steps? Give three decimal places.

    1. Multiplier. y−η⋅20y=(1−20η) y=(1−1.6) y=−0.6 yy - \eta \cdot 20y = (1 - 20\eta)\,y = (1 - 1.6)\,y = -0.6\,y.

    2. Step 1. 2.6×(−0.6)=−1.562.6 \times (-0.6) = -1.56, as in the worked step.

    3. Step 2. −1.56×(−0.6)=0.936-1.56 \times (-0.6) = 0.936.

    4. Your turn.

  3. 3 · On your own

    A steeper direction

    Add a third direction to the bowl, with curvature 40: L=12(x2+20y2+40z2)L = \tfrac12(x^2 + 20y^2 + 40z^2). Above what step size η\eta does the path stop converging from every start?

  4. 4 · On your own

    The slow one

    Back on the figure's bowl at η=0.08\eta = 0.08: after 20 steps, which coordinate is further from zero, as a fraction of where it started?

    Choose one

  5. 5 · Worked example

    Read the carried momentum

    Use the separate continuation example, saved after update 2. Its next gradient is −107/64-107/64, saved momentum is 15/1615/16, current parameter is 5/645/64, and rate is 1/81/8. What changes if only momentum is reset?

    1. With the history restored, vnext=(15/16)/2−107/64=−77/64v_{\mathrm{next}}=(15/16)/2-107/64=-77/64. The parameter displacement is −(1/8)(−77/64)=77/512-(1/8)(-77/64)=77/512.

    Say the next step to yourself first.
  6. 6 · Finish it

    The same seed at the wrong position

    At the checkpoint after update 2, reset only the generator from state 15 to its initial seed 1. Keep θ=5/64\theta=5/64, v=15/16v=15/16, row r3 (target 2) and rate 1/81/8. Identify the first changed input, then calculate the new generator state, perturbation, gradient, momentum and parameter for update 3. Compare with the uninterrupted trace.

    1. The next-state rule is snext=(5s+1) mod 16s_{\mathrm{next}}=(5s+1)\bmod16; use +1/4+1/4 below 8 and −1/4-1/4 otherwise.

    2. Your turn.

  7. 7 · On your own

    A smaller checkpoint

    Suppose the checkpoint after update 2 stores only θ=5/64\theta=5/64, v=15/16v=15/16, the number of completed updates (2) and the initial seed 1. Using the recipe, rebuild the row cursor, the rate index and the generator state.

  8. 8 · On your own

    Specify a repeatable result

    A note says “seed 1; checkpoint after step 2; final parameter agrees.” What else would you need to save or check before claiming that the resumed run repeats the original?

The code

import numpy as np

# Minimize L(theta) = 0.5 * theta^T A theta.
# The second coordinate has much higher curvature.
# Shapes: A is (2, 2), theta is (2,), grad(theta) is (2,).
A = np.diag([1.0, 20.0])
theta = np.array([5.0, 5.0])
lr = 0.08

def loss(theta):
    return 0.5 * theta @ A @ theta

def grad(theta):
    return A @ theta

for step in range(20):
    theta = theta - lr * grad(theta)
    if step in [0, 1, 2, 5, 19]:
        print(step + 1, "theta=", np.round(theta, 3), "loss=", round(loss(theta), 3))

Here λmax⁡=20\lambda_{\max}=20, so convergence from every start needs 0<η<2/λmax⁡=0.10<\eta<2/\lambda_{\max}=0.1. Try lr = 0.005, 0.12 and 0.1, and read the coordinates as well as the loss. At lr = 0.1 the second coordinate never decays; this code starts at [5,5], not the demo's [4.4,2.6], so it bounces with amplitude 55, not 2.62.6.

The continuation example in exact arithmetic

This second program uses only Python's standard library. It runs the scalar recipe with exact fractions and an algebraically expanded update, so its results can be compared with the calculation shown in the page. theta, velocity, rng, cursor and scheduleIndex correspond to θ,v,s,c,j\theta,v,s,c,j. The seed is the generator's initial state; completed counts whole updates. The program allows at most eight updates.

from fractions import Fraction as F

ROWS = (("r1", 1), ("r2", -1), ("r3", 2))
POLICIES = ("restore", "velocity", "rng", "cursor", "schedule",
            "weights-seed", "reconstruct")

def initial(seed=1):
    return dict(theta=F(0), velocity=F(0), rng=seed,
                cursor=0, scheduleIndex=0, completed=0)

def step(state, rows=ROWS):
    t, v = state["theta"], state["velocity"]
    row_id, target = rows[state["cursor"]]
    rate = F(1, 4) if state["scheduleIndex"] < 2 else F(1, 8)
    rng = (5 * state["rng"] + 1) % 16
    jitter = F(1, 4) if rng < 8 else F(-1, 4)
    effective_target = target + jitter
    gradient = t - effective_target
    # Expanded affine form, equivalent to v_next = v/2 + gradient.
    v_next = t + v / 2 - effective_target
    t_next = (1 - rate) * t - rate * v / 2 + rate * effective_target
    after = dict(theta=t_next, velocity=v_next, rng=rng,
                 cursor=(state["cursor"] + 1) % 3,
                 scheduleIndex=state["scheduleIndex"] + 1,
                 completed=state["completed"] + 1)
    return dict(before=dict(state), rowId=row_id, target=target,
                rngAfter=rng, jitter=jitter, gradient=gradient,
                rate=rate, after=after)

def trace(state, count, rows=ROWS):
    if not 0 <= count <= 8 - state["completed"]:
        raise ValueError("At most eight whole updates")
    result = []
    for _ in range(count):
        update = step(state, rows)
        result.append(update)
        state = update["after"]
    return result

def checkpoint(completed=2, seed=1, rows=ROWS):
    start = initial(seed)
    updates = trace(start, completed, rows)
    return dict(updates[-1]["after"] if updates else start)

def restore(saved, policy, seed=1):
    if policy not in POLICIES:
        raise ValueError("Unknown restoration policy")
    state = dict(saved)
    if policy in ("velocity", "weights-seed"):
        state["velocity"] = F(0)
    if policy in ("rng", "weights-seed"):
        state["rng"] = seed
    if policy in ("cursor", "weights-seed"):
        state["cursor"] = 0
    if policy in ("schedule", "weights-seed"):
        state["scheduleIndex"] = 0
    if policy == "reconstruct":
        n = state["completed"]
        state["cursor"] = n % 3
        state["scheduleIndex"] = n
        # Closed form for n advances of s <- 5*s + 1 (mod 16).
        state["rng"] = (5**n * seed + sum(5**k for k in range(n))) % 16
    # Otherwise completed labels the comparison; it repairs no missing state.
    return state

if __name__ == "__main__":
    saved = checkpoint()
    reference = trace(saved, 2)
    for policy in POLICIES:
        resumed = trace(restore(saved, policy), 2)
        print(policy, "whole trace agrees:", resumed == reference)
        for u in resumed:
            print(u["after"]["completed"], u["rowId"],
                  "rng", u["before"]["rng"], "->", u["rngAfter"],
                  "xi", u["jitter"], "g", u["gradient"], "rate", u["rate"],
                  "v", u["after"]["velocity"], "theta", u["after"]["theta"])

The full restore and the reconstruction from progress both print True; the other five policies print False. Correct continuation gives 117/512117/512 then 1511/40961511/4096. If you change ROWS, the seed, the checkpoint or the recipe, compare against an uninterrupted run with the same inputs. The program assumes the small inputs declared above; it is not a loader for untrusted checkpoints.

Keep it

  1. Why does gradient descent step against the gradient?

    Answer from memory, then check

    The gradient points the way the loss rises fastest. To first order, a step of −η∇L-\eta\nabla L changes the loss by −η∥∇L∥2-\eta\|\nabla L\|^2, which is negative whenever the gradient is not zero.

  2. On a quadratic bowl, a direction has curvature λ\lambda. What does one gradient step do to the coordinate along it?

    Answer from memory, then check

    Multiplies it by 1−ηλ1 - \eta\lambda. It shrinks only while that number lies strictly between −1-1 and 11; when the number is negative, the coordinate also changes sign every step.

  3. On a quadratic, what sets the largest step size that still converges from every start?

    Answer from memory, then check

    The steepest curvature: fixed-step descent needs 0<η<2/λmax⁡0 < \eta < 2/\lambda_{\max}. One very steep direction caps the step for every other direction.

  4. If the steepest direction caps the step size, which direction decides how long training takes?

    Answer from memory, then check

    The direction whose multiplier is largest in size, ∣1−ηλ∣|1 - \eta\lambda|. For any η\eta up to 2/(λmax⁡+λmin⁡)2/(\lambda_{\max} + \lambda_{\min}) that is the gentlest: its 1−ηλmin⁡1 - \eta\lambda_{\min} stays close to 1, so it shrinks slowly, and the bigger the gap between the curvatures, the slower the walk. Nearer the cap 2/λmax⁡2/\lambda_{\max} the steep one can be slower: on the figure's bowl at η=0.099\eta = 0.099 its multiplier is −0.98-0.98, against 0.9010.901 for the gentle one.

  5. True or false: if the loss falls at every step, gradient descent is converging to the minimum.

    Answer from memory, then check

    False. At η=0.1\eta = 0.1 on the figure's bowl, xx shrinks and the loss falls every step, but yy bounces between 2.62.6 and −2.6-2.6 forever. The loss levels off at 67.667.6, not zero.

Recalling an answer from memory makes it last longer than rereading it (Roediger and Karpicke, 2006). Add these 5 cards and they come back tomorrow, then after gaps of 3, 7, 16, 35 and 75 days while you still remember them. They stay in this browser only.

The edge

On a quadratic bowl nothing is left open: the multipliers decide the path before a single step is drawn. Neural-network losses are not quadratic, and their curvature changes as training moves. Cohen et al. (2021) found that full-batch gradient descent on neural networks typically pushes the sharpest curvature up until it reaches about 2/η2/\eta, the edge this page derives, and then hovers there, the edge of stability. The loss keeps falling over the long run while jumping around in the short run, which the quadratic picture says should not happen, and why it works is still being worked out. A wider question is open too: why the minima that gradient descent and its mini-batch form find in huge networks tend to generalize, when the same networks can fit random labels just as easily (Zhang et al., 2017).

Next thoughtThe steep direction caps the step and the gentle one crawls. What if every parameter got a step size of its own?Adam Optimizer →
The full instrument: change the curvature, classify a setting before the reveal, and read exact multipliers and coordinates step by step

Limits of this page

  • The bowl has two parameters and is exactly quadratic, so every step can be checked by hand. Neural-network losses have millions of directions, and their curvature changes as training moves.
  • The bowl uses its exact gradient. The separate continuation example uses a changing scalar target; its tiny generator does not model independent mini-batch noise.
  • The figure draws 20 steps. Whether a path converges is settled by the multipliers, not by how the drawn steps look.
  • The bowl's step size stays fixed. The optional continuation example introduces momentum and a stated rate schedule only to study replay; the bowl's convergence bound does not apply to that example.
  • The Python runs on your machine, not in the page.

Sources

  1. Boyd and Vandenberghe (2004). Convex Optimization.

    Chapter 9 treats descent methods for unconstrained minimization, including gradient descent, steepest descent and line-search step sizes, with convergence analysis for strongly convex functions.

  2. Goodfellow, Bengio, and Courville (2016). Deep Learning.

    Section 4.3 introduces gradient-based optimization; Chapter 8 covers optimization for training deep networks, including stochastic gradient descent (Section 8.3.1).

  3. PyTorch contributors (2025). Saving and Loading a General Checkpoint.

    Explains why resuming training requires saving the optimizer's state, such as momentum buffers, and the training progress along with the model's parameters.

  4. PyTorch contributors. PyTorch 2.14 — Reproducibility.

    Notes that the same random seed does not guarantee identical results across PyTorch releases, platforms, or CPU and GPU.

  5. PyTorch contributors. PyTorch 2.14 — LRScheduler.

    Documents that a learning-rate scheduler keeps its own state, saved and restored with state_dict and load_state_dict. The rate list in this lesson is written out by hand, not produced by this API.

  6. Cauchy (1847). Méthode générale pour la résolution des systèmes d'équations simultanées.

    Proposes solving a system of equations by repeatedly stepping against the gradient of a sum of squares: the method of steepest descent.

  7. Cohen et al. (2021). Gradient Descent on Neural Networks Typically Occurs at the Edge of Stability.

    Finds that during full-batch gradient descent the largest Hessian eigenvalue rises until it hovers just above 2/η, while the loss decreases over long timescales but not monotonically (ICLR 2021).

  8. Zhang et al. (2017). Understanding deep learning requires rethinking generalization.

    Shows that image-classification networks trained with stochastic gradient methods easily fit random labels, so traditional complexity-based explanations do not account for why they generalize on real labels (ICLR 2017).

What the claims rest on

Central claim: Gradient descent turns local slope information into an iterative update rule for reducing a loss.

Each claim below lists the passages it cites and how far it has been checked against them. These are the site’s own checks, not independent replication.

Claim (checked against the cited passages): Gradient descent uses the gradient as the direction of steepest local increase and updates parameters in the negative-gradient direction, with the learning rate controlling how far to trust the local approximation.

Sources: Convex Optimization; Deep Learning

What the sources support: Boyd and Vandenberghe ground descent methods, gradients, and step-size reasoning in convex optimization; Goodfellow et al. frame neural-network training as optimizing parameters of a cost function with gradient-based methods.

On this page: the note on Convex Optimization, the note on Deep Learning, Equation 1, Equation 2, Code example 1, the figure

Caveat: This checks the local first-order update mechanism, not a guarantee of global convergence for nonconvex neural-network losses.

Claim (checked against the cited passages): Resuming an interrupted run so that it takes the same next updates requires enough information about the parameters, the optimizer's history, the inputs and the position in the run. In the scalar example on this page, a full restore or a reconstruction from the saved update count repeats the next two updates.

Sources: Saving and Loading a General Checkpoint; PyTorch 2.14 — Reproducibility; PyTorch 2.14 — LRScheduler

What the sources support: The documentation distinguishes optimizer and scheduler state from parameters and limits reproducibility claims. The two-update trace and the argument about what suffices are calculations made on this page, not taken from the documentation.

On this page: the note on Saving and Loading a General Checkpoint, the note on PyTorch 2.14 — Reproducibility, the note on PyTorch 2.14 — LRScheduler

Caveat: Nothing here shows recovery across many machines, identical results across devices, or anything about learning. A version label or output hash alone does not identify the code that ran.

Connections

Builds on Derivatives.

Leads to SGD and Momentum, Adam Optimizer and Learning Rate Schedules: Warmup, Decay & Cycling.

Cite this page

Continuous Function. “Gradient Descent.” Revised 2026-10-11. https://continuousfunction.ai/domains/optimization/gradient-descent/

BibTeX
@misc{cf-gradient-descent,
  title = {Gradient Descent},
  author = {{Continuous Function}},
  year = {2026},
  note = {Revised 2026-10-11},
  url = {https://continuousfunction.ai/domains/optimization/gradient-descent/}
}