413 lines
16 KiB
HTML
413 lines
16 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
<title>embedding notebook</title>
|
||
<link rel="stylesheet" href="/static/style.css?v=17" />
|
||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||
<script type="importmap">
|
||
{
|
||
"imports": {
|
||
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
||
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
||
}
|
||
}
|
||
</script>
|
||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3' fill='%231f4e5f'/%3E%3C/svg%3E" />
|
||
<script>
|
||
(function(){try{
|
||
var t=localStorage.getItem('theme');
|
||
if(!t)t=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';
|
||
document.documentElement.setAttribute('data-theme',t);
|
||
}catch(e){}})();
|
||
</script>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="masthead">
|
||
<div>
|
||
<h1 class="title">embedding notebook <em>— drift & projection</em></h1>
|
||
</div>
|
||
<div class="meta">
|
||
<a href="#metrics" class="nav-link">metrics ↓</a>
|
||
<button type="button" class="theme-toggle" id="theme-toggle" aria-label="toggle theme">◐</button>
|
||
</div>
|
||
</header>
|
||
|
||
<details class="dataset-picker intro" id="intro">
|
||
<summary>
|
||
<span class="picker-meta">
|
||
<span class="section-number">§ 0</span>
|
||
<span class="picker-title">introduction</span>
|
||
<span class="picker-selection">
|
||
<span class="lbl">scope</span>
|
||
<code>stability of low-dim embeddings under input drift</code>
|
||
</span>
|
||
</span>
|
||
<span class="picker-toggle" aria-hidden="true"></span>
|
||
</summary>
|
||
|
||
<div class="picker-body">
|
||
<div class="intro-body">
|
||
<div class="intro-prose">
|
||
<p>
|
||
<strong>What this is.</strong> Dimensionality reduction is a workhorse
|
||
for both exploratory visualization and downstream prediction, yet the
|
||
stability of its output under small perturbations of the input is
|
||
rarely examined directly. This notebook takes a narrow, empirical
|
||
approach: a three-dimensional point cloud (§ 1) is perturbed by a
|
||
controlled amount at each of a short sequence of timesteps, the
|
||
selected reducer (§ 2) is applied independently to every snapshot,
|
||
and the resulting trajectory of two-dimensional embeddings is recorded.
|
||
</p>
|
||
<p>
|
||
<strong>What it measures.</strong> Two stability views are logged
|
||
alongside each run and plotted on the
|
||
<a href="/metrics">metrics page</a>. Per-timestep travel —
|
||
‖ y(t) − y(t−1) ‖ —
|
||
captures how much the 2-D layout moves between consecutive frames.
|
||
<em>k</em>NN retention captures how much of the input-space neighborhood
|
||
graph survives projection. Together they separate reducers that are
|
||
globally stable but locally noisy from those with the opposite failure
|
||
mode.
|
||
</p>
|
||
<p>
|
||
<strong>Why this matters.</strong> A reducer that looks well-behaved on
|
||
a single snapshot is not automatically the right tool for a streaming
|
||
or longitudinal setting. Used as the substrate for a visualization,
|
||
frame-to-frame motion will read as change the user did not request;
|
||
used as a feature-extraction step inside a classification pipeline,
|
||
drift between training and inference will quietly erode accuracy. The
|
||
aim here is to build intuition for those regimes before committing the
|
||
reducer to either role.
|
||
</p>
|
||
</div>
|
||
|
||
<figure class="intro-figure">
|
||
<svg class="intro-schema" viewBox="0 0 480 230" xmlns="http://www.w3.org/2000/svg"
|
||
role="img" aria-label="schematic: 3-D perturbed snapshots reduced independently to a 2-D trajectory">
|
||
<!-- LEFT: stack of snapshots -->
|
||
<g>
|
||
<rect class="frame depth" x="40" y="55" width="130" height="130"/>
|
||
<rect class="frame depth" x="30" y="45" width="130" height="130"/>
|
||
<rect class="frame" x="20" y="35" width="130" height="130"/>
|
||
<!-- tiny axis triad in the front frame -->
|
||
<g class="axes" transform="translate(32 156)">
|
||
<line x1="0" y1="0" x2="14" y2="0"/>
|
||
<line x1="0" y1="0" x2="0" y2="-14"/>
|
||
<line x1="0" y1="0" x2="-9" y2="7"/>
|
||
</g>
|
||
<g class="dots">
|
||
<circle cx="72" cy="112" r="2.1"/>
|
||
<circle cx="86" cy="60" r="2.1"/>
|
||
<circle cx="100" cy="92" r="2.1"/>
|
||
<circle cx="62" cy="142" r="2.1"/>
|
||
<circle cx="112" cy="124" r="2.1"/>
|
||
<circle cx="118" cy="54" r="2.1"/>
|
||
<circle cx="134" cy="148" r="2.1"/>
|
||
</g>
|
||
<g class="dots-hi">
|
||
<circle class="track-a" cx="54" cy="72" r="3.2"/>
|
||
<circle class="track-b" cx="132" cy="80" r="3.2"/>
|
||
<circle class="track-c" cx="90" cy="134" r="3.2"/>
|
||
</g>
|
||
<text class="t-hint" x="150" y="30" text-anchor="end">t = 0, 1, … T</text>
|
||
</g>
|
||
|
||
<!-- ARROW + reducer label -->
|
||
<g transform="translate(182 100)">
|
||
<line class="arrow-line" x1="0" y1="0" x2="92" y2="0"/>
|
||
<polyline class="arrow-head" points="84,-5 94,0 84,5" fill="none"/>
|
||
<text class="phi" x="47" y="-12" text-anchor="middle">φ</text>
|
||
<text class="arrow-sub" x="47" y="22" text-anchor="middle">per snapshot</text>
|
||
</g>
|
||
|
||
<!-- RIGHT: single 2-D frame with trajectories -->
|
||
<g>
|
||
<rect class="frame" x="290" y="35" width="170" height="130"/>
|
||
<g class="trajs">
|
||
<polyline class="track-a" points="308,72 316,88 326,106 336,124"/>
|
||
<polyline class="track-b" points="370,52 380,68 390,82 398,100"/>
|
||
<polyline class="track-c" points="430,152 428,132 430,108 426,84"/>
|
||
</g>
|
||
<g class="dot-start">
|
||
<circle class="track-a" cx="308" cy="72" r="3"/>
|
||
<circle class="track-b" cx="370" cy="52" r="3"/>
|
||
<circle class="track-c" cx="430" cy="152" r="3"/>
|
||
</g>
|
||
<g class="dot-end">
|
||
<circle class="track-a" cx="336" cy="124" r="2.9"/>
|
||
<circle class="track-b" cx="398" cy="100" r="2.9"/>
|
||
<circle class="track-c" cx="426" cy="84" r="2.9"/>
|
||
</g>
|
||
<g class="dots">
|
||
<circle cx="316" cy="112" r="2.1"/>
|
||
<circle cx="352" cy="136" r="2.1"/>
|
||
<circle cx="358" cy="78" r="2.1"/>
|
||
<circle cx="392" cy="58" r="2.1"/>
|
||
<circle cx="412" cy="118" r="2.1"/>
|
||
<circle cx="440" cy="140" r="2.1"/>
|
||
<circle cx="348" cy="158" r="2.1"/>
|
||
<circle cx="380" cy="142" r="2.1"/>
|
||
</g>
|
||
</g>
|
||
|
||
<!-- captions -->
|
||
<g class="cap">
|
||
<text x="85" y="200" text-anchor="middle">snapshots · Xₜ ⊂ ℝ³</text>
|
||
<text x="375" y="200" text-anchor="middle">embedded trajectory · Yₜ ⊂ ℝ²</text>
|
||
</g>
|
||
</svg>
|
||
</figure>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="dataset-picker" id="picker" open>
|
||
<summary>
|
||
<span class="picker-meta">
|
||
<span class="section-number">§ 1</span>
|
||
<span class="picker-title">input dataset</span>
|
||
<span class="picker-selection">
|
||
<span class="lbl">generator</span>
|
||
<code id="picker-summary-path">—</code>
|
||
</span>
|
||
</span>
|
||
<span class="picker-toggle" aria-hidden="true"></span>
|
||
</summary>
|
||
|
||
<div class="picker-body">
|
||
<p class="lede">
|
||
Six candidate generators for the embedding pipeline. Drag to rotate, scroll to zoom,
|
||
<kbd>←</kbd> <kbd>→</kbd> or <kbd>1</kbd> … <kbd>6</kbd> to select.
|
||
</p>
|
||
|
||
<div class="picker-controls">
|
||
<span class="ctl-label">n samples</span>
|
||
<div class="segmented count-5" role="radiogroup" aria-label="number of samples">
|
||
<label><input type="radio" name="n" value="100"><span>100</span></label>
|
||
<label><input type="radio" name="n" value="500" checked><span>500</span></label>
|
||
<label><input type="radio" name="n" value="1000"><span>1,000</span></label>
|
||
<label><input type="radio" name="n" value="2500"><span>2,500</span></label>
|
||
<label><input type="radio" name="n" value="5000"><span>5,000</span></label>
|
||
</div>
|
||
|
||
<span class="ctl-label">noise σ</span>
|
||
<div class="segmented count-3" role="radiogroup" aria-label="noise σ">
|
||
<label><input type="radio" name="j" value="0.001"><span>0.001</span></label>
|
||
<label><input type="radio" name="j" value="0.005" checked><span>0.005</span></label>
|
||
<label><input type="radio" name="j" value="0.01"><span>0.010</span></label>
|
||
</div>
|
||
|
||
<span class="ctl-label">timesteps</span>
|
||
<div class="segmented count-3" role="radiogroup" aria-label="number of timesteps">
|
||
<label><input type="radio" name="f" value="12"><span>12</span></label>
|
||
<label><input type="radio" name="f" value="24" checked><span>24</span></label>
|
||
<label><input type="radio" name="f" value="48"><span>48</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="gallery" id="gallery">
|
||
<div class="picker-loading">loading samples…</div>
|
||
</div>
|
||
|
||
<div class="picker-footer">
|
||
<div class="selection">
|
||
<span class="lbl">generator</span>
|
||
<code id="selected-path">—</code>
|
||
</div>
|
||
<button type="button" class="continue" id="continue-btn" disabled>Continue →</button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<main>
|
||
|
||
<!-- ==================== LEFT: parameter notebook ==================== -->
|
||
<section class="col-left">
|
||
|
||
<form
|
||
id="run-form"
|
||
hx-post="/submit"
|
||
hx-target="#runs-slot"
|
||
hx-swap="innerHTML"
|
||
hx-indicator="#busy"
|
||
>
|
||
|
||
<!-- Picker-driven hidden fields. Values are written by dataset-picker.js. -->
|
||
<input type="hidden" name="dataset_id" id="dataset_id" value="" />
|
||
<input type="hidden" name="num_points" id="num_points" value="500" />
|
||
<input type="hidden" name="num_timesteps" id="num_timesteps" value="24" />
|
||
<input type="hidden" name="jitter_scale" id="jitter_scale" value="0.005" />
|
||
<input type="hidden" name="seed" id="seed" value="42" />
|
||
|
||
<!-- §2 reducer -->
|
||
<div class="section">
|
||
<div class="section-label">
|
||
<span>§ 2 reducer</span><span class="ordinal">method</span>
|
||
</div>
|
||
<p class="lead">Dimensionality reduction applied to each snapshot. Only reducers whose Python package is importable are shown.</p>
|
||
|
||
<ul class="reducer-list">
|
||
{% for r in reducers %}
|
||
<li>
|
||
<input type="radio" name="reducer" id="red-{{ loop.index }}" value="{{ r.key }}"
|
||
{% if r.key == default_reducer %}checked{% endif %}
|
||
hx-get="/reducer-form?name={{ r.key }}"
|
||
hx-target="#reducer-params"
|
||
hx-swap="innerHTML"
|
||
hx-trigger="change" />
|
||
<label for="red-{{ loop.index }}">
|
||
<span class="mark">{{ "%02d"|format(loop.index) }}</span>
|
||
<span>
|
||
<span class="name">{{ r.label }}</span>
|
||
<span class="blurb">{{ r.blurb }}</span>
|
||
</span>
|
||
<span class="pkg">{{ r.key }}</span>
|
||
</label>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- §3 reducer params -->
|
||
<div class="section">
|
||
<div class="section-label">
|
||
<span>§ 3 parameters</span><span class="ordinal">kwargs</span>
|
||
</div>
|
||
<div id="reducer-params">
|
||
{% include "_reducer_form.html" with context %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="actions">
|
||
<button type="submit" class="submit">submit run</button>
|
||
<span id="busy" class="htmx-indicator">dispatching…</span>
|
||
</div>
|
||
|
||
</form>
|
||
|
||
</section>
|
||
|
||
<!-- ==================== RIGHT: runs log ============================== -->
|
||
<section class="col-right">
|
||
|
||
<div class="section-label">
|
||
<span>§ 4 recent runs</span>
|
||
<span class="run-count">
|
||
<span id="runs-count">{{ runs|length }}</span> / 10 · refresh 3s
|
||
<span id="poll-ind" class="htmx-indicator" style="margin-left:6px">●</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div
|
||
id="runs-slot"
|
||
hx-get="/runs"
|
||
hx-trigger="load delay:3s, every 3s"
|
||
hx-swap="innerHTML"
|
||
hx-indicator="#poll-ind"
|
||
>
|
||
{% include "_runs.html" with context %}
|
||
</div>
|
||
|
||
</section>
|
||
|
||
</main>
|
||
|
||
<details class="dataset-picker metrics-inline" id="metrics">
|
||
<summary>
|
||
<span class="picker-meta">
|
||
<span class="section-number">§ 5</span>
|
||
<span class="picker-title">stability metrics</span>
|
||
<span class="picker-selection">
|
||
<span class="lbl">view</span>
|
||
<code>frame-to-frame travel · vs-initial drift · kNN retention</code>
|
||
</span>
|
||
</span>
|
||
<span class="picker-toggle" aria-hidden="true"></span>
|
||
</summary>
|
||
|
||
<div class="picker-body">
|
||
<div class="filter-bar">
|
||
<div class="filter-group">
|
||
<span class="ctl-label">dataset</span>
|
||
<div class="chips" id="flt-dataset" aria-label="filter by dataset"></div>
|
||
</div>
|
||
|
||
<div class="filter-group">
|
||
<span class="ctl-label">algorithm</span>
|
||
<div class="chips" id="flt-algo" aria-label="filter by algorithm"></div>
|
||
</div>
|
||
|
||
<div class="filter-group stat-group">
|
||
<span class="ctl-label">travel stat</span>
|
||
<div class="segmented count-4" role="radiogroup" aria-label="travel stat">
|
||
<label><input type="radio" name="stat" value="mean" checked><span>mean</span></label>
|
||
<label><input type="radio" name="stat" value="median"><span>median</span></label>
|
||
<label><input type="radio" name="stat" value="p95"><span>p95</span></label>
|
||
<label><input type="radio" name="stat" value="max"><span>max</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-count">
|
||
<span id="match-count">0</span> / <span id="total-count">0</span> <span class="muted">runs</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="plots">
|
||
<figure class="plot">
|
||
<figcaption>
|
||
<span class="plot-title">frame-to-frame travel</span>
|
||
<span class="plot-sub">‖ y(t) − y(t−1) ‖ · output 2-D space</span>
|
||
</figcaption>
|
||
<div id="plot-ff" class="plot-area"></div>
|
||
</figure>
|
||
|
||
<figure class="plot">
|
||
<figcaption>
|
||
<span class="plot-title">vs-initial travel</span>
|
||
<span class="plot-sub">‖ y(t) − y(0) ‖ · drift from first timestep</span>
|
||
</figcaption>
|
||
<div id="plot-vi" class="plot-area"></div>
|
||
</figure>
|
||
|
||
<figure class="plot">
|
||
<figcaption>
|
||
<span class="plot-title">kNN retention</span>
|
||
<span class="plot-sub">fraction of input-space k-NN preserved in 2-D (higher = more faithful)</span>
|
||
</figcaption>
|
||
<div id="plot-knn" class="plot-area"></div>
|
||
</figure>
|
||
</div>
|
||
|
||
<div class="legend" id="legend"></div>
|
||
|
||
<div id="empty" class="empty" hidden>
|
||
No metrics to show. Dispatch a run above — sidecar JSONs appear in <code>figs/</code> after the flow completes.
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<footer class="colophon">
|
||
<span>© 2026 Mind the Math LLC</span>
|
||
<span class="prefect-badge">
|
||
<span class="dot {% if not deployment_id %}bad{% endif %}"></span>
|
||
{% if deployment_id %}prefect · {{ deployment_id[:8] }}{% else %}prefect · unreachable{% endif %}
|
||
</span>
|
||
</footer>
|
||
|
||
<script src="/static/theme.js?v=11"></script>
|
||
<script type="module" src="/static/dataset-picker.js?v=11"></script>
|
||
<script type="module" src="/static/metrics.js?v=11"></script>
|
||
<script>
|
||
// Anchor-links alone don't expand <details>; force it.
|
||
document.querySelector('a[href="#metrics"]')?.addEventListener('click', () => {
|
||
const d = document.getElementById('metrics');
|
||
if (d) d.open = true;
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|