/* Интерактивные виджеты вики «Нейроморфное распознавание на Алтае». * Страница вставляется через innerHTML, поэтому скрипты в ней не выполняются: здесь мы находим * элементы
и оживляем их. Данные моделей (WIKI_DATA) добавляет build.py. */ (() => { "use strict"; const W = {}; const redraws = new Set(); const cssv = (n) => getComputedStyle(document.documentElement).getPropertyValue(n).trim(); const h = (tag, attrs = {}, ...kids) => { const e = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) { if (k === "class") e.className = v; else if (k === "text") e.textContent = v; else if (k.startsWith("on")) e.addEventListener(k.slice(2), v); else e.setAttribute(k, v); } kids.flat().forEach((c) => c != null && e.append(c)); return e; }; const NS = "http://www.w3.org/2000/svg"; const s = (tag, attrs = {}, text) => { const e = document.createElementNS(NS, tag); for (const k in attrs) e.setAttribute(k, attrs[k]); if (text != null) e.textContent = text; return e; }; const fmt = (x, d = 0) => Number(x).toLocaleString("ru-RU", { maximumFractionDigits: d, minimumFractionDigits: d }); const pct = (x, d = 1) => fmt(100 * x, d) + "%"; const rgb = (c) => { c = c.trim(); if (c.startsWith("#")) { const v = parseInt(c.slice(1), 16); return [v >> 16 & 255, v >> 8 & 255, v & 255]; } const m = c.match(/\d+/g); return m ? m.slice(0, 3).map(Number) : [128, 128, 128]; }; const mix = (a, b, t) => a.map((x, i) => Math.round(x + (b[i] - x) * t)); const C = () => ({ text: cssv("--text"), muted: cssv("--muted"), faint: cssv("--faint"), border: cssv("--border"), shell: cssv("--shell"), surface: cssv("--surface"), control: cssv("--control"), spike: cssv("--w-spike"), pot: cssv("--w-pot"), good: cssv("--w-good"), bad: cssv("--w-bad") }); const dec = (b64, T) => { const b = atob(b64), u = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) u[i] = b.charCodeAt(i); return new T(u.buffer); }; function canvas(height, label) { const c = h("canvas", { class: "w-canvas", "aria-label": label || "" }); c.dataset.h = height; return c; } function fit(c) { const r = window.devicePixelRatio || 1, H = +c.dataset.h; c.style.height = H + "px"; c.width = Math.max(1, c.clientWidth * r); c.height = H * r; const g = c.getContext("2d"); g.setTransform(r, 0, 0, r, 0, 0); return g; } const font = (size, weight = 400) => `${weight} ${size}px ${cssv("--accessibility-font-family") || "system-ui"}, system-ui, sans-serif`; const seg = (items, onPick, initial = 0) => { const box = h("div", { class: "w-seg", role: "group" }); items.forEach((label, i) => box.append(h("button", { type: "button", "aria-pressed": String(i === initial), text: label, onclick: () => { [...box.children].forEach((b, k) => b.setAttribute("aria-pressed", String(k === i))); onPick(i); } }))); return box; }; const btn = (label, onclick) => h("button", { type: "button", class: "w-btn", text: label, onclick }); const slider = (label, min, max, value, onInput, show = (v) => v) => { const out = h("b", { text: show(value) }); const input = h("input", { type: "range", min, max, value }); input.addEventListener("input", () => { out.textContent = show(+input.value); onInput(+input.value); }); return h("label", { class: "w-ctl" }, h("span", {}, label, " ", out), input); }; const note = (text) => h("p", { class: "w-note", text }); const card = (el) => { el.classList.add("w-card"); return el; }; const alive = (el, fn) => { const r = () => { if (el.isConnected) fn(); else redraws.delete(r); }; redraws.add(r); return r; }; function grid28(cv, f) { const g = cv.getContext("2d"), id = g.createImageData(28, 28); for (let p = 0; p < 784; p++) id.data.set([...f(p), 255], p * 4); g.putImageData(id, 0, 0); } /* ---------- общие модели (если данные встроены) ---------- */ const D = globalThis.WIKI_DATA || null; const M = {}; if (D) { M.gal = { px: dec(D.gallery.pixels, Uint8Array), labels: D.gallery.labels, idx: D.gallery.idx }; M.cols = { ...D.cols, W: dec(D.cols.w, Int8Array) }; M.nat = { ...D.native, W1: dec(D.native.w1, Int8Array), B1: dec(D.native.b1, Int16Array), W2: dec(D.native.w2, Int16Array), B2: dec(D.native.b2, Int16Array) }; M.snn = { ...D.snn, W1: dec(D.snn.w1, Int8Array), B1: dec(D.snn.b1, Int8Array), W2: dec(D.snn.w2, Int8Array), B2: dec(D.snn.b2, Int8Array) }; } const galImg = (k) => M.gal.px.subarray(k * 784, (k + 1) * 784); /* ---------- обработка на хосте: нормализация 20×20 + центр масс, выпрямление, усреднение ---------- */ const HOST = (() => { function comShift(o) { let m = 0, mx = 0, my = 0; for (let y = 0; y < 28; y++) for (let x = 0; x < 28; x++) { const v = o[y * 28 + x]; m += v; mx += v * x; my += v * y; } if (!m) return o; const dx = Math.floor(13.5 - mx / m + .5), dy = Math.floor(13.5 - my / m + .5), r = new Uint8Array(784); for (let y = 0; y < 28; y++) for (let x = 0; x < 28; x++) { const sx = x - dx, sy = y - dy; if (sx >= 0 && sx < 28 && sy >= 0 && sy < 28) r[y * 28 + x] = o[sy * 28 + sx]; } return r; } function bbox(g) { let x0 = 28, y0 = 28, x1 = -1, y1 = -1, n = 0; for (let y = 0; y < 28; y++) for (let x = 0; x < 28; x++) if (g[y * 28 + x] > 16) { x0 = Math.min(x0, x); x1 = Math.max(x1, x); y0 = Math.min(y0, y); y1 = Math.max(y1, y); n++; } return n < 2 ? null : [x0, y0, x1, y1]; } function normalize(g) { const b = bbox(g); if (!b) return new Uint8Array(784); const [x0, y0, x1, y1] = b, cw = x1 - x0 + 1, ch = y1 - y0 + 1, r = 20 / Math.max(cw, ch), nw = Math.max(1, Math.floor(cw * r + .5)), nh = Math.max(1, Math.floor(ch * r + .5)), ox = (28 - nw) >> 1, oy = (28 - nh) >> 1, o = new Uint8Array(784); for (let y = 0; y < nh; y++) for (let x = 0; x < nw; x++) { const xx = Math.min(cw - 1, Math.max(0, (x + .5) * cw / nw - .5)), yy = Math.min(ch - 1, Math.max(0, (y + .5) * ch / nh - .5)), lx = Math.floor(xx), ly = Math.floor(yy), hx = Math.min(lx + 1, cw - 1), hy = Math.min(ly + 1, ch - 1), ax = xx - lx, ay = yy - ly, at = (a, c) => g[(a + y0) * 28 + c + x0]; o[(y + oy) * 28 + x + ox] = Math.floor((at(ly, lx) * (1 - ax) + at(ly, hx) * ax) * (1 - ay) + (at(hy, lx) * (1 - ax) + at(hy, hx) * ax) * ay + .5); } return comShift(o); } function deskew(g) { let m = 0, cx = 0, cy = 0; for (let y = 0; y < 28; y++) for (let x = 0; x < 28; x++) { const v = g[y * 28 + x]; m += v; cx += v * x; cy += v * y; } if (!m) return g; cx /= m; cy /= m; let m02 = 0, m11 = 0; for (let y = 0; y < 28; y++) for (let x = 0; x < 28; x++) { const v = g[y * 28 + x]; m02 += v * (y - cy) ** 2; m11 += v * (y - cy) * (x - cx); } m02 /= m; m11 /= m; if (m02 < 1e-3) return g; const a = m11 / m02, o = new Uint8Array(784), px = (x, y) => (x >= 0 && x < 28 && y >= 0 && y < 28) ? g[y * 28 + x] : 0; for (let y = 0; y < 28; y++) for (let x = 0; x < 28; x++) { const u = (x + .5) + a * (y + .5) - a * cy - .5, lx = Math.floor(u), ax = u - lx; o[y * 28 + x] = Math.round(px(lx, y) * (1 - ax) + px(lx + 1, y) * ax); } return o; } function box(src, sw, sh, dw, dh) { const ax = (sn, dn) => { const sc = sn / dn, w = []; for (let j = 0; j < dn; j++) { const a = j * sc, b = (j + 1) * sc, ws = []; for (let i = Math.floor(a); i < Math.ceil(b); i++) { const o = Math.min(b, i + 1) - Math.max(a, i); if (o > 0) ws.push([i, o / sc]); } w.push(ws); } return w; }; const wx = ax(sw, dw), wy = ax(sh, dh), tmp = new Float64Array(dw * sh), o = new Uint8Array(dw * dh); for (let y = 0; y < sh; y++) for (let j = 0; j < dw; j++) { let t = 0; for (const [i, w] of wx[j]) t += src[y * sw + i] * w; tmp[y * dw + j] = t; } for (let j = 0; j < dh; j++) for (let x = 0; x < dw; x++) { let t = 0; for (const [i, w] of wy[j]) t += tmp[i * dw + x] * w; o[j * dw + x] = Math.min(255, Math.round(t)); } return o; } function normArea(g) { const b = bbox(g); if (!b) return new Uint8Array(784); const [x0, y0, x1, y1] = b, cw = x1 - x0 + 1, ch = y1 - y0 + 1, r = 20 / Math.max(cw, ch), nw = Math.max(1, Math.floor(cw * r + .5)), nh = Math.max(1, Math.floor(ch * r + .5)), crop = new Uint8Array(cw * ch); for (let y = 0; y < ch; y++) for (let x = 0; x < cw; x++) crop[y * cw + x] = g[(y + y0) * 28 + x + x0]; const sm = box(crop, cw, ch, nw, nh), o = new Uint8Array(784), ox = (28 - nw) >> 1, oy = (28 - nh) >> 1; for (let y = 0; y < nh; y++) for (let x = 0; x < nw; x++) o[(y + oy) * 28 + x + ox] = sm[y * nw + x]; return comShift(o); } const prepare = (raw) => { const n = normalize(raw), d = deskew(n); return { normalized: n, deskewed: d, final: normArea(d) }; }; return { prepare, normalize }; })(); /* ---------- три сети: точный целочисленный расчёт ---------- */ function runCols(img) { const m = M.cols, n = m.thr.length, col = m.col, Wt = m.W, STEPS = 15, ACT = 10, st = new Float32Array(784), raster = []; for (let t = 0; t < STEPS; t++) { const r = new Uint8Array(784); if (t < ACT) for (let p = 0; p < 784; p++) { st[p] += Math.fround(img[p] * Math.fround(1 / 255)); if (st[p] >= 1) { r[p] = 1; st[p] -= 1; } } raster.push(r); } let pot = new Float64Array(n), prevR = new Uint8Array(784), prevF = new Uint8Array(n), first = -1, inSp = 0, syn = 0, fired = 0; const votes = new Int32Array(10); for (let t = 0; t < STEPS; t++) { for (let c = 0; c < 10; c++) for (let k = 0; k < col; k++) if (prevF[c * col + k]) { votes[c]++; break; } let kF = 0; for (let j = 0; j < n; j++) kF += prevF[j]; let rs = 0; for (let p = 0; p < 784; p++) rs += prevR[p]; inSp += rs; syn += rs * n; const f = new Uint8Array(n); for (let j = 0; j < n; j++) { let v = Math.round(pot[j]); const row = j * 784; for (let p = 0; p < 784; p++) if (prevR[p]) v += Wt[row + p]; if (m.inhib && kF) v -= m.inhib * (kF - prevF[j]); if (v >= m.thr[j]) { f[j] = 1; v = 0; fired++; if (first < 0) first = j; } pot[j] = Math.max(v, 0); } prevR = raster[t]; prevF = f; } let b = -1, bv = 0; for (let i = 0; i < 10; i++) if (votes[i] > bv) { bv = votes[i]; b = i; } return { answer: b, votes: Array.from(votes), first, energy: { ticks: STEPS, input: inSp, neurons: fired, synaptic: syn } }; } function runNative(img) { const m = M.nat, T = 32, NH = 256, r = new Int32Array(784), p = new Float64Array(NH), hc = new Int32Array(NH); let inSp = 0, syn = 0; const nz1 = m.nz1 || (m.nz1 = Int32Array.from({ length: 784 }, (_, i) => { let c = 0; for (let j = 0; j < NH; j++) if (m.W1[i * NH + j]) c++; return c; })); for (let t = 0; t < T; t++) { for (let i = 0; i < 784; i++) { r[i] += img[i]; if (r[i] >= 255) { r[i] -= 255; inSp++; syn += nz1[i]; const row = i * NH; for (let j = 0; j < NH; j++) p[j] += m.W1[row + j]; } } for (let j = 0; j < NH; j++) { p[j] += m.B1[j]; if (p[j] >= 32) { hc[j]++; p[j] -= 32; } if (p[j] < 0) p[j] = 0; } } const pos = new Int32Array(10), neg = new Int32Array(10); for (let c = 0; c < 10; c++) for (let g = 0; g < 8; g++) { let cp = 0, cn = 0; for (let k = g * 32; k < (g + 1) * 32; k++) { const w = m.W2[k * 10 + c]; if (w > 0) cp += hc[k] * w; else if (w < 0) cn -= hc[k] * w; } if (g === 0) { cp += T * Math.max(m.B2[c], 0); cn += T * Math.max(-m.B2[c], 0); } pos[c] += Math.floor(cp / m.threshold); neg[c] += Math.floor(cn / m.threshold); } let hid = 0; for (let j = 0; j < NH; j++) { hid += hc[j]; let nz = 0; for (let c = 0; c < 10; c++) if (m.W2[j * 10 + c]) nz++; syn += hc[j] * nz; } const score = Array.from(pos, (v, c) => v - neg[c]); let best = 0; for (let c = 1; c < 10; c++) if (score[c] > score[best]) best = c; return { answer: best, score, pos, neg, hidden: hc, energy: { ticks: T, input: inSp, neurons: hid + pos.reduce((a, b) => a + b, 0) + neg.reduce((a, b) => a + b, 0), synaptic: syn } }; } function runSNN(img) { const m = M.snn, T = m.ticks, NH = 256, th = m.th_h, to = m.th_o; const tin = new Int8Array(784).fill(-1); for (let p = 0; p < 784; p++) if (img[p] > 32) tin[p] = Math.min(T - 1, Math.floor((255 - img[p]) * T / 224)); const v1 = new Int32Array(NH), v2 = new Int32Array(10), cnt = new Int32Array(10), first = new Int32Array(10).fill(99); const nz1 = m.nz1 || (m.nz1 = Int32Array.from({ length: 784 }, (_, i) => { let c = 0; for (let j = 0; j < NH; j++) if (m.W1[i * NH + j]) c++; return c; })); const nz2 = m.nz2 || (m.nz2 = Int32Array.from({ length: NH }, (_, j) => { let c = 0; for (let k = 0; k < 10; k++) if (m.W2[j * 10 + k]) c++; return c; })); let prev = new Uint8Array(NH), syn = 0, hid = 0, inSp = 0; const trace = []; for (let t = 0; t <= T; t++) { const s2 = new Uint8Array(10); for (let k = 0; k < 10; k++) { let v = v2[k] + m.B2[k]; for (let j = 0; j < NH; j++) if (prev[j]) v += m.W2[j * 10 + k]; if (v >= to) { s2[k] = 1; v -= to; cnt[k]++; if (first[k] === 99) first[k] = t; } v2[k] = Math.max(v, 0); } for (let j = 0; j < NH; j++) if (prev[j]) syn += nz2[j]; const sin = new Uint8Array(784), s1 = new Uint8Array(NH); if (t < T) { for (let p = 0; p < 784; p++) if (tin[p] === t) { sin[p] = 1; inSp++; syn += nz1[p]; } for (let j = 0; j < NH; j++) { let v = v1[j] + m.B1[j]; for (let p = 0; p < 784; p++) if (sin[p]) v += m.W1[p * NH + j]; if (v >= th) { s1[j] = 1; v -= th; hid++; } v1[j] = Math.max(v, 0); } } trace.push({ sin, s1, v1: Int32Array.from(v1), s2, v2: Int32Array.from(v2), cnt: Int32Array.from(cnt), first: Int32Array.from(first) }); prev = s1; } const lead = (st) => { let a = -1, b = -1; for (let k = 0; k < 10; k++) { if (!st.cnt[k]) continue; const key = st.cnt[k] * 1000 - st.first[k]; if (key > b) { b = key; a = k; } } return a; }; const ans = lead(trace[T]); let settled = T; for (let t = T; t >= 0; t--) { if (lead(trace[t]) !== ans) break; settled = t; } return { answer: ans, trace, tin, settled, energy: { ticks: T + 1, input: inSp, neurons: hid + cnt.reduce((a, b) => a + b, 0), synaptic: syn } }; } /* ---------- рисовалка: штрихи перерисовываются пером MNIST в 28×28 ---------- */ function drawPad(onImage) { const cv = h("canvas", { class: "w-pad", width: 280, height: 280, "aria-label": "Нарисуйте цифру мышью или пальцем" }); const g = cv.getContext("2d"); let strokes = [], drawing = false, last = null; const clear = () => { g.fillStyle = "#000"; g.fillRect(0, 0, 280, 280); strokes = []; }; const pos = (e) => { const r = cv.getBoundingClientRect(); return [(e.clientX - r.left) * 280 / r.width, (e.clientY - r.top) * 280 / r.height]; }; const line = (a, b) => { g.strokeStyle = "#fff"; g.lineWidth = 16; g.lineCap = g.lineJoin = "round"; g.beginPath(); g.moveTo(...a); g.lineTo(...b); g.stroke(); }; cv.addEventListener("pointerdown", (e) => { e.preventDefault(); drawing = true; last = pos(e); cv.setPointerCapture(e.pointerId); if (cv.dataset.shown) { clear(); delete cv.dataset.shown; } strokes.push([last]); line(last, last); }); cv.addEventListener("pointermove", (e) => { if (!drawing) return; e.preventDefault(); const p = pos(e); strokes[strokes.length - 1].push(p); line(last, p); last = p; }); const end = () => { if (!drawing) return; drawing = false; render(); }; cv.addEventListener("pointerup", end); cv.addEventListener("pointercancel", end); function render() { const pts = strokes.flat(); if (!pts.length) return; const xs = pts.map((p) => p[0]), ys = pts.map((p) => p[1]), x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys); const pen = 2.5, sc = (20 - pen) / Math.max(x1 - x0, y1 - y0, 20), sm = document.createElement("canvas"); sm.width = sm.height = 28; const c = sm.getContext("2d"); c.fillStyle = "#000"; c.fillRect(0, 0, 28, 28); c.strokeStyle = "#fff"; c.lineWidth = pen; c.lineCap = c.lineJoin = "round"; const tx = (x) => 14 + (x - (x0 + x1) / 2) * sc, ty = (y) => 14 + (y - (y0 + y1) / 2) * sc; for (const st of strokes) { c.beginPath(); c.moveTo(tx(st[0][0]), ty(st[0][1])); for (const p of (st.length > 1 ? st : [st[0], st[0]])) c.lineTo(tx(p[0]), ty(p[1])); c.stroke(); } const d = c.getImageData(0, 0, 28, 28).data, img = new Uint8Array(784); for (let i = 0; i < 784; i++) img[i] = d[i * 4]; onImage(img, null); } function show(img) { clear(); const t = document.createElement("canvas"); t.width = t.height = 28; grid28(t, (p) => [img[p], img[p], img[p]]); g.imageSmoothingEnabled = false; g.drawImage(t, 0, 0, 280, 280); cv.dataset.shown = "1"; } clear(); return { el: cv, clear, show }; } function gallery(onPick, count = 24) { const box = h("div", { class: "w-gal", role: "group", "aria-label": "Тестовые цифры MNIST" }); for (let k = 0; k < Math.min(count, M.gal.labels.length); k++) { const cv = h("canvas", { width: 28, height: 28 }); grid28(cv, (p) => { const v = galImg(k)[p]; return [v, v, v]; }); box.append(h("button", { type: "button", "aria-label": `Цифра ${M.gal.labels[k]}, тест №${M.gal.idx[k]}`, onclick: () => { [...box.children].forEach((b, i) => b.setAttribute("aria-pressed", String(i === k))); onPick(galImg(k), M.gal.labels[k]); } }, cv, h("span", { text: M.gal.labels[k] }))); } return box; } /* =================== ВИДЖЕТЫ =================== */ /* 1. Нейрон AltAILIF */ W.neuron = (el) => { card(el); const P = { w: 3, t: 10, l: 0, r: .4 }; let v = 0, hist = [], auto = true, diff = false, pending = 0; const N = 60; const cv = canvas(210, "Уровень нейрона по тактам"); function tick() { let inp = pending; pending = 0; if (auto && Math.random() < P.r) inp++; v += inp * P.w; if (P.l) v -= Math.min(P.l, Math.abs(v)) * Math.sign(v); let f = false; if (v >= P.t) { f = true; v = diff ? v - P.t : 0; } if (v < 0) v = 0; hist.push({ v, inp, f }); if (hist.length > N) hist.shift(); draw(); } function draw() { if (!cv.isConnected) return; const g = fit(cv), Wd = cv.clientWidth, H = 210, top = 22, bot = 24, c = C(), mx = Math.max(P.t * 1.4, ...hist.map((q) => q.v), 1), y = (q) => top + (H - top - bot) * (1 - q / mx), dx = Wd / (N - 1), off = N - hist.length; g.clearRect(0, 0, Wd, H); g.setLineDash([5, 4]); g.strokeStyle = c.muted; g.beginPath(); g.moveTo(0, y(P.t)); g.lineTo(Wd, y(P.t)); g.stroke(); g.setLineDash([]); g.fillStyle = c.muted; g.font = font(12); g.fillText("порог " + P.t, 6, y(P.t) - 5); g.strokeStyle = c.pot; g.lineWidth = 2.2; g.beginPath(); hist.forEach((q, i) => { const x = (i + off) * dx; i ? g.lineTo(x, y(q.v)) : g.moveTo(x, y(q.v)); }); g.stroke(); g.lineWidth = 1; hist.forEach((q, i) => { const x = (i + off) * dx; if (q.f) { g.fillStyle = c.spike; g.fillRect(x - 2, 3, 4, 14); } if (q.inp) { g.fillStyle = c.text; g.fillRect(x - 1.5, H - 17, 3, 12); } }); } el.append(cv, h("div", { class: "w-legend" }, h("span", { style: "--c: var(--w-pot)", text: "уровень" }), h("span", { style: "--c: var(--w-spike)", text: "выходной спайк" }), h("span", { style: "--c: var(--text)", text: "входной спайк" })), h("div", { class: "w-row" }, btn("Послать спайк", () => { pending++; if (!auto) tick(); }), seg(["Поток спайков", "Вручную"], (i) => { auto = i === 0; }), seg(["После выстрела: в ноль", "минус порог"], (i) => { diff = i === 1; })), h("div", { class: "w-row" }, slider("Вес связи", 1, 10, 3, (x) => P.w = x), slider("Порог", 2, 30, 10, (x) => { P.t = x; draw(); }), slider("Утечка за такт", 0, 3, 0, (x) => P.l = x), slider("Частота входа", 5, 100, 40, (x) => P.r = x / 100, (x) => x + "%"))); for (let i = 0; i < N; i++) tick(); const timer = setInterval(() => { if (!el.isConnected) return clearInterval(timer); if (auto) tick(); }, 220); alive(el, draw); }; /* 2. Кодирование входа: частота против времени */ W.coding = (el) => { card(el); let mode = 0, T = 8; const bright = [250, 200, 150, 100, 60, 34]; const cv = canvas(220, "Спайки шести пикселей по тактам"); const info = note(""); function draw() { if (!cv.isConnected) return; const g = fit(cv), Wd = cv.clientWidth, H = 220, c = C(), L = 52, R = 70, rowH = H / bright.length, tw = (Wd - L - R) / T; g.clearRect(0, 0, Wd, H); let total = 0; bright.forEach((b, r) => { const y = r * rowH; g.fillStyle = `rgb(${b},${b},${b})`; g.fillRect(10, y + 6, 30, rowH - 12); g.strokeStyle = c.border; g.strokeRect(10, y + 6, 30, rowH - 12); let n = 0, acc = 0; for (let t = 0; t < T; t++) { g.fillStyle = c.surface; g.fillRect(L + t * tw + 1, y + 8, tw - 2, rowH - 16); let fire = false; if (mode === 0) { acc += b; if (acc >= 255) { acc -= 255; fire = true; } } else { const tt = b > 32 ? Math.min(T - 1, Math.floor((255 - b) * T / 224)) : -1; fire = tt === t; } if (fire) { n++; g.fillStyle = c.spike; g.fillRect(L + t * tw + tw * .25, y + 8, Math.max(2, tw * .5), rowH - 16); } } total += n; g.fillStyle = c.text; g.font = font(13, 600); g.fillText(`${n} сп.`, Wd - R + 10, y + rowH / 2 + 5); }); info.textContent = mode === 0 ? `Частотный код: яркость = как часто стреляет пиксель. За ${T} тактов эти 6 пикселей дали ${total} спайков. Нужно много тактов, чтобы различить оттенки.` : `Временной код: каждый пиксель стреляет один раз, ярче — раньше. ${total} спайков на 6 пикселей при любом числе тактов. Информация в том, КОГДА пришёл спайк.`; } el.append(cv, info, h("div", { class: "w-row" }, seg(["Частотный код", "Временной код"], (i) => { mode = i; draw(); }), slider("Тактов", 2, 32, 8, (x) => { T = x; draw(); }))); draw(); alive(el, draw); }; /* 3. Сложение сигналов в нейроне */ W.synsum = (el) => { card(el); const ins = [{ w: 4, d: 1 }, { w: 4, d: 1 }, { w: 4, d: 3 }], TH = 10, T = 7; const cv = canvas(220, "Сложение входов по тактам"), verdict = h("p", { class: "w-verdict" }); function draw() { if (!cv.isConnected) return; const g = fit(cv), Wd = cv.clientWidth, H = 220, c = C(), L = 70, R = Wd - 16, tx = (t) => L + (t + .5) * (R - L) / T; g.clearRect(0, 0, Wd, H); let v = 0, fire = -1; const pot = []; for (let t = 0; t < T; t++) { ins.forEach((q) => { if (q.d === t) v += q.w; }); if (v < 0) v = 0; if (fire < 0 && v >= TH) fire = t; pot.push(v); } g.font = font(11); g.fillStyle = c.muted; g.textAlign = "center"; for (let t = 0; t < T; t++) g.fillText("такт " + t, tx(t), 12); ins.forEach((q, i) => { const y = 24 + i * 26; g.textAlign = "left"; g.fillStyle = c.muted; g.font = font(12); g.fillText("вход " + (i + 1), 4, y + 14); g.strokeStyle = c.border; g.beginPath(); g.moveTo(L, y + 10); g.lineTo(R, y + 10); g.stroke(); g.fillStyle = c.text; g.fillRect(tx(0) - 2, y + 2, 4, 16); if (q.d) { g.setLineDash([3, 3]); g.strokeStyle = c.muted; g.beginPath(); g.moveTo(tx(0), y + 10); g.lineTo(tx(q.d), y + 10); g.stroke(); g.setLineDash([]); } g.fillStyle = q.w >= 0 ? c.good : c.bad; g.beginPath(); g.arc(tx(q.d), y + 10, 8, 0, 7); g.fill(); g.fillStyle = c.shell; g.font = font(10, 700); g.textAlign = "center"; g.fillText((q.w > 0 ? "+" : "") + q.w, tx(q.d), y + 14); }); const pT = 112, pB = H - 24, y = (q) => pB - Math.min(q, TH * 1.6) / (TH * 1.6) * (pB - pT), bw = (R - L) / T * .5; g.setLineDash([5, 4]); g.strokeStyle = c.muted; g.beginPath(); g.moveTo(L, y(TH)); g.lineTo(R, y(TH)); g.stroke(); g.setLineDash([]); g.fillStyle = c.muted; g.textAlign = "left"; g.fillText("порог", 4, y(TH) + 4); pot.forEach((q, t) => { g.fillStyle = t === fire ? c.spike : c.pot; g.globalAlpha = t === fire ? 1 : .7; g.fillRect(tx(t) - bw / 2, y(q), bw, pB - y(q)); g.globalAlpha = 1; g.fillStyle = c.text; g.font = font(11, 600); g.textAlign = "center"; g.fillText(q, tx(t), y(q) - 4); }); verdict.className = "w-verdict " + (fire >= 0 ? "ok" : "no"); verdict.textContent = fire >= 0 ? `Выстрел на такте ${fire}: сошлось ${pot[fire]} при пороге ${TH}.` : `Молчит: максимум ${Math.max(...pot)} при пороге ${TH}.`; } const ctl = h("div", { class: "w-stack" }); ins.forEach((q, i) => ctl.append(h("div", { class: "w-row" }, h("span", { class: "w-mono", text: "вход " + (i + 1) }), slider("вес", -6, 8, q.w, (x) => { q.w = x; draw(); }, (x) => (x > 0 ? "+" : "") + x), slider("задержка, тактов", 0, 5, q.d, (x) => { q.d = x; draw(); })))); el.append(cv, verdict, ctl, note("Одинаковая задержка — спайки приходят вместе и складываются: нейрон становится детектором совпадения. Отрицательный вес — тормозящая связь, она может отменить выстрел.")); draw(); alive(el, draw); }; /* 4. STDP: вход до выстрела крепнет, после — слабеет */ W.stdp = (el) => { card(el); const T = 30, TH = 10, WIN = 5, NM = ["А", "Б", "В", "Г"]; let w, run = null, n = 0, timer = null; const cv = canvas(290, "Четыре входа нейрона и правило STDP"), say = note(""), info = h("span", { class: "w-mono" }); const reset = () => { w = [3.5, 3.5, 3.5, 3.5]; n = 0; trial(); }; function trial() { const base = 8 + Math.floor(Math.random() * 3), sp = [0, 1, 2].map((i) => [base + i * 2 + Math.floor(Math.random() * 2)]); sp.push([2 + Math.floor(Math.random() * (T - 4))]); if (Math.random() < .5) sp[3].push(2 + Math.floor(Math.random() * (T - 4))); let v = 0; const pot = [], fires = []; for (let t = 0; t < T; t++) { sp.forEach((q, i) => { if (q.includes(t)) v += w[i]; }); if (v >= TH) { fires.push(t); pot.push(TH); v = 0; } else pot.push(v); } const before = w.slice(), marks = sp.map((q) => q.map((t) => { let b = null; fires.forEach((f) => { const d = t - f; if (b === null || Math.abs(d) < Math.abs(b)) b = d; }); if (b === null) return { t, k: "far", dw: 0 }; if (b <= 0 && b >= -WIN) return { t, k: "help", dw: .6 * Math.exp(b / 3) }; if (b > 0 && b <= WIN) return { t, k: "late", dw: -.5 * Math.exp(-b / 3) }; return { t, k: "far", dw: 0 }; })); marks.forEach((m, i) => m.forEach((q) => { w[i] = Math.max(0, Math.min(6, w[i] + q.dw)); })); n++; run = { sp, pot, fires, marks, before }; draw(); } function draw() { if (!cv.isConnected || !run) return; const g = fit(cv), Wd = cv.clientWidth, H = 290, c = C(), L = 48, R = Wd - 74, tx = (t) => L + (t + .5) * (R - L) / T; g.clearRect(0, 0, Wd, H); const col = (k) => k === "help" ? c.good : k === "late" ? c.bad : c.faint; run.sp.forEach((q, i) => { const y = 12 + i * 28; g.strokeStyle = c.border; g.beginPath(); g.moveTo(L, y + 10); g.lineTo(R, y + 10); g.stroke(); g.fillStyle = c.text; g.font = font(12, 600); g.textAlign = "left"; g.fillText("вход " + NM[i], 2, y + 14); run.marks[i].forEach((m) => { g.fillStyle = col(m.k); g.fillRect(tx(m.t) - 2.5, y, 5, 20); }); const a = w[i], b = run.before[i]; g.fillStyle = a > b + .01 ? c.good : a < b - .01 ? c.bad : c.muted; g.font = font(12, 600); g.fillText("вес " + fmt(a, 1), R + 8, y + 14); }); const pT = 132, pB = H - 40, y = (q) => pB - q / TH * (pB - pT); run.fires.forEach((f) => { g.globalAlpha = .1; g.fillStyle = c.good; g.fillRect(tx(f - WIN) - 4, 6, tx(f) - tx(f - WIN) + 4, pB - 6); g.fillStyle = c.bad; g.fillRect(tx(f), 6, tx(f + WIN) - tx(f), pB - 6); g.globalAlpha = 1; }); g.setLineDash([5, 4]); g.strokeStyle = c.muted; g.beginPath(); g.moveTo(L, y(TH)); g.lineTo(R, y(TH)); g.stroke(); g.setLineDash([]); g.strokeStyle = c.pot; g.lineWidth = 2.2; g.beginPath(); run.pot.forEach((q, t) => { t ? g.lineTo(tx(t), y(q)) : g.moveTo(tx(t), y(q)); }); g.stroke(); g.lineWidth = 1; run.fires.forEach((f) => { g.fillStyle = c.spike; g.fillRect(tx(f) - 3, pB + 6, 6, 14); g.font = font(11, 600); g.textAlign = "center"; g.fillText("выстрел", tx(f), pB + 34); }); info.textContent = "показов: " + n; const abc = (w[0] + w[1] + w[2]) / 3; say.textContent = !run.fires.length ? "Входы не дотянули нейрон до порога, учиться не на чем. Покажите ещё раз." : n < 3 ? "А, Б, В пришли почти вместе и вместе дотянули нейрон до порога — их связи крепнут. Г — шум: если он пришёл после выстрела, его связь слабеет." : `После ${n} показов связи узора А-Б-В ≈ ${fmt(abc, 1)}, связь шума Г = ${fmt(w[3], 1)}. Нейрон сам выучил узор, без учителя.`; } el.append(cv, h("div", { class: "w-legend" }, h("span", { style: "--c: var(--w-good)", text: "помог — крепнет" }), h("span", { style: "--c: var(--w-bad)", text: "опоздал — слабеет" }), h("span", { style: "--c: var(--faint)", text: "далеко — без изменений" })), h("div", { class: "w-row" }, btn("Показать ещё раз", trial), seg(["Пауза", "Показывать подряд"], (i) => { clearInterval(timer); timer = i ? setInterval(() => { if (!el.isConnected) return clearInterval(timer); trial(); }, 900) : null; }), btn("Сначала", reset), info), say); reset(); alive(el, draw); }; /* 5. Соревнование нейронов (WTA) */ W.wta = (el) => { card(el); const pat = (q) => q.replace(/\s/g, "").split("").map(Number), X = pat("10001 01010 00100 01010 10001"), O = pat("01110 10001 10001 10001 01110"); const cos = (a, b) => { let t = 0, na = 0, nb = 0; for (let i = 0; i < a.length; i++) { t += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2; } return na && nb ? t / Math.sqrt(na * nb) : 0; }; let comp = true, wts, shown = 0, last = null, winner = -1; const init = () => { wts = [0, 1].map(() => Array.from({ length: 25 }, () => .25 + Math.random() * .2)); shown = 0; last = null; winner = -1; }; const present = () => { const im = Math.random() < .5 ? X : O, sc = wts.map((w) => w.reduce((s2, x, i) => s2 + x * im[i], 0) / (w.reduce((a, b) => a + b, 0) || 1)), win = sc[0] >= sc[1] ? 0 : 1; wts.forEach((w, n2) => { if (!comp || n2 === win) for (let i = 0; i < 25; i++) w[i] += .25 * (im[i] - w[i]); }); last = im; winner = comp ? win : -1; shown++; }; const g5 = (vals, f) => { const d = h("div", { class: "w-g5" }); vals.forEach((v) => d.append(h("div", { style: `background:${f(v)}` }))); return d; }; const box = h("div", { class: "w-row w-top" }), say = note(""), info = h("span", { class: "w-mono" }); function draw() { box.replaceChildren(h("div", { class: "w-fig" }, h("span", { class: "w-cap", text: "показали" }), g5(last || new Array(25).fill(0), (v) => v ? "var(--text)" : "var(--control)")), ...wts.map((w, n2) => { const f = h("div", { class: "w-fig" + (winner === n2 ? " w-win" : "") }, h("span", { class: "w-cap", text: "веса нейрона " + (n2 + 1) }), g5(w, (v) => `color-mix(in srgb, var(--w-pot) ${Math.round(Math.max(0, Math.min(1, v)) * 95)}%, var(--control))`)); return f; })); const d = (a, b) => a.reduce((s2, x, i) => s2 + Math.abs(x - b[i]), 0) / 25, split = (d(wts[0], X) < .25 && d(wts[1], O) < .25) || (d(wts[0], O) < .25 && d(wts[1], X) < .25); info.textContent = "показано: " + shown; say.textContent = !shown ? "Покажем вперемешку крестики и нолики. Рамка отмечает победителя — учится только он." : split ? "Нейроны поделили работу: один выучил крестик, другой нолик. Никто не говорил им, кому что." : !comp && shown >= 10 ? "Без соревнования оба нейрона выучили одинаковую смесь крестика и нолика и не различают картинки." : "Покажите ещё."; } el.append(box, h("div", { class: "w-row" }, seg(["С соревнованием", "Без соревнования"], (i) => { comp = i === 0; init(); draw(); }), btn("Показать 10 картинок", () => { for (let i = 0; i < 10; i++) present(); draw(); }), btn("Сначала", () => { init(); draw(); }), info), say); init(); draw(); }; /* 6. Сетка ядер Алтая */ W.chipgrid = (el) => { card(el); const svg = s("svg", { viewBox: "0 0 420 420", class: "w-svg", role: "img", "aria-label": "Сетка 4×4 ядра" }), say = note("Нажмите на ядро: пакет со спайком войдёт через порт и пройдёт по соседям."); let path = [], step = 0, target = null, timer = null; const gx = (i) => 40 + i * 90, gy = (j) => 80 + j * 82; function draw() { const c = C(); svg.replaceChildren(); svg.append(s("rect", { x: 34, y: 18, width: 352, height: 30, rx: 10, fill: c.spike, opacity: .18 }), s("text", { x: 210, y: 38, "text-anchor": "middle", fill: c.text, "font-size": 13 }, "порт: вход с камеры и выход ответа")); for (let i = 0; i < 4; i++) { svg.append(s("line", { x1: gx(i) + 35, y1: 48, x2: gx(i) + 35, y2: gy(0), stroke: c.border, "stroke-width": 3 })); for (let j = 0; j < 3; j++) svg.append(s("line", { x1: gx(i) + 35, y1: gy(j) + 70, x2: gx(i) + 35, y2: gy(j + 1), stroke: c.border, "stroke-width": 3 }), s("line", { x1: gx(j) + 70, y1: gy(i) + 35, x2: gx(j + 1), y2: gy(i) + 35, stroke: c.border, "stroke-width": 3 })); } const vis = new Set(path.slice(0, step + 1).map((p) => p.join())); for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) { const isT = target && target[0] === i && target[1] === j, v = vis.has(i + "," + j); const r = s("rect", { x: gx(i), y: gy(j), width: 70, height: 70, rx: 14, fill: isT ? c.spike : v ? c.control : c.shell, stroke: v ? c.spike : c.text, "stroke-width": 1.5, tabindex: 0, role: "button", "aria-label": "ядро " + (j * 4 + i + 1), style: "cursor:pointer" }); r.addEventListener("click", () => send(i, j)); r.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); send(i, j); } }); svg.append(r, s("text", { x: gx(i) + 35, y: gy(j) + 33, "text-anchor": "middle", fill: isT ? "#fff" : c.text, "font-size": 13, "pointer-events": "none" }, "ядро " + (j * 4 + i + 1)), s("text", { x: gx(i) + 35, y: gy(j) + 51, "text-anchor": "middle", fill: isT ? "#fff" : c.muted, "font-size": 11, "pointer-events": "none" }, "≈512 нейр.")); } if (path.length) { const p = path[Math.min(step, path.length - 1)]; svg.append(s("circle", { cx: gx(p[0]) + 35, cy: p[1] < 0 ? 33 : gy(p[1]) + 35, r: 8, fill: c.spike, stroke: c.shell, "stroke-width": 2 })); } } function send(i, j) { target = [i, j]; path = [[i, -1]]; for (let y = 0; y <= j; y++) path.push([i, y]); step = 0; clearInterval(timer); timer = setInterval(() => { if (!el.isConnected) return clearInterval(timer); step++; draw(); if (step >= path.length - 1) { clearInterval(timer); say.textContent = `Пакет вошёл через порт и дошёл до ядра ${j * 4 + i + 1} за ${j + 1} переход(а). Чем дальше ядро и чем больше пакетов, тем дольше путь — поэтому размещатель кладёт связанные нейроны рядом.`; } }, 260); draw(); } el.append(svg, say); draw(); alive(el, draw); }; /* 7. Кроссбар ядра */ W.crossbar = (el) => { card(el); const cyc = [0, 2, 3, 4, 5, 6, 8, -3, -5]; let Wt = [[4, 2, 0, 3], [4, 0, 5, 3], [3, 4, 0, 0], [0, 4, 6, 2], [0, 0, 3, 5], [-3, 0, 0, -5]], on = [true, true, false, false, false, false], sums = null; const tb = h("div", { class: "w-xb" }), say = note(""); function draw() { tb.replaceChildren(); tb.append(h("span", { class: "w-cap", text: "аксон ↓ / нейрон →" })); for (let n2 = 0; n2 < 4; n2++) tb.append(h("span", { class: "w-cap", text: "нейрон " + (n2 + 1) })); Wt.forEach((r, a) => { tb.append(h("button", { type: "button", class: "w-btn", "aria-pressed": String(on[a]), text: "аксон " + (a + 1), onclick: () => { on[a] = !on[a]; sums = null; draw(); } })); r.forEach((w, n2) => tb.append(h("button", { type: "button", class: "w-cell" + (w > 0 ? " pos" : w < 0 ? " neg" : ""), text: w ? (w > 0 ? "+" + w : String(w)) : "·", "aria-label": `вес ${w}`, onclick: () => { r[n2] = cyc[(cyc.indexOf(w) + 1) % cyc.length]; sums = null; draw(); } }))); }); tb.append(h("span", { class: "w-cap", text: "разных весов" })); const bad = []; for (let n2 = 0; n2 < 4; n2++) { const k = new Set(Wt.map((r) => Math.abs(r[n2])).filter(Boolean)).size; if (k > 4) bad.push(n2 + 1); tb.append(h("span", { class: "w-cnt " + (k > 4 ? "no" : "ok"), text: k + " из 4" })); } tb.append(h("span", { class: "w-cap", text: "сумма за такт" })); for (let n2 = 0; n2 < 4; n2++) tb.append(h("span", { class: "w-cnt" + (sums && sums[n2] >= 10 ? " fire" : ""), text: sums ? String(sums[n2]) : "—" })); say.textContent = bad.length ? `У нейрона ${bad.join(", ")} больше 4 разных величин веса — такой столбец чип не примет.` : sums ? (sums.some((v) => v >= 10) ? "Выстрелили нейроны, набравшие порог 10: каждый сложил веса из строк, по которым пришёл спайк." : "Никто не набрал порог 10.") : "Включите строки (входящие спайки), нажмите «Такт». Нажатие на клетку меняет вес."; } el.append(tb, h("div", { class: "w-row" }, btn("Такт: сложить и проверить порог", () => { sums = [0, 1, 2, 3].map((n2) => Wt.reduce((t, r, a) => t + (on[a] ? r[n2] : 0), 0)); draw(); })), say); draw(); }; /* 8. Квантование весов */ W.quant = (el) => { card(el); const N = 12; let w = [], K = 4; const cv = canvas(230, "Дробные и целые веса нейрона"), info = h("p", { class: "w-mono" }), verdict = h("p", { class: "w-verdict" }); const fresh = () => { w = Array.from({ length: N }, () => { const v = Math.random() * 2 - 1; return Math.abs(v) < .05 ? .05 * Math.sign(v || 1) : v; }); }; function pal(k) { const m = w.map(Math.abs).sort((a, b) => a - b); let lv = Array.from({ length: k }, (_, i) => m[Math.min(m.length - 1, Math.floor((i + .5) * m.length / k))]); for (let it = 0; it < 25; it++) { const sm = new Array(k).fill(0), ct = new Array(k).fill(0); m.forEach((x) => { let b = 0; lv.forEach((l, i) => { if (Math.abs(x - l) < Math.abs(x - lv[b])) b = i; }); sm[b] += x; ct[b]++; }); lv = lv.map((l, i) => ct[i] ? sm[i] / ct[i] : l); } return [...new Set(lv.map((v) => +v.toFixed(6)))].sort((a, b) => a - b); } function draw() { if (!cv.isConnected) return; const p = pal(K), mx = Math.max(...w.map(Math.abs)), toInt = (v) => Math.round(v / mx * 255), q = w.map((x) => { let b = p[0]; p.forEach((l) => { if (Math.abs(Math.abs(x) - l) < Math.abs(Math.abs(x) - b)) b = l; }); return Math.sign(x) * b; }); const g = fit(cv), Wd = cv.clientWidth, H = 230, c = C(), mid = H / 2, amp = H / 2 - 22, bw = (Wd - 20) / N; g.clearRect(0, 0, Wd, H); g.strokeStyle = c.border; g.beginPath(); g.moveTo(10, mid); g.lineTo(Wd - 10, mid); g.stroke(); p.forEach((l) => [1, -1].forEach((sg) => { const y = mid - sg * l / mx * amp; g.setLineDash([3, 4]); g.strokeStyle = c.pot; g.globalAlpha = .5; g.beginPath(); g.moveTo(10, y); g.lineTo(Wd - 10, y); g.stroke(); g.globalAlpha = 1; g.setLineDash([]); })); w.forEach((x, i) => { const X0 = 10 + i * bw; g.fillStyle = c.control; g.fillRect(X0 + bw * .12, Math.min(mid, mid - x / mx * amp), bw * .34, Math.abs(x / mx * amp)); g.fillStyle = q[i] >= 0 ? c.pot : c.bad; g.fillRect(X0 + bw * .5, Math.min(mid, mid - q[i] / mx * amp), bw * .34, Math.abs(q[i] / mx * amp)); g.fillStyle = c.text; g.font = font(10, 600); g.textAlign = "center"; g.fillText(toInt(q[i]), X0 + bw * .67, q[i] >= 0 ? mid - q[i] / mx * amp - 5 : mid - q[i] / mx * amp + 13); }); const err = Math.abs(w.reduce((a, b) => a + b, 0) - q.reduce((a, b) => a + b, 0)) / w.reduce((a, b) => a + Math.abs(b), 0); info.textContent = `ступеньки на чипе: ${p.map(toInt).join(", ")} · ошибка суммы ${pct(err)}`; verdict.className = "w-verdict " + (K <= 4 ? "ok" : "no"); verdict.textContent = K <= 4 ? "Такой нейрон можно загрузить на Алтай." : `${K} разных величин: точнее, но чип такой нейрон не примет.`; } el.append(cv, info, verdict, h("div", { class: "w-row" }, slider("Сколько разных величин разрешено", 1, 10, 4, (x) => { K = x; draw(); }), btn("Другой нейрон", () => { fresh(); draw(); })), note("Серые столбики — дробные веса после обучения, цветные — целые веса на чипе. Ошибка — насколько изменится сумма, если все входы выстрелят разом.")); fresh(); draw(); alive(el, draw); }; /* 9. Влезет ли сеть в чип */ W.fit = (el) => { card(el); const LIM = { n: 8192, w: 4, p: 32767 }, PRE = [["Нативная сеть цифр", { n: 416, w: 3, p: 12864 }], ["Импульсная сеть по тактам", { n: 266, w: 3, p: (D && D.snn.max_potential) || 900 }], ["Детекторы + выход (кошки/собаки)", { n: 39458, w: 110, p: 569761 }]]; let F = { ...PRE[0][1] }; const meters = h("div", { class: "w-stack" }), verdict = h("p", { class: "w-verdict" }); const lg = (v, lo, hi) => Math.round(Math.exp(Math.log(lo) + (Math.log(hi) - Math.log(lo)) * v / 100)), il = (v, lo, hi) => Math.round(100 * (Math.log(v) - Math.log(lo)) / (Math.log(hi) - Math.log(lo))); const sN = h("input", { type: "range", min: 0, max: 100 }), sW = h("input", { type: "range", min: 1, max: 120 }), sP = h("input", { type: "range", min: 0, max: 100 }); function meter(name, v, lim) { const ok = v <= lim; return h("div", { class: "w-meter" }, h("div", { class: "w-row w-between" }, h("span", { text: name }), h("span", { class: "w-mono " + (ok ? "ok" : "no"), text: `${fmt(v)} / предел ${fmt(lim)}` })), h("div", { class: "w-bar" }, h("i", { class: ok ? "ok" : "no", style: `width:${Math.min(v / lim * 50, 100)}%` }))); } function draw() { meters.replaceChildren(meter("Нейронов", F.n, LIM.n), meter("Разных весов на нейрон", F.w, LIM.w), meter("Максимальный уровень потенциала", F.p, LIM.p)); sN.value = il(F.n, 100, 131072); sW.value = F.w; sP.value = il(Math.max(F.p, 100), 100, 1e6); const bad = []; if (F.n > LIM.n) bad.push("нейронов больше, чем в прототипе"); if (F.w > LIM.w) bad.push("слишком много разных весов"); if (F.p > LIM.p) bad.push("уровень переполняет 16 бит"); verdict.className = "w-verdict " + (bad.length ? "no" : "ok"); verdict.textContent = bad.length ? "Не влезет: " + bad.join(", ") + "." : "Влезает в прототип Алтая."; } sN.oninput = () => { F.n = lg(+sN.value, 100, 131072); draw(); }; sW.oninput = () => { F.w = +sW.value; draw(); }; sP.oninput = () => { F.p = lg(+sP.value, 100, 1e6); draw(); }; el.append(seg(PRE.map((p) => p[0]), (i) => { F = { ...PRE[i][1] }; draw(); }), meters, verdict, h("div", { class: "w-row" }, h("label", { class: "w-ctl" }, h("span", { text: "Нейронов" }), sN), h("label", { class: "w-ctl" }, h("span", { text: "Разных весов" }), sW), h("label", { class: "w-ctl" }, h("span", { text: "Уровень" }), sP)), note("Черта посередине полосы — предел чипа. Оценка по трём ограничениям; настоящий размещатель проверяет ещё входящие линии ядер.")); draw(); }; /* 10. Схема трёх архитектур */ W.arch = (el) => { card(el); let mode = 0; const svg = s("svg", { viewBox: "0 0 700 300", class: "w-svg w-wide", role: "img", "aria-label": "Схема архитектуры" }), tab = h("div", { class: "w-kv" }); const ROWS = [ [["Слои", "пиксели → 10 колонок × 20 нейронов"], ["Ответ", "колонка, которая выстрелила больше"], ["Обучение", "в KNP: STDP и дофамин, без учителя ошибок"], ["Слабость", "учит трафареты, не видит отсутствие линий"]], [["Слои", "пиксели → 256 скрытых → 160 «за/против»"], ["Ответ", "«за» минус «против», максимум"], ["Обучение", "обычная сеть на компьютере → перенос весов"], ["Цена", "32 такта частотного кода, сотни тысяч синаптических событий"]], [["Слои", "пиксели → 256 скрытых → 10 выходов"], ["Ответ", "больше спайков, при равенстве — раньше"], ["Обучение", "градиент прямо по тактам динамики Алтая"], ["Выигрыш", "каждый пиксель стреляет один раз, ответ за несколько тактов"]]]; const txt = (x, y, t, size = 12, color, weight = 400, anchor = "middle") => svg.append(s("text", { x, y, "text-anchor": anchor, fill: color || C().muted, "font-size": size, "font-weight": weight }, t)); function draw() { const c = C(); svg.replaceChildren(); const lines = []; const D7 = [[1, 0], [2, 0], [3, 0], [4, 0], [4, 1], [3, 2], [2, 3], [2, 4], [2, 5]]; for (let y = 0; y < 6; y++) for (let x = 0; x < 6; x++) svg.append(s("rect", { x: 16 + x * 13, y: 110 + y * 13, width: 11, height: 11, rx: 2, fill: D7.some(([a, b]) => a === x && b === y) ? c.text : c.control })); txt(55, 96, mode === 2 ? "пиксели → один спайк" : "пиксели → спайки"); if (mode === 0) { for (let k = 0; k < 10; k++) { const cx = 190 + k * 36, win = k === 7; svg.append(s("rect", { x: cx - 14, y: 56, width: 28, height: 176, rx: 9, fill: win ? c.control : "none", stroke: win ? c.spike : c.border, "stroke-width": win ? 2 : 1 })); for (let n2 = 0; n2 < 10; n2++) svg.append(s("circle", { cx: cx - 5 + (n2 % 2) * 10, cy: 70 + Math.floor(n2 / 2) * 34, r: 4, fill: win && n2 < 6 ? c.spike : c.pot })); txt(cx, 252, String(k), 13, win ? c.spike : c.text, 600); lines.push([96, 145, cx - 14, 90 + (k * 37) % 120]); } txt(352, 40, "10 колонок по 20 нейронов: каждая колонка — трафарет своей цифры", 12, c.text, 600); txt(352, 280, "ответ — колонка «7»: выстрелила больше всех", 13, c.text, 600); } else { const hx = 210, hy = 70; for (let i = 0; i < 64; i++) svg.append(s("circle", { cx: hx + (i % 8) * 16, cy: hy + Math.floor(i / 8) * 20, r: 4.5, fill: (i * 29) % 7 < 2 ? c.spike : c.pot })); txt(hx + 56, 50, "256 скрытых нейронов (показана четверть)", 12, c.text, 600); for (let k = 0; k < 8; k++) lines.push([96, 145, hx - 6, hy + k * 20]); const ox = 420, oy = 64; if (mode === 1) { for (let d = 0; d < 10; d++) { const y = oy + d * 19; txt(ox - 10, y + 5, String(d), 12, d === 7 ? c.spike : c.text, 600, "end"); for (let g2 = 0; g2 < 8; g2++) svg.append(s("rect", { x: ox + g2 * 9, y: y - 5, width: 7, height: 10, rx: 2, fill: c.good, opacity: d === 7 ? 1 : .3 }), s("rect", { x: ox + 80 + g2 * 9, y: y - 5, width: 7, height: 10, rx: 2, fill: c.bad, opacity: d === 4 || d === 9 ? .9 : .25 })); lines.push([hx + 118, hy + (d * 31) % 140, ox - 2, y]); } txt(ox + 32, 48, "за ×8", 11, c.good, 600); txt(ox + 112, 48, "против ×8", 11, c.bad, 600); svg.append(s("rect", { x: 600, y: 140, width: 90, height: 54, rx: 12, fill: c.text })); txt(645, 163, "хост:", 11, c.shell, 600); txt(645, 181, "за − против", 11, c.shell); txt(330, 282, "32 такта · каждый пиксель стреляет до 32 раз", 13, c.text, 600); } else { for (let d = 0; d < 10; d++) { const y = oy + d * 19; txt(ox - 10, y + 5, String(d), 12, d === 7 ? c.spike : c.text, 600, "end"); svg.append(s("circle", { cx: ox + 10, cy: y, r: 6, fill: d === 7 ? c.spike : c.pot })); for (let t = 0; t < 8; t++) svg.append(s("rect", { x: ox + 26 + t * 12, y: y - 4, width: 9, height: 8, rx: 2, fill: d === 7 && t >= 3 && t % 2 ? c.spike : c.control })); lines.push([hx + 118, hy + (d * 31) % 140, ox + 4, y]); } txt(ox + 74, 48, "такты 0…7", 11, c.text, 600); txt(330, 282, "8 тактов · каждый пиксель стреляет один раз", 13, c.text, 600); } } lines.forEach(([x1, y1, x2, y2]) => svg.prepend(s("line", { x1, y1, x2, y2, stroke: c.border }))); tab.replaceChildren(...ROWS[mode].flatMap(([k, v]) => [h("b", { text: k }), h("span", { text: v })])); } el.append(seg(["Колонки STDP", "Нативная сеть", "Импульсная по тактам"], (i) => { mode = i; draw(); }), h("div", { class: "w-scroll" }, svg), tab); draw(); alive(el, draw); }; /* 11. Одна цифра — три сети */ W.compare = (el) => { card(el); if (!D) { el.append(note("Данные моделей не загружены.")); return; } const pad = drawPad((img) => run(img, null)), out = h("div", { class: "w-cmp" }), raw = h("canvas", { class: "w-px", width: 28, height: 28 }), prep = h("canvas", { class: "w-px", width: 28, height: 28 }); let lastRaw = null, lastLabel = null; function panel(name, r, label, extra) { const ok = r.answer === label, vd = label == null ? "" : r.answer < 0 ? "молчит" : ok ? "верно" : `ошибка, это ${label}`; return h("div", { class: "w-panel" }, h("div", { class: "w-row w-between" }, h("b", { text: name }), h("span", { class: "w-ans", text: r.answer < 0 ? "–" : r.answer }), h("span", { class: "w-badge " + (label == null ? "" : ok ? "ok" : "no"), text: vd })), extra, h("div", { class: "w-energy" }, h("span", { text: `тактов ${r.energy.ticks}` }), h("span", { text: `входных спайков ${fmt(r.energy.input)}` }), h("span", { text: `синапт. событий ${fmt(r.energy.synaptic)}` }))); } const bars = (vals, ans, fmtv) => { const mx = Math.max(1, ...vals.map((v) => Math.max(v, 0))); return h("div", { class: "w-bars" }, ...vals.flatMap((v, d) => [h("span", { text: d }), h("i", { class: d === ans ? "win" : "", style: `width:${100 * Math.max(v, 0) / mx}%` }), h("b", { text: fmtv(d) })])); }; function run(img, label) { lastRaw = img; lastLabel = label; const p = HOST.prepare(img); grid28(raw, (q) => [img[q], img[q], img[q]]); grid28(prep, (q) => [p.final[q], p.final[q], p.final[q]]); const a = runCols(img), b = runNative(p.final), c2 = runSNN(p.final); out.replaceChildren(panel("Колонки STDP", a, label, bars(a.votes, a.answer, (d) => a.votes[d] + " гол.")), panel("Нативная сеть", b, label, bars(b.score, b.answer, (d) => `${b.pos[d]}−${b.neg[d]}`)), panel(`Импульсная, ${M.snn.ticks} тактов`, c2, label, bars(Array.from(c2.trace[c2.trace.length - 1].cnt), c2.answer, (d) => c2.trace[c2.trace.length - 1].cnt[d] + " сп."))); } el.append(h("div", { class: "w-row w-top" }, h("div", { class: "w-fig" }, pad.el, btn("Очистить", () => pad.clear())), h("div", { class: "w-fig" }, h("span", { class: "w-cap", text: "вход" }), raw, h("span", { class: "w-cap", text: "после хоста" }), prep)), gallery((img, lab) => { pad.show(img); run(img, lab); }), out, note("Все три сети считаются здесь целочисленно, так же как на чипе. Колонки получают исходную картинку, две другие — картинку после обработки на хосте (центровка и выпрямление). Синаптическое событие — одно сложение «спайк × вес»: на чипе энергия тратится именно на них.")); el.querySelector(".w-gal button").click(); alive(el, () => lastRaw && run(lastRaw, lastLabel)); }; /* 12. Импульсная сеть по тактам */ W.ticks = (el) => { card(el); if (!D) { el.append(note("Данные моделей не загружены.")); return; } const m = M.snn, T = m.ticks; let res = null, t = T, label = null, timer = null, inp = null; const pad = drawPad((img) => load(img, null)), inCv = h("canvas", { class: "w-px", width: 28, height: 28, "aria-label": "Входные спайки по тактам" }), hidCv = h("canvas", { class: "w-px", width: 16, height: 16, "aria-label": "256 скрытых нейронов" }); const outBox = h("div", { class: "w-race" }), tickLab = h("b", { class: "w-mono" }), say = note(""), en = h("div", { class: "w-energy" }); const range = h("input", { type: "range", min: 0, max: T, value: T, "aria-label": "такт" }); range.oninput = () => { stop(); t = +range.value; draw(); }; const play = btn("▶ Пуск", () => timer ? stop() : start()); function start() { if (t >= T) t = -1; play.textContent = "❚❚ Пауза"; timer = setInterval(() => { if (!el.isConnected) return stop(); t++; draw(); if (t >= T) stop(); }, 650); } function stop() { clearInterval(timer); timer = null; play.textContent = "▶ Пуск"; } function load(img, lab) { label = lab; inp = HOST.prepare(img).final; res = runSNN(inp); t = -1; draw(); stop(); start(); } function draw() { if (!res || !inCv.isConnected) return; const c = C(), tt = Math.max(0, t), st = res.trace[tt], sp = rgb(c.spike), pot = rgb(c.pot), bg = rgb(c.control), dim = rgb(c.muted); range.value = tt; tickLab.textContent = `такт ${tt} из ${T}`; grid28(inCv, (p) => { const tp = res.tin[p]; if (tp === tt && tt < T) return sp; if (tp >= 0 && tp < tt) return mix(bg, dim, .55); if (tp > tt) return mix(bg, [inp[p], inp[p], inp[p]], .25); return bg; }); const fired = new Uint8Array(256); for (let k = 0; k <= tt && k < res.trace.length; k++) res.trace[k].s1.forEach((v, j) => { if (v) fired[j] = 1; }); const hg = hidCv.getContext("2d"), id = hg.createImageData(16, 16); for (let j = 0; j < 256; j++) { const col = st.s1[j] ? sp : fired[j] ? mix(bg, sp, .35) : mix(bg, pot, Math.min(1, st.v1[j] / m.th_h)); id.data.set([...col, 255], j * 4); } hg.putImageData(id, 0, 0); const ans = res.answer; outBox.replaceChildren(...Array.from({ length: 10 }, (_, d) => { const lv = Math.min(1, st.v2[d] / m.th_o), n = st.cnt[d]; return h("div", { class: "w-lane" + (st.s2[d] ? " fire" : "") + (tt >= res.settled && d === ans ? " lead" : "") }, h("span", { class: "w-mono", text: d }), h("div", { class: "w-tank" }, h("i", { style: `height:${lv * 100}%` })), h("div", { class: "w-dots" }, ...Array.from({ length: n }, () => h("i"))), h("b", { class: "w-mono", text: n + " сп." })); })); const e = res.energy, nat = 338328; en.replaceChildren(h("span", { text: `входных спайков: ${fmt(e.input)}` }), h("span", { text: `спайков нейронов: ${fmt(e.neurons)}` }), h("span", { text: `синаптических событий: ${fmt(e.synaptic)}` }), h("span", { text: `нативная сеть на 32 тактах тратит в среднем ≈${fmt(nat)}` })); say.textContent = tt === 0 ? "Такт 0: стреляют самые яркие пиксели. Скрытый слой копит заряд; выходы пока пусты — спайкам нужен такт, чтобы дойти." : tt < res.settled ? `Такт ${tt}: подходят пиксели потусклее, скрытые нейроны стреляют и передают спайки выходам. Лидер ещё может смениться.` : `С такта ${res.settled} лидер больше не меняется: ответ «${ans}»${label == null ? "" : ans === label ? " — верно" : ` — ошибка, это ${label}`}. Дальше можно не считать: это и есть ранний ответ.`; } el.append(h("div", { class: "w-row w-top" }, h("div", { class: "w-fig" }, pad.el, btn("Очистить", () => pad.clear())), h("div", { class: "w-fig" }, h("span", { class: "w-cap", text: "входные спайки этого такта" }), inCv), h("div", { class: "w-fig" }, h("span", { class: "w-cap", text: "256 скрытых нейронов" }), hidCv), h("div", { class: "w-fig w-grow" }, h("span", { class: "w-cap", text: "10 выходов: заряд и спайки" }), outBox)), h("div", { class: "w-row" }, play, range, tickLab), say, en, gallery((img, lab) => { pad.show(img); load(img, lab); }), h("div", { class: "w-legend" }, h("span", { style: "--c: var(--w-spike)", text: "спайк на этом такте" }), h("span", { style: "--c: var(--w-pot)", text: "заряд до порога" }), h("span", { style: "--c: var(--muted)", text: "пиксель уже выстрелил" }))); el.querySelector(".w-gal button").click(); alive(el, draw); }; /* 13. Обработка на хосте по шагам */ W.hostprep = (el) => { card(el); if (!D) { el.append(note("Данные моделей не загружены.")); return; } const cvs = ["рисунок 28×28", "вписан в 20×20 и отцентрован", "наклон выпрямлен", "время спайка каждого пикселя"].map((cap) => [h("canvas", { class: "w-px", width: 28, height: 28 }), cap]); const pad = drawPad((img) => show(img)); function show(img) { const p = HOST.prepare(img), T = M.snn.ticks, c = C(), sp = rgb(c.spike), bg = rgb(c.control); grid28(cvs[0][0], (q) => [img[q], img[q], img[q]]); grid28(cvs[1][0], (q) => [p.normalized[q], p.normalized[q], p.normalized[q]]); grid28(cvs[2][0], (q) => [p.final[q], p.final[q], p.final[q]]); grid28(cvs[3][0], (q) => { const v = p.final[q]; if (v <= 32) return bg; const tt = Math.min(T - 1, Math.floor((255 - v) * T / 224)); return mix(sp, bg, tt / T); }); } el.append(h("div", { class: "w-row w-top" }, h("div", { class: "w-fig" }, pad.el, btn("Очистить", () => pad.clear())), ...cvs.map(([cv, cap]) => h("div", { class: "w-fig" }, h("span", { class: "w-cap", text: cap }), cv))), gallery((img) => { pad.show(img); show(img); }, 12), note("Последняя картинка: чем ярче оранжевый, тем раньше стреляет пиксель. Тусклые пиксели стреляют в последних тактах, самые тёмные не стреляют вовсе.")); el.querySelector(".w-gal button").click(); }; /* 14. Точность против энергии */ W.tradeoff = (el) => { card(el); const P = (D && D.tradeoff) || []; const cv = canvas(300, "Точность против синаптических событий"), tip = note("Наведите на точку."); let pts = []; function draw() { if (!cv.isConnected) return; const g = fit(cv), Wd = cv.clientWidth, H = 300, c = C(), L = 52, R = Wd - 18, T0 = 16, B = H - 42, lx = (v) => L + (Math.log10(v) - 3.8) / (6 - 3.8) * (R - L), y = (a) => B - (a - 88) / (100 - 88) * (B - T0); g.clearRect(0, 0, Wd, H); g.strokeStyle = c.border; g.fillStyle = c.muted; g.font = font(11); g.textAlign = "right"; [88, 92, 96, 100].forEach((a) => { g.beginPath(); g.moveTo(L, y(a)); g.lineTo(R, y(a)); g.stroke(); g.fillText(a + "%", L - 6, y(a) + 4); }); g.textAlign = "center"; [1e4, 3e4, 1e5, 3e5, 1e6].forEach((v) => { g.beginPath(); g.moveTo(lx(v), T0); g.lineTo(lx(v), B); g.stroke(); g.fillText(v >= 1e6 ? "1 млн" : fmt(v / 1000) + " тыс.", lx(v), B + 16); }); g.fillText("синаптических событий на картинку (логарифм) →", (L + R) / 2, H - 6); pts = P.map((p) => ({ ...p, x: lx(p.syn), y: y(p.acc) })); pts.forEach((p) => { g.fillStyle = p.kind === "snn" ? c.spike : p.kind === "native" ? c.pot : c.muted; g.beginPath(); g.arc(p.x, p.y, p.best ? 8 : 6, 0, 7); g.fill(); if (p.best) { g.strokeStyle = c.text; g.lineWidth = 2; g.stroke(); g.lineWidth = 1; } g.fillStyle = c.text; g.font = font(11, 600); g.textAlign = "left"; g.fillText(p.short, p.x + 10, p.y + 4 + (p.dy || 0)); }); } cv.addEventListener("pointermove", (e) => { const r = cv.getBoundingClientRect(), mx = e.clientX - r.left, my = e.clientY - r.top; let b = null, bd = 30; pts.forEach((p) => { const d = Math.hypot(p.x - mx, p.y - my); if (d < bd) { bd = d; b = p; } }); if (b) tip.textContent = `${b.name}: ${fmt(b.acc, 2)}% на тесте, ${b.ticks} тактов, ≈${fmt(b.syn)} синаптических событий, ≈${fmt(b.inp)} входных спайков.`; }); el.append(cv, h("div", { class: "w-legend" }, h("span", { style: "--c: var(--muted)", text: "колонки STDP" }), h("span", { style: "--c: var(--w-pot)", text: "нативная сеть, частотный код" }), h("span", { style: "--c: var(--w-spike)", text: "импульсная по тактам" })), tip); draw(); alive(el, draw); }; /* 15. Бюджет нейронов для распознавания мусора */ W.budget = (el) => { card(el); let S = 32, st = 3, K = 24, win = 6, Hd = 128, out = 4; const res = h("div", { class: "w-stack" }); function draw() { const p = Math.floor((S - win) / st) + 1, det = p * p * K, total = det + Hd + out * 2, cores = Math.ceil(total / 512), ok = total <= 8192; res.replaceChildren(h("div", { class: "w-energy" }, h("span", { text: `позиций окна ${p}×${p} = ${fmt(p * p)}` }), h("span", { text: `детекторов ${fmt(det)}` }), h("span", { text: `всего нейронов ${fmt(total)}` }), h("span", { text: `≈${cores} ядер из 16` })), h("div", { class: "w-bar" }, h("i", { class: ok ? "ok" : "no", style: `width:${Math.min(total / 8192 * 50, 100)}%` })), h("p", { class: "w-verdict " + (ok ? "ok" : "no"), text: ok ? `Влезает: занято ${Math.round(total / 8192 * 100)}% прототипа.` : `Не влезает: нужно в ${fmt(total / 8192, 1)} раза больше нейронов.` })); } el.append(h("div", { class: "w-row" }, slider("Размер окна кадра, пикселей", 16, 64, S, (x) => { S = x; draw(); }), slider("Шаг детекторов", 1, 6, st, (x) => { st = x; draw(); }), slider("Типов детекторов", 4, 128, K, (x) => { K = x; draw(); })), h("div", { class: "w-row" }, slider("Размер детектора", 3, 9, win, (x) => { win = x; draw(); }), slider("Скрытых нейронов", 16, 512, Hd, (x) => { Hd = x; draw(); }), slider("Классов", 2, 10, out, (x) => { out = x; draw(); })), res, note("Детекторов = позиций × типов: на чипе нет общих весов, каждый детектор в каждой позиции — отдельный нейрон. Предел прототипа — 8192 нейрона, ≈512 на ядро.")); draw(); }; /* --------- подключение --------- */ function mountAll() { document.querySelectorAll("[data-widget]:not([data-mounted])").forEach((el) => { el.dataset.mounted = "1"; const f = W[el.dataset.widget]; if (!f) return; try { f(el); } catch (e) { el.textContent = "Виджет не загрузился: " + e.message; console.error(e); } }); } new MutationObserver(() => mountAll()).observe(document.documentElement, { childList: true, subtree: true }); document.addEventListener("docslib:rendered", mountAll); new MutationObserver(() => redraws.forEach((f) => f())).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] }); let rt; window.addEventListener("resize", () => { clearTimeout(rt); rt = setTimeout(() => redraws.forEach((f) => f()), 120); }); mountAll(); globalThis.WIKI_WIDGETS = W; })();