"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
);
}