Add project wiki built on SharedDocsLib
Definitions, system overview, architecture comparison and interactive widgets: AltAILIF neuron, input coding, STDP, Altai core grid and crossbar, quantisation, chip budget, and three recognisers (STDP columns, native network, tick-trained spiking network) computed in the browser with integer arithmetic on their real weights. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFVgZ8Nct8BEh9k7bjuRdw
This commit is contained in:
parent
4936928a8d
commit
917808edda
6 changed files with 1228 additions and 0 deletions
2
wiki/.gitignore
vendored
Normal file
2
wiki/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
assets/inject.generated.js
|
||||
build/
|
||||
24
wiki/README.md
Normal file
24
wiki/README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Вики «Нейроморфное распознавание на Алтае»
|
||||
|
||||
Документация проекта на [SharedDocsLib](https://git.snw.su/snw/SharedDocsLib): определения, устройство системы, сравнение архитектур и интерактивные виджеты.
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
uv pip install "git+https://git.snw.su/snw/SharedDocsLib.git@main"
|
||||
python wiki/knp_wiki.py build # статический комплект в wiki/build: index.html, manifest.json, version, index.zip
|
||||
python wiki/knp_wiki.py # live-preview на http://127.0.0.1:1234/index.html
|
||||
```
|
||||
|
||||
## Устройство
|
||||
|
||||
| Файл | Что внутри |
|
||||
|---|---|
|
||||
| `knp_wiki.py` | страницы (`@Page`) и компоненты |
|
||||
| `assets/widgets.js` | интерактивные виджеты; оживляют `<div data-widget="…">` после вставки страницы |
|
||||
| `assets/widgets.css` | стили виджетов на переменных темы SharedDocsLib |
|
||||
| `data/models.json` | веса трёх сетей (колонки STDP, нативная, импульсная по тактам), 24 тестовые цифры MNIST, точки графика «точность — энергия» |
|
||||
|
||||
Страницы вставляются через `innerHTML`, поэтому `<script>` внутри страницы не выполняется. Виджеты подключаются через `custom_js`: при сборке `data/models.json` и `assets/widgets.js` склеиваются в `assets/inject.generated.js`.
|
||||
|
||||
Все три сети в виджетах считаются целочисленно, так же как на чипе. Расчёт сверен с Python-эталоном на 10 000 тестовых цифрах MNIST: нативная сеть и импульсная сеть по тактам дают те же ответы и ту же точность.
|
||||
92
wiki/assets/widgets.css
Normal file
92
wiki/assets/widgets.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/* Виджеты вики: монохромная тема SharedDocsLib плюс три смысловых цвета (спайк, заряд, верно/неверно). */
|
||||
:root {
|
||||
--w-spike: #c8571b;
|
||||
--w-pot: #1f7a83;
|
||||
--w-good: #2f7d47;
|
||||
--w-bad: #b83a3a;
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--w-spike: #f08a3e;
|
||||
--w-pot: #45bcc4;
|
||||
--w-good: #5cc27a;
|
||||
--w-bad: #ef6b6b;
|
||||
}
|
||||
.content .lead { max-width: 720px; color: var(--muted); font-size: 1.22em; line-height: 1.5; }
|
||||
.content .w-card { margin: 1.4rem 0 1.8rem; padding: 18px; border: 1px solid var(--border); border-radius: 20px; background: var(--shell); display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
||||
.content .w-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||
.content .w-row.w-top { align-items: flex-start; }
|
||||
.content .w-row.w-between { justify-content: space-between; }
|
||||
.content .w-stack { display: flex; flex-direction: column; gap: 12px; }
|
||||
.content .w-canvas { width: 100%; display: block; border-radius: 14px; background: var(--surface); }
|
||||
.content .w-svg { width: 100%; max-width: 440px; display: block; margin: 0 auto; }
|
||||
.content .w-svg.w-wide { max-width: none; min-width: 560px; }
|
||||
.content .w-scroll { overflow-x: auto; }
|
||||
.content .w-note { margin: 0; color: var(--muted); font-size: .95em; }
|
||||
.content .w-mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; font-size: .9em; }
|
||||
.content .w-cap { color: var(--muted); font-size: .85em; }
|
||||
.content .w-btn, .content .w-seg button { font: inherit; font-size: .92em; color: var(--text); background: var(--control); border: 1px solid var(--border); border-radius: 12px; padding: 7px 13px; cursor: pointer; }
|
||||
.content .w-btn:hover, .content .w-seg button:hover { border-color: var(--muted); }
|
||||
.content .w-btn[aria-pressed="true"], .content .w-seg button[aria-pressed="true"] { background: var(--selection-bg); color: var(--selection-text); border-color: var(--selection-bg); }
|
||||
.content .w-seg { display: inline-flex; flex-wrap: wrap; gap: 4px; }
|
||||
.content .w-btn:focus-visible, .content .w-seg button:focus-visible, .content .w-card input:focus-visible { outline: 2px solid var(--w-pot); outline-offset: 2px; }
|
||||
.content .w-ctl { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 150px; color: var(--muted); font-size: .88em; }
|
||||
.content .w-ctl b { color: var(--text); font-family: ui-monospace, Menlo, Consolas, monospace; }
|
||||
.content .w-card input[type="range"] { width: 100%; accent-color: var(--w-pot); }
|
||||
.content .w-legend { display: flex; flex-wrap: wrap; gap: 16px; color: var(--muted); font-size: .85em; }
|
||||
.content .w-legend span::before { content: ""; display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; background: var(--c); }
|
||||
.content .w-verdict { margin: 0; font-weight: 600; }
|
||||
.content .w-verdict.ok, .content .ok { color: var(--w-good); }
|
||||
.content .w-verdict.no, .content .no { color: var(--w-bad); }
|
||||
.content .w-fig { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
|
||||
.content .w-fig.w-grow { flex: 1; min-width: 220px; }
|
||||
.content .w-fig.w-win .w-g5 { outline: 2px solid var(--w-spike); outline-offset: 3px; border-radius: 6px; }
|
||||
.content .w-g5 { display: grid; grid-template-columns: repeat(5, 24px); gap: 3px; }
|
||||
.content .w-g5 div { width: 24px; height: 24px; border-radius: 4px; }
|
||||
.content .w-px { width: 120px; height: 120px; image-rendering: pixelated; border-radius: 10px; background: #000; display: block; }
|
||||
.content .w-pad { width: 220px; max-width: 100%; aspect-ratio: 1; border-radius: 14px; background: #000; touch-action: none; cursor: crosshair; display: block; }
|
||||
.content .w-gal { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.content .w-gal button { position: relative; padding: 2px; border: 2px solid var(--border); border-radius: 10px; background: var(--shell); cursor: pointer; line-height: 0; }
|
||||
.content .w-gal button[aria-pressed="true"] { border-color: var(--text); }
|
||||
.content .w-gal canvas { width: 38px; height: 38px; image-rendering: pixelated; border-radius: 6px; }
|
||||
.content .w-gal span { position: absolute; right: 5px; bottom: 4px; font: 600 10px ui-monospace, monospace; color: #fff; text-shadow: 0 0 2px #000; line-height: 1; }
|
||||
.content .w-cmp { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
|
||||
.content .w-panel { border: 1px solid var(--border); border-radius: 16px; padding: 14px; display: flex; flex-direction: column; gap: 10px; min-width: 0; background: var(--surface); }
|
||||
.content .w-ans { font-size: 2em; font-weight: 700; line-height: 1; }
|
||||
.content .w-badge { font-size: .8em; padding: 2px 9px; border-radius: 99px; border: 1px solid var(--border); color: var(--muted); }
|
||||
.content .w-badge.ok { border-color: var(--w-good); }
|
||||
.content .w-badge.no { border-color: var(--w-bad); }
|
||||
.content .w-bars { display: grid; grid-template-columns: 14px 1fr 60px; gap: 3px 8px; align-items: center; font: .8em ui-monospace, monospace; }
|
||||
.content .w-bars i { display: block; height: 9px; border-radius: 3px; background: var(--faint); min-width: 1px; }
|
||||
.content .w-bars i.win { background: var(--text); }
|
||||
.content .w-bars b { font-weight: 400; color: var(--muted); text-align: right; }
|
||||
.content .w-energy { display: flex; flex-wrap: wrap; gap: 6px 14px; color: var(--muted); font-size: .85em; font-variant-numeric: tabular-nums; }
|
||||
.content .w-race { display: flex; flex-direction: column; gap: 3px; width: 100%; }
|
||||
.content .w-lane { display: grid; grid-template-columns: 16px 14px 1fr 54px; gap: 8px; align-items: center; padding: 2px 6px; border-radius: 8px; }
|
||||
.content .w-lane.fire { background: color-mix(in srgb, var(--w-spike) 18%, transparent); }
|
||||
.content .w-lane.lead { outline: 1.5px solid var(--w-good); }
|
||||
.content .w-tank { width: 14px; height: 16px; border-radius: 3px; background: var(--control); position: relative; overflow: hidden; }
|
||||
.content .w-tank i { position: absolute; left: 0; right: 0; bottom: 0; background: var(--w-pot); }
|
||||
.content .w-dots { display: flex; gap: 3px; flex-wrap: wrap; }
|
||||
.content .w-dots i { width: 9px; height: 9px; border-radius: 50%; background: var(--w-spike); }
|
||||
.content .w-lane b { font-weight: 400; color: var(--muted); text-align: right; }
|
||||
.content .w-kv { display: grid; grid-template-columns: minmax(110px, max-content) 1fr; gap: 8px 16px; font-size: .95em; }
|
||||
.content .w-kv b { font-weight: 600; }
|
||||
.content .w-xb { display: grid; grid-template-columns: minmax(84px, auto) repeat(4, minmax(46px, 1fr)); gap: 5px; align-items: center; }
|
||||
.content .w-cell { height: 38px; border-radius: 10px; border: 1px solid var(--border); background: var(--control); color: var(--text); font: 600 .9em ui-monospace, monospace; cursor: pointer; }
|
||||
.content .w-cell.pos { background: color-mix(in srgb, var(--w-pot) 22%, var(--control)); }
|
||||
.content .w-cell.neg { background: color-mix(in srgb, var(--w-bad) 20%, var(--control)); }
|
||||
.content .w-cnt { text-align: center; font: 600 .85em ui-monospace, monospace; padding: 4px 0; border-radius: 8px; }
|
||||
.content .w-cnt.fire { background: var(--w-spike); color: #fff; }
|
||||
.content .w-meter { display: flex; flex-direction: column; gap: 5px; }
|
||||
.content .w-bar { height: 10px; border-radius: 99px; background: var(--control); position: relative; overflow: hidden; }
|
||||
.content .w-bar::after { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; background: var(--text); opacity: .5; }
|
||||
.content .w-bar i { position: absolute; inset: 0 auto 0 0; border-radius: 99px; background: var(--w-good); }
|
||||
.content .w-bar i.no { background: var(--w-bad); }
|
||||
.content .def { border-left: 2px solid var(--text); padding: .2rem 0 .2rem 1rem; margin: 1rem 0 1.4rem; font-size: 1.08em; }
|
||||
.content .facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin: 1.2rem 0 1.6rem; }
|
||||
.content .facts div { border: 1px solid var(--border); border-radius: 16px; padding: 12px 14px; background: var(--shell); display: flex; flex-direction: column; gap: 2px; }
|
||||
.content .facts b { font-size: 1.5em; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.content .facts span { color: var(--muted); font-size: .88em; }
|
||||
@media (max-width: 640px) { .content .w-px { width: 96px; height: 96px; } .content .w-kv { grid-template-columns: 1fr; } }
|
||||
@media print { .content .w-card { display: none; } }
|
||||
@media (prefers-reduced-motion: reduce) { .content * { transition: none !important; } }
|
||||
468
wiki/assets/widgets.js
Normal file
468
wiki/assets/widgets.js
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
/* Интерактивные виджеты вики «Нейроморфное распознавание на Алтае».
|
||||
* Страница вставляется через innerHTML, поэтому скрипты в ней не выполняются: здесь мы находим
|
||||
* элементы <div data-widget="имя"> и оживляем их. Данные моделей (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;
|
||||
})();
|
||||
1
wiki/data/models.json
Normal file
1
wiki/data/models.json
Normal file
File diff suppressed because one or more lines are too long
641
wiki/knp_wiki.py
Normal file
641
wiki/knp_wiki.py
Normal file
|
|
@ -0,0 +1,641 @@
|
|||
"""Вики «Нейроморфное распознавание на Алтае» на SharedDocsLib.
|
||||
|
||||
Сборка статического комплекта: python wiki/knp_wiki.py build
|
||||
Live-preview на порту 1234: python wiki/knp_wiki.py
|
||||
|
||||
Интерактивные виджеты лежат в assets/widgets.js и подключаются через custom_js: страница
|
||||
вставляется через innerHTML, поэтому скрипты внутри неё не выполняются. Данные моделей
|
||||
(data/models.json) дописываются в начало custom_js при сборке.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from docslib import HeaderButton, build, run
|
||||
from docslib.components import Callout, Code, Details, H1, H2, H3, List, LocalLink, P, Table
|
||||
from docslib.hooks import Component, Page
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ASSETS = HERE / "assets"
|
||||
TITLE = "Нейроморфное распознавание на Алтае"
|
||||
|
||||
|
||||
@Component
|
||||
def Lead(text):
|
||||
return f'<p class="lead">{text}</p>'
|
||||
|
||||
|
||||
@Component
|
||||
def Def(text):
|
||||
return f'<p class="def">{text}</p>'
|
||||
|
||||
|
||||
@Component
|
||||
def Widget(name):
|
||||
return f'<div data-widget="{name}"></div>'
|
||||
|
||||
|
||||
@Component
|
||||
def Links(items):
|
||||
"""Список со ссылками: пункты уже содержат доверенный HTML LocalLink."""
|
||||
return "<ul>" + "".join(f"<li>{item}</li>" for item in items) + "</ul>"
|
||||
|
||||
|
||||
@Component
|
||||
def Facts(items):
|
||||
cells = "".join(f"<div><b>{value}</b><span>{label}</span></div>" for value, label in items)
|
||||
return f'<div class="facts">{cells}</div>'
|
||||
|
||||
|
||||
@Component
|
||||
def Pipeline():
|
||||
"""Схема системы: хост → чип → хост. Цвета берутся из темы через currentColor."""
|
||||
boxes = [
|
||||
(10, "Камера", "кадр"), (165, "Хост", "окно и центровка"), (320, "Кодирование", "яркость → такт"),
|
||||
(475, "Алтай", "импульсная сеть"), (630, "Хост", "счёт спайков"),
|
||||
]
|
||||
parts = []
|
||||
for i, (x, title, sub) in enumerate(boxes):
|
||||
chip = title == "Алтай"
|
||||
parts.append(
|
||||
f'<rect x="{x}" y="30" width="140" height="70" rx="16" fill="{"currentColor" if chip else "none"}" '
|
||||
f'stroke="currentColor" stroke-width="1.5" opacity="{1 if chip else .9}"/>'
|
||||
f'<text x="{x + 70}" y="60" text-anchor="middle" font-size="15" font-weight="600" '
|
||||
f'fill="{"var(--shell)" if chip else "currentColor"}">{title}</text>'
|
||||
f'<text x="{x + 70}" y="82" text-anchor="middle" font-size="11" '
|
||||
f'fill="{"var(--shell)" if chip else "currentColor"}" opacity=".75">{sub}</text>'
|
||||
)
|
||||
if i < len(boxes) - 1:
|
||||
parts.append(f'<path d="M{x + 143},65 L{x + 162},65" stroke="currentColor" stroke-width="1.5" marker-end="url(#a)"/>')
|
||||
return (
|
||||
'<figure><div class="w-scroll"><svg viewBox="0 0 780 130" style="width:100%;min-width:620px;color:var(--text)" role="img" '
|
||||
'aria-label="Камера, хост, кодирование, процессор Алтай, хост">'
|
||||
'<defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">'
|
||||
'<path d="M0,0 L8,4 L0,8" fill="none" stroke="currentColor"/></marker></defs>'
|
||||
+ "".join(parts)
|
||||
+ '<text x="390" y="122" text-anchor="middle" font-size="12" fill="currentColor" opacity=".7">'
|
||||
"обучение — на обычном компьютере; на чип загружаются готовые целочисленные веса</text>"
|
||||
"</svg></div><figcaption>Состав системы распознавания</figcaption></figure>"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 1. Обзор
|
||||
|
||||
@Page("1", "Обзор")
|
||||
def overview():
|
||||
return [
|
||||
H1("Нейроморфное распознавание на Алтае"),
|
||||
Lead("Система, в которой распознавание изображений выполняет импульсная нейронная сеть "
|
||||
"на нейроморфном процессоре «Алтай», а подготовка входа и чтение ответа — обычный компьютер робота."),
|
||||
Def("<b>Нейроморфное распознавание</b> — распознавание образов сетью из импульсных нейронов, "
|
||||
"которые обмениваются короткими событиями-спайками и тратят энергию только в момент события. "
|
||||
"В отличие от обычной нейросети, вычисление идёт по тактам и разреженно: молчащий нейрон ничего не считает."),
|
||||
Pipeline(),
|
||||
H2("Из чего состоит"),
|
||||
Table(["Часть", "Что это", "Роль в системе"], [
|
||||
["Алтай", "нейроморфный процессор Мотив НТ: сетка ядер с целочисленными нейронами", "исполняет импульсную сеть"],
|
||||
["KNP", "Kaspersky Neuromorphic Platform: программная платформа импульсных сетей", "описывает сеть, моделирует её на CPU/GPU, загружает на Алтай"],
|
||||
["GoldenModel", "программная копия Алтая из бэкенда Мотив НТ", "доказывает, что сеть заработает на чипе"],
|
||||
["Хост", "обычный процессор робота", "готовит вход, кодирует его спайками, читает ответ"],
|
||||
["Обучение", "обычный компьютер, PyTorch / NumPy", "находит веса с учётом ограничений чипа"],
|
||||
]),
|
||||
H2("Ключевые свойства"),
|
||||
Facts([
|
||||
("97,4%", "точность импульсной сети на цифрах MNIST"),
|
||||
("8 тактов", "от первого спайка до ответа"),
|
||||
("≈143", "входных спайка на картинку — по одному на пиксель"),
|
||||
("≈20 тыс.", "синаптических событий на картинку"),
|
||||
("8192", "нейрона в 16-ядерном прототипе Алтая"),
|
||||
("4", "разных величины веса на нейрон"),
|
||||
]),
|
||||
Callout("Чип не обучается: в официальном бэкенде Алтая пластичность выключена. Поэтому сеть учат на компьютере "
|
||||
"сразу под рамки чипа, а на Алтай загружают готовые целые веса.", title="Главное ограничение", kind="note"),
|
||||
H2("Как читать вики"),
|
||||
Links([
|
||||
LocalLink("Импульсные сети", snn_section) + " — что такое импульсный нейрон, как кодируется вход, как сети учатся.",
|
||||
LocalLink("Платформа KNP", knp_section) + " — программная модель сети и способы её исполнения.",
|
||||
LocalLink("Процессор Алтай", altai_section) + " — устройство чипа и правила, которым должна подчиняться сеть.",
|
||||
LocalLink("Архитектуры распознавания", arch_section) + " — четыре способа построить распознаватель и их сравнение.",
|
||||
LocalLink("Распознавание мусора", trash) + " — как всё это ложится на задачу робота.",
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@Page("1.1", "Глоссарий")
|
||||
def glossary():
|
||||
return [
|
||||
H1("Глоссарий"),
|
||||
Table(["Термин", "Определение"], [
|
||||
["Спайк", "Короткое событие «нейрон выстрелил». Несёт один бит и момент времени."],
|
||||
["Такт", "Шаг дискретного времени. За такт нейрон суммирует пришедшие спайки и решает, стрелять ли."],
|
||||
["Потенциал (уровень)", "Накопленный заряд нейрона. Когда он достигает порога, нейрон стреляет."],
|
||||
["Порог", "Уровень, при котором нейрон выдаёт спайк. На Алтае — целое число."],
|
||||
["Синапс, вес", "Связь между нейронами. Вес — на сколько изменится потенциал получателя от одного спайка."],
|
||||
["Синаптическое событие", "Одно сложение «спайк × вес». Основная мера работы и энергии импульсной сети."],
|
||||
["Популяция", "Группа однотипных нейронов в KNP."],
|
||||
["Проекция", "Пучок связей из одной популяции в другую, с весами и задержками."],
|
||||
["Частотный код", "Яркость пикселя передаётся частотой спайков."],
|
||||
["Временной код", "Яркость передаётся моментом единственного спайка: ярче — раньше."],
|
||||
["STDP", "Правило обучения: связь крепнет, если входной спайк пришёл незадолго до выстрела, и слабеет, если после."],
|
||||
["Дофамин", "Третий сигнал обучения: награда или наказание, превращающие недавние изменения связей в постоянные."],
|
||||
["Суррогатный градиент", "Способ обучать импульсную сеть обратным распространением: ступенька спайка заменяется гладкой функцией."],
|
||||
["Квантование", "Перевод дробных весов в небольшой набор целых, который понимает чип."],
|
||||
["Кроссбар", "Таблица связей внутри ядра Алтая: строки — входящие линии, столбцы — нейроны."],
|
||||
["Размещатель", "Часть бэкенда Алтая, раскладывающая нейроны и связи по ядрам."],
|
||||
["GoldenModel", "Эталонная программная модель Алтая, совпадающая с чипом по счётчикам спайков."],
|
||||
["Хост", "Обычный процессор, который готовит вход для чипа и читает его ответ."],
|
||||
["Валидация и тест", "На валидации выбирают настройки; тест смотрят один раз, иначе оценка завышена."],
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 2. Импульсные сети
|
||||
|
||||
@Page("2", "Импульсные сети")
|
||||
def snn_section():
|
||||
return [
|
||||
H1("Импульсные нейронные сети"),
|
||||
Def("<b>Импульсная нейронная сеть</b> (spiking neural network, SNN) — сеть, в которой нейроны хранят внутреннее "
|
||||
"состояние и обмениваются не числами, а событиями-спайками во времени."),
|
||||
H2("Чем отличается от обычной нейросети"),
|
||||
Table(["", "Обычная нейросеть", "Импульсная сеть"], [
|
||||
["Сигнал между нейронами", "вещественное число", "спайк: есть или нет, и когда"],
|
||||
["Время", "нет: один проход", "дискретные такты"],
|
||||
["Состояние нейрона", "нет", "потенциал, копится между тактами"],
|
||||
["Объём вычислений", "все нейроны на каждом входе", "только там, где пришли спайки"],
|
||||
["Обучение", "обратное распространение", "STDP, дофамин или суррогатный градиент"],
|
||||
["Где выгодна", "сервер, видеокарта", "нейроморфный чип: дрон, робот, датчик"],
|
||||
]),
|
||||
P("Выигрыш импульсной сети — в разреженности. Если на картинке 150 закрашенных пикселей из 784, "
|
||||
"то при временном коде на чип приходит около 150 событий, и каждое затрагивает только свои связи."),
|
||||
Links([
|
||||
LocalLink("Импульсный нейрон", neuron_page),
|
||||
LocalLink("Кодирование входа", coding_page),
|
||||
LocalLink("Связи и сложение сигналов", links_page),
|
||||
LocalLink("Обучение", learning_page),
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@Page("2.1", "Импульсный нейрон")
|
||||
def neuron_page():
|
||||
return [
|
||||
H1("Импульсный нейрон AltAILIF"),
|
||||
Def("<b>AltAILIF</b> — модель нейрона «накопление–утечка–выстрел», повторяющая нейрон процессора «Алтай»: "
|
||||
"целочисленный 16-битный потенциал, целые порог, утечка и значение сброса."),
|
||||
P("Удобная аналогия — стакан. Каждый входной спайк доливает в него воду (вес связи), вода понемногу "
|
||||
"испаряется (утечка), а когда уровень доходит до черты (порог), нейрон выдаёт спайк и стакан частично или полностью опустошается."),
|
||||
Widget("neuron"),
|
||||
H2("Шаг нейрона за такт"),
|
||||
List([
|
||||
"к потенциалу прибавляются веса всех пришедших спайков (тормозящие — со знаком минус);",
|
||||
"прибавляется утечка; при флаге leak_rev её знак следует за знаком потенциала;",
|
||||
"если потенциал ≥ порога — спайк; при is_diff из потенциала вычитается порог, при is_reset он сбрасывается;",
|
||||
"если потенциал ниже отрицательного порога — при saturate он упирается в этот порог.",
|
||||
], ordered=True),
|
||||
Table(["Флаг", "Что делает", "Как используется"], [
|
||||
["is_diff", "после выстрела вычитает порог, остаток сохраняется", "в наших сетях: точный счёт заряда"],
|
||||
["is_reset", "после выстрела сбрасывает потенциал в значение сброса", "штатный пример KNP"],
|
||||
["saturate", "не даёт потенциалу уйти ниже отрицательного порога", "отрицательный порог 0: уровень не ниже нуля"],
|
||||
["leak_rev", "утечка всегда тянет потенциал к нулю", "в наших сетях утечка 0"],
|
||||
]),
|
||||
Callout("Все числа нейрона целые, потенциал — 16 бит со знаком (от −32 768 до 32 767). "
|
||||
"Сеть, у которой сумма входов за такт выходит за этот диапазон, на чипе не заработает.", title="Ограничение", kind="note"),
|
||||
]
|
||||
|
||||
|
||||
@Page("2.2", "Кодирование входа")
|
||||
def coding_page():
|
||||
return [
|
||||
H1("Кодирование входа"),
|
||||
Def("<b>Кодирование</b> — правило, по которому хост превращает яркость пикселей в спайки на входе сети."),
|
||||
Widget("coding"),
|
||||
Table(["", "Частотный код", "Временной код"], [
|
||||
["Что несёт информацию", "сколько раз пиксель выстрелил", "в какой такт пиксель выстрелил"],
|
||||
["Спайков на пиксель", "до T (числа тактов)", "не больше одного"],
|
||||
["Входных спайков на цифру MNIST", "≈730 за 8 тактов, ≈3200 за 32", "≈143 при любом T"],
|
||||
["Сколько нужно тактов", "много: точность оттенков растёт с T", "мало: 4–8"],
|
||||
["Как обучать", "можно перенести обычную сеть", "нужно обучать по тактам"],
|
||||
]),
|
||||
P("Частотный код проще: число спайков приближённо равно активации обычной нейросети, поэтому её веса можно перенести. "
|
||||
"Временной код экономнее, но нейроны должны реагировать на порядок прихода спайков, поэтому сеть учат прямо на динамике."),
|
||||
Details("Почему вход ON/OFF не нужен сети, обученной на компьютере",
|
||||
"Канал OFF стреляет на пустых пикселях, его значение равно «255 − яркость». У нейрона с весами разного знака и смещением "
|
||||
"это уже выражается через канал ON, поэтому нового сигнала OFF не даёт, а входов и спайков становится вдвое больше. "
|
||||
"Проверка на нативной сети: 96,3% с ON/OFF против 97,7% без него. Колонкам STDP такой сигнал нужен, но они "
|
||||
"от лишних спайков замолкают."),
|
||||
]
|
||||
|
||||
|
||||
@Page("2.3", "Связи и сложение сигналов")
|
||||
def links_page():
|
||||
return [
|
||||
H1("Связи и сложение сигналов"),
|
||||
Def("<b>Проекция</b> — набор связей из одной группы нейронов в другую. Схему соединения выбирает конструктор сети, "
|
||||
"обучение меняет только веса."),
|
||||
Table(["Схема в KNP", "Кто с кем связан", "Где применяется"], [
|
||||
["all_to_all", "каждый с каждым", "полносвязные слои: 784 → 256"],
|
||||
["one_to_one", "i-й с i-м", "передача без изменений"],
|
||||
["index_based", "по своему правилу, например окно", "детекторы признаков: каждый смотрит в свой кусок"],
|
||||
["fixed_probability", "случайно с заданной вероятностью", "разреженные сети"],
|
||||
["exclusive", "каждый с каждым, кроме себя", "взаимное торможение, «победитель забирает всё»"],
|
||||
]),
|
||||
H2("Сложение"),
|
||||
P("Всё, что пришло в нейрон за один такт, складывается. У каждой связи есть задержка: спайк, отправленный на такте N "
|
||||
"с задержкой D, учитывается на такте N + D."),
|
||||
Widget("synsum"),
|
||||
H2("Соревнование"),
|
||||
P("Нейроны, которые видят одно и то же и учатся по одному правилу, выучивают одно и то же. Взаимное торможение "
|
||||
"заставляет их делить работу: учится только тот, кто выстрелил первым."),
|
||||
Widget("wta"),
|
||||
]
|
||||
|
||||
|
||||
@Page("2.4", "Обучение")
|
||||
def learning_page():
|
||||
return [
|
||||
H1("Обучение импульсных сетей"),
|
||||
Def("Спайк — это ступенька, у неё нет производной. Поэтому обычное обратное распространение к импульсной сети напрямую "
|
||||
"не применимо, и существует несколько семейств правил обучения."),
|
||||
H2("STDP: локальное правило без учителя"),
|
||||
P("После выстрела каждый вход разбирается отдельно. Спайк, пришедший незадолго до выстрела, помог — связь крепнет. "
|
||||
"Пришедший после — нейрон обошёлся без него, связь слабеет."),
|
||||
Widget("stdp"),
|
||||
H2("Сравнение способов"),
|
||||
Table(["Способ", "Где идёт", "Чему учит", "Цифры MNIST"], [
|
||||
["STDP (классический)", "в KNP", "частые узоры, без меток", "—"],
|
||||
["STDP с ресурсом + дофамин (колонки KNP)", "в KNP", "«похоже на мою цифру»", "≈92%"],
|
||||
["Дофамин по ошибке (дельта-правило)", "в KNP", "один слой по ошибке ответа", "≈94,8% (валидация)"],
|
||||
["Перенос обычной сети", "на компьютере", "всю сеть обратным распространением по аналоговой копии", "97,8%"],
|
||||
["Суррогатный градиент по тактам", "на компьютере", "всю сеть прямо на динамике спайков", "97,4% за 8 тактов"],
|
||||
]),
|
||||
Callout("Чем больше обучения уходит на компьютер, тем выше точность. Алтай не обучается вообще, поэтому для него "
|
||||
"естественный путь — обучение на компьютере с учётом ограничений чипа.", title="Вывод", kind="note"),
|
||||
H3("Суррогатный градиент"),
|
||||
P("Прямой проход идёт по тактам с настоящими спайками. При обратном проходе производная ступеньки заменяется "
|
||||
"гладкой функцией 1/(1+4|v|)². Сеть видит ту же арифметику, что и чип: целые веса, вычитание порога, уровень не ниже нуля, задержку в такт."),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 3. KNP
|
||||
|
||||
@Page("3", "Платформа KNP")
|
||||
def knp_section():
|
||||
return [
|
||||
H1("Kaspersky Neuromorphic Platform"),
|
||||
Def("<b>KNP</b> — открытая программная платформа (C++ и Python, Apache 2.0) для описания, обучения и исполнения "
|
||||
"импульсных сетей на CPU, GPU и нейроморфном процессоре «Алтай»."),
|
||||
H2("Модель сети"),
|
||||
Table(["Уровень", "Что это"], [
|
||||
["Популяция", "группа однотипных нейронов; параметры задаёт функция-генератор"],
|
||||
["Проекция", "связи из популяции в популяцию: вес, задержка, тип, номера нейронов"],
|
||||
["Network", "популяции и проекции"],
|
||||
["Model", "сеть плюс входные и выходные каналы"],
|
||||
["ModelExecutor", "прогоняет модель по тактам на выбранном бэкенде"],
|
||||
]),
|
||||
P("Каждый такт: популяции обновляют нейроны → шина доставляет сообщения → проекции превращают спайки в воздействия."),
|
||||
Code('''P = knp::core::Population<AltAILIF>
|
||||
R = knp::core::Projection<DeltaSynapse>
|
||||
|
||||
P hidden(make_neuron, 256); // 256 нейронов AltAILIF
|
||||
R input(input_uid, hidden.get_uid(), gen, n); // связи вход → скрытый слой
|
||||
Network net; net.add_population(hidden); net.add_projection(input);
|
||||
Model model(net); model.add_input_channel(...); model.add_output_channel(...);
|
||||
ModelExecutor exec(model, backend, inputs); exec.start(until_step);''', "cpp"),
|
||||
Links([LocalLink("Бэкенды и модели нейронов", backends_page)]),
|
||||
]
|
||||
|
||||
|
||||
@Page("3.1", "Бэкенды и модели нейронов")
|
||||
def backends_page():
|
||||
return [
|
||||
H1("Бэкенды и модели нейронов"),
|
||||
Def("<b>Бэкенд</b> — исполнитель сети. Сеть описывается один раз, а запускается на любом бэкенде, который поддерживает её типы нейронов."),
|
||||
Table(["Бэкенд", "Нейроны", "Обучение", "Замечание"], [
|
||||
["CPU, один поток", "LIF, BLIFAT, AltAILIF и STDP-варианты", "да", "эталон"],
|
||||
["CPU, много потоков", "LIF, BLIFAT", "частично", "AltAILIF не поддерживает"],
|
||||
["CUDA (официальный)", "BLIFAT", "нет", "AltAILIF не поддерживает"],
|
||||
["CUDA для AltAILIF (наш)", "AltAILIF, Delta", "нет", "побайтно совпадает с CPU; при угрозе потери точности float32 падает с ошибкой"],
|
||||
["Алтай: GoldenModel и чип", "AltAILIF, Delta", "нет", "закрытая библиотека Мотив НТ, deb-пакет 1.0.2"],
|
||||
]),
|
||||
Table(["Модель нейрона", "Что это", "Где исполняется"], [
|
||||
["LIF", "классический нейрон с утечкой", "CPU"],
|
||||
["BLIFAT", "расширенная модель Kaspersky: адаптивный порог, торможение", "CPU, CUDA"],
|
||||
["AltAILIF", "ровно возможности нейрона Алтая", "CPU, наш CUDA, Алтай"],
|
||||
]),
|
||||
P("Для распознавателя на Алтае имеет значение только AltAILIF с синапсами Delta: остальные модели на чип не переносятся."),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 4. Алтай
|
||||
|
||||
@Page("4", "Процессор Алтай")
|
||||
def altai_section():
|
||||
return [
|
||||
H1("Процессор «Алтай»"),
|
||||
Def("<b>Алтай</b> — нейроморфный процессор компании Мотив НТ: сетка одинаковых ядер, каждое из которых хранит нейроны "
|
||||
"и таблицу их связей и обменивается спайками с соседями пакетами."),
|
||||
Facts([("16", "ядер в прототипе"), ("8192", "нейрона в прототипе"), ("131 072", "нейрона в максимальной конфигурации"),
|
||||
("16 бит", "потенциал со знаком"), ("4", "величины веса на нейрон"), ("нет", "обучения на кристалле")]),
|
||||
Links([LocalLink("Сетка ядер", grid_page), LocalLink("Ядро и кроссбар", crossbar_page),
|
||||
LocalLink("Ограничения и квантование", rules_page), LocalLink("Размещение и проверка", placement_page)]),
|
||||
]
|
||||
|
||||
|
||||
@Page("4.1", "Сетка ядер")
|
||||
def grid_page():
|
||||
return [
|
||||
H1("Сетка ядер"),
|
||||
Def("Ядра соединены с соседями; внешний мир подключается через <b>порт</b> — сейчас только с северной стороны сетки, стандартной длины 4 ядра."),
|
||||
Widget("chipgrid"),
|
||||
P("В эмуляторе по умолчанию сетка 4×4. Число нейронов в ядре — около 512 (8192 нейрона прототипа на 16 ядер; расчётная величина)."),
|
||||
]
|
||||
|
||||
|
||||
@Page("4.2", "Ядро и кроссбар")
|
||||
def crossbar_page():
|
||||
return [
|
||||
H1("Ядро и кроссбар"),
|
||||
Def("<b>Кроссбар</b> — таблица связей ядра: строки — входящие линии (аксоны), столбцы — нейроны, в клетке — вес. "
|
||||
"Пришёл спайк по строке — каждый нейрон-столбец прибавляет вес из этой строки."),
|
||||
Widget("crossbar"),
|
||||
P("Отсюда главное ограничение: у нейрона не может быть больше 4 разных величин веса. Знак задаётся отдельно и в счёт не идёт."),
|
||||
]
|
||||
|
||||
|
||||
@Page("4.3", "Ограничения и квантование")
|
||||
def rules_page():
|
||||
return [
|
||||
H1("Ограничения и квантование"),
|
||||
Table(["Правило", "Следствие для сети"], [
|
||||
["вес — целое, |w| ≤ 255", "веса после обучения переводятся в целые"],
|
||||
["не больше 4 разных величин веса на нейрон", "учить сразу с палитрой, например {0, ±1, ±2, ±4}"],
|
||||
["нулевых связей нет", "связь с весом 0 не создаётся"],
|
||||
["один тип связи, знак в весе", "торможение — отрицательный вес"],
|
||||
["потенциал 16 бит со знаком", "сумма входов за такт ограничена"],
|
||||
["начальный потенциал игнорируется", "смещение подаётся отдельными входными спайками"],
|
||||
["одинаковые номера входа в одном такте склеиваются", "каждой линии свой номер"],
|
||||
["обучения нет", "веса загружаются готовыми"],
|
||||
]),
|
||||
H2("Квантование"),
|
||||
Def("<b>Квантование</b> — замена дробных весов ступеньками из небольшого набора. Чем меньше ступенек, тем грубее нейрон."),
|
||||
Widget("quant"),
|
||||
H2("Влезет ли сеть"),
|
||||
Widget("fit"),
|
||||
]
|
||||
|
||||
|
||||
@Page("4.4", "Размещение и проверка")
|
||||
def placement_page():
|
||||
return [
|
||||
H1("Размещение и проверка"),
|
||||
Def("<b>Размещатель</b> — часть бэкенда Алтая, которая оценивает число нужных ядер, раскладывает нейроны и связи и отказывается, "
|
||||
"если сеть не помещается. Нейрон, которому нужно больше входов, чем строк в ядре, он делит на несколько."),
|
||||
Table(["Ступень проверки", "Что доказывает"], [
|
||||
["Целочисленный эталон (NumPy)", "арифметика сети записана верно"],
|
||||
["KNP на CPU", "граф собран правильно, счётчики спайков совпадают с эталоном"],
|
||||
["KNP на GPU", "то же на большом объёме данных, побайтно"],
|
||||
["GoldenModel", "размещение и правила чипа соблюдены, счётчики совпадают"],
|
||||
["Чип AltAI_HW", "работа на железе"],
|
||||
]),
|
||||
Callout("Принятие сети размещателем ещё не доказывает правильность. Конфигурация с 8 копиями сети в сетке 4×4 "
|
||||
"была принята, но счётчики спайков разошлись с эталоном. Критерий — совпадение счётчиков каждого нейрона.",
|
||||
title="Наблюдение", kind="note"),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 5. Архитектуры
|
||||
|
||||
@Page("5", "Архитектуры распознавания")
|
||||
def arch_section():
|
||||
return [
|
||||
H1("Архитектуры распознавания"),
|
||||
Def("<b>Архитектура</b> распознавателя — устройство слоёв, способ кодирования входа и способ обучения. "
|
||||
"Все четыре архитектуры ниже построены на нейронах AltAILIF."),
|
||||
Widget("arch"),
|
||||
Table(["Архитектура", "Устройство", "Обучение", "Ложится на чип"], [
|
||||
["Колонки STDP", "пиксели → 10 колонок × 20 нейронов", "в KNP, STDP + дофамин", "да"],
|
||||
["Детекторы + выход", "200 типов детекторов × 196 позиций → хост → 256 → 2", "детекторы k-средними, выход на компьютере", "нет"],
|
||||
["Нативная сеть", "784 → 256 → 160 «за/против», 32 такта", "перенос обычной сети", "да, проверено"],
|
||||
["Импульсная по тактам", "784 → 256 → 10, 8 тактов, временной код", "суррогатный градиент", "да, по рамкам"],
|
||||
]),
|
||||
Links([LocalLink("Колонки STDP", columns_page), LocalLink("Детекторы признаков", detectors_page),
|
||||
LocalLink("Нативная сеть", native_page), LocalLink("Импульсная сеть по тактам", ticks_page),
|
||||
LocalLink("Сравнение архитектур", compare_page)]),
|
||||
]
|
||||
|
||||
|
||||
@Page("5.1", "Колонки STDP")
|
||||
def columns_page():
|
||||
return [
|
||||
H1("Колонки STDP"),
|
||||
Def("<b>Колонки</b> — архитектура штатного примера KNP: на каждый класс отводится колонка из 20 нейронов, "
|
||||
"которые учатся по STDP с дофамином; ответ — колонка, выстрелившая больше всех."),
|
||||
H2("Как работает"),
|
||||
List(["картинка кодируется частотно за 10 тактов;", "спайки идут во все колонки;",
|
||||
"при обучении метка через вспомогательную популяцию будит колонку правильного класса, и её связи с активными пикселями крепнут;",
|
||||
"при распознавании соседи в колонке тормозят друг друга, чтобы нейроны специализировались."], ordered=True),
|
||||
H2("Свойства"),
|
||||
Table(["Свойство", "Значение"], [
|
||||
["Точность на MNIST", "≈92%"], ["Обучение на платформе", "да"],
|
||||
["Сдвиг цифры на 2 пикселя", "точность падает до ≈64–67%"],
|
||||
["Типичная ошибка", "4 → 9: трафарет девятки целиком накрывает четвёрку"],
|
||||
]),
|
||||
P("Колонка учит «похоже на мою цифру», но не «не путай с соседней», и не получает сигнала «здесь линии нет»: "
|
||||
"пустой пиксель молчит. Это потолок архитектуры, а не настроек."),
|
||||
]
|
||||
|
||||
|
||||
@Page("5.2", "Детекторы признаков")
|
||||
def detectors_page():
|
||||
return [
|
||||
H1("Детекторы признаков с хостом"),
|
||||
Def("Архитектура для фотографий: слой нейронов-детекторов ищет выученные узоры по всей картинке, хост суммирует "
|
||||
"их отклики по областям и нормирует, затем небольшая сеть выдаёт класс."),
|
||||
Table(["Ступень", "Где исполняется", "Размер"], [
|
||||
["пиксели → спайки, 10 тактов", "хост", "32×32×3"],
|
||||
["детекторы узоров 6×6×3, шаг 2", "KNP", "200 типов × 196 позиций = 39 200 нейронов"],
|
||||
["суммирование по четвертям, нормировка", "хост", "800 чисел"],
|
||||
["скрытый слой и выход", "KNP", "256 + 2 нейрона"],
|
||||
]),
|
||||
P("Точность на кошках и собаках CIFAR-10 — 71,35%, на транспорте — 71,25% (валидация). На Алтай архитектура не ложится: "
|
||||
"нейронов в 5 раз больше прототипа, у нейронов 55–110 разных весов вместо 4, потенциалы доходят до ±570 000, "
|
||||
"а между слоями работает хост."),
|
||||
Callout("На чипе нет общих весов: детектор «вертикальная черта» в каждой позиции — отдельный нейрон. "
|
||||
"Поэтому число нейронов растёт как «типы × позиции».", title="Почему так много нейронов", kind="note"),
|
||||
]
|
||||
|
||||
|
||||
@Page("5.3", "Нативная сеть")
|
||||
def native_page():
|
||||
return [
|
||||
H1("Нативная сеть"),
|
||||
Def("<b>Нативная сеть</b> — двухслойная сеть, целиком составленная из нейронов AltAILIF и уложенная в рамки чипа. "
|
||||
"Её веса найдены обучением обычной сети и перенесены."),
|
||||
Table(["Слой", "Размер", "Параметры"], [
|
||||
["вход", "784", "частотный код, 32 такта"],
|
||||
["скрытый", "256 AltAILIF", "веса {0, ±1, ±2, ±4}, порог 32, вычитание порога"],
|
||||
["выход", "160 AltAILIF", "10 классов × 8 групп «за» и 8 групп «против», порог 4"],
|
||||
["хост", "—", "«за» минус «против», максимум"],
|
||||
]),
|
||||
H2("Почему выход «за/против»"),
|
||||
P("Потенциал не опускается ниже нуля, а у нейрона мало разных весов. Поэтому отрицательные доводы собираются отдельными "
|
||||
"нейронами «против», а вычитание делает хост. Так сеть учитывает «здесь есть крыша — значит не 4»."),
|
||||
H2("Обучение"),
|
||||
List(["обычная сеть 784 → 256 → 10 учится обратным распространением; активация ограничена 0…4 и округляется, как счёт спайков;",
|
||||
"веса скрытого слоя переводятся в целые {0, ±1, ±2, ±4};",
|
||||
"выход заново обучается по настоящим счётчикам спайков скрытого слоя и раскладывается на группы."], ordered=True),
|
||||
Table(["Свойство", "Значение"], [
|
||||
["Точность на MNIST", "97,8%; с выпрямлением наклона на хосте — 98,0%"],
|
||||
["Проверка", "KNP CPU и CUDA — все счётчики совпали; GoldenModel — 975 из 1000"],
|
||||
["Цена", "≈3200 входных спайков и ≈338 тыс. синаптических событий на картинку"],
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@Page("5.4", "Импульсная сеть по тактам")
|
||||
def ticks_page():
|
||||
return [
|
||||
H1("Импульсная сеть по тактам"),
|
||||
Def("<b>Импульсная сеть по тактам</b> — сеть 784 → 256 → 10 нейронов AltAILIF с временным кодом входа, "
|
||||
"обученная суррогатным градиентом прямо на целочисленной динамике чипа."),
|
||||
Widget("ticks"),
|
||||
H2("Устройство"),
|
||||
Table(["Элемент", "Значение"], [
|
||||
["Вход", "каждый пиксель ярче 32 стреляет один раз; такт = ⌊(255 − яркость)·8/224⌋"],
|
||||
["Скрытый слой", "256 нейронов, порог 8, веса и смещения {0, ±1, ±2, ±4}"],
|
||||
["Выход", "10 нейронов, порог 8, задержка 1 такт"],
|
||||
["Ответ", "больше спайков; при равенстве — раньше первый спайк"],
|
||||
["Ранний ответ", "такт, после которого лидер больше не меняется"],
|
||||
]),
|
||||
H2("Обучение"),
|
||||
List(["прямой проход идёт по 8 тактам с настоящими спайками и вычитанием порога;",
|
||||
"с 9-й эпохи веса в прямом проходе огрубляются до палитры чипа, градиент проходит «насквозь»;",
|
||||
"к ошибке добавлен штраф за спайки скрытого слоя — сеть учится молчать, когда может;",
|
||||
"настройки выбираются по валидации, итог считается отдельным целочисленным симулятором."], ordered=True),
|
||||
Table(["Свойство", "Значение"], [
|
||||
["Точность на MNIST", "97,4%"], ["Имитация рисования мышью", "95,9%"], ["Тактов", "8"],
|
||||
["Входных спайков", "≈143"], ["Синаптических событий", "≈20 тыс. — в 17 раз меньше нативной сети"],
|
||||
["Проверка", "целочисленный эталон; KNP и GoldenModel — следующий шаг"],
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
@Page("5.5", "Сравнение архитектур")
|
||||
def compare_page():
|
||||
return [
|
||||
H1("Сравнение архитектур"),
|
||||
Def("Сравнение на одной задаче — цифры MNIST, тест 10 000 изображений — по точности, числу тактов и работе чипа."),
|
||||
Table(["", "Колонки STDP", "Нативная сеть", "Импульсная по тактам"], [
|
||||
["Точность", "92,2%", "97,8%", "97,4%"],
|
||||
["Тактов", "15", "32", "8"],
|
||||
["Входных спайков", "≈870", "≈3200", "≈143"],
|
||||
["Синаптических событий", "≈175 тыс.", "≈338 тыс.", "≈20 тыс."],
|
||||
["Нейронов", "200", "416", "266"],
|
||||
["Учится на платформе", "да", "нет", "нет"],
|
||||
["Проверено в GoldenModel", "—", "да", "ещё нет"],
|
||||
]),
|
||||
H2("Одна цифра — три сети"),
|
||||
P("Нарисуйте цифру или выберите тестовую. Все три сети считаются здесь целочисленно, как на чипе."),
|
||||
Widget("compare"),
|
||||
H2("Точность против работы чипа"),
|
||||
P("Каждая точка — сеть или режим. Чем левее, тем меньше сложений «спайк × вес» и энергии; чем выше, тем точнее."),
|
||||
Widget("tradeoff"),
|
||||
P("Обрезать нативную сеть по тактам можно до 8 — дальше точность обваливается (84,6% на 4 тактах). "
|
||||
"Сеть, обученная по тактам с временным кодом, держит 97% даже на 4 тактах."),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 6–8
|
||||
|
||||
@Page("6", "Обработка на хосте")
|
||||
def host_page():
|
||||
return [
|
||||
H1("Обработка на хосте"),
|
||||
Def("<b>Хост</b> — обычный процессор рядом с чипом. Он делает то, что дёшево сделать один раз на картинку: "
|
||||
"вырезает и центрирует объект, выпрямляет наклон и превращает яркость в моменты спайков."),
|
||||
Widget("hostprep"),
|
||||
Table(["Шаг", "Что делает", "Зачем"], [
|
||||
["рамка 20×20", "вписывает объект в квадрат 20×20 и ставит центр масс в центр 28×28", "размер и положение не мешают"],
|
||||
["выпрямление", "сдвигает строки так, чтобы главная ось стала вертикальной", "наклонный почерк узнаётся лучше: +0,35 п.п. на MNIST"],
|
||||
["усреднение", "уменьшает с усреднением по площади", "сохраняет серые края, как в обучающих данных"],
|
||||
["временной код", "яркость → номер такта", "один спайк на пиксель"],
|
||||
]),
|
||||
Callout("Обработка на хосте не должна вклиниваться между слоями сети. Тогда вся сеть живёт на чипе, а хост только готовит вход и читает выход.",
|
||||
title="Граница хоста", kind="note"),
|
||||
]
|
||||
|
||||
|
||||
@Page("7", "Метрики и методика")
|
||||
def metrics_page():
|
||||
return [
|
||||
H1("Метрики и методика"),
|
||||
Table(["Метрика", "Определение"], [
|
||||
["Точность", "доля правильных ответов на тесте"],
|
||||
["Синаптические события", "число сложений «спайк × вес» на одну картинку; главная мера энергии на нейроморфном чипе"],
|
||||
["Входные спайки", "сколько событий хост отправляет на чип"],
|
||||
["Такты", "длительность распознавания в шагах чипа"],
|
||||
["Совпадение счётчиков", "счётчик спайков каждого нейрона одинаков в эталоне, KNP и GoldenModel"],
|
||||
]),
|
||||
H2("Правила"),
|
||||
List(["настройки выбираются только на валидации; тест смотрится один раз;",
|
||||
"цифра без источника (файл метрик или журнал проверки) не считается результатом;",
|
||||
"разница меньше 0,3 п.п. при одном прогоне — в пределах шума;",
|
||||
"точность на тесте MNIST не равна точности на рисунках мышью: это другой «почерк»."]),
|
||||
P("Для оценки рисования используется имитация: скелет цифры MNIST перерисовывается пером постоянной толщины. "
|
||||
"Нативная сеть теряет на ней около 4 п.п.; обучение на таких копиях и выпрямление сокращают потерю до 2 п.п."),
|
||||
]
|
||||
|
||||
|
||||
@Page("8", "Распознавание мусора")
|
||||
def trash():
|
||||
return [
|
||||
H1("Распознавание мусора"),
|
||||
Def("Целевая задача: робот с процессором «Алтай» распознаёт мусор в кадре своей камеры."),
|
||||
H2("Постановка"),
|
||||
List(["хост режет кадр на окна и для каждого окна готовит вход сети;",
|
||||
"чип отвечает по окну: «мусор / не мусор» или тип;",
|
||||
"где находится мусор, видно по сработавшим окнам."]),
|
||||
H2("Бюджет нейронов"),
|
||||
P("Детектор узоров на чипе стоит нейрон на каждую позицию. Подберите размер окна, шаг и число типов детекторов так, чтобы сеть влезла в прототип."),
|
||||
Widget("budget"),
|
||||
H2("Что переносится из распознавания цифр"),
|
||||
Table(["Приём", "Почему подходит"], [
|
||||
["обучение на компьютере под рамки чипа", "чип не учится; точность выше, чем у правил STDP"],
|
||||
["временной код и обучение по тактам", "мало спайков и тактов — меньше энергии на кадр"],
|
||||
["обработка на хосте только на входе", "вся сеть остаётся на кристалле"],
|
||||
["проверка счётчиков в KNP и GoldenModel", "доказательство без платы"],
|
||||
]),
|
||||
H2("Открытые вопросы"),
|
||||
List(["конфигурация чипа и наличие платы;", "классы: «мусор / не мусор» или типы;",
|
||||
"камера робота, в том числе событийная;", "данные: открытые наборы и снимки с камеры робота."]),
|
||||
]
|
||||
|
||||
|
||||
HEADER = [
|
||||
HeaderButton("Обзор", overview),
|
||||
HeaderButton("Импульсные сети", snn_section),
|
||||
HeaderButton("Алтай", altai_section),
|
||||
HeaderButton("Архитектуры", arch_section),
|
||||
HeaderButton("Сравнение", compare_page),
|
||||
]
|
||||
|
||||
|
||||
def custom_js() -> Path:
|
||||
"""Данные моделей + виджеты в одном файле custom_js."""
|
||||
out = ASSETS / "inject.generated.js"
|
||||
data = json.loads((HERE / "data" / "models.json").read_text(encoding="utf-8"))
|
||||
out.write_text("globalThis.WIKI_DATA = " + json.dumps(data, ensure_ascii=False, separators=(",", ":")) + ";\n"
|
||||
+ (ASSETS / "widgets.js").read_text(encoding="utf-8"), encoding="utf-8")
|
||||
return out
|
||||
|
||||
|
||||
OPTIONS = dict(title=TITLE, assets_dir=str(ASSETS), custom_css=str(ASSETS / "widgets.css"), header_buttons=HEADER)
|
||||
|
||||
if __name__ == "__main__":
|
||||
js = custom_js()
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "build":
|
||||
result = build(build_directory=str(HERE / "build"), custom_js=str(js), manifest_path="./manifest.json", **OPTIONS)
|
||||
print("built", HERE / "build")
|
||||
else:
|
||||
run(port=1234, host="0.0.0.0", custom_js=str(js), live_preview=True, **OPTIONS)
|
||||
Loading…
Add table
Reference in a new issue