degeneration-probe / probe_setup_interactive.html
mo378's picture
Colour-blind friendly mode: app-level toggle on both tabs
8d6190c verified
Raw
History Blame Contribute Delete
23.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Probe Setup: Interactive</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" />
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<style>
foreignObject .katex { font-size: 1em; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #fff;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 32px;
}
.scoreBar { transition: width 0.5s cubic-bezier(.4,0,.2,1); }
/* Color-blind friendly mode: swaps red/blue channels so the red
degeneration signal becomes blue, kept in sync with the toggle in the
"Visualization of Internal Representation" section via localStorage. */
body.colorblind-mode #viz { filter: url(#cb-recolor-filter); }
</style>
</head>
<body>
<svg id="viz" width="1110" height="534" viewBox="0 0 1060 510"
style="font-family:Helvetica, Arial, sans-serif; display:block;"></svg>
<svg width="0" height="0" style="position:absolute;overflow:hidden" aria-hidden="true">
<filter id="cb-recolor-filter" color-interpolation-filters="sRGB">
<feColorMatrix type="matrix" values="0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 1 0" />
</filter>
</svg>
<script>
// Sync color-blind mode with the toggle in the visualization section
// (shared via localStorage; "storage" fires across same-origin iframes).
(function() {
function apply(on) { document.body.classList.toggle('colorblind-mode', on); }
apply(localStorage.getItem('cb-mode') === '1');
window.addEventListener('storage', function(e) {
if (e.key === 'cb-mode') apply(e.newValue === '1');
});
})();
</script>
<script>
const svg = document.getElementById('viz');
const ns = 'http://www.w3.org/2000/svg';
function el(tag, attrs, text) {
const e = document.createElementNS(ns, tag);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
if (text !== undefined) e.textContent = text;
return e;
}
function txt(x, y, str, attrs = {}) {
return el('text', { x, y, 'text-anchor': 'middle',
'font-family': 'Inter,sans-serif', 'font-size': 13, fill: '#4b5563', ...attrs }, str);
}
// Render a LaTeX formula into the SVG via a foreignObject + KaTeX, centered on (cx, baselineY).
// suffix: optional plain-text string appended after the formula (e.g. " Β· label").
function mathFO(cx, baselineY, tex, fontSize, color, suffix) {
const fo = el('foreignObject', { x: cx, y: baselineY - fontSize * 0.32, width: 1, height: 1 });
fo.setAttribute('style', 'overflow: visible;');
const div = document.createElement('div');
div.style.cssText = 'position:absolute; left:0; top:0; transform:translate(-50%,-50%); '
+ 'white-space:nowrap; line-height:1; font-size:' + fontSize + 'px; color:' + color + ';';
div.innerHTML = katex.renderToString(tex, { throwOnError: false })
+ (suffix ? '<span style="font-family:Inter,sans-serif;">' + suffix + '</span>' : '');
fo.appendChild(div);
return fo;
}
function arrowEl(x1, y1, x2, y2, color = '#9ca3af', dash = '') {
const g = el('g', {});
g.appendChild(el('line', { x1, y1, x2, y2, stroke: color, 'stroke-width': 2,
'stroke-linecap': 'round', ...(dash ? { 'stroke-dasharray': dash } : {}) }));
const a = Math.atan2(y2 - y1, x2 - x1), L = 8, s = 0.42;
g.appendChild(el('polygon', {
points: `${x2},${y2} ${x2 - L*Math.cos(a-s)},${y2 - L*Math.sin(a-s)} ${x2 - L*Math.cos(a+s)},${y2 - L*Math.sin(a+s)}`,
fill: color
}));
return g;
}
// curved connector (S-curve) with arrowhead at the end
function arrowCurve(x1, y1, x2, y2, color = '#9ca3af') {
const g = el('g', {});
const mx = (x1 + x2) / 2;
g.appendChild(el('path', {
d: `M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}`,
stroke: color, 'stroke-width': 2, fill: 'none', 'stroke-linecap': 'round'
}));
const a = 0, L = 8, s = 0.45;
g.appendChild(el('polygon', {
points: `${x2},${y2} ${x2 - L*Math.cos(a-s)},${y2 - L*Math.sin(a-s)} ${x2 - L*Math.cos(a+s)},${y2 - L*Math.sin(a+s)}`,
fill: color
}));
return g;
}
// auto-width token chip; returns next x
function chip(parent, x, y, text, bg, tc) {
const w = Math.max(text.length * 6 + 10, 20), h = 16;
parent.appendChild(el('rect', { x, y, width: w, height: h, rx: 0, fill: bg }));
parent.appendChild(el('text', {
x: x + w/2, y: y + 11.5, 'text-anchor': 'middle',
'font-size': 9, 'font-weight': 600, fill: tc,
'font-family': 'JetBrains Mono, monospace'
}, text));
return x + w + 4;
}
// fixed-width chip (for aligned tables); returns {rect, text} for later restyling
function chipFixed(parent, x, y, text, w, h, bg, tc) {
const rect = el('rect', { x, y, width: w, height: h, rx: 0, fill: bg });
parent.appendChild(rect);
const t = el('text', {
x: x + w/2, y: y + h/2 + 3.2, 'text-anchor': 'middle',
'font-size': 8.5, 'font-weight': 600, fill: tc,
'font-family': 'JetBrains Mono, monospace'
}, text);
parent.appendChild(t);
return { rect, text: t };
}
// ── Palette (minimal: neutral grays + one signal color) ───
const C = {
line: '#9ca3af',
faint: '#d1d5db',
rest: '#e5e7eb',
sel: '#374151',
selStk: '#1f2937',
ink: '#374151',
mute: '#9ca3af',
signal: '#dc2626', // degeneration: the only meaningful color
signalBg:'#fef2f2',
okBg: '#f3f4f6',
okInk: '#4b5563',
barLow: '#cbd5e1',
};
const THRESH = 0.5;
// Single source of truth for the generated response + probe scores.
// s = probe repetition score Ε·[t] in [0,1]; high (>THRESH) = degeneration.
// y = deterministic repetition score computed beforehand (training target).
const RESPONSE = [
{ t: 'Fever', s: 0.05, y: 0.00 },
{ t: 'results', s: 0.07, y: 0.02 },
{ t: 'from', s: 0.05, y: 0.00 },
{ t: 'immune', s: 0.10, y: 0.04 },
{ t: 'response', s: 0.09, y: 0.05 },
{ t: 'infection', s: 0.66, y: 0.74 },
{ t: 'infection', s: 0.83, y: 0.88 },
{ t: 'to', s: 0.78, y: 0.86 },
{ t: 'infection', s: 0.90, y: 0.95 },
{ t: 'infection', s: 0.93, y: 0.97 },
];
const isHi = s => s >= THRESH;
// ── Per-layer variation ───────────────────────────────────
// Deterministic pseudo-noise so each layer looks distinct but stable.
function rand(i, k) {
const x = Math.sin(i * 127.1 + k * 311.7) * 43758.5453;
return x - Math.floor(x);
}
// Probe quality by depth: poor in early layers, sharp in the deep ones.
function quality(i) {
const t = i / (N_LAYERS - 1);
let q = Math.max(0, Math.min(1, (t - 0.04) / 0.5));
q = q * q * (3 - 2 * q); // smoothstep
if (t > 0.82) q *= 1 - (t - 0.82) * 0.8; // slight late-layer decline
return q;
}
// Probe score for a token at layer i: collapses toward an uninformative
// baseline (β‰ˆ0.36) at shallow layers, sharpens toward the true score deep down.
function layerScore(i, tok, k) {
const base = 0.36, q = quality(i);
const jit = (rand(i, k) - 0.5) * 0.22 * (1 - q); // noisier when uninformative
const s = base + (tok.s - base) * q + jit;
return Math.max(0.02, Math.min(0.98, s));
}
// ── Layout constants ─────────────────────────────────────
const CY = 250;
const N_LAYERS = 32;
const REKT_W = 72;
const REKT_H = 9;
const REKT_GAP = 3;
const totalH = N_LAYERS * REKT_H + (N_LAYERS - 1) * REKT_GAP;
const stackTopY = CY - totalH / 2;
const stackCX = 350;
const stackRight = stackCX + REKT_W / 2;
const stackLeft = stackCX - REKT_W / 2;
const layer0CY = stackTopY + REKT_H / 2;
const DEFAULT_LAYER = 16;
// Prompt box
const PBX = 18, PBY = 100, PBW = 188, PBH = 300;
// Probe panel
const PRX = 470, PRW = 330, PRH = 364;
const PRY = CY - PRH / 2;
// ── Groups (render order) ─────────────────────────────────
const fanGroup = el('g', {});
const probeGroup = el('g', {});
const stackGroup = el('g', {});
const pBox = el('g', {});
const arrowGrp = el('g', {});
// ══ PROMPT / RESPONSE BOX ════════════════════════════════
pBox.appendChild(el('rect', {
x: PBX, y: PBY, width: PBW, height: PBH, rx: 0,
fill: '#fff', stroke: C.faint, 'stroke-width': 1.5
}));
pBox.appendChild(txt(PBX+PBW/2, PBY+22, 'Input Sequence',
{ 'font-size': 12, 'font-weight': 700, fill: C.ink }));
pBox.appendChild(txt(PBX+PBW/2, PBY+36, 'Apertus-8B Β· instruct mode',
{ 'font-size': 8, fill: C.mute, 'font-style': 'normal' }));
// PROMPT section
pBox.appendChild(el('text', {
x: PBX+12, y: PBY+58, 'font-size': 7.5, 'font-weight': 700, fill: C.mute,
'font-family': 'Inter,sans-serif', 'letter-spacing': '0.1em'
}, 'PROMPT'));
let cx = PBX+12;
cx = chip(pBox, cx, PBY+64, 'What', C.okBg, C.okInk);
cx = chip(pBox, cx, PBY+64, 'causes', C.okBg, C.okInk);
cx = PBX+12;
cx = chip(pBox, cx, PBY+84, 'fever', C.okBg, C.okInk);
cx = chip(pBox, cx, PBY+84, 'in', C.okBg, C.okInk);
cx = chip(pBox, cx, PBY+84, 'patients?',C.okBg, C.okInk);
pBox.appendChild(el('line', {
x1: PBX+12, y1: PBY+110, x2: PBX+PBW-12, y2: PBY+110,
stroke: C.rest, 'stroke-width': 1
}));
// RESPONSE section: generated from RESPONSE (wraps within box)
pBox.appendChild(el('text', {
x: PBX+12, y: PBY+126, 'font-size': 7.5, 'font-weight': 700, fill: C.mute,
'font-family': 'Inter,sans-serif', 'letter-spacing': '0.1em'
}, 'RESPONSE'));
let rx = PBX+12, ry = PBY+132;
const maxX = PBX + PBW - 12;
for (const tok of RESPONSE) {
const w = Math.max(tok.t.length*6+10, 20);
if (rx + w > maxX) { rx = PBX+12; ry += 20; }
const hi = isHi(tok.s);
chip(pBox, rx, ry, tok.t, hi ? C.signalBg : C.okBg, hi ? C.signal : C.okInk);
rx += w + 4;
}
pBox.appendChild(txt(PBX+PBW/2, PBY+PBH-14, 'Β· Β· Β·', { 'font-size': 13, fill: C.faint }));
// ══ ARROW: prompt box β†’ Layer 0 ══════════════════════════
const ax1 = PBX + PBW + 3;
const ax2 = stackLeft - 6;
arrowGrp.appendChild(arrowCurve(ax1, PBY + 110, ax2, layer0CY, C.line));
arrowGrp.appendChild(txt((ax1+ax2)/2 + 4, layer0CY - 10, 'tokens β†’ Layer 0',
{ 'font-size': 9, fill: C.mute, 'font-style': 'normal' }));
// ══ STACK ════════════════════════════════════════════════
stackGroup.appendChild(txt(stackCX, stackTopY-30, 'Apertus-8B',
{ 'font-size': 13, 'font-weight': 700, fill: C.ink }));
stackGroup.appendChild(txt(stackCX, stackTopY-16, '32 transformer layers',
{ 'font-size': 10, fill: C.mute }));
stackGroup.appendChild(txt(stackRight + 8, stackTopY + 9, 'Layer 0',
{ 'font-size': 9, fill: C.faint, 'text-anchor': 'start' }));
stackGroup.appendChild(txt(stackRight + 8,
stackTopY + (N_LAYERS-1) * (REKT_H+REKT_GAP) + 9,
'Layer 31', { 'font-size': 9, fill: C.faint, 'text-anchor': 'start' }));
stackGroup.appendChild(txt(stackCX, stackTopY + totalH + 20,
'hover layers: activations & probe scores change',
{ 'font-size': 10, fill: C.faint, 'font-style': 'normal' }));
const hoverLabel = txt(stackRight + 8, stackTopY, '',
{ 'font-size': 9, fill: C.sel, 'font-weight': 700, 'text-anchor': 'start', opacity: 0 });
const layerRects = [];
for (let i = 0; i < N_LAYERS; i++) {
const lry = stackTopY + i * (REKT_H + REKT_GAP);
const r = el('rect', {
x: stackLeft, y: lry, width: REKT_W, height: REKT_H, rx: 0,
fill: C.rest, stroke: C.faint, 'stroke-width': 0.8
});
r.style.cursor = 'pointer';
stackGroup.appendChild(r);
layerRects.push(r);
}
stackGroup.appendChild(hoverLabel);
// ══ PROBE PANEL ══════════════════════════════════════════
probeGroup.appendChild(el('rect', {
x: PRX, y: PRY, width: PRW, height: PRH, rx: 0,
fill: '#fff', stroke: C.faint, 'stroke-width': 1.5
}));
const probeTitleEl = txt(PRX + PRW/2, PRY+22, 'Probe: Layer ?',
{ 'font-size': 13, 'font-weight': 700, fill: C.ink });
probeGroup.appendChild(probeTitleEl);
probeGroup.appendChild(txt(PRX + PRW/2, PRY+37, 'a linear head reading the layer activations',
{ 'font-size': 8.5, fill: C.mute, 'font-style': 'normal' }));
// ── Mechanism strip: hβ‚“ (4096) β†’ W β†’ Ε·[t] ─────────────────
const mY = PRY + 90; // mechanism centre line
// hβ‚“ activation vector (segmented to suggest 4096 dims)
const vx = PRX+26, vTop = mY-30, vw = 20, vh = 60;
const cells = 9, ch = vh / cells;
const actCells = [];
for (let k = 0; k < cells; k++) {
const rect = el('rect', {
x: vx, y: vTop + k*ch + 0.5, width: vw, height: ch-1, fill: '#d1d5db'
});
probeGroup.appendChild(rect);
actCells.push(rect); // recoloured per layer in show()
}
probeGroup.appendChild(el('rect', {
x: vx, y: vTop, width: vw, height: vh, rx: 0,
fill: 'none', stroke: C.faint, 'stroke-width': 1.2
}));
probeGroup.appendChild(txt(vx+vw/2, vTop-7, 'hβ‚“[t]',
{ 'font-size': 9, fill: C.ink, 'font-weight': 700 }));
probeGroup.appendChild(txt(vx+vw/2, vTop+vh+12, '4096',
{ 'font-size': 8.5, fill: C.mute, 'font-weight': 600 }));
probeGroup.appendChild(txt(vx+vw/2, vTop+vh+22, 'activations',
{ 'font-size': 7.5, fill: C.mute }));
// hβ‚“ β†’ W
const wbx = PRX+96, wbw = 44, wbh = 40, wby = mY-wbh/2;
probeGroup.appendChild(arrowEl(vx+vw+2, mY, wbx-2, mY, C.line));
probeGroup.appendChild(el('rect', {
x: wbx, y: wby, width: wbw, height: wbh, rx: 0,
fill: '#fff', stroke: C.faint, 'stroke-width': 1.5
}));
probeGroup.appendChild(txt(wbx+wbw/2, wby+11, 'linear',
{ 'font-size': 7, fill: C.mute }));
probeGroup.appendChild(txt(wbx+wbw/2, wby+wbh/2+8, 'W',
{ 'font-size': 16, 'font-weight': 700, fill: C.ink }));
// W β†’ Ε·[t]
const yNodeX = PRX+210;
probeGroup.appendChild(arrowEl(wbx+wbw+2, mY, yNodeX-26, mY, C.line));
probeGroup.appendChild(mathFO(yNodeX, mY-4, '\\hat{y}[t]', 12, C.ink));
probeGroup.appendChild(mathFO(yNodeX, mY+9, '\\in [0,1]', 8, C.mute));
probeGroup.appendChild(txt(PRX+PRW-20, mY, 'one score',
{ 'font-size': 8, fill: C.mute, 'text-anchor': 'end' }));
probeGroup.appendChild(txt(PRX+PRW-20, mY+10, 'per token',
{ 'font-size': 8, fill: C.mute, 'text-anchor': 'end' }));
// dashed connector: Ε·[t] expands into the per-token list below
const listTop = PRY + 152;
probeGroup.appendChild(el('path', {
d: `M ${yNodeX} ${mY+14} L ${yNodeX} ${listTop-14} L ${PRX+30} ${listTop-14}`,
stroke: C.faint, 'stroke-width': 1, 'stroke-dasharray': '4 4', fill: 'none'
}));
// ── Per-token score list ──────────────────────────────────
const colTokX = PRX+16, tokW = 58;
const barX = PRX+82, barW = 178, barH = 7;
const valX = PRX+PRW-16;
probeGroup.appendChild(el('text', {
x: colTokX, y: listTop-2, 'font-size': 7.5, 'font-weight': 700,
fill: C.mute, 'font-family': 'Inter,sans-serif', 'letter-spacing': '0.05em'
}, 'TOKEN'));
probeGroup.appendChild(txt(barX+barW/2, listTop-2, 'repetition score Ε·[t]',
{ 'font-size': 7.5, 'font-weight': 700, fill: C.mute, 'letter-spacing': '0.05em' }));
probeGroup.appendChild(el('text', {
x: valX, y: listTop-2, 'font-size': 7.5, 'font-weight': 700,
fill: C.mute, 'text-anchor': 'end', 'font-family': 'Inter,sans-serif'
}, 'VAL'));
probeGroup.appendChild(el('line', {
x1: colTokX, y1: listTop+2, x2: valX, y2: listTop+2,
stroke: C.rest, 'stroke-width': 1
}));
const rowH = 18, rowY0 = listTop + 12, chipH = 13;
const scoreRows = [];
RESPONSE.forEach((tok, k) => {
const yy = rowY0 + k*rowH;
const by = yy + (chipH-barH)/2;
const chip = chipFixed(probeGroup, colTokX, yy, tok.t, tokW, chipH, C.okBg, C.okInk);
// bar track
probeGroup.appendChild(el('rect', {
x: barX, y: by, width: barW, height: barH, rx: 0, fill: C.okBg
}));
// bar fill (width/colour set per layer)
const fill = el('rect', { x: barX, y: by, width: 0, height: barH, rx: 0, fill: C.barLow });
fill.setAttribute('class', 'scoreBar');
probeGroup.appendChild(fill);
// value (set per layer)
const valText = el('text', {
x: valX, y: yy + chipH/2 + 3.2, 'text-anchor': 'end',
'font-size': 8.5, 'font-weight': 700, fill: C.okInk,
'font-family': 'JetBrains Mono, monospace'
}, '');
probeGroup.appendChild(valText);
scoreRows.push({ tok, k, chip, fill, valText, target: 0 });
});
// ══ TRAINING SIGNAL: MSE (comparison table) ═════════════
const TSX = 824, TSW = 210, TSY = 96, TSH = 320;
probeGroup.appendChild(el('rect', {
x: TSX, y: TSY, width: TSW, height: TSH, rx: 0,
fill: '#fff', stroke: C.faint, 'stroke-width': 1.5
}));
probeGroup.appendChild(txt(TSX+TSW/2, TSY+22, 'Training signal: MSE',
{ 'font-size': 12, 'font-weight': 700, fill: C.ink }));
probeGroup.appendChild(mathFO(TSX+TSW/2, TSY+41, '\\mathcal{L} = \\tfrac{1}{N}\\sum (\\hat{y} - y)^2', 11, C.mute));
// column centres
const tcTok = TSX+12, tcW = 54;
const tcY = TSX+100, tcYh = TSX+140, tcErr = TSX+186;
const hY2 = TSY+66;
probeGroup.appendChild(el('text', { x: tcTok, y: hY2, 'font-size': 7.5, 'font-weight': 700,
fill: C.mute, 'font-family': 'Inter,sans-serif', 'letter-spacing': '0.05em' }, 'TOKEN'));
probeGroup.appendChild(mathFO(tcY, hY2, 'y', 9, C.mute));
probeGroup.appendChild(mathFO(tcYh, hY2, '\\hat{y}', 9, C.mute));
probeGroup.appendChild(mathFO(tcErr, hY2, '|\\hat{y} - y|', 8.5, C.mute));
probeGroup.appendChild(el('line', { x1: tcTok, y1: hY2+5, x2: TSX+TSW-12, y2: hY2+5,
stroke: C.rest, 'stroke-width': 1 }));
const tsRowH = 18, tsRowY0 = TSY+82, tsChipH = 13;
const tsRows = [];
RESPONSE.forEach((tok, k) => {
const yy = tsRowY0 + k*tsRowH;
const chip = chipFixed(probeGroup, tcTok, yy, tok.t, tcW, tsChipH, C.okBg, C.okInk);
// y target (fixed, deterministic)
probeGroup.appendChild(el('text', { x: tcY, y: yy+tsChipH/2+3.2, 'text-anchor': 'middle',
'font-size': 8.5, fill: C.mute, 'font-family': 'JetBrains Mono, monospace' }, tok.y.toFixed(2)));
// Ε· prediction (per layer)
const yhText = el('text', { x: tcYh, y: yy+tsChipH/2+3.2, 'text-anchor': 'middle',
'font-size': 8.5, 'font-weight': 700, fill: C.okInk, 'font-family': 'JetBrains Mono, monospace' }, '');
probeGroup.appendChild(yhText);
// |Ε·βˆ’y| error (per layer)
const errText = el('text', { x: tcErr, y: yy+tsChipH/2+3.2, 'text-anchor': 'middle',
'font-size': 8.5, 'font-weight': 600, fill: C.ink, 'font-family': 'JetBrains Mono, monospace' }, '');
probeGroup.appendChild(errText);
tsRows.push({ chip, yhText, errText });
});
// loss readout
probeGroup.appendChild(el('line', { x1: tcTok, y1: TSY+TSH-42, x2: TSX+TSW-12, y2: TSY+TSH-42,
stroke: C.rest, 'stroke-width': 1 }));
const lossFO = el('foreignObject', { x: TSX+TSW/2, y: (TSY+TSH-20) - 14 * 0.32, width: 1, height: 1 });
lossFO.setAttribute('style', 'overflow: visible;');
const lossDiv = document.createElement('div');
lossDiv.style.cssText = 'position:absolute; left:0; top:0; transform:translate(-50%,-50%); '
+ 'white-space:nowrap; line-height:1; font-size:14px; color:' + C.sel + ';';
lossFO.appendChild(lossDiv);
probeGroup.appendChild(lossFO);
// forward arrow: probe scores Ε· β†’ training signal
probeGroup.appendChild(arrowEl(PRX+PRW+2, CY, TSX-3, CY, C.line));
// backprop: dashed loop from the loss back into the probe weights W
probeGroup.appendChild(el('path', {
d: `M ${TSX+TSW-8} ${TSY+TSH-20} L 1044 ${TSY+TSH-20} L 1044 30 L 530 30 L 530 ${mY} L 555 ${mY}`,
stroke: C.line, 'stroke-width': 1.5, fill: 'none', 'stroke-dasharray': '5 4'
}));
probeGroup.appendChild(el('polygon', {
points: `${wbx-2},${mY} ${wbx-11},${mY-4.5} ${wbx-11},${mY+4.5}`, fill: C.line
}));
probeGroup.appendChild(mathFO(787, 24, '\\partial\\mathcal{L}/\\partial W', 9.5, C.ink, 'Β Β Β·Β Β parameters updating'));
// ══ ASSEMBLY ═════════════════════════════════════════════
svg.appendChild(fanGroup);
svg.appendChild(probeGroup);
svg.appendChild(pBox);
svg.appendChild(arrowGrp);
svg.appendChild(stackGroup);
// ── Interaction ──────────────────────────────────────────
function updateFan(i) {
while (fanGroup.firstChild) fanGroup.removeChild(fanGroup.firstChild);
const ly = stackTopY + i * (REKT_H + REKT_GAP);
fanGroup.appendChild(el('polygon', {
points: `${stackRight},${ly} ${PRX},${PRY} ${PRX},${PRY+PRH} ${stackRight},${ly+REKT_H}`,
fill: '#f8fafc', stroke: 'none'
}));
fanGroup.appendChild(el('line', {
x1: stackRight, y1: ly, x2: PRX, y2: PRY,
stroke: C.faint, 'stroke-width': 1, 'stroke-dasharray': '5 4'
}));
fanGroup.appendChild(el('line', {
x1: stackRight, y1: ly+REKT_H, x2: PRX, y2: PRY+PRH,
stroke: C.faint, 'stroke-width': 1, 'stroke-dasharray': '5 4'
}));
}
function animateBars() {
scoreRows.forEach(r => r.fill.setAttribute('width', 0));
requestAnimationFrame(() => requestAnimationFrame(() => {
scoreRows.forEach(r => r.fill.setAttribute('width', r.target));
}));
}
// recolour the 4096-activation cells + per-token scores + loss for layer i
function updateForLayer(i) {
// activations: grayscale pattern shifts with the layer
actCells.forEach((c, k) => {
const v = rand(i, k * 3.1 + 1);
c.setAttribute('fill', `hsl(220,9%,${Math.round(82 - v * 42)}%)`);
});
// scores: recompute, recolour, update values
let loss = 0;
scoreRows.forEach(row => {
const ls = layerScore(i, row.tok, row.k);
loss += (ls - row.tok.y) ** 2;
row.target = ls * barW;
const hi = ls >= THRESH;
row.fill.setAttribute('fill', hi ? C.signal : C.barLow);
row.valText.textContent = ls.toFixed(2);
row.valText.setAttribute('fill', hi ? C.signal : C.okInk);
row.chip.rect.setAttribute('fill', hi ? C.signalBg : C.okBg);
row.chip.text.setAttribute('fill', hi ? C.signal : C.okInk);
// training-signal comparison table
const ts = tsRows[row.k];
ts.yhText.textContent = ls.toFixed(2);
ts.yhText.setAttribute('fill', hi ? C.signal : C.okInk);
ts.errText.textContent = Math.abs(ls - row.tok.y).toFixed(2);
ts.chip.rect.setAttribute('fill', hi ? C.signalBg : C.okBg);
ts.chip.text.setAttribute('fill', hi ? C.signal : C.okInk);
});
lossDiv.innerHTML = katex.renderToString('\\mathcal{L} = ' + (loss / scoreRows.length).toFixed(4), { throwOnError: false });
}
let current = null;
function show(i) {
if (current === i) return;
current = i;
layerRects.forEach((r, idx) => {
r.setAttribute('fill', idx === i ? C.sel : C.rest);
r.setAttribute('stroke', idx === i ? C.selStk : C.faint);
r.setAttribute('stroke-width', idx === i ? 1.5 : 0.8);
});
const ly = stackTopY + i * (REKT_H + REKT_GAP);
hoverLabel.setAttribute('y', ly + 9);
hoverLabel.textContent = `Layer ${i}`;
hoverLabel.setAttribute('opacity', 1);
probeTitleEl.textContent = `Probe: Layer ${i}`;
updateFan(i);
updateForLayer(i);
animateBars();
}
layerRects.forEach((r, i) => r.addEventListener('mouseenter', () => show(i)));
// keep the probe attached at the middle layer by default
svg.addEventListener('mouseleave', () => show(DEFAULT_LAYER));
// boot: probe attached to the middle layer
show(DEFAULT_LAYER);
</script>
</body>
</html>