Spaces:
Running
Running
feat: kmeans viz, new media via LFS, classification layout fix
Browse files- .gitattributes +4 -0
- .gitignore +1 -0
- components/viz/ClassificationDemo.tsx +189 -0
- components/viz/KMeans.tsx +243 -77
- components/viz/VideoDemo.tsx +60 -0
- content/slides/ch04-classical-ml.tsx +94 -9
- content/slides/ch05-deep-learning.tsx +32 -21
- content/slides/ch07-cv-tasks.tsx +91 -38
- public/team/01.jpg +3 -0
- public/team/02.jpg +3 -0
- public/team/03.jpg +3 -0
- public/team/04.jpg +3 -0
- public/team/05.jpg +3 -0
- public/team/det-1.mp4 +3 -0
- public/team/det-2.mp4 +3 -0
- public/team/seg-1.mp4 +3 -0
- public/team/seg-2.mp4 +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -5,3 +5,4 @@ out
|
|
| 5 |
*.log
|
| 6 |
.env*
|
| 7 |
.vercel
|
|
|
|
|
|
| 5 |
*.log
|
| 6 |
.env*
|
| 7 |
.vercel
|
| 8 |
+
*.tsbuildinfo
|
components/viz/ClassificationDemo.tsx
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { motion } from "framer-motion";
|
| 4 |
+
import { useEffect, useState } from "react";
|
| 5 |
+
import { COLORS } from "./common";
|
| 6 |
+
import { useReducedMotion } from "@/lib/hooks/useReducedMotion";
|
| 7 |
+
|
| 8 |
+
type Sample = {
|
| 9 |
+
src: string;
|
| 10 |
+
alt: string;
|
| 11 |
+
// First entry is the predicted label (highest confidence). Probabilities
|
| 12 |
+
// must sum to (approximately) 1.
|
| 13 |
+
probs: { label: string; p: number }[];
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const SAMPLES: Sample[] = [
|
| 17 |
+
{
|
| 18 |
+
src: "/team/01.jpg",
|
| 19 |
+
alt: "team member 1",
|
| 20 |
+
probs: [
|
| 21 |
+
{ label: "que mulher maravilhosa (UAU)", p: 0.94 },
|
| 22 |
+
{ label: "readstone", p: 0.030 },
|
| 23 |
+
{ label: "modelo de comercial de shampoo", p: 0.012 },
|
| 24 |
+
{ label: "sereia", p: 0.011 },
|
| 25 |
+
{ label: "engenheira da computação", p: 0.007 },
|
| 26 |
+
],
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
src: "/team/02.jpg",
|
| 30 |
+
alt: "team member 2",
|
| 31 |
+
probs: [
|
| 32 |
+
{ label: "frutinha++", p: 0.91 },
|
| 33 |
+
{ label: "morango maduro", p: 0.040 },
|
| 34 |
+
{ label: "compota artesanal", p: 0.025 },
|
| 35 |
+
{ label: "smoothie de açaí", p: 0.015 },
|
| 36 |
+
{ label: "fruta exótica", p: 0.010 },
|
| 37 |
+
],
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
src: "/team/03.jpg",
|
| 41 |
+
alt: "team member 3",
|
| 42 |
+
probs: [
|
| 43 |
+
{ label: "homens extremamente atraentes", p: 0.96 },
|
| 44 |
+
{ label: "testosterona", p: 0.018 },
|
| 45 |
+
{ label: "hardware", p: 0.012 },
|
| 46 |
+
{ label: "modelos", p: 0.006 },
|
| 47 |
+
{ label: "gambiarra", p: 0.004 },
|
| 48 |
+
],
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
src: "/team/04.jpg",
|
| 52 |
+
alt: "team member 4",
|
| 53 |
+
probs: [
|
| 54 |
+
{ label: "Jorge", p: 0.97 },
|
| 55 |
+
{ label: "homem em flagrante", p: 0.015 },
|
| 56 |
+
{ label: "professor disfarçado", p: 0.008 },
|
| 57 |
+
{ label: "George Clooney brasileiro", p: 0.005 },
|
| 58 |
+
{ label: "vendedor de seguros", p: 0.002 },
|
| 59 |
+
],
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
src: "/team/05.jpg",
|
| 63 |
+
alt: "team member 5",
|
| 64 |
+
probs: [
|
| 65 |
+
{ label: "boiadeira", p: 0.62 },
|
| 66 |
+
{ label: "princesa da roça", p: 0.31 },
|
| 67 |
+
{ label: "agro pop", p: 0.040 },
|
| 68 |
+
{ label: "festa junina", p: 0.020 },
|
| 69 |
+
{ label: "cowgirl", p: 0.010 },
|
| 70 |
+
],
|
| 71 |
+
},
|
| 72 |
+
];
|
| 73 |
+
|
| 74 |
+
export function ClassificationDemo({
|
| 75 |
+
intervalMs = 5500,
|
| 76 |
+
maxImageHeight = 360,
|
| 77 |
+
}: {
|
| 78 |
+
intervalMs?: number;
|
| 79 |
+
maxImageHeight?: number;
|
| 80 |
+
}) {
|
| 81 |
+
const [idx, setIdx] = useState(0);
|
| 82 |
+
const [paused, setPaused] = useState(false);
|
| 83 |
+
const reduce = useReducedMotion();
|
| 84 |
+
|
| 85 |
+
useEffect(() => {
|
| 86 |
+
if (paused || reduce) return;
|
| 87 |
+
const id = setInterval(
|
| 88 |
+
() => setIdx((i) => (i + 1) % SAMPLES.length),
|
| 89 |
+
intervalMs,
|
| 90 |
+
);
|
| 91 |
+
return () => clearInterval(id);
|
| 92 |
+
}, [paused, reduce, intervalMs]);
|
| 93 |
+
|
| 94 |
+
const sample = SAMPLES[idx];
|
| 95 |
+
|
| 96 |
+
return (
|
| 97 |
+
<figure className="mx-auto flex w-full max-w-[920px] flex-col items-center">
|
| 98 |
+
<div className="w-full overflow-hidden rounded-md border border-stroke bg-surface">
|
| 99 |
+
<div className="grid grid-cols-5 items-center gap-5 p-5">
|
| 100 |
+
{/* Image — left 2 cols, fixed max-height to keep portrait and
|
| 101 |
+
landscape photos at the same vertical footprint. */}
|
| 102 |
+
<div className="col-span-2 flex items-center justify-center">
|
| 103 |
+
<motion.img
|
| 104 |
+
key={sample.src}
|
| 105 |
+
src={sample.src}
|
| 106 |
+
alt={sample.alt}
|
| 107 |
+
initial={{ opacity: 0 }}
|
| 108 |
+
animate={{ opacity: 1 }}
|
| 109 |
+
transition={{ duration: 0.4 }}
|
| 110 |
+
className="rounded-md border border-stroke object-contain"
|
| 111 |
+
style={{ maxHeight: maxImageHeight, maxWidth: "100%" }}
|
| 112 |
+
/>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
{/* Probability list — right 3 cols, auto-sized */}
|
| 116 |
+
<div className="col-span-3 flex flex-col gap-2.5">
|
| 117 |
+
<div className="mb-1 font-mono text-[11px] uppercase tracking-[0.14em] text-muted">
|
| 118 |
+
predicted distribution
|
| 119 |
+
</div>
|
| 120 |
+
{sample.probs.map((row, i) => {
|
| 121 |
+
const isTop = i === 0;
|
| 122 |
+
const pct = row.p * 100;
|
| 123 |
+
return (
|
| 124 |
+
<div key={`${idx}-${i}`} className="space-y-1">
|
| 125 |
+
<div className="flex items-baseline justify-between gap-3">
|
| 126 |
+
<span
|
| 127 |
+
className={
|
| 128 |
+
"truncate text-[13px] " +
|
| 129 |
+
(isTop ? "text-ink" : "text-muted")
|
| 130 |
+
}
|
| 131 |
+
>
|
| 132 |
+
{row.label}
|
| 133 |
+
</span>
|
| 134 |
+
<span className="font-mono text-[11px] tabular-nums text-ink">
|
| 135 |
+
{pct < 10 ? pct.toFixed(1) : pct.toFixed(0)}%
|
| 136 |
+
</span>
|
| 137 |
+
</div>
|
| 138 |
+
<div className="h-1.5 w-full overflow-hidden rounded-full bg-stroke">
|
| 139 |
+
<motion.div
|
| 140 |
+
key={`${idx}-${i}-bar`}
|
| 141 |
+
initial={{ width: 0 }}
|
| 142 |
+
animate={{ width: `${pct}%` }}
|
| 143 |
+
transition={{
|
| 144 |
+
duration: 0.7,
|
| 145 |
+
delay: 0.15 + i * 0.05,
|
| 146 |
+
ease: [0.22, 0.61, 0.36, 1],
|
| 147 |
+
}}
|
| 148 |
+
className="h-full"
|
| 149 |
+
style={{
|
| 150 |
+
background: isTop ? COLORS.accent : COLORS.honey,
|
| 151 |
+
}}
|
| 152 |
+
/>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
);
|
| 156 |
+
})}
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
<figcaption className="mt-3 text-center font-mono text-[11px] uppercase tracking-[0.12em] text-muted">
|
| 162 |
+
softmax over a class vocabulary — argmax wins
|
| 163 |
+
</figcaption>
|
| 164 |
+
|
| 165 |
+
{/* Thumbnail strip + pause */}
|
| 166 |
+
<div className="mt-3 flex items-center gap-3">
|
| 167 |
+
<div className="flex gap-1.5">
|
| 168 |
+
{SAMPLES.map((_, i) => (
|
| 169 |
+
<button
|
| 170 |
+
key={i}
|
| 171 |
+
type="button"
|
| 172 |
+
aria-label={`Sample ${i + 1}`}
|
| 173 |
+
onClick={() => setIdx(i)}
|
| 174 |
+
data-active={i === idx}
|
| 175 |
+
className="h-1.5 w-1.5 rounded-full bg-stroke transition-all hover:bg-muted data-[active=true]:w-6 data-[active=true]:bg-ink"
|
| 176 |
+
/>
|
| 177 |
+
))}
|
| 178 |
+
</div>
|
| 179 |
+
<button
|
| 180 |
+
type="button"
|
| 181 |
+
onClick={() => setPaused((p) => !p)}
|
| 182 |
+
className="rounded-md border border-stroke bg-surface px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.12em] text-muted transition hover:border-ink hover:text-ink"
|
| 183 |
+
>
|
| 184 |
+
{paused ? "play" : "pause"}
|
| 185 |
+
</button>
|
| 186 |
+
</div>
|
| 187 |
+
</figure>
|
| 188 |
+
);
|
| 189 |
+
}
|
components/viz/KMeans.tsx
CHANGED
|
@@ -1,9 +1,18 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import {
|
|
|
|
| 4 |
import { COLORS, VizFrame } from "./common";
|
|
|
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
let s = seed;
|
| 8 |
return () => {
|
| 9 |
s = (s * 1664525 + 1013904223) % 4294967296;
|
|
@@ -11,132 +20,289 @@ function rng(seed = 7) {
|
|
| 11 |
};
|
| 12 |
}
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
const r = rng(11);
|
| 16 |
-
const
|
| 17 |
-
const pts:
|
| 18 |
-
|
| 19 |
for (let i = 0; i < perCluster; i++) {
|
| 20 |
pts.push({
|
| 21 |
-
x: c
|
| 22 |
-
y: c
|
| 23 |
-
trueK: k,
|
| 24 |
});
|
| 25 |
}
|
| 26 |
});
|
| 27 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
-
function
|
| 31 |
-
points
|
| 32 |
-
|
| 33 |
-
) {
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
);
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
const cls = points.filter((_, i) => assign[i] === k);
|
| 42 |
-
if (!cls.length) return
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
| 46 |
});
|
| 47 |
-
return { assign, next };
|
| 48 |
}
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
export function KMeans({
|
| 53 |
width = 720,
|
| 54 |
-
height =
|
| 55 |
-
K =
|
| 56 |
}: {
|
| 57 |
width?: number;
|
| 58 |
height?: number;
|
| 59 |
K?: number;
|
| 60 |
}) {
|
| 61 |
const padX = 50;
|
| 62 |
-
const padY =
|
| 63 |
-
const
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
const sx = (x: number) => padX + (x / 10) * (width - padX * 2);
|
| 72 |
-
const sy = (y: number) => height - padY - (y / 8) * (height - padY * 2);
|
| 73 |
|
| 74 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
setCentroids(next);
|
| 79 |
-
setIter((i) => i + 1);
|
| 80 |
-
};
|
| 81 |
-
const reset = () => {
|
| 82 |
setCentroids(initial);
|
| 83 |
setIter(0);
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
return (
|
| 87 |
<div className="flex w-full max-w-full flex-col items-center">
|
| 88 |
-
<VizFrame
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
<svg viewBox={`0 0 ${width} ${height}`} className="h-full w-full">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
{points.map((p, i) => (
|
| 91 |
<circle
|
| 92 |
-
key={i}
|
| 93 |
cx={sx(p.x)}
|
| 94 |
cy={sy(p.y)}
|
| 95 |
r={3}
|
| 96 |
fill={PALETTE[assign[i] % PALETTE.length]}
|
| 97 |
-
fillOpacity={0.
|
| 98 |
/>
|
| 99 |
))}
|
|
|
|
| 100 |
{centroids.map((c, k) => (
|
| 101 |
-
<g
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
fill={PALETTE[k % PALETTE.length]}
|
| 107 |
stroke={COLORS.ink}
|
| 108 |
strokeWidth={1.5}
|
| 109 |
/>
|
| 110 |
-
<text
|
| 111 |
-
|
| 112 |
-
|
| 113 |
textAnchor="middle"
|
| 114 |
fontSize={11}
|
| 115 |
fill={COLORS.surface}
|
| 116 |
fontFamily="JetBrains Mono, monospace"
|
| 117 |
>
|
| 118 |
-
{k + 1}
|
| 119 |
-
</text>
|
| 120 |
-
</g>
|
| 121 |
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
</svg>
|
| 123 |
</VizFrame>
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
</div>
|
| 141 |
</div>
|
| 142 |
);
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import { motion } from "framer-motion";
|
| 4 |
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
| 5 |
import { COLORS, VizFrame } from "./common";
|
| 6 |
+
import { useReducedMotion } from "@/lib/hooks/useReducedMotion";
|
| 7 |
|
| 8 |
+
type Pt = { x: number; y: number };
|
| 9 |
+
|
| 10 |
+
const X_MIN = 0.5;
|
| 11 |
+
const X_MAX = 9.5;
|
| 12 |
+
const Y_MIN = 0.5;
|
| 13 |
+
const Y_MAX = 7.5;
|
| 14 |
+
|
| 15 |
+
function rng(seed: number) {
|
| 16 |
let s = seed;
|
| 17 |
return () => {
|
| 18 |
s = (s * 1664525 + 1013904223) % 4294967296;
|
|
|
|
| 20 |
};
|
| 21 |
}
|
| 22 |
|
| 23 |
+
// Well-separated cluster centres — pick K from a fixed pool placed at the
|
| 24 |
+
// corners and middles of the plot area so the groups stay visually distinct.
|
| 25 |
+
const CENTRE_POOL: Pt[] = [
|
| 26 |
+
{ x: 1.8, y: 1.6 },
|
| 27 |
+
{ x: 8.2, y: 1.6 },
|
| 28 |
+
{ x: 8.4, y: 6.3 },
|
| 29 |
+
{ x: 1.6, y: 6.2 },
|
| 30 |
+
{ x: 5.0, y: 4.0 },
|
| 31 |
+
];
|
| 32 |
+
|
| 33 |
+
function genPoints(K: number, perCluster: number) {
|
| 34 |
const r = rng(11);
|
| 35 |
+
const centres = CENTRE_POOL.slice(0, K);
|
| 36 |
+
const pts: Pt[] = [];
|
| 37 |
+
centres.forEach((c) => {
|
| 38 |
for (let i = 0; i < perCluster; i++) {
|
| 39 |
pts.push({
|
| 40 |
+
x: c.x + (r() - 0.5) * 1.6,
|
| 41 |
+
y: c.y + (r() - 0.5) * 1.6,
|
|
|
|
| 42 |
});
|
| 43 |
}
|
| 44 |
});
|
| 45 |
+
return pts;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Random initial centroids spread across the *full* plot — not just where the
|
| 49 |
+
// data lives — so the first few iterations clearly show centroids moving.
|
| 50 |
+
function genInitial(K: number, seed: number): Pt[] {
|
| 51 |
+
const r = rng(seed);
|
| 52 |
+
return Array.from({ length: K }, () => ({
|
| 53 |
+
x: X_MIN + r() * (X_MAX - X_MIN),
|
| 54 |
+
y: Y_MIN + r() * (Y_MAX - Y_MIN),
|
| 55 |
+
}));
|
| 56 |
}
|
| 57 |
|
| 58 |
+
function assignStep(points: Pt[], centroids: Pt[]): number[] {
|
| 59 |
+
return points.map((p) =>
|
| 60 |
+
centroids.reduce(
|
| 61 |
+
(best, c, k) => {
|
| 62 |
+
const d = (p.x - c.x) ** 2 + (p.y - c.y) ** 2;
|
| 63 |
+
return d < best.d ? { d, k } : best;
|
| 64 |
+
},
|
| 65 |
+
{ d: Infinity, k: 0 },
|
| 66 |
+
).k,
|
| 67 |
);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function updateStep(points: Pt[], assign: number[], K: number, prev: Pt[]): Pt[] {
|
| 71 |
+
return Array.from({ length: K }, (_, k) => {
|
| 72 |
const cls = points.filter((_, i) => assign[i] === k);
|
| 73 |
+
if (!cls.length) return prev[k];
|
| 74 |
+
return {
|
| 75 |
+
x: cls.reduce((a, p) => a + p.x, 0) / cls.length,
|
| 76 |
+
y: cls.reduce((a, p) => a + p.y, 0) / cls.length,
|
| 77 |
+
};
|
| 78 |
});
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
+
function inertia(points: Pt[], assign: number[], centroids: Pt[]): number {
|
| 82 |
+
return points.reduce((acc, p, i) => {
|
| 83 |
+
const c = centroids[assign[i]];
|
| 84 |
+
return acc + (p.x - c.x) ** 2 + (p.y - c.y) ** 2;
|
| 85 |
+
}, 0);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
const PALETTE = [COLORS.accent, COLORS.honey, COLORS.green, COLORS.red, "#7E57C2"];
|
| 89 |
+
|
| 90 |
+
type Phase = "assign" | "update";
|
| 91 |
|
| 92 |
export function KMeans({
|
| 93 |
width = 720,
|
| 94 |
+
height = 500,
|
| 95 |
+
K = 4,
|
| 96 |
}: {
|
| 97 |
width?: number;
|
| 98 |
height?: number;
|
| 99 |
K?: number;
|
| 100 |
}) {
|
| 101 |
const padX = 50;
|
| 102 |
+
const padY = 50;
|
| 103 |
+
const points = useMemo(() => genPoints(K, 28), [K]);
|
| 104 |
+
// Bumped on every Reset; drives a fresh random centroid initialisation.
|
| 105 |
+
const [seedTick, setSeedTick] = useState(0);
|
| 106 |
+
const initial = useMemo(
|
| 107 |
+
() => genInitial(K, 1 + seedTick * 7919),
|
| 108 |
+
[K, seedTick],
|
| 109 |
+
);
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
const [centroids, setCentroids] = useState<Pt[]>(initial);
|
| 112 |
+
const [iter, setIter] = useState(0);
|
| 113 |
+
const [phase, setPhase] = useState<Phase>("assign");
|
| 114 |
+
const [playing, setPlaying] = useState(false);
|
| 115 |
+
const reduce = useReducedMotion();
|
| 116 |
|
| 117 |
+
// Whenever the seed bumps, reset state to the new initial centroids.
|
| 118 |
+
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
setCentroids(initial);
|
| 120 |
setIter(0);
|
| 121 |
+
setPhase("assign");
|
| 122 |
+
setPlaying(false);
|
| 123 |
+
}, [initial]);
|
| 124 |
+
|
| 125 |
+
const sx = useCallback(
|
| 126 |
+
(x: number) => padX + (x / 10) * (width - padX * 2),
|
| 127 |
+
[width],
|
| 128 |
+
);
|
| 129 |
+
const sy = useCallback(
|
| 130 |
+
(y: number) => height - padY - (y / 8) * (height - padY * 2),
|
| 131 |
+
[height],
|
| 132 |
+
);
|
| 133 |
+
|
| 134 |
+
const assign = useMemo(
|
| 135 |
+
() => assignStep(points, centroids),
|
| 136 |
+
[points, centroids],
|
| 137 |
+
);
|
| 138 |
+
const J = useMemo(
|
| 139 |
+
() => inertia(points, assign, centroids),
|
| 140 |
+
[points, assign, centroids],
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
// Run the next half-step. assign → update → next iter.
|
| 144 |
+
const advance = useCallback(() => {
|
| 145 |
+
if (phase === "assign") {
|
| 146 |
+
setPhase("update");
|
| 147 |
+
} else {
|
| 148 |
+
const next = updateStep(points, assign, K, centroids);
|
| 149 |
+
const moved = next.some(
|
| 150 |
+
(c, k) =>
|
| 151 |
+
Math.abs(c.x - centroids[k].x) > 1e-4 ||
|
| 152 |
+
Math.abs(c.y - centroids[k].y) > 1e-4,
|
| 153 |
+
);
|
| 154 |
+
setCentroids(next);
|
| 155 |
+
setIter((i) => i + 1);
|
| 156 |
+
setPhase("assign");
|
| 157 |
+
if (!moved) setPlaying(false);
|
| 158 |
+
}
|
| 159 |
+
}, [phase, points, assign, K, centroids]);
|
| 160 |
+
|
| 161 |
+
const advanceRef = useRef(advance);
|
| 162 |
+
advanceRef.current = advance;
|
| 163 |
+
|
| 164 |
+
useEffect(() => {
|
| 165 |
+
if (!playing || reduce) return;
|
| 166 |
+
const id = setInterval(() => advanceRef.current(), 900);
|
| 167 |
+
return () => clearInterval(id);
|
| 168 |
+
}, [playing, reduce]);
|
| 169 |
+
|
| 170 |
+
// Reset draws a *new* random initial centroid placement each time.
|
| 171 |
+
const reset = () => setSeedTick((t) => t + 1);
|
| 172 |
+
|
| 173 |
+
// For the assign phase, draw a thin line from each point to its winning
|
| 174 |
+
// centroid. This is the "compute distance, pick the smallest" step made
|
| 175 |
+
// visible.
|
| 176 |
+
const showLinks = phase === "assign";
|
| 177 |
|
| 178 |
return (
|
| 179 |
<div className="flex w-full max-w-full flex-col items-center">
|
| 180 |
+
<VizFrame
|
| 181 |
+
width={width}
|
| 182 |
+
height={height}
|
| 183 |
+
caption={
|
| 184 |
+
phase === "assign"
|
| 185 |
+
? "step 1 — assign each point to the nearest centroid"
|
| 186 |
+
: "step 2 — move each centroid to the mean of its cluster"
|
| 187 |
+
}
|
| 188 |
+
>
|
| 189 |
<svg viewBox={`0 0 ${width} ${height}`} className="h-full w-full">
|
| 190 |
+
{/* Faint outline at the initial centroid spot — anchors the eye. */}
|
| 191 |
+
{initial.map((c, k) => (
|
| 192 |
+
<circle
|
| 193 |
+
key={`init-${k}`}
|
| 194 |
+
cx={sx(c.x)}
|
| 195 |
+
cy={sy(c.y)}
|
| 196 |
+
r={10}
|
| 197 |
+
fill="none"
|
| 198 |
+
stroke={PALETTE[k % PALETTE.length]}
|
| 199 |
+
strokeOpacity={0.35}
|
| 200 |
+
strokeDasharray="3 3"
|
| 201 |
+
strokeWidth={1}
|
| 202 |
+
/>
|
| 203 |
+
))}
|
| 204 |
+
|
| 205 |
+
{showLinks
|
| 206 |
+
? points.map((p, i) => {
|
| 207 |
+
const c = centroids[assign[i]];
|
| 208 |
+
return (
|
| 209 |
+
<line
|
| 210 |
+
key={`l-${i}`}
|
| 211 |
+
x1={sx(p.x)}
|
| 212 |
+
y1={sy(p.y)}
|
| 213 |
+
x2={sx(c.x)}
|
| 214 |
+
y2={sy(c.y)}
|
| 215 |
+
stroke={PALETTE[assign[i] % PALETTE.length]}
|
| 216 |
+
strokeOpacity={0.18}
|
| 217 |
+
strokeWidth={1}
|
| 218 |
+
/>
|
| 219 |
+
);
|
| 220 |
+
})
|
| 221 |
+
: null}
|
| 222 |
+
|
| 223 |
{points.map((p, i) => (
|
| 224 |
<circle
|
| 225 |
+
key={`p-${i}`}
|
| 226 |
cx={sx(p.x)}
|
| 227 |
cy={sy(p.y)}
|
| 228 |
r={3}
|
| 229 |
fill={PALETTE[assign[i] % PALETTE.length]}
|
| 230 |
+
fillOpacity={0.85}
|
| 231 |
/>
|
| 232 |
))}
|
| 233 |
+
|
| 234 |
{centroids.map((c, k) => (
|
| 235 |
+
<motion.g
|
| 236 |
+
key={`c-${k}`}
|
| 237 |
+
animate={{ cx: sx(c.x), cy: sy(c.y) }}
|
| 238 |
+
transition={{ duration: 0.5, ease: [0.22, 0.61, 0.36, 1] }}
|
| 239 |
+
>
|
| 240 |
+
<motion.circle
|
| 241 |
+
animate={{ cx: sx(c.x), cy: sy(c.y) }}
|
| 242 |
+
transition={{ duration: 0.5, ease: [0.22, 0.61, 0.36, 1] }}
|
| 243 |
+
r={10}
|
| 244 |
fill={PALETTE[k % PALETTE.length]}
|
| 245 |
stroke={COLORS.ink}
|
| 246 |
strokeWidth={1.5}
|
| 247 |
/>
|
| 248 |
+
<motion.text
|
| 249 |
+
animate={{ x: sx(c.x), y: sy(c.y) + 4 }}
|
| 250 |
+
transition={{ duration: 0.5, ease: [0.22, 0.61, 0.36, 1] }}
|
| 251 |
textAnchor="middle"
|
| 252 |
fontSize={11}
|
| 253 |
fill={COLORS.surface}
|
| 254 |
fontFamily="JetBrains Mono, monospace"
|
| 255 |
>
|
| 256 |
+
μ{k + 1}
|
| 257 |
+
</motion.text>
|
| 258 |
+
</motion.g>
|
| 259 |
))}
|
| 260 |
+
|
| 261 |
+
{/* Step badge top-left of plot area */}
|
| 262 |
+
<g transform={`translate(${padX}, ${padY - 22})`}>
|
| 263 |
+
<text
|
| 264 |
+
fontSize={11}
|
| 265 |
+
fontFamily="JetBrains Mono, monospace"
|
| 266 |
+
fill={phase === "assign" ? COLORS.accent : COLORS.honey}
|
| 267 |
+
style={{ textTransform: "uppercase", letterSpacing: "0.16em" }}
|
| 268 |
+
>
|
| 269 |
+
{phase === "assign"
|
| 270 |
+
? `iter ${iter} · assignment`
|
| 271 |
+
: `iter ${iter} · update`}
|
| 272 |
+
</text>
|
| 273 |
+
</g>
|
| 274 |
</svg>
|
| 275 |
</VizFrame>
|
| 276 |
+
|
| 277 |
+
<div className="mt-4 flex w-full max-w-[640px] flex-wrap items-center justify-between gap-3 font-mono text-[11px] uppercase tracking-[0.12em]">
|
| 278 |
+
<div className="flex items-center gap-2">
|
| 279 |
+
<button
|
| 280 |
+
type="button"
|
| 281 |
+
onClick={advance}
|
| 282 |
+
className="rounded-md border border-stroke bg-surface px-3 py-1.5 text-muted transition hover:border-ink hover:text-ink"
|
| 283 |
+
>
|
| 284 |
+
Step
|
| 285 |
+
</button>
|
| 286 |
+
<button
|
| 287 |
+
type="button"
|
| 288 |
+
onClick={() => setPlaying((p) => !p)}
|
| 289 |
+
className="rounded-md border border-stroke bg-surface px-3 py-1.5 text-muted transition hover:border-ink hover:text-ink"
|
| 290 |
+
>
|
| 291 |
+
{playing ? "Pause" : "Play"}
|
| 292 |
+
</button>
|
| 293 |
+
<button
|
| 294 |
+
type="button"
|
| 295 |
+
onClick={reset}
|
| 296 |
+
className="rounded-md border border-stroke bg-surface px-3 py-1.5 text-muted transition hover:border-ink hover:text-ink"
|
| 297 |
+
>
|
| 298 |
+
Reset
|
| 299 |
+
</button>
|
| 300 |
+
</div>
|
| 301 |
+
<div className="text-muted">
|
| 302 |
+
iter <span className="text-ink">{String(iter).padStart(2, "0")}</span>{" "}
|
| 303 |
+
· J ={" "}
|
| 304 |
+
<span className="text-ink">{J.toFixed(2)}</span>
|
| 305 |
+
</div>
|
| 306 |
</div>
|
| 307 |
</div>
|
| 308 |
);
|
components/viz/VideoDemo.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState } from "react";
|
| 4 |
+
import { COLORS, VizFrame } from "./common";
|
| 5 |
+
|
| 6 |
+
export type VideoClip = {
|
| 7 |
+
src: string;
|
| 8 |
+
label: string;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export function VideoDemo({
|
| 12 |
+
clips,
|
| 13 |
+
caption,
|
| 14 |
+
width = 880,
|
| 15 |
+
height = 500,
|
| 16 |
+
}: {
|
| 17 |
+
clips: VideoClip[];
|
| 18 |
+
caption?: string;
|
| 19 |
+
width?: number;
|
| 20 |
+
height?: number;
|
| 21 |
+
}) {
|
| 22 |
+
const [idx, setIdx] = useState(0);
|
| 23 |
+
const clip = clips[idx];
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<div className="flex w-full max-w-full flex-col items-center">
|
| 27 |
+
<VizFrame width={width} height={height} caption={caption}>
|
| 28 |
+
<div className="flex h-full w-full items-center justify-center bg-bone p-3">
|
| 29 |
+
<video
|
| 30 |
+
key={clip.src}
|
| 31 |
+
src={clip.src}
|
| 32 |
+
autoPlay
|
| 33 |
+
muted
|
| 34 |
+
loop
|
| 35 |
+
playsInline
|
| 36 |
+
controls
|
| 37 |
+
className="max-h-full max-w-full rounded-md border border-stroke bg-black"
|
| 38 |
+
/>
|
| 39 |
+
</div>
|
| 40 |
+
</VizFrame>
|
| 41 |
+
|
| 42 |
+
{clips.length > 1 ? (
|
| 43 |
+
<div className="mt-4 flex flex-wrap items-center gap-2 font-mono text-[11px] uppercase tracking-[0.12em]">
|
| 44 |
+
{clips.map((c, i) => (
|
| 45 |
+
<button
|
| 46 |
+
key={c.src}
|
| 47 |
+
type="button"
|
| 48 |
+
onClick={() => setIdx(i)}
|
| 49 |
+
data-active={i === idx}
|
| 50 |
+
className="rounded-md border border-stroke bg-surface px-3 py-1.5 text-muted transition hover:border-ink hover:text-ink data-[active=true]:border-ink data-[active=true]:text-ink"
|
| 51 |
+
style={i === idx ? { color: COLORS.ink } : undefined}
|
| 52 |
+
>
|
| 53 |
+
{c.label}
|
| 54 |
+
</button>
|
| 55 |
+
))}
|
| 56 |
+
</div>
|
| 57 |
+
) : null}
|
| 58 |
+
</div>
|
| 59 |
+
);
|
| 60 |
+
}
|
content/slides/ch04-classical-ml.tsx
CHANGED
|
@@ -167,22 +167,107 @@ export const ch04: Chapter = {
|
|
| 167 |
},
|
| 168 |
{
|
| 169 |
id: "ch04-07",
|
| 170 |
-
title: "k-means
|
| 171 |
eyebrow: "Unsupervised baseline",
|
| 172 |
-
layout: "
|
| 173 |
content: (
|
| 174 |
<div className="space-y-4">
|
| 175 |
<p>
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
| 179 |
</p>
|
| 180 |
-
<MBlock>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
<p className="text-muted">
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
</div>
|
| 187 |
),
|
| 188 |
viz: <KMeans />,
|
|
|
|
| 167 |
},
|
| 168 |
{
|
| 169 |
id: "ch04-07",
|
| 170 |
+
title: "k-means — the algorithm",
|
| 171 |
eyebrow: "Unsupervised baseline",
|
| 172 |
+
layout: "prose",
|
| 173 |
content: (
|
| 174 |
<div className="space-y-4">
|
| 175 |
<p>
|
| 176 |
+
Given <M>N</M> points <M>{"\\{x_i\\} \\subset \\mathbb{R}^d"}</M>{" "}
|
| 177 |
+
and a chosen number of clusters <M>K</M>, find an assignment{" "}
|
| 178 |
+
<M>{"r_{ik} \\in \\{0, 1\\}"}</M> and centroids{" "}
|
| 179 |
+
<M>{"\\mu_k \\in \\mathbb{R}^d"}</M> that minimise the{" "}
|
| 180 |
+
<em>within-cluster sum of squares</em> (also called inertia):
|
| 181 |
</p>
|
| 182 |
+
<MBlock>
|
| 183 |
+
{"J = \\sum_{i=1}^{N}\\sum_{k=1}^{K} r_{ik}\\,\\|x_i - \\mu_k\\|^2"}
|
| 184 |
+
</MBlock>
|
| 185 |
+
<p>
|
| 186 |
+
Joint minimisation over <M>r</M> and{" "}
|
| 187 |
+
<M>{"\\mu"}</M> is NP-hard. Lloyd's algorithm (1957) is the
|
| 188 |
+
workhorse heuristic — alternate two closed-form steps until
|
| 189 |
+
nothing changes:
|
| 190 |
+
</p>
|
| 191 |
+
<div className="space-y-3 rounded-md border border-stroke bg-surface px-5 py-4 text-[14px]">
|
| 192 |
+
<div>
|
| 193 |
+
<div className="mb-1 font-mono text-[11px] uppercase tracking-[0.12em] text-accent">
|
| 194 |
+
step 1 · assignment
|
| 195 |
+
</div>
|
| 196 |
+
<p className="mb-2">
|
| 197 |
+
Fix the centroids. Send each point to the closest one.
|
| 198 |
+
</p>
|
| 199 |
+
<MBlock>
|
| 200 |
+
{"r_{ik} = \\begin{cases} 1 & k = \\arg\\min_j \\|x_i - \\mu_j\\|^2 \\\\ 0 & \\text{otherwise} \\end{cases}"}
|
| 201 |
+
</MBlock>
|
| 202 |
+
</div>
|
| 203 |
+
<div>
|
| 204 |
+
<div className="mb-1 font-mono text-[11px] uppercase tracking-[0.12em] text-honey">
|
| 205 |
+
step 2 · update
|
| 206 |
+
</div>
|
| 207 |
+
<p className="mb-2">
|
| 208 |
+
Fix the assignment. Move each centroid to the mean of its
|
| 209 |
+
points (this is the value that minimises <M>J</M> for fixed{" "}
|
| 210 |
+
<M>r</M>):
|
| 211 |
+
</p>
|
| 212 |
+
<MBlock>
|
| 213 |
+
{"\\mu_k = \\frac{\\sum_i r_{ik}\\,x_i}{\\sum_i r_{ik}}"}
|
| 214 |
+
</MBlock>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
<p className="text-muted">
|
| 218 |
+
Both steps strictly decrease <M>J</M> (or leave it unchanged), so
|
| 219 |
+
the algorithm <em>always converges</em> — typically in fewer than
|
| 220 |
+
20 iterations. Cost per iteration:{" "}
|
| 221 |
+
<M>{"O(N \\cdot K \\cdot d)"}</M>.
|
| 222 |
+
</p>
|
| 223 |
+
</div>
|
| 224 |
+
),
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
id: "ch04-07b",
|
| 228 |
+
title: "k-means in action",
|
| 229 |
+
eyebrow: "Watch J fall",
|
| 230 |
+
layout: "split",
|
| 231 |
+
content: (
|
| 232 |
+
<div className="space-y-4">
|
| 233 |
+
<p>
|
| 234 |
+
<span className="font-mono text-[12px] text-accent">Step</span>{" "}
|
| 235 |
+
advances one half-iteration at a time, alternating between the
|
| 236 |
+
two phases. <span className="font-mono text-[12px] text-accent">Play</span>{" "}
|
| 237 |
+
runs to convergence. <span className="font-mono text-[12px]">J</span>{" "}
|
| 238 |
+
is the inertia; watch it monotonically decrease.
|
| 239 |
</p>
|
| 240 |
+
<ul className="space-y-2 text-[14px] text-ink/85">
|
| 241 |
+
<li>
|
| 242 |
+
· Thin lines in the assignment phase show each point pulled to
|
| 243 |
+
its nearest centroid (the <M>{"\\arg\\min"}</M>).
|
| 244 |
+
</li>
|
| 245 |
+
<li>
|
| 246 |
+
· In the update phase, the centroids translate to the cluster
|
| 247 |
+
mean — usually the largest <M>J</M> drop happens here.
|
| 248 |
+
</li>
|
| 249 |
+
<li>
|
| 250 |
+
· The animation stops automatically when no centroid moves.
|
| 251 |
+
</li>
|
| 252 |
+
</ul>
|
| 253 |
+
<Callout label="Gotchas" tone="warm">
|
| 254 |
+
<ul className="space-y-1">
|
| 255 |
+
<li>
|
| 256 |
+
· <strong>Initialisation matters.</strong> Plain k-means is
|
| 257 |
+
sensitive to the starting centroids;{" "}
|
| 258 |
+
<strong>k-means++</strong> seeds them apart on purpose and
|
| 259 |
+
almost always converges to a better minimum.
|
| 260 |
+
</li>
|
| 261 |
+
<li>
|
| 262 |
+
· <strong>You must pick K.</strong> Use the elbow method on{" "}
|
| 263 |
+
<M>J(K)</M> or the silhouette score.
|
| 264 |
+
</li>
|
| 265 |
+
<li>
|
| 266 |
+
· <strong>Assumes spherical, equal-size clusters.</strong>{" "}
|
| 267 |
+
For arbitrary shapes use DBSCAN or a Gaussian mixture model.
|
| 268 |
+
</li>
|
| 269 |
+
</ul>
|
| 270 |
+
</Callout>
|
| 271 |
</div>
|
| 272 |
),
|
| 273 |
viz: <KMeans />,
|
content/slides/ch05-deep-learning.tsx
CHANGED
|
@@ -9,8 +9,18 @@ import { RegularizationViz } from "@/components/viz/RegularizationViz";
|
|
| 9 |
import { Callout } from "@/components/ui/Callout";
|
| 10 |
import { M, MBlock } from "@/components/math/Math";
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
export const ch05: Chapter = {
|
| 16 |
id: "ch05",
|
|
@@ -131,30 +141,31 @@ export const ch05: Chapter = {
|
|
| 131 |
interact is to build a tiny network in a browser and watch it
|
| 132 |
train.
|
| 133 |
</p>
|
| 134 |
-
<
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
<p className="text-muted">
|
| 149 |
Things to look for: how the spiral dataset needs ≥ 2 hidden layers;
|
| 150 |
how ReLU vs sigmoid affects convergence; how a too-large learning
|
| 151 |
rate explodes; how regularisation smooths the boundary.
|
| 152 |
</p>
|
| 153 |
-
<p className="font-mono text-[11px] uppercase tracking-[0.12em] text-muted">
|
| 154 |
-
<span className="text-honey">tip</span> · the playground URL is a
|
| 155 |
-
constant in <code className="font-mono normal-case">ch05-deep-learning.tsx</code>; replace it with
|
| 156 |
-
your own demo.
|
| 157 |
-
</p>
|
| 158 |
</div>
|
| 159 |
),
|
| 160 |
},
|
|
|
|
| 9 |
import { Callout } from "@/components/ui/Callout";
|
| 10 |
import { M, MBlock } from "@/components/math/Math";
|
| 11 |
|
| 12 |
+
const PLAYGROUNDS = [
|
| 13 |
+
{
|
| 14 |
+
name: "Neural Network Playground",
|
| 15 |
+
url: "https://playground.tensorflow.org/",
|
| 16 |
+
note: "TensorFlow · the original interactive playground",
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
name: "Samuel's NN Playground",
|
| 20 |
+
url: "https://samuellimabraz.github.io/#nn-playground",
|
| 21 |
+
note: "custom build · same idea, our notation",
|
| 22 |
+
},
|
| 23 |
+
];
|
| 24 |
|
| 25 |
export const ch05: Chapter = {
|
| 26 |
id: "ch05",
|
|
|
|
| 141 |
interact is to build a tiny network in a browser and watch it
|
| 142 |
train.
|
| 143 |
</p>
|
| 144 |
+
<div className="flex flex-col gap-3">
|
| 145 |
+
{PLAYGROUNDS.map((p) => (
|
| 146 |
+
<a
|
| 147 |
+
key={p.url}
|
| 148 |
+
href={p.url}
|
| 149 |
+
target="_blank"
|
| 150 |
+
rel="noreferrer"
|
| 151 |
+
className="inline-flex items-center gap-3 rounded-md border border-ink/40 bg-surface px-5 py-3 transition hover:border-ink hover:bg-bone"
|
| 152 |
+
>
|
| 153 |
+
<span className="font-mono text-[11px] uppercase tracking-[0.16em] text-muted">
|
| 154 |
+
Open
|
| 155 |
+
</span>
|
| 156 |
+
<span className="font-serif text-lg text-ink">{p.name}</span>
|
| 157 |
+
<span className="ml-auto font-mono text-[11px] text-muted">
|
| 158 |
+
{p.note}
|
| 159 |
+
</span>
|
| 160 |
+
<span className="font-mono text-[11px] text-muted">↗</span>
|
| 161 |
+
</a>
|
| 162 |
+
))}
|
| 163 |
+
</div>
|
| 164 |
<p className="text-muted">
|
| 165 |
Things to look for: how the spiral dataset needs ≥ 2 hidden layers;
|
| 166 |
how ReLU vs sigmoid affects convergence; how a too-large learning
|
| 167 |
rate explodes; how regularisation smooths the boundary.
|
| 168 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
</div>
|
| 170 |
),
|
| 171 |
},
|
content/slides/ch07-cv-tasks.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import type { Chapter } from "@/components/slide/types";
|
| 2 |
import { MaskOverlay } from "@/components/viz/Scene";
|
| 3 |
import { BboxFormats } from "@/components/viz/BboxFormats";
|
|
|
|
|
|
|
| 4 |
import { Callout } from "@/components/ui/Callout";
|
| 5 |
import { M, MBlock } from "@/components/math/Math";
|
| 6 |
|
|
@@ -19,17 +21,24 @@ export const ch07: Chapter = {
|
|
| 19 |
content: (
|
| 20 |
<div className="space-y-4">
|
| 21 |
<p>
|
| 22 |
-
|
| 23 |
-
|
| 24 |
</p>
|
| 25 |
<MBlock>{"\\hat p_k = \\frac{e^{z_k}}{\\sum_j e^{z_j}}"}</MBlock>
|
| 26 |
-
<p
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
| 29 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
</div>
|
| 31 |
),
|
| 32 |
-
viz: <
|
| 33 |
},
|
| 34 |
{
|
| 35 |
id: "ch07-01",
|
|
@@ -39,16 +48,26 @@ export const ch07: Chapter = {
|
|
| 39 |
content: (
|
| 40 |
<div className="space-y-4">
|
| 41 |
<p>
|
| 42 |
-
Predict a
|
| 43 |
-
|
|
|
|
| 44 |
</p>
|
| 45 |
-
<p
|
| 46 |
-
Most Black Bee perception lives here: gates, posts, drones.
|
| 47 |
-
|
|
|
|
| 48 |
</p>
|
| 49 |
</div>
|
| 50 |
),
|
| 51 |
-
viz:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
},
|
| 53 |
{
|
| 54 |
id: "ch07-02",
|
|
@@ -58,22 +77,27 @@ export const ch07: Chapter = {
|
|
| 58 |
content: (
|
| 59 |
<div className="space-y-4">
|
| 60 |
<p>
|
| 61 |
-
Three conventions you will see daily, all describing the same
|
|
|
|
| 62 |
</p>
|
| 63 |
<ul className="space-y-2 text-[14px] text-ink/85">
|
| 64 |
<li>
|
| 65 |
-
<strong>xyxy</strong> · top-left and bottom-right corners. COCO,
|
|
|
|
| 66 |
</li>
|
| 67 |
<li>
|
| 68 |
-
<strong>xywh</strong> · centre with width and height. Internal
|
|
|
|
| 69 |
</li>
|
| 70 |
<li>
|
| 71 |
-
<strong>normalized</strong> · everything divided by image size.
|
|
|
|
| 72 |
</li>
|
| 73 |
</ul>
|
| 74 |
<Callout label="In Nectar">
|
| 75 |
-
<code className="font-mono text-[12px]">FormatConverter</code> in
|
| 76 |
-
handles COCO ↔ YOLO conversion automatically. Chapter 11
|
|
|
|
| 77 |
</Callout>
|
| 78 |
</div>
|
| 79 |
),
|
|
@@ -87,14 +111,28 @@ export const ch07: Chapter = {
|
|
| 87 |
content: (
|
| 88 |
<div className="space-y-4">
|
| 89 |
<p>
|
| 90 |
-
Every pixel gets a class label.
|
| 91 |
-
size as the input
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</p>
|
| 93 |
-
<MBlock>{"\\hat Y \\in \\mathbb{R}^{H \\times W \\times C}, \\quad \\hat y_{ij} = \\arg\\max_c \\hat Y_{ijc}"}</MBlock>
|
| 94 |
<p className="text-muted">
|
| 95 |
-
|
| 96 |
-
|
|
|
|
| 97 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
</div>
|
| 99 |
),
|
| 100 |
viz: <MaskOverlay mode="semantic" />,
|
|
@@ -107,17 +145,31 @@ export const ch07: Chapter = {
|
|
| 107 |
content: (
|
| 108 |
<div className="space-y-4">
|
| 109 |
<p>
|
| 110 |
-
Detection
|
| 111 |
-
mask head on top of Faster R-CNN; YOLO-seg and
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
</p>
|
| 113 |
<p className="text-muted">
|
| 114 |
The Nectar SDK supports this through{" "}
|
| 115 |
-
<code className="font-mono text-[12px]">Segmentor</code> with the
|
| 116 |
-
three frameworks (YOLO, DETR, RF-DETR). See chapter 11.
|
| 117 |
</p>
|
| 118 |
</div>
|
| 119 |
),
|
| 120 |
-
viz:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
},
|
| 122 |
{
|
| 123 |
id: "ch07-05",
|
|
@@ -127,12 +179,13 @@ export const ch07: Chapter = {
|
|
| 127 |
content: (
|
| 128 |
<div className="space-y-4">
|
| 129 |
<p>
|
| 130 |
-
Predict a small set of named points per object — corners of a
|
| 131 |
-
of a body. Either as a heatmap per keypoint, or as a
|
|
|
|
| 132 |
</p>
|
| 133 |
<p className="text-muted">
|
| 134 |
-
Useful when downstream geometry (pose estimation, alignment) needs
|
| 135 |
-
anchors.
|
| 136 |
</p>
|
| 137 |
</div>
|
| 138 |
),
|
|
@@ -146,16 +199,16 @@ export const ch07: Chapter = {
|
|
| 146 |
content: (
|
| 147 |
<div className="space-y-4">
|
| 148 |
<p>
|
| 149 |
-
Per-pixel distance in metres. Stereo, time-of-flight, or learned
|
| 150 |
-
depth from a single RGB image.
|
| 151 |
</p>
|
| 152 |
<p>
|
| 153 |
-
Black Bee uses this directly: the RealSense and OAK-D drivers in
|
| 154 |
-
Nectar vision module return depth alongside colour.
|
| 155 |
</p>
|
| 156 |
<Callout label="Tip" tone="warm">
|
| 157 |
-
Monocular depth is up to a scale factor. Useful for ordering
|
| 158 |
-
useful for absolute distances without calibration.
|
| 159 |
</Callout>
|
| 160 |
</div>
|
| 161 |
),
|
|
|
|
| 1 |
import type { Chapter } from "@/components/slide/types";
|
| 2 |
import { MaskOverlay } from "@/components/viz/Scene";
|
| 3 |
import { BboxFormats } from "@/components/viz/BboxFormats";
|
| 4 |
+
import { ClassificationDemo } from "@/components/viz/ClassificationDemo";
|
| 5 |
+
import { VideoDemo } from "@/components/viz/VideoDemo";
|
| 6 |
import { Callout } from "@/components/ui/Callout";
|
| 7 |
import { M, MBlock } from "@/components/math/Math";
|
| 8 |
|
|
|
|
| 21 |
content: (
|
| 22 |
<div className="space-y-4">
|
| 23 |
<p>
|
| 24 |
+
One label per image. Output is a probability distribution over a
|
| 25 |
+
fixed vocabulary, produced by softmax over the network logits:
|
| 26 |
</p>
|
| 27 |
<MBlock>{"\\hat p_k = \\frac{e^{z_k}}{\\sum_j e^{z_j}}"}</MBlock>
|
| 28 |
+
<p>
|
| 29 |
+
The prediction is <M>{"\\arg\\max_k \\hat p_k"}</M>; loss is
|
| 30 |
+
categorical cross-entropy. Top-1 and top-5 accuracy are the
|
| 31 |
+
standard metrics. ImageNet (1000 classes) was the long-standing
|
| 32 |
+
benchmark.
|
| 33 |
</p>
|
| 34 |
+
<Callout label="Watch">
|
| 35 |
+
Bars on the right are the full distribution, not just the winner.
|
| 36 |
+
Image 5 is the same person as image 1 — see how the model splits
|
| 37 |
+
its mass between two classes when the appearance changes.
|
| 38 |
+
</Callout>
|
| 39 |
</div>
|
| 40 |
),
|
| 41 |
+
viz: <ClassificationDemo />,
|
| 42 |
},
|
| 43 |
{
|
| 44 |
id: "ch07-01",
|
|
|
|
| 48 |
content: (
|
| 49 |
<div className="space-y-4">
|
| 50 |
<p>
|
| 51 |
+
Predict a variable-length list of (box, class, confidence). The
|
| 52 |
+
output structure is what makes detection harder than classification
|
| 53 |
+
— a single image can contain zero or many objects.
|
| 54 |
</p>
|
| 55 |
+
<p>
|
| 56 |
+
Most Black Bee perception lives here: gates, posts, drones. The
|
| 57 |
+
video on the right is one of our trained YOLO models running on a
|
| 58 |
+
real flight log. Chapter 8 unpacks the architecture.
|
| 59 |
</p>
|
| 60 |
</div>
|
| 61 |
),
|
| 62 |
+
viz: (
|
| 63 |
+
<VideoDemo
|
| 64 |
+
caption="real Black Bee mission · YOLO detection on flight footage"
|
| 65 |
+
clips={[
|
| 66 |
+
{ src: "/team/det-1.mp4", label: "clip 1" },
|
| 67 |
+
{ src: "/team/det-2.mp4", label: "clip 2" },
|
| 68 |
+
]}
|
| 69 |
+
/>
|
| 70 |
+
),
|
| 71 |
},
|
| 72 |
{
|
| 73 |
id: "ch07-02",
|
|
|
|
| 77 |
content: (
|
| 78 |
<div className="space-y-4">
|
| 79 |
<p>
|
| 80 |
+
Three conventions you will see daily, all describing the same
|
| 81 |
+
rectangle:
|
| 82 |
</p>
|
| 83 |
<ul className="space-y-2 text-[14px] text-ink/85">
|
| 84 |
<li>
|
| 85 |
+
<strong>xyxy</strong> · top-left and bottom-right corners. COCO,
|
| 86 |
+
supervision.
|
| 87 |
</li>
|
| 88 |
<li>
|
| 89 |
+
<strong>xywh</strong> · centre with width and height. Internal
|
| 90 |
+
to many models.
|
| 91 |
</li>
|
| 92 |
<li>
|
| 93 |
+
<strong>normalized</strong> · everything divided by image size.
|
| 94 |
+
YOLO labels.
|
| 95 |
</li>
|
| 96 |
</ul>
|
| 97 |
<Callout label="In Nectar">
|
| 98 |
+
<code className="font-mono text-[12px]">FormatConverter</code> in
|
| 99 |
+
the SDK handles COCO ↔ YOLO conversion automatically. Chapter 11
|
| 100 |
+
returns to it.
|
| 101 |
</Callout>
|
| 102 |
</div>
|
| 103 |
),
|
|
|
|
| 111 |
content: (
|
| 112 |
<div className="space-y-4">
|
| 113 |
<p>
|
| 114 |
+
Every pixel gets a class label. The output has the same spatial
|
| 115 |
+
size as the input, with one channel per class:
|
| 116 |
+
</p>
|
| 117 |
+
<MBlock>
|
| 118 |
+
{"\\hat Y \\in \\mathbb{R}^{H \\times W \\times C}, \\quad \\hat y_{ij} = \\arg\\max_c \\hat Y_{ijc}"}
|
| 119 |
+
</MBlock>
|
| 120 |
+
<p>
|
| 121 |
+
All pixels of the same class share one mask — "all gate
|
| 122 |
+
pixels", not "this gate vs that gate". Loss is
|
| 123 |
+
per-pixel cross-entropy, often combined with Dice loss to handle
|
| 124 |
+
class imbalance.
|
| 125 |
</p>
|
|
|
|
| 126 |
<p className="text-muted">
|
| 127 |
+
Typical use cases: free-space mapping for autonomous driving, lane
|
| 128 |
+
segmentation, terrain classification from aerial imagery.
|
| 129 |
+
Architectures: U-Net, DeepLab, SegFormer, Mask2Former.
|
| 130 |
</p>
|
| 131 |
+
<Callout>
|
| 132 |
+
We do not use this on Black Bee yet — most of our targets are
|
| 133 |
+
countable objects, where instance segmentation or detection fits
|
| 134 |
+
better.
|
| 135 |
+
</Callout>
|
| 136 |
</div>
|
| 137 |
),
|
| 138 |
viz: <MaskOverlay mode="semantic" />,
|
|
|
|
| 145 |
content: (
|
| 146 |
<div className="space-y-4">
|
| 147 |
<p>
|
| 148 |
+
Detection plus a per-pixel mask, separate for each object. Mask
|
| 149 |
+
R-CNN added a small mask head on top of Faster R-CNN; YOLO-seg and
|
| 150 |
+
DETR-seg do similarly.
|
| 151 |
+
</p>
|
| 152 |
+
<p>
|
| 153 |
+
The video on the right is one of our segmentation models on a real
|
| 154 |
+
flight; each instance gets its own coloured mask, which lets us
|
| 155 |
+
count, sort, or pick objects individually.
|
| 156 |
</p>
|
| 157 |
<p className="text-muted">
|
| 158 |
The Nectar SDK supports this through{" "}
|
| 159 |
+
<code className="font-mono text-[12px]">Segmentor</code> with the
|
| 160 |
+
same three frameworks (YOLO, DETR, RF-DETR). See chapter 11.
|
| 161 |
</p>
|
| 162 |
</div>
|
| 163 |
),
|
| 164 |
+
viz: (
|
| 165 |
+
<VideoDemo
|
| 166 |
+
caption="real Black Bee mission · instance segmentation on flight footage"
|
| 167 |
+
clips={[
|
| 168 |
+
{ src: "/team/seg-2.mp4", label: "clip 1" },
|
| 169 |
+
{ src: "/team/seg-1.mp4", label: "clip 2" },
|
| 170 |
+
]}
|
| 171 |
+
/>
|
| 172 |
+
),
|
| 173 |
},
|
| 174 |
{
|
| 175 |
id: "ch07-05",
|
|
|
|
| 179 |
content: (
|
| 180 |
<div className="space-y-4">
|
| 181 |
<p>
|
| 182 |
+
Predict a small set of named points per object — corners of a
|
| 183 |
+
gate, joints of a body. Either as a heatmap per keypoint, or as a
|
| 184 |
+
regressed coordinate.
|
| 185 |
</p>
|
| 186 |
<p className="text-muted">
|
| 187 |
+
Useful when downstream geometry (pose estimation, alignment) needs
|
| 188 |
+
precise anchors.
|
| 189 |
</p>
|
| 190 |
</div>
|
| 191 |
),
|
|
|
|
| 199 |
content: (
|
| 200 |
<div className="space-y-4">
|
| 201 |
<p>
|
| 202 |
+
Per-pixel distance in metres. Stereo, time-of-flight, or learned
|
| 203 |
+
monocular depth from a single RGB image.
|
| 204 |
</p>
|
| 205 |
<p>
|
| 206 |
+
Black Bee uses this directly: the RealSense and OAK-D drivers in
|
| 207 |
+
the Nectar vision module return depth alongside colour.
|
| 208 |
</p>
|
| 209 |
<Callout label="Tip" tone="warm">
|
| 210 |
+
Monocular depth is up to a scale factor. Useful for ordering
|
| 211 |
+
objects, less useful for absolute distances without calibration.
|
| 212 |
</Callout>
|
| 213 |
</div>
|
| 214 |
),
|
public/team/01.jpg
ADDED
|
Git LFS Details
|
public/team/02.jpg
ADDED
|
Git LFS Details
|
public/team/03.jpg
ADDED
|
Git LFS Details
|
public/team/04.jpg
ADDED
|
Git LFS Details
|
public/team/05.jpg
ADDED
|
Git LFS Details
|
public/team/det-1.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d06d26c358f209b881c5fe426b62bd9f6a17d3b22aa00259caeff109c78fa8ca
|
| 3 |
+
size 3008225
|
public/team/det-2.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:87d841d138b67d4e49a0ba18bae57c76985b4af95f2885faba0a37a81aa42b8a
|
| 3 |
+
size 3549299
|
public/team/seg-1.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3603d67b1a2660df5284649d937073fb8227d7bd9cd95eb0ab5b812c83f4b788
|
| 3 |
+
size 18444491
|
public/team/seg-2.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:34e1a8f2c0ecb4b03c041e530b4caec53470ab3a4a2534574bbd1a8c01b498e7
|
| 3 |
+
size 5527552
|