ai-learning / components /viz /GradDescentExplainer.tsx
samuellimabraz's picture
feat: initial app
7198b5e unverified
Raw
History Blame Contribute Delete
5.36 kB
"use client";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import { COLORS, VizFrame } from "./common";
import { useReducedMotion } from "@/lib/hooks/useReducedMotion";
/**
* 1D gradient descent on a clean convex parabola, with the tangent line drawn
* at the current point and the update Δθ = -η ∇L visualised as a horizontal
* arrow. Auto-iterates and stops near the minimum.
*/
export function GradDescentExplainer({
width = 760,
height = 420,
}: {
width?: number;
height?: number;
}) {
const padX = 60;
const padY = 50;
const xMin = -4;
const xMax = 4;
const yMin = 0;
const yMax = 10;
const sx = (x: number) => padX + ((x - xMin) / (xMax - xMin)) * (width - padX * 2);
const sy = (y: number) => height - padY - ((y - yMin) / (yMax - yMin)) * (height - padY * 2);
// L(θ) = (θ - 1)² + 1
const L = (t: number) => (t - 1) ** 2 + 1;
const dL = (t: number) => 2 * (t - 1);
const [theta, setTheta] = useState(-3);
const [lr, setLr] = useState(0.18);
const [paused, setPaused] = useState(false);
const reduce = useReducedMotion();
useEffect(() => {
if (paused || reduce) return;
const id = setInterval(() => {
setTheta((t) => {
if (Math.abs(t - 1) < 0.05) return -3; // restart
return t - lr * dL(t);
});
}, 220);
return () => clearInterval(id);
}, [lr, paused, reduce]);
const xs = Array.from({ length: 200 }, (_, i) => xMin + (i / 199) * (xMax - xMin));
const path = `M ${xs.map((x) => `${sx(x)},${sy(L(x))}`).join(" L ")}`;
// Tangent
const slope = dL(theta);
const tangentX1 = theta - 1.0;
const tangentX2 = theta + 1.0;
const tangentY1 = L(theta) + slope * (tangentX1 - theta);
const tangentY2 = L(theta) + slope * (tangentX2 - theta);
return (
<div className="flex w-full max-w-full flex-col items-center">
<VizFrame width={width} height={height} caption="θ ← θ − η · ∇L(θ)">
<svg viewBox={`0 0 ${width} ${height}`} className="h-full w-full">
{/* Axes */}
<line x1={padX} x2={width - padX} y1={sy(0)} y2={sy(0)} stroke={COLORS.ink} strokeOpacity={0.3} />
<line x1={sx(0)} x2={sx(0)} y1={padY} y2={height - padY} stroke={COLORS.ink} strokeOpacity={0.18} />
<text x={width - padX} y={sy(0) + 18} textAnchor="end" fontSize={11} fontFamily="JetBrains Mono, monospace" fill={COLORS.muted}>
θ
</text>
<text x={padX - 10} y={padY - 8} textAnchor="end" fontSize={11} fontFamily="JetBrains Mono, monospace" fill={COLORS.muted}>
L(θ)
</text>
{/* Loss curve */}
<path d={path} fill="none" stroke={COLORS.accent} strokeWidth={1.6} />
{/* Tangent line at θ */}
<line
x1={sx(tangentX1)}
y1={sy(tangentY1)}
x2={sx(tangentX2)}
y2={sy(tangentY2)}
stroke={COLORS.honey}
strokeWidth={1.4}
/>
{/* Slope label */}
<text
x={sx(theta) + 14}
y={sy(L(theta)) - 14}
fontSize={11}
fontFamily="JetBrains Mono, monospace"
fill={COLORS.honey}
>
∇L = {slope.toFixed(2)}
</text>
{/* Δθ horizontal arrow */}
<motion.line
x1={sx(theta)}
y1={sy(L(theta))}
x2={sx(theta - lr * slope)}
y2={sy(L(theta))}
stroke={COLORS.ink}
strokeWidth={2}
animate={{ x1: sx(theta), y1: sy(L(theta)), x2: sx(theta - lr * slope), y2: sy(L(theta)) }}
transition={{ duration: 0.2 }}
/>
{/* Marker */}
<motion.circle
cx={sx(theta)}
cy={sy(L(theta))}
r={6}
fill={COLORS.ink}
stroke={COLORS.surface}
strokeWidth={2}
animate={{ cx: sx(theta), cy: sy(L(theta)) }}
transition={{ duration: 0.2 }}
/>
{/* Minimum */}
<line
x1={sx(1)}
x2={sx(1)}
y1={padY}
y2={height - padY}
stroke={COLORS.muted}
strokeOpacity={0.4}
strokeDasharray="3 3"
/>
<text
x={sx(1)}
y={padY - 6}
textAnchor="middle"
fontSize={11}
fontFamily="JetBrains Mono, monospace"
fill={COLORS.muted}
>
θ*
</text>
</svg>
</VizFrame>
<div className="mt-3 grid w-full max-w-[640px] grid-cols-2 gap-4 font-mono text-[11px] uppercase tracking-[0.12em] text-muted">
<label className="flex flex-col">
η = {lr.toFixed(2)}
<input
type="range"
min={0.02}
max={0.6}
step={0.01}
value={lr}
onChange={(e) => setLr(parseFloat(e.target.value))}
className="mt-1 w-full accent-ink"
/>
</label>
<button
type="button"
onClick={() => setPaused((p) => !p)}
className="self-end rounded-md border border-stroke bg-surface px-3 py-1.5 transition hover:border-ink hover:text-ink"
>
{paused ? "play" : "pause"}
</button>
</div>
</div>
);
}