ai-learning / components /viz /NoiseVsClean.tsx
samuellimabraz's picture
feat: initial app
7198b5e unverified
Raw
History Blame Contribute Delete
4.35 kB
"use client";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import { COLORS, VizFrame } from "./common";
function rng(seed: number) {
let s = seed;
return () => {
s = (s * 1664525 + 1013904223) % 4294967296;
return s / 4294967296 - 0.5;
};
}
function genPoints(N: number, slope: number, intercept: number, noise: number, seed: number) {
const r = rng(seed);
return Array.from({ length: N }, () => {
const x = (r() + 0.5) * 9 + 0.5;
const y = slope * x + intercept + r() * noise;
return { x, y };
});
}
function fitLine(pts: { x: number; y: number }[]) {
const n = pts.length;
const sx = pts.reduce((a, p) => a + p.x, 0) / n;
const sy = pts.reduce((a, p) => a + p.y, 0) / n;
const sxx = pts.reduce((a, p) => a + (p.x - sx) ** 2, 0);
const sxy = pts.reduce((a, p) => a + (p.x - sx) * (p.y - sy), 0);
const w = sxy / sxx;
return { w, b: sy - w * sx };
}
function Panel({
title,
noise,
width,
height,
seed,
truth,
}: {
title: string;
noise: number;
width: number;
height: number;
seed: number;
truth: { w: number; b: number };
}) {
const padX = 36;
const padY = 30;
const xMin = 0;
const xMax = 10;
const yMin = 0;
const yMax = 9;
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);
const [seedTick, setSeedTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setSeedTick((t) => t + 1), 1800);
return () => clearInterval(id);
}, []);
const pts = genPoints(40, truth.w, truth.b, noise, seed + seedTick * 1000);
const fit = fitLine(pts);
return (
<g>
{/* Frame */}
<rect
x={0}
y={0}
width={width}
height={height}
fill={COLORS.surface}
stroke={COLORS.stroke}
/>
<text
x={padX}
y={padY - 10}
fontSize={11}
fontFamily="JetBrains Mono, monospace"
fill={COLORS.muted}
style={{ textTransform: "uppercase", letterSpacing: "0.14em" }}
>
{title}
</text>
{/* Axes */}
<line
x1={padX}
x2={width - padX}
y1={height - padY}
y2={height - padY}
stroke={COLORS.ink}
strokeOpacity={0.3}
/>
<line
x1={padX}
x2={padX}
y1={padY}
y2={height - padY}
stroke={COLORS.ink}
strokeOpacity={0.3}
/>
{/* Truth line */}
<line
x1={sx(xMin)}
y1={sy(truth.w * xMin + truth.b)}
x2={sx(xMax)}
y2={sy(truth.w * xMax + truth.b)}
stroke={COLORS.muted}
strokeOpacity={0.7}
strokeDasharray="3 4"
strokeWidth={1}
/>
{/* Points */}
{pts.map((p, i) => (
<circle key={i} cx={sx(p.x)} cy={sy(p.y)} r={3} fill={COLORS.ink} />
))}
{/* Fit line */}
<motion.line
animate={{
x1: sx(xMin),
y1: sy(fit.w * xMin + fit.b),
x2: sx(xMax),
y2: sy(fit.w * xMax + fit.b),
}}
transition={{ duration: 0.45, ease: "easeOut" }}
stroke={COLORS.accent}
strokeWidth={1.75}
/>
{/* Stats */}
<text
x={width - padX}
y={padY - 10}
textAnchor="end"
fontSize={11}
fontFamily="JetBrains Mono, monospace"
fill={COLORS.muted}
>
ŵ {fit.w.toFixed(2)} · b̂ {fit.b.toFixed(2)}
</text>
</g>
);
}
export function NoiseVsClean({
width = 880,
height = 380,
}: {
width?: number;
height?: number;
}) {
const panelW = (width - 30) / 2;
const panelH = height - 30;
const truth = { w: 0.6, b: 1.2 };
return (
<VizFrame width={width} height={height} caption="same model, same truth — noise alone moves the fit">
<svg viewBox={`0 0 ${width} ${height}`} className="h-full w-full">
<g transform="translate(10, 20)">
<Panel title="Clean labels" noise={0.4} seed={3} width={panelW} height={panelH} truth={truth} />
</g>
<g transform={`translate(${panelW + 20}, 20)`}>
<Panel title="Noisy labels" noise={2.6} seed={11} width={panelW} height={panelH} truth={truth} />
</g>
</svg>
</VizFrame>
);
}