"use client"; import { motion } from "framer-motion"; import { useEffect, useState } from "react"; import { COLORS, VizFrame } from "./common"; import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; /** * Three side-by-side panels: dropout (random neurons grayed each "step"), * weight decay (weights shrinking over time), early stopping (val curve * crossing train curve). */ export function RegularizationViz({ width = 980, height = 380, }: { width?: number; height?: number; }) { const panelW = (width - 50) / 3; const panelH = height - 30; return ( ); } function PanelFrame({ w, h, title, children, }: { w: number; h: number; title: string; children: React.ReactNode; }) { return ( {title} {children} ); } function DropoutPanel({ w, h }: { w: number; h: number }) { const reduce = useReducedMotion(); const [tick, setTick] = useState(0); useEffect(() => { if (reduce) return; const id = setInterval(() => setTick((t) => t + 1), 700); return () => clearInterval(id); }, [reduce]); const layers = [4, 5, 5, 3]; const padX = 30; const padY = 50; const colW = (w - padX * 2) / (layers.length - 1); // Pseudo-random mask seeded by tick function masked(li: number, ni: number) { if (li === 0 || li === layers.length - 1) return false; // keep input/output const x = Math.sin(tick * 17 + li * 23 + ni * 7) * 10000; return x - Math.floor(x) < 0.4; } const positions = layers.map((n, li) => Array.from({ length: n }, (_, ni) => { const ySpan = h - padY - 40; const step = ySpan / (n + 1); return { x: padX + li * colW, y: padY + step * (ni + 1) }; }), ); return ( {/* Edges */} {layers.slice(0, -1).map((_, li) => positions[li].flatMap((a, i) => positions[li + 1].map((b, j) => { const dropA = masked(li, i); const dropB = masked(li + 1, j); return ( ); }), ), )} {/* Nodes */} {positions.flatMap((col, li) => col.map((p, ni) => { const dropped = masked(li, ni); return ( ); }), )} zero a random fraction each forward pass ); } function WeightDecayPanel({ w, h }: { w: number; h: number }) { const reduce = useReducedMotion(); const [tick, setTick] = useState(0); useEffect(() => { if (reduce) { setTick(40); return; } const id = setInterval(() => setTick((t) => Math.min(80, t + 1)), 80); return () => clearInterval(id); }, [reduce]); const padX = 26; const padY = 50; const cols = 6; const rows = 5; const cellW = (w - padX * 2) / cols; const cellH = (h - padY - 40) / rows; const decay = Math.max(0.18, 1 - tick / 60); return ( {Array.from({ length: rows * cols }, (_, k) => { const r = Math.floor(k / cols); const c = k % cols; const seed = Math.sin(r * 13 + c * 5) * 0.5 + 0.5; const v = seed * decay; return ( 0.5 ? COLORS.accent : COLORS.honey} fillOpacity={Math.abs(v - 0.5) * 1.2 + 0.1} /> ); })} L = L₀ + λ‖W‖² → weights shrink toward zero ); } function EarlyStopPanel({ w, h }: { w: number; h: number }) { const padX = 30; const padY = 50; const innerW = w - padX * 2; const innerH = h - padY - 40; const N = 80; const sx = (i: number) => padX + (i / (N - 1)) * innerW; const sy = (v: number) => padY + (1 - v) * innerH; const train = Array.from({ length: N }, (_, i) => { const t = i / (N - 1); return Math.max(0.05, 0.9 * Math.exp(-t * 3.6)); }); const val = Array.from({ length: N }, (_, i) => { const t = i / (N - 1); const baseline = 0.95 * Math.exp(-t * 2.6); const overfit = 0.55 * Math.max(0, t - 0.45) ** 1.8; return Math.min(1, Math.max(0.1, baseline + overfit)); }); const stopIdx = val.indexOf(Math.min(...val)); return ( {/* Axes */} `${sx(i)},${sy(v)}`).join(" L ")}`} fill="none" stroke={COLORS.accent} strokeWidth={1.5} initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ duration: 1.0 }} /> `${sx(i)},${sy(v)}`).join(" L ")}`} fill="none" stroke={COLORS.honey} strokeWidth={1.5} initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} transition={{ duration: 1.0, delay: 0.2 }} /> {/* Stop marker */} stop {/* Legend */} train val stop when validation loss starts climbing ); }