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.

Decaying memory comparison. Mode: Adagrad. 0 steps. Cache 0, effective learning rate 0.100.no stepscontribution of each past gradient to current cacheoldestnewesttap Step to add gradientstotal cache0effective lr0.100
Mode: Adagrad. Step 0. Cache: 0, effective LR: 0.100.

Adagrad remembers every gradient forever. Each squared gradient adds to the cache, and nothing ever subtracts.

Customize
State
adagrad
Layout

Installation

npx shadcn@latest add https://craftbits.dev/r/decaying-memory.json

Usage

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

  1. 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.
  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.
  3. 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.
  4. 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.
  5. Bars spring imperatively. Each bar element is targeted by ref; on every step, motion's animate() springs from the previous height to the new one with SPRINGS.snap. Under prefers-reduced-motion: reduce, bars snap into place without animation.

Props

PropTypeDefaultDescription
mode"adagrad" | "rmsprop"Controlled mode. Pair with onModeChange.
defaultMode"adagrad" | "rmsprop""adagrad"Initial mode for the uncontrolled API.
onModeChange(next) => voidFires after the active mode switches.
onStep({ mode, steps }) => voidFires after a step button press with the updated count for that mode.
transitionTransitionSPRINGS.snapOverride the spring for bar growth and decay.
narrationCopyPartial<Record<phase, string>>Override the narration copy per phase.
hideNarrationbooleanfalseHide the narration paragraph.
hideControlsbooleanfalseHide the built-in step / switch / reset row.
renderControls(args) => ReactNodeRender slot for a custom control row.
classNamestringMerged onto the root via cn().

Accessibility

  • The root carries aria-labelledby pointing 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 same aria-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-visible outline, disabled state 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-specific SvgLabel and ChallengeBtn chrome in favour of plain <text> and cn()-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-error token vars, replaces the inline SPRINGS.snappy reference with the canonical SPRINGS.snap from @craft-bits/core/motion, and adds a controlled mode / onModeChange API plus onStep callback and renderControls slot so consumers can drive the optimizer from a parent stepper, a keyboard, or a custom control surface.