Dropout Ensemble Viz

Dropout taught as ensemble learning. A small input → hidden → output network is drawn at the top; the learner taps Sample mask to apply a fresh Bernoulli mask over the hidden layer, killing a random subset of neurons (and the connections that flow through them). Each sampled mask is preserved as a mini-network thumbnail in a strip underneath — a "voter" in the ensemble.

The phase machine derives the narration from the strip length: observe (no masks), sample (1–3), diverse (4–5), insight (6+). At the insight phase a callout reveals the punchline: at inference time, every neuron is active and the prediction is the average of every subnetwork that was ever trained. Dropout gives you ensemble learning for free.

Dropout ensemble visualisation. 4 input, 6 hidden, 2 output neurons. Dropout rate 0.50.
Full network. No masks sampled yet.

The full network uses all 6 hidden neurons. During training, dropout creates a different subnetwork each step. Sample a mask to see one.

Customize
Network
6
Dropout
0.50
42

Installation

npx shadcn@latest add https://craftbits.dev/r/dropout-ensemble-viz.json

Usage

import { DropoutEnsembleViz } from "@craft-bits/viz/dropout-ensemble-viz";
 
<DropoutEnsembleViz />

Tune the dropout rate and hidden-layer width:

<DropoutEnsembleViz numHidden={8} dropoutRate={0.3} />

Pin the sampler to a deterministic seed (useful in tests, SSR, and recorded walkthroughs):

<DropoutEnsembleViz seed={1729} />

Drive the mask history from outside (controlled mode):

const [masks, setMasks] = useState<ReadonlyArray<readonly boolean[]>>([]);
 
<DropoutEnsembleViz masks={masks} onMasksChange={setMasks} />

Understanding the component

  1. Three-layer network. The top half renders an input → hidden → output network with numInput, numHidden, and numOutput neurons. Hidden neurons are the only ones dropout acts on; the input and output layers are intact every frame.
  2. Bernoulli mask per sample. Tapping Sample mask generates a fresh mask of length numHidden: each entry is independently kept with probability 1 − dropoutRate. When numHidden ≥ 4, the sampler guarantees at least two alive and at least two dead neurons so the teaching picture stays legible.
  3. Imperative neuron animation. The hidden-neuron radius pulse, fill, stroke, and dropped-neuron error flash are all driven imperatively via motion's animate() against refs — the SVG never re-renders per frame as masks come and go.
  4. Mask strip as voter accumulator. Each sampled mask is preserved as a mini-network thumbnail in a strip underneath. Once the strip exceeds maxVisibleMasks, the oldest masks scroll off but stay in the controlled history; an ellipsis indicates the overflow.
  5. Phase machine. The narration phase derives from the mask count: observe (0), sample (1–3), diverse (4–5), insight (6+). The phase drives the accent colour of the narration paragraph and gates the "Inference = average of all N voters" callout.
  6. Deterministic sampling. When a seed is provided, masks are sampled via mulberry32(seed XOR history.length). This keeps SSR and client renders in lockstep and makes recorded walkthroughs reproducible. Omitting seed falls back to Math.random() for genuine variety.
  7. Reduced motion. Under prefers-reduced-motion: reduce, every animation collapses to an instant attribute set — radius pulses, error flashes, and connection-opacity easing all snap.

Props

PropTypeDefaultDescription
numInputnumber4Input-layer width. Clamped to ≥ 1.
numHiddennumber6Hidden-layer width (the layer dropout acts on). Clamped to ≥ 1.
numOutputnumber2Output-layer width. Clamped to ≥ 1.
dropoutRatenumber0.5Per-neuron Bernoulli drop probability in [0, 1].
maxVisibleMasksnumber6Maximum thumbnails shown in the strip; older masks scroll off.
seednumberWhen set, masks are sampled deterministically via mulberry32.
masksReadonlyArray<readonly boolean[]>Controlled mask history. Pair with onMasksChange.
defaultMasksReadonlyArray<readonly boolean[]>[]Uncontrolled initial mask history.
onMasksChange(masks) => voidFires after each sample / reset.
transitionTransitionSPRINGS.snapOverride the hidden-neuron radius-pulse spring.
classNamestringMerged onto the root via cn().

Accessibility

  • The SVG is role="img" with an aria-label summarising the mask count and the current subnetwork's active-neuron count.
  • A live region below the SVG narrates each phase transition as plain prose so screen-reader users learn the same lesson sighted users do.
  • The narration paragraph is also aria-live="polite" and reads as plain prose; it is the canonical explanation for each phase.
  • Colour is never the only signal — dropped neurons display a bold × glyph, the hidden-layer label flips to the warning tone, and the active-neuron counter reads as text.
  • The Sample mask and Reset buttons have visible labels, are reachable in tab order, and show a focus ring via :focus-visible.
  • Motion respects prefers-reduced-motion: reduce — every neuron-radius pulse, error flash, and connection-opacity ease collapses to an instant attribute set.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/nn/DropoutEnsembleViz.tsx). The source was a tightly bundled lesson component — it consumed SvgLabel from the SVG primitives and ChallengeBtn from the lesson chrome, hardcoded the 4 → 6 → 2 architecture and p = 0.5, depended on per-track lesson palette tokens (--color-accent-500, --color-warn-400, --color-fail-500, --color-success-500, --color-ink-*), and embedded Math.random() directly into the sampler. The viz extract drops the lesson chrome (raw <text> plus token-styled buttons), parameterises numInput / numHidden / numOutput / dropoutRate / maxVisibleMasks, remaps the palette to var(--cb-*) semantic tokens so consumer themes repaint freely, surfaces the mask history as a controlled-or-uncontrolled Radix-style prop (masks / defaultMasks / onMasksChange), and adds a deterministic seed for SSR / hydration parity and reproducible walkthroughs. Inline { type: "spring", stiffness, damping } tunings and the STAGGER.tight constant from the source were swapped for SPRINGS.snap and STAGGER from @craft-bits/core/motion.