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, , seen from above as contour lines around a minimum at the center. It is gentle along and twenty times steeper along , 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 , 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 is , so a step replaces with ; along it replaces with . Each coordinate is multiplied by its own fixed number, every step, and that number decides its fate. Between and , the coordinate shrinks steadily. Between and , it shrinks but changes sign, jumping to the other side of the valley. Below , every jump lands further out than the last.
Two speeds. As grows, the steep multiplier falls twenty times faster than the gentle one, so it is the first to reach , when . That is the edge: no larger step converges. Yet at any step below the edge, the gentle multiplier is still close to , so 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.
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.
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.
- Read the next row and rate. Advance the generator: s next = (5s + 1) mod 16.
- Use perturbation ξ = +1/4 if s next is below 8, otherwise −1/4. For loss ½(θ − target − ξ)², the gradient is g = θ − target − ξ.
- 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.
| Quantity | Uninterrupted | Your continuation | Comparison |
|---|---|---|---|
| Parameter θ before | 5/64 | 5/64 | Same |
| Momentum v before | 15/16 | 15/16 | Same |
| Row and target | r3 · target 2 | r3 · target 2 | Same |
| Generator s → s next | 15 → 12 | 15 → 12 | Same |
| Perturbation ξ | -1/4 | -1/4 | Same |
| Gradient g | -107/64 | -107/64 | Same |
| Carried momentum v/2 | 15/32 | 15/32 | Same |
| Next momentum v next | -77/64 | -77/64 | Same |
| Applied rate η | 1/8 | 1/8 | Same |
| Next parameter θ next | 117/512 | 117/512 | Same |
| Row cursor c → c next | 2 → 0 | 2 → 0 | Same |
| Rate index j → j next | 2 → 3 | 2 → 3 | Same |
| Completed updates | 2 → 3 | 2 → 3 | Same |
| Quantity | Uninterrupted | Your continuation | Comparison |
|---|---|---|---|
| Parameter θ before | 117/512 | 117/512 | Same |
| Momentum v before | -77/64 | -77/64 | Same |
| Row and target | r1 · target 1 | r1 · target 1 | Same |
| Generator s → s next | 12 → 13 | 12 → 13 | Same |
| Perturbation ξ | -1/4 | -1/4 | Same |
| Gradient g | -267/512 | -267/512 | Same |
| Carried momentum v/2 | -77/128 | -77/128 | Same |
| Next momentum v next | -575/512 | -575/512 | Same |
| Applied rate η | 1/8 | 1/8 | Same |
| Next parameter θ next | 1511/4096 | 1511/4096 | Same |
| Row cursor c → c next | 0 → 1 | 0 → 1 | Same |
| Rate index j → j next | 3 → 4 | 3 → 4 | Same |
| Completed updates | 3 → 4 | 3 → 4 | Same |
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 be a loss function and let be the current parameter vector. Under the standard Euclidean inner product, the gradient
points in the direction of steepest local increase. Gradient descent uses the update
where is the learning rate. Cauchy (1847) proposed this method of steepest descent for solving systems of equations.
If is differentiable near and , the first-order Taylor approximation says
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, , 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,
with symmetric positive definite, the gradient is . In an eigen-direction of with curvature , the update becomes
For convergence to zero from every starting point, fixed-step descent needs for every direction, so
At equality, the steepest direction's factor is 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 exactly at the edge and start , the recurrence gives and . So falls at every step, toward 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 examples, define the empirical risk
For a mini-batch , we instead compute
With uniform sampling, is an unbiased estimate of . Using it turns gradient descent into stochastic gradient descent. The update has the same form, but the direction is noisy:
Replay math
For the continuation example, all quantities are dimensionless scalars. At the end of update , retain parameter , momentum , generator state , next-row cursor , and next-rate index . Cursors start at zero. The ordered targets are , with row labels r1, r2, r3. Initially .
Before each update, read its row and rate. Advance the generator once, then derive a perturbation from its new state:
The update-specific loss is . Different updates can have different effective targets; this is not the fixed quadratic above. Differentiating, then carrying half the previous gradient history, gives
Only after these calculations, set and , and increment the completed-update count. Save after the whole update, not between the parameter and cursor changes. After update 2, the checkpoint is , with completed count 2.
For update 3, read target 2 and rate . The generator advances from 15 to 12, giving perturbation . Thus , the momentum is , and . Resetting only momentum leaves the same gradient but gives . The current parameter alone does not determine the next step.
For this recipe, saved progress reconstructs , and the generator state after exactly advances from its seed. Restoring and 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 · Worked example
One step by hand
The figure's bowl is . Start at with and take one step.
Slope. . The slope along is almost twelve times the slope along .
Say the next step to yourself first.2 · Finish it
Three steps along the steep direction
Stay at and follow only , starting from . Every step multiplies it by the same number. What is after three steps? Give three decimal places.
Multiplier. .
Step 1. , as in the worked step.
Step 2. .
- Your turn.
3 · On your own
A steeper direction
Add a third direction to the bowl, with curvature 40: . Above what step size does the path stop converging from every start?
4 · On your own
The slow one
Back on the figure's bowl at : after 20 steps, which coordinate is further from zero, as a fraction of where it started?
5 · Worked example
Read the carried momentum
Use the separate continuation example, saved after update 2. Its next gradient is , saved momentum is , current parameter is , and rate is . What changes if only momentum is reset?
With the history restored, . The parameter displacement is .
Say the next step to yourself first.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 , , row r3 (target 2) and rate . Identify the first changed input, then calculate the new generator state, perturbation, gradient, momentum and parameter for update 3. Compare with the uninterrupted trace.
The next-state rule is ; use below 8 and otherwise.
- Your turn.
7 · On your own
A smaller checkpoint
Suppose the checkpoint after update 2 stores only , , 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 · 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 , so convergence from every start needs . 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 , not .
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 .
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 then
. 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
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 changes the loss by , which is negative whenever the gradient is not zero.
On a quadratic bowl, a direction has curvature . What does one gradient step do to the coordinate along it?
Answer from memory, then check
Multiplies it by . It shrinks only while that number lies strictly between and ; when the number is negative, the coordinate also changes sign every step.
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 . One very steep direction caps the step for every other direction.
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, . For any up to that is the gentlest: its stays close to 1, so it shrinks slowly, and the bigger the gap between the curvatures, the slower the walk. Nearer the cap the steep one can be slower: on the figure's bowl at its multiplier is , against for the gentle one.
True or false: if the loss falls at every step, gradient descent is converging to the minimum.
Answer from memory, then check
False. At on the figure's bowl, shrinks and the loss falls every step, but bounces between and forever. The loss levels off at , 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 , 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).