Decaying Memory
Side-by-side Adagrad vs RMSProp cache visualization. A horizontal bar timeline shows each past gradient's contribution to the current cache. Adagrad's bars stack uniformly forever; RMSProp's decay as decay^age times (1 − decay) times g^2, and only the last ~10 steps matter.
The component runs as a four-phase narrative — observe, growing, toggle, insight — advanced by the built-in step / switch / reset row or by a controlling parent via the mode / onModeChange API. Step counts are tracked independently per mode so the visitor can build up Adagrad's runaway cache, then flip to RMSProp and watch the bounded cache converge.
Adagrad remembers every gradient forever. Each squared gradient adds to the cache, and nothing ever subtracts.
Installation
npx shadcn@latest add https://craftbits.dev/r/decaying-memory.jsonUsage
import { DecayingMemory } from "@craft-bits/viz/decaying-memory";
<DecayingMemory />Drive the mode from a parent:
const [mode, setMode] = useState("adagrad" as const);
<DecayingMemory mode={mode} onModeChange={setMode} />Render a custom control row instead of the built-in one:
<DecayingMemory
hideControls
renderControls={({ onStep, onToggleMode, onReset, mode }) => (
<MyControls
mode={mode}
onStep={onStep}
onToggleMode={onToggleMode}
onReset={onReset}
/>
)}
/>Hide the narration when embedding inside an article that already explains the optimizers:
<DecayingMemory hideNarration />Understanding the component
- Fixed gradient, two optimizers. Every step adds the same g^2 = 4 to the cache. Adagrad accumulates: cache_n = n · g^2. RMSProp blends with decay: cache_n = decay · cache_n−1 + (1 − decay) · g^2.
- Bars as contributions. Each bar in the timeline represents the contribution of a past gradient to the current cache. In Adagrad every bar is g^2 tall, forever. In RMSProp the bar for step i shrinks to decay^age · (1 − decay) · g^2 — exponential decay you can see.
- Per-mode step counters. The component tracks Adagrad steps and RMSProp steps independently, so the visitor can build up Adagrad's runaway cache and then flip to RMSProp to compare; the readout below shows the active cache plus a faint reference row for Adagrad when in RMSProp.
- Phases drive the narration. observe (empty), growing (8+ Adagrad steps), toggle (switched to RMSProp), insight (stepped in RMSProp). Each phase tints the narration panel and surfaces a different copy.
- Bars spring imperatively. Each bar element is targeted by ref; on every step,
motion'sanimate()springs from the previous height to the new one withSPRINGS.snap. Underprefers-reduced-motion: reduce, bars snap into place without animation.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
mode | "adagrad" | "rmsprop" | — | Controlled mode. Pair with onModeChange. |
defaultMode | "adagrad" | "rmsprop" | "adagrad" | Initial mode for the uncontrolled API. |
onModeChange | (next) => void | — | Fires after the active mode switches. |
onStep | ({ mode, steps }) => void | — | Fires after a step button press with the updated count for that mode. |
transition | Transition | SPRINGS.snap | Override the spring for bar growth and decay. |
narrationCopy | Partial<Record<phase, string>> | — | Override the narration copy per phase. |
hideNarration | boolean | false | Hide the narration paragraph. |
hideControls | boolean | false | Hide the built-in step / switch / reset row. |
renderControls | (args) => ReactNode | — | Render slot for a custom control row. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The root carries
aria-labelledbypointing to a hidden<span>summary of the current mode, step count, cache, and effective learning rate, so screen readers hear the full state without parsing the SVG. - The SVG itself is
role="img"with the samearia-label, so consumers who slot the SVG into another tree still surface a readable name. - An
aria-live="polite"region under the SVG announces the same status on every change, so non-sighted users hear the cache and effective-LR updates as they step. - All three controls are real
<button type="button">with:focus-visibleoutline,disabledstate during animation, and standard keyboard activation. - Status is never the only signal — every phase emits both an aria-live update and a visible narration; colour signals (warning / accent / success / error) are reinforced by position and copy.
- Motion respects
prefers-reduced-motion: reduce— bar springs collapse to immediate attribute sets, including the idle breathing pulse and insight celebration pulse.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/math/DecayingMemory.tsx). The library extract drops the project-specificSvgLabelandChallengeBtnchrome in favour of plain<text>andcn()-styled<button>s, swaps the per-tone--color-warn-*/--color-accent-*/--color-success-*/--color-fail-*palette for the canonical--cb-warning/--cb-accent/--cb-success/--cb-errortoken vars, replaces the inlineSPRINGS.snappyreference with the canonicalSPRINGS.snapfrom@craft-bits/core/motion, and adds a controlledmode/onModeChangeAPI plusonStepcallback andrenderControlsslot so consumers can drive the optimizer from a parent stepper, a keyboard, or a custom control surface.