Optimizer Racetrack
Three (or more) optimizers racing on the same 2D loss landscape, each leaving a coloured trace. Pick a surface (Rosenbrock banana, saddle, ill-conditioned bowl), nudge the learning rate, and watch SGD zig-zag while Adam glides — exactly the diagnostic a real training run buys you, on a single page.
Iteration 0 of 100. SGD loss 17.500. Adam loss 17.500. Momentum loss 17.500.
Rosenbrockstep 000 / 100
- SGDloss 17.500
- Adamloss 17.500
- Momentumloss 17.500
Customize
Landscape
rosenbrock
Race
0.010
100
Playback
Installation
npx shadcn@latest add https://craftbits.dev/r/optimizer-racetrack.jsonUsage
import { OptimizerRacetrack } from "@craft-bits/core";
<OptimizerRacetrack
optimizers={["sgd", "adam", "momentum"]}
loss="rosenbrock"
learningRate={0.01}
iterations={100}
/>Drive playback from outside the component, e.g. synced to a scrollytelling scrubber:
const [iter, setIter] = useState(0);
<OptimizerRacetrack
iteration={iter}
onIterationChange={setIter}
loss="saddle"
/>Add a custom optimizer alongside the built-ins:
<OptimizerRacetrack
optimizers={["sgd", "adam"]}
customOptimizers={[
{
name: "Nesterov",
tone: "default",
step: ({ pos, grad, lr, state }) => {
const beta = 0.9;
const vx = (state.vx ?? 0) * beta + grad[0];
const vy = (state.vy ?? 0) * beta + grad[1];
state.vx = vx;
state.vy = vy;
return [
pos[0] - lr * (beta * vx + grad[0]),
pos[1] - lr * (beta * vy + grad[1]),
];
},
},
]}
/>Understanding the component
- One surface, many descents. Every optimizer starts from the same point on the same loss landscape with the same learning rate — the only thing that changes is the update rule, so the visible divergence between traces is the divergence between algorithms, not hyperparameters.
- Three landscapes, each with a teaching point.
rosenbrockis the narrow curved valley — SGD zig-zags, Adam tracks.saddle(x² − y²) has zero gradient at the saddle — SGD stalls, Momentum escapes.quadratic(x² + 10y²) is the ill-conditioned bowl — Adam's per-axis scaling shines. - Analytic gradients, not finite differences. Each landscape ships an explicit
∇f— what the optimizer sees on screen matches what it would compute in a real training loop. - Marching-squares contours. Iso-lines are built on the fly via a 64×64 marching-squares pass on the loss function. Change the surface and the contour plot follows; no precomputed assets.
- RAF-driven race. Autoplay advances the shared iteration counter at ~6 steps/second via
requestAnimationFrame. The traces always rebuild from precomputed trajectories, so scrubbing is instant. SPRINGS.snapon the head. Each optimizer's current-position dot springs to its new location between iterations — racing-feel, not lazy-spring.- Reduced-motion fallback. With
prefers-reduced-motion: reduce, the race jumps to the final iteration on mount and disables autoplay — every trace renders statically.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
optimizers | readonly Optimizer[] | ['sgd','adam','momentum'] | Built-in optimizers to race. |
customOptimizers | readonly OptimizerConfig[] | — | Pluggable optimizers with a step function. |
loss | 'rosenbrock' | 'saddle' | 'quadratic' | 'rosenbrock' | Loss landscape. |
learningRate | number | 0.01 | Shared learning rate. |
iterations | number | 100 | Steps simulated per optimizer. |
playing | boolean | — | Controlled play state. Pair with onPlayingChange. |
defaultPlaying | boolean | true | Uncontrolled initial play state. |
onPlayingChange | (playing: boolean) => void | — | Fires when play / pause flips. |
iteration | number | — | Controlled iteration index. Pair with onIterationChange. |
defaultIteration | number | 0 | Uncontrolled initial iteration. |
onIterationChange | (iteration: number) => void | — | Fires on tick and on scrub. |
size | number | 360 | Pixel size of the SVG (square). |
transition | Transition | SPRINGS.snap | Spring for the current-position dot. |
className | string | — | Merged onto the root <div> via cn(). |
Accessibility
- The figure is
role="figure"witharia-label="Optimizer comparison on <loss> landscape". - An
aria-live="polite"summary announces iteration + per-optimizer loss whenever the race advances. - The play / pause button is
aria-pressed; the label flips between "Play race" and "Pause race". - The scrubber is a native
<input type="range">with an explicitaria-label, so keyboard arrows nudge the iteration and screen readers narrate the value. - Color is never the only signal — every trace gets a labelled legend entry, and diverged runs read "diverged" textually.
prefers-reduced-motion: reducesnaps to the final iteration and disables autoplay; no per-iteration motion is shown.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/viz/OptimizerRacetrack.tsx). Stripped the lesson-specific mode strip, predict-quiz, narration heuristics, and the companionLossLandscapeExplorer. Generalized to a single comparative primitive with controlled / uncontrolled state pairs and a pluggablecustomOptimizersAPI.