/* 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) + " = " + k.fmt(prob[i]) + "", 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 }; })();