Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Image assets

`affirm-logo-primary.png` is the official Affirm primary logo downloaded on
October 5, 2026 from https://cdn-assets.affirm.com/images/logo-01-primary.png,
linked by [Affirm's logo resource page](https://businesshub.affirm.com/hc/en-us/articles/4425266541204-Logos-Banners-and-Buttons).
The original asset is 429 × 171 pixels and retains its original colors and transparency.

The footer of `tabular-tutorial-overview.png` uses this asset at 128 × 51 pixels,
preserving its aspect ratio to the nearest pixel. Only the footer logo region was
replaced; the rest of the infographic is unchanged.
Binary file added assets/affirm-logo-primary.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/tabular-tutorial-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 11 additions & 7 deletions materials/tabicl-explainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
Interactive visualization of TabICLv2 inference on fixed UCI Iris examples.

The 12-row context is intentionally compact for tracing. It is outside the
officially documented TabICLv2 pretraining range of 300 to 48K rows, and the
upstream authors state that sub-300-row generalization has not been tested.
Treat its measured output as an out-of-regime illustration, not a quality claim.
officially documented TabICLv2 pretraining range of 300 to 48K rows. The revised
paper includes sub-300-row few-shot evaluations
([Appendix L.3–L.4, September 2026 revision](https://arxiv.org/html/2602.11139v2#A12.SS3)).
Those results do not establish the quality of this particular 12-row Iris demo.
Treat its measured output as an illustration of computation, not a quality claim.
The interface adapts the MIT-licensed
[Transformer Explainer](https://github.com/poloclub/transformer-explainer)
layout while replacing GPT-2 generation with tabular in-context learning.
Expand Down Expand Up @@ -55,10 +57,12 @@ The original interface is used under the MIT License reproduced in

This adaptation preserves the interface composition while replacing GPT
inference and examples with fixed UCI Iris records and a browser TabICLv2
checkpoint. The explorer uses a nanoTabICL-derived bridge for selected-view
inspection with the released TabICLv2 checkpoint's feature-group offsets. It is
not the standalone nanoTabICL model and does not expose every preprocessing and
ensemble option in the official `TabICLClassifier`.
checkpoint. The explorer uses a nanoTabICL-derived bridge for fixed single-pass
core inspection with the released TabICLv2 checkpoint's feature-group offsets. It is
not the standalone nanoTabICL model. It uses context-only z-score standardization,
original feature and class order, and softmax temperature 1. It is not a selected
view of the main playground's eight-view `TabICLClassifier`, which applies its own
preprocessing, permutations, and temperature 0.9.

Checkpoint source, hashes, runtime behavior, and limitations are documented in
[`../website/model/PROVENANCE.md`](../website/model/PROVENANCE.md). TabICL code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
return (value: number) => (Number.isFinite(value) ? scale(value) : '#f3f4f6');
};

$: signedColor = createSignedColor(raw ?? []);
$: scaledColor = createSignedColor(scaled ?? []);
$: signedColor = createSignedColor([...(raw ?? []), ...(scaled ?? [])]);
const weightColor = (value: number) =>
Number.isFinite(value) ? d3.interpolateRgb('#ffffff', '#6d28d9')(Math.min(1, value * 4)) : '#f3f4f6';
</script>
Expand All @@ -39,6 +38,7 @@
class:expanded
on:click={() => (expanded = !expanded)}
aria-expanded={expanded}
aria-label={expanded ? 'Collapse attention calculation; logits share a color scale' : 'Expand attention calculation'}
>
{#if expanded}
<div class="calculation-step">
Expand All @@ -48,7 +48,7 @@
</div>
<span class="arrow">→</span>
<div class="calculation-step">
<AttentionRow values={scaledValues} colorScale={scaledColor} />
<AttentionRow values={scaledValues} colorScale={signedColor} />
<div class="matrix-label">QASSMax-scaled logits</div>
<code>QASSMax(Q) · Kᵀ / √d</code>
</div>
Expand Down
20 changes: 13 additions & 7 deletions materials/tabicl-explainer/src/components/Sankey.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@
const draw = async () => {
await tick();
if (!svgEl) return;
const host = svgEl.parentElement?.getBoundingClientRect();
const nodes = Array.from(document.querySelectorAll<HTMLElement>('[data-flow-node]'));
if (!host || nodes.length < 2) return;
const host = svgEl.parentElement;
const matrix = svgEl.getScreenCTM();
const nodes = Array.from(host?.querySelectorAll<HTMLElement>('[data-flow-node]') ?? []);
if (!matrix || nodes.length < 2) return;
// Bounding rectangles use screen coordinates, including the app's CSS scale.
// Convert back to SVG coordinates so the scale is applied only once.
const inverse = matrix.inverse();
const links = nodes.slice(0, -1).map((node, index) => {
const source = node.getBoundingClientRect();
const target = nodes[index + 1].getBoundingClientRect();
const x1 = source.right - host.left;
const x2 = target.left - host.left;
const y1 = source.top + source.height / 2 - host.top;
const y2 = target.top + target.height / 2 - host.top;
const start = new DOMPoint(source.right, source.top + source.height / 2).matrixTransform(inverse);
const end = new DOMPoint(target.left, target.top + target.height / 2).matrixTransform(inverse);
const { x: x1, y: y1 } = start;
const { x: x2, y: y2 } = end;
const curve = Math.max(20, (x2 - x1) * 0.45);
return {
path: `M${x1},${y1} C${x1 + curve},${y1} ${x2 - curve},${y2} ${x2},${y2}`,
Expand All @@ -38,10 +42,12 @@
observer = new ResizeObserver(draw);
const host = svgEl.parentElement;
if (host) observer.observe(host);
host?.addEventListener('transitionend', draw);
window.addEventListener('resize', draw);
draw();
return () => {
observer.disconnect();
host?.removeEventListener('transitionend', draw);
window.removeEventListener('resize', draw);
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@
const pages = [
{
title: 'A table becomes a prediction',
body: 'This nanoTabICL-shaped inspector receives twelve labeled Iris examples and one unlabeled query using mapped official TabICLv2 weights. It is not the standalone nanoTabICL model. This trace is outside the documented 300 to 48K-row pretraining regime and is not evidence of expected model quality. Choose Query A, B, or C above. Its hidden species is never supplied to the model.'
body: 'This inspector runs a fixed single core pass with quantized official TabICLv2 weights on twelve labeled Iris examples and one unlabeled query. The context is below the documented 300 to 48K-row pretraining range. Choose Query A, B, or C; its label is never supplied. This illustrates computation, not model quality.'
},
{
title: '1 · Read the table',
body: 'Each E row is a labeled context example. Q is the selected unlabeled query. Row IDs E1–E12 and Q stay fixed through the full visualization.'
},
{
title: '2 · Standardize, group, embed',
body: 'Feature values are standardized with context-only mean and standard deviation, circularly grouped at offsets 1, 2, and 4, then projected into 128-dimensional column tokens.'
body: 'This core pass uses context-only mean and standard deviation, feature groups at checkpoint offsets 1, 2, and 4, and a projection to 128 dimensions per group. Context tokens also receive label embeddings; the query does not. Activation colors are scaled separately for each tensor and cannot be compared across panels.'
},
{
title: '3 · Column attention',
body: 'Three induced Column Transformer blocks process each feature column. The colored strips are sampled live activations; the thin repeated cards describe symbolic architecture.'
body: 'Context rows first build three blocks of inducing summaries. The query reads cached summaries through the second attention operation of each block; it does not rebuild them. Colored strips show sampled query activations. The inducing cards are symbolic.'
},
{
title: '4 · Compress each row',
body: 'Three Row Transformer blocks combine feature tokens with four learned CLS tokens. The final four CLS outputs concatenate into one 512-dimensional row vector.'
body: 'Three Row Transformer blocks combine grouped-feature tokens with four learned CLS tokens. The final CLS outputs are normalized and concatenated into a 512-dimensional row vector. Context vectors receive a second label embedding before dataset-level ICL; the query does not.'
},
{
title: '5 · Route through context',
body: 'Twelve ICL Transformer blocks let Q attend to E1–E12. Attention is routing, not feature importance. Change block and head, or expand the matrix to inspect real logits and weights.'
body: 'Twelve ICL Transformer blocks let Q attend to cached context keys and values from E1–E12. Attention weights describe routing, not causal attribution. Change block or head, or expand to compare logits on a shared color scale and inspect softmax weights on their separate scale.'
},
{
title: '6 · Predict the class',
body: 'The final query vector passes through output normalization and an MLP. The three bars are computed softmax probabilities for one selected TabICLv2 model view using mapped official weights.'
body: 'Output normalization and an MLP produce three class logits, converted to probabilities with temperature 1. This fixed core pass keeps the original feature and class order. It does not select one of the playground classifier’s eight views or use its temperature 0.9.'
}
];

Expand Down
4 changes: 2 additions & 2 deletions materials/tabicl-explainer/src/lib/tabicl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ async function fetchWithProgress(
export async function loadTabICL(
onStatus: (status: LoadStatus) => void
): Promise<{ model: TabICLModel; cache: unknown }> {
onStatus({ phase: 'downloading', progress: 0, message: 'Downloading official TabICLv2 weights' });
onStatus({ phase: 'downloading', progress: 0, message: 'Downloading quantized TabICLv2 weights' });
const modelBase = new URL('../model/', window.location.href);
const manifestResponse = await fetch(new URL('manifest.json', modelBase), { cache: 'no-store' });
if (!manifestResponse.ok) throw new Error(`Manifest download failed (${manifestResponse.status})`);
Expand All @@ -150,6 +150,6 @@ export async function loadTabICL(
CONTEXT.map((row) => [...row.x]),
CONTEXT.map((row) => row.y)
);
onStatus({ phase: 'ready', progress: 1, message: 'nanoTabICL trace ready' });
onStatus({ phase: 'ready', progress: 1, message: 'TabICLv2 core trace ready' });
return { model, cache };
}
57 changes: 30 additions & 27 deletions materials/tabicl-explainer/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,25 @@
const DESIGN_WIDTH = 1880;
let fitScale = 1;
let designHeight = 900;
let inspectionRequest = 0;

$: query = QUERIES.find((item) => item.id === selectedQuery) ?? QUERIES[0];
$: selectedAttention = inspection?.iclBlocks[iclBlock]?.attention ?? null;
$: selectedColumn = inspection?.columnBlocks[columnBlock]?.activation ?? null;
$: selectedRow = inspection?.rowBlocks[rowBlock]?.tokens ?? null;
$: revision = `${selectedQuery}-${columnBlock}-${rowBlock}-${iclBlock}-${attentionHead}-${loadStatus.phase}`;
$: revision = `${selectedQuery}-${columnBlock}-${rowBlock}-${iclBlock}-${attentionHead}-${loadStatus.phase}-${guidePage}-${textbookOpen}-${fitScale}`;

async function runInspection(activeModel: TabICLModel, activeCache: unknown, queryId: string) {
const request = ++inspectionRequest;
const selected = QUERIES.find((item) => item.id === queryId) ?? QUERIES[0];
inspection = null;
loadStatus = { phase: 'running', progress: 0.98, message: `Running real Query ${queryId}` };
await tick();
await new Promise((resolve) => setTimeout(resolve, 20));
if (request !== inspectionRequest) return;
try {
inspection = activeModel.inspectQuery(activeCache, [...selected.x], 3);
loadStatus = { phase: 'ready', progress: 1, message: 'nanoTabICL trace ready' };
loadStatus = { phase: 'ready', progress: 1, message: 'TabICLv2 core trace ready' };
} catch (error) {
loadStatus = {
phase: 'error',
Expand Down Expand Up @@ -99,9 +102,9 @@
return `rgb(${target.map((channel) => Math.round(255 + (channel - 255) * amount)).join(',')})`;
}

function stageClass(page: number) {
if (!textbookOpen || guidePage === 0) return '';
return guidePage === page ? 'spotlit' : 'muted';
function stageClass(page: number, activePage: number, open: boolean) {
if (!open || activePage === 0) return '';
return activePage === page ? 'spotlit' : 'muted';
}

function cycle(value: number, delta: number, length: number) {
Expand All @@ -113,7 +116,7 @@
<title>TabICL Explainer: Tabular In-Context Learning, Visually Explained</title>
<meta
name="description"
content="An educational nanoTabICL visualization using mapped official TabICLv2 classifier weights and Iris data."
content="A single-pass TabICLv2 core inspector using quantized official classifier weights and Iris data."
/>
</svelte:head>

Expand All @@ -128,7 +131,7 @@
<div class="architecture resize-watch">
<Sankey {revision} />

<section class={`stage raw-stage ${stageClass(1)}`}>
<section class={`stage raw-stage ${stageClass(1, guidePage, textbookOpen)}`}>
<div class="stage-title">IRIS TABLE</div>
<div class="stage-subtitle">real UCI records · cm</div>
<div class="table-card" data-flow-node>
Expand All @@ -153,7 +156,7 @@
<div class="truth-note">Query {selectedQuery} is visibly unlabeled</div>
</section>

<section class={`stage preprocess-stage ${stageClass(2)}`}>
<section class={`stage preprocess-stage ${stageClass(2, guidePage, textbookOpen)}`}>
<div class="stage-title">PREPROCESS + EMBED</div>
<div class="stage-subtitle">live query path</div>
<div class="preprocess-flow" data-flow-node>
Expand All @@ -172,13 +175,13 @@
<div class="operation">
<span>W<sub>x</sub></span>
<b>Linear embed</b>
<small>4 × 128</small>
<small>output: 4 groups × 128</small>
</div>
<div class="activation-label">LIVE TENSOR SAMPLE</div>
<div class="column-strips">
{#each inspection?.embedding.values ?? Array(4).fill(Array(24).fill(Number.NaN)) as values, index}
<div class="strip-row">
<span class="strip-label">f{index + 1}</span>
<span class="strip-label">g{index + 1}</span>
<div class="heat-strip">
{#each values as value}
<i
Expand All @@ -193,7 +196,7 @@
</div>
</section>

<section class={`stage column-stage ${stageClass(3)}`}>
<section class={`stage column-stage ${stageClass(3, guidePage, textbookOpen)}`}>
<div class="stage-title">COLUMN TRANSFORMER</div>
<div class="stage-subtitle">induced attention · 8 heads</div>
<div class="block-stack" data-flow-node>
Expand All @@ -202,9 +205,9 @@
{/each}
<div class="block-card">
<div class="pager">
<button on:click={() => (columnBlock = cycle(columnBlock, -1, 3))}>‹</button>
<button aria-label="Previous column block" on:click={() => (columnBlock = cycle(columnBlock, -1, 3))}>‹</button>
<span>Block {columnBlock + 1} of 3</span>
<button on:click={() => (columnBlock = cycle(columnBlock, 1, 3))}>›</button>
<button aria-label="Next column block" on:click={() => (columnBlock = cycle(columnBlock, 1, 3))}>›</button>
</div>
<div class="inducing">128 inducing vectors</div>
<div class="mini-block"><span>TFM 1</span><small>QASSMax</small></div>
Expand All @@ -213,7 +216,7 @@
<div class="column-strips live">
{#each selectedColumn?.values ?? Array(4).fill(Array(24).fill(Number.NaN)) as values, index}
<div class="strip-row">
<span class="strip-label">f{index + 1}</span>
<span class="strip-label">g{index + 1}</span>
<div class="heat-strip">
{#each values as value}
<i
Expand All @@ -230,18 +233,18 @@
</div>
</section>

<section class={`stage row-stage ${stageClass(4)}`}>
<section class={`stage row-stage ${stageClass(4, guidePage, textbookOpen)}`}>
<div class="stage-title">ROW TRANSFORMER</div>
<div class="stage-subtitle">feature mixing + CLS compression</div>
<div class="row-card" data-flow-node>
<div class="pager">
<button on:click={() => (rowBlock = cycle(rowBlock, -1, 3))}>‹</button>
<button aria-label="Previous row block" on:click={() => (rowBlock = cycle(rowBlock, -1, 3))}>‹</button>
<span>Block {rowBlock + 1} of 3</span>
<button on:click={() => (rowBlock = cycle(rowBlock, 1, 3))}>›</button>
<button aria-label="Next row block" on:click={() => (rowBlock = cycle(rowBlock, 1, 3))}>›</button>
</div>
<div class="token-set">
{#each Array(4) as _, index}<div class="cls">CLS {index + 1}</div>{/each}
{#each Array(rowBlock === 2 ? 0 : 4) as _, index}<div class="feature-token">f{index + 1}</div>{/each}
{#each Array(rowBlock === 2 ? 0 : 4) as _, index}<div class="feature-token">g{index + 1}</div>{/each}
</div>
<div class="row-attention">8-head self-attention + MLP</div>
<div class="selected-token-strip">
Expand All @@ -256,7 +259,7 @@
</div>
</section>

<section class={`stage vectors-stage ${stageClass(4)}`}>
<section class={`stage vectors-stage ${stageClass(4, guidePage, textbookOpen)}`}>
<div class="stage-title">ROW VECTORS</div>
<div class="stage-subtitle">4 CLS × 128 → 512</div>
<div class="vector-column" data-flow-node>
Expand All @@ -280,7 +283,7 @@
</div>
</section>

<section class={`stage icl-stage ${stageClass(5)}`}>
<section class={`stage icl-stage ${stageClass(5, guidePage, textbookOpen)}`}>
<div class="stage-title">ICL TRANSFORMER × 12</div>
<div class="stage-subtitle">query-to-context routing</div>
<div class="icl-stack" data-flow-node>
Expand All @@ -290,14 +293,14 @@
<div class="icl-card">
<div class="dual-pager">
<div class="pager">
<button on:click={() => (iclBlock = cycle(iclBlock, -1, 12))}>‹</button>
<button aria-label="Previous ICL block" on:click={() => (iclBlock = cycle(iclBlock, -1, 12))}>‹</button>
<span>Block {iclBlock + 1} / 12</span>
<button on:click={() => (iclBlock = cycle(iclBlock, 1, 12))}>›</button>
<button aria-label="Next ICL block" on:click={() => (iclBlock = cycle(iclBlock, 1, 12))}>›</button>
</div>
<div class="pager">
<button on:click={() => (attentionHead = cycle(attentionHead, -1, 8))}>‹</button>
<button aria-label="Previous attention head" on:click={() => (attentionHead = cycle(attentionHead, -1, 8))}>‹</button>
<span>Head {attentionHead + 1} / 8</span>
<button on:click={() => (attentionHead = cycle(attentionHead, 1, 8))}>›</button>
<button aria-label="Next attention head" on:click={() => (attentionHead = cycle(attentionHead, 1, 8))}>›</button>
</div>
</div>
<AttentionMatrix
Expand All @@ -320,9 +323,9 @@
</div>
</section>

<section class={`stage output-stage ${stageClass(6)}`}>
<section class={`stage output-stage ${stageClass(6, guidePage, textbookOpen)}`}>
<div class="stage-title">OUTPUT PROBABILITIES</div>
<div class="stage-subtitle">LayerNorm · MLP · softmax</div>
<div class="stage-subtitle">single core pass · temperature 1</div>
<div class="output-card" data-flow-node>
<div class="output-vector"></div>
<span class="output-arrow">→</span>
Expand All @@ -343,7 +346,7 @@
<footer class="provenance">
<div>
<strong>Model provenance</strong>
<span>Mapped official TabICLv2 weights; SHA-256 verified before loading. The 12-row Iris trace is an out-of-regime illustration because the documented pretraining range starts at 300 rows.</span>
<span>Quantized official TabICLv2 weights; SHA-256 verified. Fixed single core pass, not the eight-view ensemble. The 12-row context is below the documented pretraining range; this illustrates computation, not model quality.</span>
</div>
</footer>
<Textbook bind:open={textbookOpen} bind:currentPage={guidePage} />
Expand Down
Loading
Loading