manimo-playground / widgets /softmax.js
jacooob521's picture
fix 14 medium review findings (correctness/robustness/a11y)
9dde5d4 verified
Raw
History Blame Contribute Delete
4.51 kB
/* Softmax tester — the detailed recipe attention and classifiers use: subtract the max (stability),
* exponentiate each logit, sum, then divide each by the sum → a probability distribution, animated into
* bars. Interconnected: if the paper feeds in upstream scores (io.consumes), it softmaxes the first row
* of that score matrix and publishes the resulting weights downstream. Same math the video narrates. */
(function () {
const k = MANIMO.kit, bus = MANIMO.bus;
function mount(root, concept, theme) {
const p = (concept && concept.params) || {};
const io = (concept && concept.io) || {};
const up = io.consumes ? bus.get(io.consumes) : null;
const v0 = (up && up.length ? up[0].slice() : (p.logits || [2.0, 1.0, 0.1, -0.5]));
const grid = k.grid(1, v0.length, [v0]);
const cv = document.createElement("canvas"); cv.width = 460; cv.height = 180; cv.className = "plot";
cv.setAttribute("role", "img"); cv.setAttribute("aria-label", "Softmax probability distribution bar chart");
const log = k.panel("steps");
const note = up ? k.el("div", { class: "matlbl" }, ["row 0 of S = Q·Kᵀ/√d (the scaled scores)"]) : null;
function calc() {
const v = grid.get()[0], mx = Math.max.apply(null, v);
const ex = v.map((x) => Math.exp(x - mx));
const sum = ex.reduce((a, b) => a + b, 0);
const prob = ex.map((e) => e / sum);
return { v, mx, ex, sum, prob };
}
function publish() { if (io.produces) bus.set(io.produces, [calc().prob]); }
// steps: 1 = max, 2..1+n = exp each element, 2+n = sum, 3+n..2+2n = divide each element.
const n = v0.length, S_MAX = 1, S_EXP = 1, S_SUM = 1 + n + 1, total = 2 * n + 2;
function render(step) {
const { v, mx, ex, sum, prob } = calc();
k.clear(log); log.appendChild(k.el("div", { class: "steps-h" }, ["softmax (" + step + " / " + total + ")"]));
if (step >= S_MAX) k.logLine(log, "1. subtract max = " + k.fmt(mx) + " (numerical stability)", step === S_MAX);
const expShown = k.clamp(step - S_EXP, 0, n);
for (let i = 0; i < expShown; i++) k.logLine(log, "2." + (i + 1) + " e^(" + k.fmt(v[i]) + "−" + k.fmt(mx) + ") = " + k.fmt(ex[i]), i === expShown - 1 && step < S_SUM);
if (step >= S_SUM) k.logLine(log, "3. sum of exponentials = " + k.fmt(sum), step === S_SUM);
const divShown = k.clamp(step - S_SUM, 0, n);
for (let i = 0; i < divShown; i++) k.logLine(log, "4." + (i + 1) + " p[" + i + "] = " + k.fmt(ex[i]) + " / " + k.fmt(sum) + " = <b>" + k.fmt(prob[i]) + "</b>", i === divShown - 1);
// bars: logits → (exp, partially) → probabilities
const done = divShown >= n;
bars(done ? prob : (expShown > 0 ? norm(ex) : v.map(() => 0)), done);
}
function bars(vals, asProb) {
const ctx = cv.getContext("2d"), W = cv.width, H = cv.height, n2 = vals.length, bw = (W - 20) / n2;
ctx.fillStyle = theme.bg; ctx.fillRect(0, 0, W, H);
const mx = Math.max.apply(null, vals.concat([1e-6]));
vals.forEach((val, i) => {
const h = (val / mx) * (H - 36), x = 10 + i * bw;
ctx.fillStyle = i === argmax(vals) && asProb ? theme.accent2 : theme.accent;
ctx.fillRect(x + 4, H - 22 - h, bw - 8, h);
ctx.fillStyle = theme.ink; ctx.font = "12px system-ui"; ctx.textAlign = "center";
ctx.fillText(asProb ? k.fmt(val) : "", x + bw / 2, H - 26 - h);
ctx.fillText("x" + i, x + bw / 2, H - 6);
});
}
function norm(a) { const m = Math.max.apply(null, a.concat([1e-6])); return a.map((e) => e / m); }
function argmax(a) { let m = 0; a.forEach((val, i) => { if (val > a[m]) m = i; }); return m; }
const ctrl = k.stepper(new Array(total), render);
function rebuild() { ctrl.reset(); publish(); }
grid.node.addEventListener("input", rebuild);
if (io.consumes) bus.on(io.consumes, (S) => { if (S && S.length) { grid.set([S[0]]); rebuild(); } });
publish();
root.appendChild(k.row([labeled("logits", grid.node), note].filter(Boolean), "mats"));
root.appendChild(cv);
root.appendChild(k.row([k.button("▸ Step", () => ctrl.next()), k.button("▶ Play", () => ctrl.play(880)), k.button("↺ Reset", () => ctrl.reset(), "ghost")], "ctrls"));
root.appendChild(log);
return k.player(ctrl, 780);
}
function labeled(t, node) { return MANIMO.kit.el("div", { class: "matcol" }, [MANIMO.kit.el("div", { class: "matlbl" }, [t]), node]); }
MANIMO.registry["softmax"] = { title: "Softmax", mount };
})();