Compare commits
3 commits
main
...
wiki-share
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597e40afaf | ||
|
|
69ad76bde2 | ||
|
|
917808edda |
6 changed files with 1541 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: нативная сеть и импульсная сеть по тактам дают те же ответы и ту же точность.
|
||||||
89
wiki/assets/widgets.css
Normal file
89
wiki/assets/widgets.css
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/* Виджеты вики: монохромная тема 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); }
|
||||||
|
@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; } }
|
||||||
|
.content .next { margin-top: 2.4rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--muted); }
|
||||||
|
.content .next a { font-weight: 600; }
|
||||||
429
wiki/assets/widgets.js
Normal file
429
wiki/assets/widgets.js
Normal file
|
|
@ -0,0 +1,429 @@
|
||||||
|
/* Интерактивные виджеты вики «Нейроморфное распознавание на Алтае».
|
||||||
|
* Страница вставляется через 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();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 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. Схема трёх архитектур */
|
||||||
|
|
||||||
|
|
||||||
|
/* 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
996
wiki/knp_wiki.py
Normal file
996
wiki/knp_wiki.py
Normal file
|
|
@ -0,0 +1,996 @@
|
||||||
|
"""Вики «Нейроморфное распознавание на Алтае» на 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, H1, H2, H3, List, LocalLink, 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 Text(html):
|
||||||
|
"""Абзац с доверенной разметкой: жирный текст и ссылки LocalLink."""
|
||||||
|
return f"<p>{html}</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 Steps(items, ordered=False):
|
||||||
|
"""Список с доверенной разметкой в пунктах."""
|
||||||
|
tag = "ol" if ordered else "ul"
|
||||||
|
return f"<{tag}>" + "".join(f"<li>{item}</li>" for item in items) + f"</{tag}>"
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
def Next(title, page, why):
|
||||||
|
return f'<p class="next">Дальше: {LocalLink(title, page)} — {why}</p>'
|
||||||
|
|
||||||
|
|
||||||
|
@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("Как заставить нейроморфный процессор «Алтай» распознавать изображения: что умеет чип, какие сети на него ложатся, "
|
||||||
|
"как их обучать и как доказать, что сеть заработает на кристалле."),
|
||||||
|
H2("Задача"),
|
||||||
|
Text("Конечная цель — робот, который сам находит мусор в кадре своей камеры, а распознавание выполняет «Алтай». "
|
||||||
|
"Пока цель недостижима напрямую: нет ни данных с робота, ни платы с чипом, а сам путь от картинки до ответа на "
|
||||||
|
"чипе нужно было сначала построить. Поэтому вся техника отработана на рукописных цифрах MNIST — "
|
||||||
|
"задаче, где ответ известен, а ошибку легко увидеть глазами. Всё, что описано в вики, переносится на мусор, "
|
||||||
|
"кроме самих весов сети."),
|
||||||
|
H2("Зачем нейроморфный чип"),
|
||||||
|
Text("Обычная нейросеть на каждую картинку перемножает все входы на все веса. Для сети 784 → 256 → 10 это "
|
||||||
|
"<b>203 264</b> умножения со сложением, сколько бы пикселей ни было закрашено. Импульсная сеть считает только там, "
|
||||||
|
"где что-то произошло: нейрон, получивший спайк, прибавляет к своему уровню вес связи, и всё. Лучшая сеть из этой "
|
||||||
|
"вики распознаёт цифру за <b>≈20 тысяч</b> таких сложений — в 10 раз меньше операций, и это сложения, а не умножения. "
|
||||||
|
"На роботе с батареей это и есть довод в пользу чипа."),
|
||||||
|
Callout("Энергию самого «Алтая» на этих сетях никто не измерял: платы в проекте нет. Сравнение идёт по числу операций — "
|
||||||
|
"синаптических событий, а не по джоулям.", title="Оговорка", kind="note"),
|
||||||
|
H2("Как устроена система"),
|
||||||
|
Pipeline(),
|
||||||
|
Text("Камера даёт кадр. Хост — обычный процессор робота — вырезает окно с объектом, выравнивает его и превращает яркость "
|
||||||
|
"пикселей в спайки. Чип прогоняет спайки через сеть по тактам. Хост считает выходные спайки и выбирает ответ. "
|
||||||
|
"Обучение в эту цепочку не входит: «Алтай» не умеет менять веса, поэтому сеть учат на компьютере и загружают готовой."),
|
||||||
|
H2("Что уже доказано, а что нет"),
|
||||||
|
Table(["Сеть", "Точность на MNIST", "Где проверена", "Статус"], [
|
||||||
|
["Нативная сеть, 784 → 256 → 160", "97,76%", "KNP на CPU и GPU — 10 000 цифр; GoldenModel — 1000 цифр (97,50%)",
|
||||||
|
"счётчики спайков всех нейронов совпали с эталоном; ближе всего к чипу"],
|
||||||
|
["Импульсная сеть по тактам, 784 → 256 → 10", "97,42%", "целочисленный эталон на Python и в браузере",
|
||||||
|
"в 17 раз экономнее нативной; в KNP и GoldenModel ещё не запускалась"],
|
||||||
|
["Колонки STDP (пример KNP)", "92,2%", "KNP на CPU", "единственная сеть, которая учится в самой платформе; держится за положение цифры"],
|
||||||
|
["Дофамин по ошибке", "94,8% (валидация)", "KNP на CPU с доработанным бэкендом", "учится в платформе, но признаки штрихов считает хост"],
|
||||||
|
["Детекторы признаков, кошки/собаки", "71,35% на CIFAR-10", "KNP на GPU", "на «Алтай» не ложится: в 5 раз больше нейронов и десятки весов на нейрон"],
|
||||||
|
]),
|
||||||
|
Text("Физический чип не проверялся ни разу. Самое сильное доказательство — программная копия чипа GoldenModel из "
|
||||||
|
"официального бэкенда: на ней нативная сеть дала те же спайки, что и эталон, нейрон в нейрон."),
|
||||||
|
H2("Порядок чтения"),
|
||||||
|
Text("Вики построена как цепочка вопросов. "
|
||||||
|
f"{LocalLink('Импульсная сеть', snn_section)} — как считает сеть из спайков. "
|
||||||
|
f"{LocalLink('Процессор «Алтай»', altai_section)} — что из этого умеет чип и какие правила он навязывает. "
|
||||||
|
f"{LocalLink('Платформа KNP', knp_section)} — чем сеть описывают и запускают. "
|
||||||
|
f"{LocalLink('Обучение под «Алтай»', learning_section)} — главная часть: четыре способа получить веса и их сравнение. "
|
||||||
|
f"{LocalLink('Вход: хост и рисунки', host_page)} — почему рисунок мышью распознаётся хуже теста и как это лечить. "
|
||||||
|
f"{LocalLink('От цифр к мусору', trash_section)} — что из этого переносится на робота и чего не хватает."),
|
||||||
|
Next("Импульсная сеть", snn_section, "с чего начинается любая сеть на «Алтае»."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 2. Импульсная сеть
|
||||||
|
|
||||||
|
@Page("2", "Импульсная сеть")
|
||||||
|
def snn_section():
|
||||||
|
return [
|
||||||
|
H1("Импульсная сеть"),
|
||||||
|
Lead("Сеть, в которой нейроны хранят заряд между тактами и обмениваются не числами, а событиями-спайками."),
|
||||||
|
Text("Обычная нейросеть — это формула: картинка на входе, числа на выходе, один проход. Импульсная сеть — это процесс во "
|
||||||
|
"времени. На каждом такте нейроны получают спайки, копят заряд и, дойдя до порога, сами выдают спайк. "
|
||||||
|
"Информация живёт в том, <b>сколько</b> спайков пришло и <b>когда</b>."),
|
||||||
|
Table(["", "Обычная нейросеть", "Импульсная сеть"], [
|
||||||
|
["Сигнал между нейронами", "вещественное число", "спайк: есть или нет"],
|
||||||
|
["Время", "нет, один проход", "такты; нейрон помнит заряд между ними"],
|
||||||
|
["Работа на картинку", "все веса, всегда", "только веса связей, по которым пришёл спайк"],
|
||||||
|
["Арифметика", "умножение и сложение", "сложение веса"],
|
||||||
|
["Обучение", "обратное распространение ошибки", "спайк не дифференцируется — нужны обходные пути"],
|
||||||
|
]),
|
||||||
|
Text("Три страницы раздела идут снизу вверх: "
|
||||||
|
f"{LocalLink('нейрон', neuron_page)}, {LocalLink('вход', coding_page)}, {LocalLink('слой', layer_page)}. "
|
||||||
|
"Все примеры — на нейроне AltAILIF, том самом, что стоит на «Алтае»."),
|
||||||
|
Next("Нейрон «Алтая»", neuron_page, "из чего всё собирается."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("2.1", "Нейрон «Алтая»")
|
||||||
|
def neuron_page():
|
||||||
|
return [
|
||||||
|
H1("Нейрон «Алтая»: AltAILIF"),
|
||||||
|
Lead("Целочисленный нейрон «накопление — утечка — выстрел». Всё, что умеет сеть на чипе, сводится к правилам этого нейрона."),
|
||||||
|
H2("Правило такта"),
|
||||||
|
List([
|
||||||
|
"к уровню (потенциалу) прибавляются веса всех связей, по которым в этот такт пришёл спайк;",
|
||||||
|
"прибавляется утечка — постоянная добавка, тянущая уровень вверх или вниз;",
|
||||||
|
"если уровень ≥ порога — нейрон выдаёт спайк, и уровень либо уменьшается на порог (флаг is_diff), "
|
||||||
|
"либо сбрасывается в заданное значение (флаг is_reset);",
|
||||||
|
"если уровень ниже отрицательного порога — при флаге saturate он упирается в этот порог.",
|
||||||
|
], ordered=True),
|
||||||
|
Text("Все числа целые: веса, порог, утечка. Уровень хранится в 16 битах со знаком, от −32 768 до 32 767."),
|
||||||
|
H2("Пример по тактам"),
|
||||||
|
Text("Порог 8, после выстрела порог вычитается, ниже нуля уровень не опускается — ровно так настроены скрытые нейроны "
|
||||||
|
"сетей в этой вики."),
|
||||||
|
Table(["Такт", "Пришло", "Уровень после сложения", "Спайк", "Уровень в конце такта"], [
|
||||||
|
["0", "+4, +4, +2", "10", "да", "2 — остаток сохранён"],
|
||||||
|
["1", "+4", "6", "нет", "6"],
|
||||||
|
["2", "+2, +4", "12", "да", "4"],
|
||||||
|
["3", "−4, −4", "−4", "нет", "0 — ниже нуля нельзя"],
|
||||||
|
["4", "+4", "4", "нет", "4"],
|
||||||
|
]),
|
||||||
|
Widget("neuron"),
|
||||||
|
Text("Поставьте вес 3 и порог 9 и посмотрите, как часто нейрон стреляет при вычитании порога и при сбросе в ноль. "
|
||||||
|
"С вычитанием спайков ровно ⌊заряд / порог⌋: заряд 21 при пороге 3 даёт 7 спайков. Это проверено на официальной "
|
||||||
|
"программной копии чипа, а не только в описании."),
|
||||||
|
H2("Почему такие флаги"),
|
||||||
|
Text("<b>Вычитание порога</b> превращает нейрон в счётчик: число спайков пропорционально полученному заряду. "
|
||||||
|
"Поэтому частоту спайков можно приравнять к выходу обычного нейрона и переносить в импульсную сеть веса обычной — "
|
||||||
|
f"на этом построена {LocalLink('нативная сеть', native_page)}. При сбросе в ноль излишек сгорает, и счёт искажается."),
|
||||||
|
Text("<b>Пол на нуле</b> защищает от переполнения и упрощает проверку, но у него есть цена: нейрон не помнит "
|
||||||
|
"«отрицательных доводов». Если на такте 0 пришло −8, а на такте 1 +8, нейрон всё равно выстрелит, хотя в сумме "
|
||||||
|
"заряд нулевой. Отсюда две особенности сетей ниже: выход «за/против» у нативной сети и важность порядка спайков "
|
||||||
|
"у сети по тактам."),
|
||||||
|
Table(["Флаг", "Что делает", "Как выставлен в сетях вики"], [
|
||||||
|
["is_diff", "после выстрела вычитает порог", "включён: остаток не теряется"],
|
||||||
|
["is_reset", "после выстрела сбрасывает уровень", "выключен"],
|
||||||
|
["saturate + отрицательный порог", "не пускает уровень ниже отрицательного порога", "включён, порог 0: уровень ≥ 0"],
|
||||||
|
["утечка", "добавка каждый такт; при leak_rev тянет к нулю", "0: нейрон копит заряд всю картинку"],
|
||||||
|
]),
|
||||||
|
Next("Вход: яркость в спайки", coding_page, "как в нейрон попадает картинка."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("2.2", "Вход: яркость в спайки")
|
||||||
|
def coding_page():
|
||||||
|
return [
|
||||||
|
H1("Вход: как яркость становится спайками"),
|
||||||
|
Lead("Чип принимает только спайки. Хост решает, каким правилом превратить яркость пикселя в события, — и от этого "
|
||||||
|
"правила зависят и точность, и число тактов, и работа чипа."),
|
||||||
|
H2("Частотный код"),
|
||||||
|
Text("У каждого пикселя есть накопитель. На каждом такте к нему прибавляется яркость (0…255); как только накопитель "
|
||||||
|
"дошёл до 255, пиксель выдаёт спайк, и 255 вычитается. Белый пиксель стреляет каждый такт, серый 128 — через такт, "
|
||||||
|
"чёрный молчит. За T тактов пиксель даёт примерно яркость × T / 255 спайков."),
|
||||||
|
H2("Временной код"),
|
||||||
|
Text("Каждый пиксель ярче 32 стреляет <b>один раз</b>, и яркость записана в моменте: такт = ⌊(255 − яркость) · T / 224⌋. "
|
||||||
|
"При T = 8 белый пиксель стреляет на такте 0, яркость 200 — на такте 1, 128 — на такте 4, 40 — на такте 7. "
|
||||||
|
"Тусклые края штриха приходят последними."),
|
||||||
|
Widget("coding"),
|
||||||
|
Table(["", "Частотный код", "Временной код"], [
|
||||||
|
["Чем несёт яркость", "числом спайков", "моментом единственного спайка"],
|
||||||
|
["Входных спайков на цифру MNIST", "≈730 за 8 тактов, ≈3200 за 32", "≈143 при любом числе тактов"],
|
||||||
|
["Что нужно для точности", "много тактов: оттенки различаются только при большом T", "4–8 тактов"],
|
||||||
|
["Как получить веса", "можно перенести обычную сеть: число спайков ≈ активация", "сеть надо учить прямо на тактах"],
|
||||||
|
]),
|
||||||
|
Text("Частотный код удобен для переноса, но дорог: картинка из 143 закрашенных пикселей превращается в 3200 событий. "
|
||||||
|
"Временной код отправляет на чип по одному событию на закрашенный пиксель — меньше уже нельзя."),
|
||||||
|
Callout("Подать яркость пачкой спайков в одном такте нельзя: если в одном такте по одной входной линии пришло несколько "
|
||||||
|
"спайков, «Алтай» засчитывает один. Это проверено на GoldenModel: 25 одинаковых спайков дали эффект одного. "
|
||||||
|
"Поэтому сила сигнала разворачивается во времени или раскладывается по разным линиям.",
|
||||||
|
title="Ограничение чипа", kind="warning"),
|
||||||
|
Next("Слой: сложение и торможение", layer_page, "что происходит, когда спайки доходят до слоя нейронов."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("2.3", "Слой: сложение и торможение")
|
||||||
|
def layer_page():
|
||||||
|
return [
|
||||||
|
H1("Слой: сложение, задержка, торможение"),
|
||||||
|
Lead("Слой — это группа нейронов и набор связей к ней. Связи только складывают; всё остальное делает порог."),
|
||||||
|
H2("Сложение за такт"),
|
||||||
|
Text("Всё, что пришло в нейрон за такт, складывается со своими весами, включая отрицательные. У каждой связи есть задержка "
|
||||||
|
"не меньше одного такта: спайк, выпущенный на такте N, доходит на такт N + 1. Поэтому в сети из двух слоёв ответ "
|
||||||
|
"отстаёт от входа на такт, а после последнего входного такта нужен ещё один, чтобы выход получил всё."),
|
||||||
|
Widget("synsum"),
|
||||||
|
H2("Торможение и «победитель забирает всё»"),
|
||||||
|
Text("Отрицательный вес — это торможение. Если связать нейроны слоя друг с другом отрицательными связями, первый "
|
||||||
|
"выстреливший гасит остальных. Так делают, когда нейроны видят одно и то же и должны поделить работу: без торможения "
|
||||||
|
"одинаковые нейроны с одинаковым правилом обучения выучивают одно и то же. На этом построено соревнование в "
|
||||||
|
f"{LocalLink('колонках STDP', columns_page)}."),
|
||||||
|
Widget("wta"),
|
||||||
|
H2("Кто с кем связан"),
|
||||||
|
Text("Схему связей задаёт конструктор сети, обучение меняет только веса. В сетях этой вики встречаются две схемы: "
|
||||||
|
"полносвязная (каждый вход с каждым нейроном слоя, 784 × 256) и локальная, когда нейрон смотрит в своё окно "
|
||||||
|
f"картинки. Локальная — основа {LocalLink('детекторов признаков', objects_page)}, и у неё на чипе есть цена: "
|
||||||
|
"общих весов нет, каждый детектор в каждой позиции — отдельный нейрон."),
|
||||||
|
Next("Процессор «Алтай»", altai_section, "какие из этих возможностей есть у чипа и с какими ограничениями."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 3. Алтай
|
||||||
|
|
||||||
|
@Page("3", "Процессор «Алтай»")
|
||||||
|
def altai_section():
|
||||||
|
return [
|
||||||
|
H1("Процессор «Алтай»"),
|
||||||
|
Lead("Нейроморфный процессор компании Мотив НТ: сетка одинаковых ядер, в каждом — нейроны AltAILIF и таблица их связей."),
|
||||||
|
Table(["Характеристика", "Значение"], [
|
||||||
|
["Прототип «Алтай-2»", "16 ядер, 8192 нейрона"],
|
||||||
|
["Масштабируемая конфигурация", "до 131 072 нейронов"],
|
||||||
|
["Нейрон", "AltAILIF: целые числа, уровень 16 бит со знаком"],
|
||||||
|
["Связь", "целый вес, |w| ≤ 255; не больше 4 разных величин веса на нейрон"],
|
||||||
|
["Обучение на кристалле", "нет: в официальном бэкенде пластичность выключена"],
|
||||||
|
["Ввод и вывод", "через порт с северной стороны сетки ядер"],
|
||||||
|
]),
|
||||||
|
Text("Источник цифр — руководство KNP 2.0. В нём описан «Алтай-2»; переносить эти числа на другие поколения чипа нельзя."),
|
||||||
|
H2("Чем мы пользуемся вместо чипа"),
|
||||||
|
Text("Платы нет, поэтому вместо чипа — официальный бэкенд Мотив НТ для KNP, пакет knp-altai-backend 1.0.2 из релиза "
|
||||||
|
"KNP 2.0.0. В нём два устройства: AltAI_HW — сам чип, и AltAI_GM — <b>GoldenModel</b>, программная копия, "
|
||||||
|
"которая считает так же, как кристалл, и раскладывает сеть по ядрам тем же размещателем. По умолчанию это "
|
||||||
|
"сетка 4 × 4 ядра. Библиотека закрытая: её поведение изучено только опытами, без дизассемблирования. "
|
||||||
|
"В самом пакете устройство называется AltAIv1_GoldenModel, хотя руководство говорит об «Алтае-2»."),
|
||||||
|
Links([
|
||||||
|
LocalLink("Ядро и веса", core_page) + " — откуда правило «4 величины веса на нейрон».",
|
||||||
|
LocalLink("Правила для сети и как их обходить", rules_page) + " — полный список ограничений и решения, проверенные на GoldenModel.",
|
||||||
|
LocalLink("Проверка в GoldenModel", verify_page) + " — как доказать, что сеть заработает, и на чём легко обмануться.",
|
||||||
|
]),
|
||||||
|
Next("Ядро и веса", core_page, "почему у нейрона так мало разных весов."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("3.1", "Ядро и веса")
|
||||||
|
def core_page():
|
||||||
|
return [
|
||||||
|
H1("Ядро и веса"),
|
||||||
|
Lead("Ядро хранит своих нейронов и связи к ним. Входящий спайк приходит по линии, и каждый нейрон ядра прибавляет вес, "
|
||||||
|
"записанный для этой линии."),
|
||||||
|
Text("Удобно представлять ядро таблицей: строки — входящие линии, столбцы — нейроны, в клетке — вес. Спайк по строке "
|
||||||
|
"заставляет каждый нейрон-столбец прибавить свою клетку. Руководство KNP формулирует главное ограничение так: "
|
||||||
|
"у нейрона не больше <b>четырёх разных величин веса</b>, знак выбирается отдельно и в счёт не идёт. "
|
||||||
|
"Вес 3 и вес −3 — одна величина; веса 1, 2, 4 и 255 — уже четыре."),
|
||||||
|
Widget("crossbar"),
|
||||||
|
Text("Поменяйте веса в таблице: когда у столбца появляется пятая величина, нейрон перестаёт помещаться в правило. "
|
||||||
|
"Обычная сеть после обучения имеет у каждого нейрона сотни разных весов — поэтому без квантования её на чип не положить."),
|
||||||
|
H2("Что занимает «величину»"),
|
||||||
|
Text("Величины расходуют не только обученные веса. В нативной сети у скрытого нейрона веса {1, 2, 4} и ещё −255 для "
|
||||||
|
"очистки уровня между картинками — ровно четыре. Обученным весам остаётся три уровня, и это определило палитру "
|
||||||
|
f"{{0, ±1, ±2, ±4}} во всех сетях вики. Подробно — на странице {LocalLink('правил', rules_page)}."),
|
||||||
|
Next("Правила для сети и обходы", rules_page, "все ограничения чипа сразу и обход каждого."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("3.2", "Правила для сети и обходы")
|
||||||
|
def rules_page():
|
||||||
|
return [
|
||||||
|
H1("Правила для сети и как их обходить"),
|
||||||
|
Lead("Сеть, которая нарушает хоть одно правило, на «Алтай» не загрузится или посчитает не то. Ниже — все ограничения, "
|
||||||
|
"найденные в руководстве и опытах на GoldenModel, и решения, которыми нативная сеть их обходит."),
|
||||||
|
Table(["Правило чипа", "Чем мешает", "Решение"], [
|
||||||
|
["Вес — целое, |w| ≤ 255", "обученные веса дробные", "квантование: вес × масштаб, округление до палитры"],
|
||||||
|
["Не больше 4 величин веса на нейрон", "у обученного нейрона сотни разных весов",
|
||||||
|
"палитра {0, ±1, ±2, ±4}; у выходных нейронов вход делится на группы по 32 со своими уровнями"],
|
||||||
|
["Связи с весом 0 не бывает", "нулевые веса после квантования",
|
||||||
|
"нулевая связь просто не создаётся; входные линии без связей не отправляются, иначе ошибка «неизвестный вход»"],
|
||||||
|
["Один тип связи, знак в весе", "тормозящая связь типа INHIBITORY не прошла в GoldenModel",
|
||||||
|
"торможение — связь EXCITATORY с отрицательным весом"],
|
||||||
|
["Начальный уровень нейрона не учитывается", "смещение (bias) нельзя задать стартовым зарядом",
|
||||||
|
"смещение +b — это b «тактовых» входных линий с весом +1, которые стреляют каждый такт"],
|
||||||
|
["Повтор одной линии в такте = один спайк", "нельзя подать сильный сигнал пачкой",
|
||||||
|
"сила разворачивается во времени; очистка идёт по 128 разным линиям"],
|
||||||
|
["Уровень — 16 бит со знаком", "сумма за картинку может переполниться",
|
||||||
|
"до загрузки считается верхняя граница: T × сумма положительных весов; у нативной сети 12 864 и 1 120"],
|
||||||
|
["Уровень не сбрасывается между картинками сам", "заряд прошлой картинки портит следующую",
|
||||||
|
"после картинки — пачка −255 по 128 линиям, всего −32 640: сначала чистится скрытый слой, потом выход"],
|
||||||
|
["Обучения нет", "веса нельзя подстроить на месте", "обучение на компьютере, загрузка готовых весов"],
|
||||||
|
]),
|
||||||
|
H2("Квантование"),
|
||||||
|
Text("Квантование заменяет дробный вес ближайшим из палитры. Чем меньше ступенек, тем грубее нейрон: из тонкого "
|
||||||
|
"профиля весов остаются три уровня. Ошибку квантования сеть переживает, если учится уже с огрублёнными весами — "
|
||||||
|
f"так сделано в {LocalLink('сети по тактам', ticks_page)}."),
|
||||||
|
Widget("quant"),
|
||||||
|
H2("Влезет ли сеть"),
|
||||||
|
Text("Три проверки, которые можно сделать до размещателя: число нейронов, число разных весов у самого «разнообразного» "
|
||||||
|
"нейрона и наибольший уровень. Переключитесь на сеть детекторов — она проваливает все три."),
|
||||||
|
Widget("fit"),
|
||||||
|
Callout("Эти три числа необходимы, но недостаточны. Размещатель проверяет ещё входящие линии ядер и маршруты, а "
|
||||||
|
"правильность счёта доказывает только сравнение спайков с эталоном.", title="Предел оценки", kind="note"),
|
||||||
|
Next("Проверка в GoldenModel", verify_page, "как это сравнение устроено."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("3.3", "Проверка в GoldenModel")
|
||||||
|
def verify_page():
|
||||||
|
return [
|
||||||
|
H1("Проверка в GoldenModel"),
|
||||||
|
Lead("Сеть считается рабочей на «Алтае», когда счётчик спайков каждого нейрона на каждой картинке совпал с "
|
||||||
|
"эталоном. Совпадения точности недостаточно."),
|
||||||
|
H2("Лестница проверки"),
|
||||||
|
Table(["Ступень", "Что доказывает", "Время на нативной сети"], [
|
||||||
|
["Целочисленный эталон (NumPy)", "арифметика сети записана верно", "—"],
|
||||||
|
["KNP, однопоточный CPU", "граф собран правильно; счётчики равны эталону", "65 с на 10 000 цифр"],
|
||||||
|
["KNP, наш CUDA-бэкенд", "то же на больших объёмах, побайтно равно CPU", "11,6 с на 10 000 цифр"],
|
||||||
|
["GoldenModel", "сеть размещается по ядрам и считает по правилам чипа", "≈6 с на одну цифру; 1000 цифр — 143 с в 8 процессах"],
|
||||||
|
["Чип AltAI_HW", "работа на железе", "не проверялось"],
|
||||||
|
]),
|
||||||
|
Text("Почему счётчики, а не точность: две сети с разными ошибками могут дать одинаковый процент. Совпадение 256 скрытых "
|
||||||
|
"и 160 выходных счётчиков на каждой картинке исключает и ошибку в весах, и потерянный спайк, и неочищенный уровень."),
|
||||||
|
H2("Размещатель может принять сеть и всё равно посчитать не то"),
|
||||||
|
Text("Чтобы ускорить проверку, в GoldenModel загрузили 8 копий нативной сети сразу. Размещатель граф принял и выделил "
|
||||||
|
"24 ядра при стандартной сетке 4 × 4. Счёт разошёлся: 26 495 расхождений скрытых счётчиков на 319 картинках из 1000, "
|
||||||
|
"точность 93,6% вместо 97,5%. Явная сетка 8 × 8 тоже не помогла. Причина не установлена."),
|
||||||
|
Text("Вывод: принятие графа размещателем ничего не доказывает. Рабочий режим — не больше двух копий сети на процесс "
|
||||||
|
"в стандартной сетке 4 × 4, а параллельность — отдельными процессами эмулятора. Исполнитель нативной сети "
|
||||||
|
"отказывается запускать GoldenModel с большим числом копий."),
|
||||||
|
H2("Что проверено опытами"),
|
||||||
|
Text("Поведение закрытой библиотеки установлено крошечными сетями на GoldenModel:"),
|
||||||
|
Table(["Опыт", "Результат", "Вывод"], [
|
||||||
|
["заряд 21, порог 3, вычитание порога", "7 спайков", "остаток сохраняется"],
|
||||||
|
["25 одинаковых входных спайков в такте, вес 1, порог 21", "0 спайков", "повторы схлопываются в один"],
|
||||||
|
["начальный уровень 21, порог 3, без входа", "0 спайков", "начальный уровень не учитывается"],
|
||||||
|
["утечка +1, порог 3", "спайки на тактах 3, 6, 9, 12, 15", "утечка работает как постоянный вход"],
|
||||||
|
["24 / 128 / 512 тактов крошечной сети", "0,69 / 3,3 / 13 с", "у эмулятора большие накладные расходы на такт"],
|
||||||
|
]),
|
||||||
|
H2("Правила прогонов"),
|
||||||
|
List([
|
||||||
|
"каждый прогон — в новый пустой каталог; старый результат не может замаскировать новый сбой;",
|
||||||
|
"до и после прогона сверяются хеши модели и исполняемого файла;",
|
||||||
|
"проверяется точный список номеров картинок, а не только их количество;",
|
||||||
|
"при первом расхождении счётчиков прогон останавливается и пишет FAILED.json; VERIFIED.json появляется только при полном совпадении.",
|
||||||
|
]),
|
||||||
|
Text("Итог для нативной сети: 975 из 1000 первых тестовых цифр в GoldenModel, все счётчики совпали. "
|
||||||
|
"Полные 10 000 в GoldenModel не запускались."),
|
||||||
|
Next("Платформа KNP", knp_section, "в чём эту сеть описывают и чем запускают."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 4. KNP
|
||||||
|
|
||||||
|
@Page("4", "Платформа KNP")
|
||||||
|
def knp_section():
|
||||||
|
return [
|
||||||
|
H1("Платформа KNP"),
|
||||||
|
Lead("Kaspersky Neuromorphic Platform — открытая библиотека (C++ и Python, Apache 2.0): сеть описывается один раз и "
|
||||||
|
"исполняется на CPU, GPU или «Алтае»."),
|
||||||
|
H2("Из чего состоит модель"),
|
||||||
|
Table(["Объект", "Что это"], [
|
||||||
|
["Population", "группа однотипных нейронов; параметры каждого задаёт функция-генератор"],
|
||||||
|
["Projection", "связи в популяцию: для каждой — вес, задержка, тип, номер источника и получателя"],
|
||||||
|
["Network", "набор популяций и проекций"],
|
||||||
|
["Model", "сеть плюс входные каналы (куда хост шлёт спайки) и выходные (откуда читает)"],
|
||||||
|
["ModelExecutor", "гоняет модель по тактам на выбранном бэкенде и вызывает код хоста каждый такт"],
|
||||||
|
]),
|
||||||
|
Text("Каждый такт исполнитель берёт входные спайки из каналов, бэкенд обновляет нейроны и доставляет спайки по проекциям, "
|
||||||
|
"выходные спайки складываются в каналы. Хост между тактами решает, что подать дальше."),
|
||||||
|
H2("Нативная сеть в коде"),
|
||||||
|
Text("Сокращённый фрагмент из mnist-altai-native/native_digits.cpp. Видны все обходы со страницы "
|
||||||
|
f"{LocalLink('правил', rules_page)}: тактовые линии смещения, очистка по 128 линиям весом −255, только тип EXCITATORY."),
|
||||||
|
Code('''using P = knp::core::Population<knp::neuron_traits::AltAILIF>;
|
||||||
|
using R = knp::core::Projection<knp::synapse_traits::DeltaSynapse>;
|
||||||
|
|
||||||
|
auto make = [&](int thr) { // нейрон: вычитание порога, уровень не ниже 0, без утечки
|
||||||
|
N n; n.activation_threshold_ = thr; n.negative_activation_threshold_ = 0;
|
||||||
|
n.is_diff_ = true; n.is_reset_ = false; n.saturate_ = true; n.potential_leak_ = 0; return n;
|
||||||
|
};
|
||||||
|
P hidden([&](size_t) { return make(32); }, 256);
|
||||||
|
P output([&](size_t) { return make(4); }, 160);
|
||||||
|
|
||||||
|
auto add = [](auto& v, int pre, int post, int w) { // нулевых связей нет, знак — в весе
|
||||||
|
if (w) v.push_back(R::Synapse{{float(w), 1, OutputType::EXCITATORY}, size_t(pre), size_t(post)});
|
||||||
|
};
|
||||||
|
for (int h = 0; h < 256; h++) {
|
||||||
|
for (int p = 0; p < 784; p++) add(hin, p, h, w1[p * 256 + h]); // пиксели
|
||||||
|
for (int c = 0; c < abs(b1[h]); c++) add(hin, 784 + c, h, b1[h] > 0 ? 1 : -1); // смещение
|
||||||
|
for (int c = 0; c < 128; c++) add(hin, 784 + clock + c, h, -255); // очистка
|
||||||
|
}
|
||||||
|
Network net; net.add_population(hidden); net.add_population(output);
|
||||||
|
net.add_projection(R(input_uid, hidden.get_uid(), from(hin), hin.size())); /* … */
|
||||||
|
Model model(std::move(net));
|
||||||
|
model.add_input_channel(in_ch, input_projection_uid);
|
||||||
|
model.add_output_channel(out_ch, output.get_uid());
|
||||||
|
ModelExecutor exec(model, backend, {{in_ch, send_spikes}});
|
||||||
|
exec.start([&](size_t step) { return !done; });''', "cpp"),
|
||||||
|
H2("Бэкенды"),
|
||||||
|
Table(["Бэкенд", "Нейроны", "Обучение", "Замечание"], [
|
||||||
|
["CPU, однопоточный", "LIF, BLIFAT, AltAILIF и их STDP-варианты", "да", "эталон KNP"],
|
||||||
|
["CPU, многопоточный", "LIF, BLIFAT", "частично", "AltAILIF не поддерживает"],
|
||||||
|
["CUDA официальный", "BLIFAT", "нет", "AltAILIF не поддерживает"],
|
||||||
|
["CUDA для AltAILIF", "AltAILIF + Delta", "нет", "написан в проекте; побайтно совпадает с CPU, в 5,6 раза быстрее на пачке из 8 картинок"],
|
||||||
|
["AltAI: GoldenModel и чип", "AltAILIF + Delta", "нет", "закрытая библиотека Мотив НТ, пакет 1.0.2"],
|
||||||
|
]),
|
||||||
|
Text("Для «Алтая» имеет смысл только связка AltAILIF + DeltaSynapse: модели LIF и BLIFAT на чип не переносятся, "
|
||||||
|
"а STDP работает только на CPU."),
|
||||||
|
Next("Подводные камни KNP", knp_pitfalls, "где KNP ведёт себя не так, как ожидаешь."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("4.1", "Подводные камни KNP")
|
||||||
|
def knp_pitfalls():
|
||||||
|
return [
|
||||||
|
H1("Подводные камни KNP"),
|
||||||
|
Lead("Ошибки и неочевидное поведение, найденные при работе с KNP 2.0.0. Каждое стоило дней, поэтому записано."),
|
||||||
|
Table(["Что", "Симптом", "Причина и решение"], [
|
||||||
|
["ModelLoader::init_channels", "примерно каждое 8–12-е обучение молча проваливается: веса остаются стартовыми, точность случайная",
|
||||||
|
"вход картинки иногда подписывается на канал меток. Исправлено в репозитории проекта, в KasperskyLab/knp не отправлено"],
|
||||||
|
["Порог BLIFAT после обучения", "пример mnist-learn молчит на 68% тестовых цифр (30,9% верных)",
|
||||||
|
"добавка к порогу растёт при обучении и застывает. Порог на проверке задаётся отдельно: 75,5%"],
|
||||||
|
["Одинаковые нейроны колонки", "20 нейронов колонки имеют одинаковые веса и работают как один",
|
||||||
|
"одинаковый старт и общий сигнал метки. Разброс стартовых весов σ = 20: 82,5% → 92,2%"],
|
||||||
|
["Дофамин опаздывает на такт", "обучение по ошибке не сходится",
|
||||||
|
"дофамин приходит на такт позже входных спайков; нужна поправка KNP_DELTA_LAG=1"],
|
||||||
|
["Обучение молчащего нейрона", "штатный STDP не меняет связи нейрона, который не выстрелил",
|
||||||
|
"для обучения по ошибке собрана отдельная копия CPU-бэкенда (readout/build_delta_cpu.py)"],
|
||||||
|
["stochastic_stimulation", "параметр задан, но ни на что не влияет", "CPU-бэкенд его не читает"],
|
||||||
|
["Ничья в «победитель забирает всё»", "один прогон из пяти схлопывает все веса к нулю",
|
||||||
|
"предположительно случайный выбор победителя с seed из std::random_device; не подтверждено"],
|
||||||
|
["Сохранение сети", "обученную сеть примера нельзя загрузить для проверки",
|
||||||
|
"пример не вызывает save_network, режим inference_only не загружает модель"],
|
||||||
|
["get_grid_cols_rows", "ошибка компоновки с бэкендом AltAI", "функция объявлена в заголовке, но отсутствует в пакете 1.0.2"],
|
||||||
|
]),
|
||||||
|
H2("Переключатели примера mnist-learn"),
|
||||||
|
Text("Изменения в примере сделаны переменными окружения: без них пример работает как в KNP."),
|
||||||
|
Table(["Переменная", "Что делает"], [
|
||||||
|
["KNP_MNIST_THR, KNP_MNIST_ADD_THR_SCALE", "порог входных нейронов на проверке и множитель добавки к нему"],
|
||||||
|
["KNP_COL_JITTER", "случайный разброс стартовых весов колонок"],
|
||||||
|
["KNP_COL_PUNISH", "отрицательный дофамин колонкам чужих классов; лучшее 0,012–0,02"],
|
||||||
|
["KNP_DELTA, KNP_DELTA_LAG", "обучение выхода по ошибке и сдвиг дофамина на такт"],
|
||||||
|
["KNP_W_RANGE", "диапазон весов при обучении по ошибке"],
|
||||||
|
["KNP_MNIST_INPUT_SIZE", "размер входа: признаки вместо 784 пикселей"],
|
||||||
|
["KNP_MNIST_DUMP", "выгрузка весов и порогов после обучения"],
|
||||||
|
]),
|
||||||
|
Next("Обучение под «Алтай»", learning_section, "главный вопрос: откуда взять веса."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 5. Обучение
|
||||||
|
|
||||||
|
@Page("5", "Обучение под «Алтай»")
|
||||||
|
def learning_section():
|
||||||
|
return [
|
||||||
|
H1("Обучение под «Алтай»"),
|
||||||
|
Lead("«Алтай» не учится, а спайк — ступенька без производной. Поэтому вопрос «как обучить сеть» здесь главный, и ответов "
|
||||||
|
"на него четыре."),
|
||||||
|
Text("Обычную сеть учат обратным распространением ошибки: считают, как ответ зависит от каждого веса, и сдвигают веса. "
|
||||||
|
"Для импульсной сети это напрямую не работает: выход нейрона меняется скачком, производная почти везде равна нулю. "
|
||||||
|
"Отсюда два пути: учиться <b>локальными правилами</b>, которым производная не нужна, или учиться <b>на компьютере</b>, "
|
||||||
|
"заменив ступеньку чем-то гладким."),
|
||||||
|
Table(["Способ", "Где учится", "Чему учит", "Цифры MNIST"], [
|
||||||
|
["Колонки STDP", "в KNP", "«похоже на мою цифру», без сравнения с чужими", "92,2%"],
|
||||||
|
["Дофамин по ошибке", "в KNP", "один слой по ошибке ответа; признаки готовит хост", "94,8% (валидация)"],
|
||||||
|
["Перенос обычной сети", "на компьютере", "всю сеть через её аналоговую копию", "97,76%"],
|
||||||
|
["Обучение по тактам", "на компьютере", "всю сеть прямо на спайковой динамике чипа", "97,42% за 8 тактов"],
|
||||||
|
]),
|
||||||
|
Text("Закономерность видна сразу: чем больше обучения уходит на компьютер, тем выше точность. Для «Алтая», который "
|
||||||
|
"учиться не умеет, это не уступка, а естественный путь. Обучение внутри KNP ценно другим — оно показывает, что "
|
||||||
|
"платформа может учиться сама, если когда-нибудь появится чип с пластичностью."),
|
||||||
|
Links([
|
||||||
|
LocalLink("Колонки STDP", columns_page) + " — как учится штатный пример KNP и почему он упирается в 92%.",
|
||||||
|
LocalLink("Дофамин по ошибке", delta_page) + " — правило перцептрона, собранное из дофамина KNP.",
|
||||||
|
LocalLink("Перенос обычной сети", native_page) + " — нативная сеть, проверенная в GoldenModel.",
|
||||||
|
LocalLink("Обучение по тактам", ticks_page) + " — та же точность в 17 раз дешевле.",
|
||||||
|
LocalLink("Сравнение", compare_page) + " — все сети на одной цифре и на одном графике.",
|
||||||
|
]),
|
||||||
|
Next("Колонки STDP", columns_page, "с чего начинал KNP."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("5.1", "Колонки STDP")
|
||||||
|
def columns_page():
|
||||||
|
return [
|
||||||
|
H1("Колонки STDP"),
|
||||||
|
Lead("Штатный пример KNP mnist-learn: на каждую цифру — колонка из 20 нейронов, которые учатся по STDP с дофамином. "
|
||||||
|
"Ответ — колонка, которая стреляет больше всех."),
|
||||||
|
H2("Как учится"),
|
||||||
|
Text("Картинка подаётся частотным кодом за 10 тактов во все колонки сразу. Во время обучения метка через вспомогательную "
|
||||||
|
"популяцию будит колонку правильной цифры, и у её нейронов крепнут связи с пикселями, которые стреляли перед "
|
||||||
|
"выстрелом нейрона. Это <b>STDP</b> — правило, зависящее от порядка спайков: вход за мгновение до выстрела помог, "
|
||||||
|
"связь растёт; вход после выстрела не понадобился, связь слабеет."),
|
||||||
|
Widget("stdp"),
|
||||||
|
Text("В KNP используется вариант с синаптическим ресурсом: вес растёт тем медленнее, чем ближе к пределу, и изменения "
|
||||||
|
"закрепляются наградой — дофамином. Внутри колонки нейроны тормозят друг друга, чтобы каждый выучил свой стиль "
|
||||||
|
"написания."),
|
||||||
|
H2("Что пришлось исправить"),
|
||||||
|
Text("В исходном примере все 20 нейронов колонки стартовали с одинаковых весов и получали один и тот же сигнал метки — "
|
||||||
|
"колонка работала как один нейрон, 82,5%. Небольшой случайный разброс стартовых весов дал нейронам разные "
|
||||||
|
"стили, а торможение −255 между всеми нейронами на проверке оставило голос самому похожему: <b>92,2%</b> на полном "
|
||||||
|
f"MNIST. Попутно найдена ошибка загрузчика KNP — см. {LocalLink('подводные камни', knp_pitfalls)}."),
|
||||||
|
H2("Почему дальше 92% не идёт"),
|
||||||
|
Text("Колонка выучивает <b>трафарет</b>: «где у моей цифры обычно есть линия». Отсюда четыре следствия, и все они "
|
||||||
|
"измерены:"),
|
||||||
|
List([
|
||||||
|
"сдвиг цифры на 2 пикселя роняет точность до 64–67%, утолщение или утончение линии — до 75–81%: трафарет перестаёт совпадать;",
|
||||||
|
"типичная ошибка 4 → 9: трафарет девятки накрывает четвёрку целиком, а «не путай с девяткой» колонку никто не учил — "
|
||||||
|
"награда бывает только за свою цифру; наказание чужих колонок дофамином даёт +2…4 п.п., но не снимает потолок;",
|
||||||
|
"пустой пиксель молчит, поэтому колонка не видит довода «здесь линии нет». Попытка добавить такие доводы отдельными "
|
||||||
|
"входами «пиксель тёмный» (кодирование ON/OFF) утроила число входных спайков, и сеть замолчала;",
|
||||||
|
"на плотном входе колонки молчат вообще: после STDP большинство весов отрицательные, сумма уходит в минус.",
|
||||||
|
]),
|
||||||
|
Text("Это ограничение архитектуры, а не настроек: колонки сравнивают с образцом, но не учатся различать классы."),
|
||||||
|
Next("Дофамин по ошибке", delta_page, "как научить сеть именно различать, не выходя из KNP."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("5.2", "Дофамин по ошибке")
|
||||||
|
def delta_page():
|
||||||
|
return [
|
||||||
|
H1("Дофамин по ошибке"),
|
||||||
|
Lead("Правило перцептрона, собранное из механизмов KNP: нейрон получает дофамин «цель минус ответ», и связь меняется "
|
||||||
|
"на эту величину, если по ней только что пришёл спайк."),
|
||||||
|
H2("Правило"),
|
||||||
|
Text("Метка даёт нейрону своей цифры положительный дофамин на каждом такте, собственный спайк нейрона — отрицательный. "
|
||||||
|
"Если нейрон должен был стрелять и стрелял, сумма около нуля, ничего не меняется. Если должен, но молчал — связи с "
|
||||||
|
"активными входами крепнут. Если стрелял зря — слабеют. В отличие от колонок, сеть учится и на чужих цифрах: "
|
||||||
|
"«не стреляй на девятку» получается само."),
|
||||||
|
Text("В штатном KNP так не выйдет: STDP меняет только связи выстрелившего нейрона, а здесь нужно учить и молчащего. "
|
||||||
|
"Для этого собрана отдельная копия CPU-бэкенда (readout/build_delta_cpu.py, режим KNP_DELTA). Оригинальные "
|
||||||
|
"библиотеки не менялись. Дофамин в KNP приходит на такт позже входа, поэтому обязателен KNP_DELTA_LAG=1."),
|
||||||
|
H2("Результаты"),
|
||||||
|
Table(["Задача (валидация)", "Колонки STDP", "Дофамин по ошибке в KNP", "Тот же слой, обученный на компьютере"], [
|
||||||
|
["Цифры, признаки штрихов", "83,5% (200 нейронов)", "94,8% (10 нейронов)", "94,9%"],
|
||||||
|
["Цифры со сдвигом на 2 пикселя, обучение со сдвигами", "—", "91,3%", "89,9%"],
|
||||||
|
["Кошки/собаки, выученные признаки", "64–65%", "65–69%", "74%"],
|
||||||
|
]),
|
||||||
|
Text("На цифрах обучение внутри платформы догнало обучение на компьютере. На объектах разрыв 5–9 п.п. не объяснён: "
|
||||||
|
"диапазон весов и число проходов не помогают."),
|
||||||
|
H2("Ограничение"),
|
||||||
|
Text("Правило учит один слой, а один слой на пикселях — это линейный классификатор, около 92% на MNIST. Поэтому вход "
|
||||||
|
"у него — <b>признаки штрихов</b>, которые считает хост: 8 направлений линии × 2 толщины, сложенные по клеткам 4 × 4. "
|
||||||
|
"Получается, что половина распознавания идёт не на чипе. Чтобы на чипе была вся сеть, нужны веса для нескольких "
|
||||||
|
"слоёв — а их даёт только обучение на компьютере."),
|
||||||
|
Next("Перенос обычной сети", native_page, "первая сеть, целиком живущая на «Алтае»."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("5.3", "Перенос обычной сети")
|
||||||
|
def native_page():
|
||||||
|
return [
|
||||||
|
H1("Перенос обычной сети: нативная сеть"),
|
||||||
|
Lead("Двухслойная сеть из нейронов AltAILIF, уложенная во все правила чипа. Веса взяты у обычной сети, обученной так, "
|
||||||
|
"чтобы её числа можно было заменить счётом спайков."),
|
||||||
|
Table(["Слой", "Размер", "Устройство"], [
|
||||||
|
["вход", "784 линии + тактовые + 128 линий очистки", "частотный код, 32 такта"],
|
||||||
|
["скрытый", "256 AltAILIF", "порог 32, вычитание порога; веса {0, ±1, ±2, ±4}; смещение тактовыми линиями"],
|
||||||
|
["выход", "160 AltAILIF", "10 цифр × 8 групп × «за» и «против»; порог 4"],
|
||||||
|
["хост", "—", "по каждой цифре: спайки «за» минус спайки «против», затем максимум"],
|
||||||
|
]),
|
||||||
|
H2("Почему это работает"),
|
||||||
|
Text("Скрытый нейрон с вычитанием порога за 32 такта выдаёт ⌊заряд / 32⌋ спайков, но не больше 32. Это ступенчатая "
|
||||||
|
"функция с насыщением, почти та же, что ReLU с ограничением у обычной сети. Значит, если обычную сеть учить с "
|
||||||
|
"активацией, ограниченной и округлённой так же, её веса подойдут импульсной. Учитель — сеть 784 → 256 → 10 "
|
||||||
|
"(mnist-fast), обученная со сдвигами, поворотами и изменением толщины."),
|
||||||
|
H2("Рецепт"),
|
||||||
|
List([
|
||||||
|
"веса скрытого слоя учителя × 8 округляются до {0, ±1, ±2, ±4}, смещения — до целых;",
|
||||||
|
"через импульсный скрытый слой прогоняются все 70 000 картинок и записываются настоящие счётчики спайков;",
|
||||||
|
"выходной слой обучается заново уже на этих счётчиках — так он учитывает ошибки квантования скрытого слоя;",
|
||||||
|
"выходные веса квантуются отдельно для каждой группы из 32 скрытых нейронов: уровни {0, 1, ≈0,45·max, max}, "
|
||||||
|
"чтобы у выходного нейрона было не больше трёх величин плюс −255 очистки;",
|
||||||
|
"масштаб и порог выхода выбираются по валидации (5000 картинок): масштаб 4, порог 4; тест открывается после выбора.",
|
||||||
|
], ordered=True),
|
||||||
|
H2("Почему выход «за/против»"),
|
||||||
|
Text("Выходной вес бывает и отрицательным: «здесь есть верхняя перекладина — значит, не 4». Но уровень нейрона не "
|
||||||
|
"опускается ниже нуля, и отрицательный довод, пришедший раньше положительного, теряется. Поэтому положительные "
|
||||||
|
"веса идут в нейрон «за», отрицательные — со знаком плюс в нейрон «против», а вычитание делает хост. Деление на 8 "
|
||||||
|
"групп нужно уже из-за правила четырёх величин: у каждой группы свои три уровня."),
|
||||||
|
H2("Результаты"),
|
||||||
|
Table(["Свойство", "Значение"], [
|
||||||
|
["Точность на тесте MNIST", "97,76% — эталон, KNP на CPU и наш CUDA дают одинаковые счётчики на всех 10 000"],
|
||||||
|
["GoldenModel", "975 из 1000 первых тестовых цифр, все счётчики совпали"],
|
||||||
|
["Цена", "≈3200 входных спайков и ≈338 тыс. синаптических событий на цифру"],
|
||||||
|
["Наибольший уровень", "оценка сверху 12 864 (скрытый) и 1 120 (выход) — в 16 бит помещается"],
|
||||||
|
]),
|
||||||
|
H2("Можно ли короче"),
|
||||||
|
Text("32 такта частотного кода — дорого. Если просто обрезать ту же сеть по тактам, точность держится до 8, а потом "
|
||||||
|
"обваливается: сеть училась на 32 оттенках, и при 4 тактах серые пиксели неотличимы от чёрных."),
|
||||||
|
Table(["Тактов", "Точность", "Синаптических событий"], [
|
||||||
|
["32", "97,76%", "338 тыс."], ["16", "97,66%", "164 тыс."], ["8", "97,19%", "77 тыс."], ["4", "84,63%", "33 тыс."],
|
||||||
|
]),
|
||||||
|
Next("Обучение по тактам", ticks_page, "как сделать сеть, которой мало тактов не мешает."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("5.4", "Обучение по тактам")
|
||||||
|
def ticks_page():
|
||||||
|
return [
|
||||||
|
H1("Обучение по тактам"),
|
||||||
|
Lead("Сеть 784 → 256 → 10 из AltAILIF, обученная прямо на спайковой целочисленной динамике чипа. Вход — временной код: "
|
||||||
|
"один спайк на пиксель, ответ за 8 тактов."),
|
||||||
|
Widget("ticks"),
|
||||||
|
Text("Нарисуйте цифру и прокрутите такты. Яркие пиксели стреляют первыми, скрытые нейроны начинают отвечать с первого "
|
||||||
|
"такта, и выход обычно определяется задолго до восьмого: такт, после которого лидер больше не меняется, показан "
|
||||||
|
"как ранний ответ."),
|
||||||
|
H2("Как обучить ступеньку"),
|
||||||
|
Text("Прямой проход идёт по настоящим тактам: спайки, вычитание порога, пол на нуле, задержка в такт между слоями — та же "
|
||||||
|
"арифметика, что на чипе. При обратном проходе производная ступеньки заменяется гладким «колоколом» "
|
||||||
|
"1 / (1 + 4|v|)², где v — расстояние уровня до порога. Это <b>суррогатный градиент</b>: он говорит весам, "
|
||||||
|
"в какую сторону сдвинуть уровень, чтобы нейрон стрельнул или промолчал."),
|
||||||
|
H2("Рецепт"),
|
||||||
|
List([
|
||||||
|
"30 эпох, Adam, шаг 0,002 с косинусным спадом, пачки по 256; половина каждой пачки — те же цифры, перерисованные "
|
||||||
|
"пером постоянной толщины, плюс сдвиги и повороты;",
|
||||||
|
"первые 8 эпох веса дробные; с 9-й в прямом проходе они огрубляются до {0, ±1, ±2, ±4}, а градиент проходит мимо "
|
||||||
|
"огрубления. Сеть доучивается уже с теми весами, что будут на чипе;",
|
||||||
|
"ответ сети при обучении — число выходных спайков плюс половина остатка уровня, чтобы ошибка была видна и при равенстве счёта;",
|
||||||
|
"к ошибке добавлен штраф λ за спайки скрытого слоя: сеть учится молчать, когда может;",
|
||||||
|
"после каждой эпохи веса переводятся в целые и проверяются отдельным целочисленным симулятором на валидации; "
|
||||||
|
"лучшая эпоха выбирается по нему.",
|
||||||
|
], ordered=True),
|
||||||
|
H2("Варианты"),
|
||||||
|
Table(["Код", "Тактов", "λ", "Валидация", "Тест", "Тест, рисунки", "Синапт. событий", "Скрытых спайков"], [
|
||||||
|
["временной", "8", "0,1", "96,66%", "97,42%", "95,85%", "20 150", "45"],
|
||||||
|
["временной", "8", "0,5", "96,13%", "97,11%", "95,10%", "25 175", "21"],
|
||||||
|
["временной", "6", "0,5", "96,10%", "97,12%", "95,07%", "24 927", "18"],
|
||||||
|
["временной", "4", "0,5", "95,88%", "97,08%", "95,05%", "24 129", "17"],
|
||||||
|
["временной", "4", "0,1", "95,17%", "96,11%", "93,93%", "18 142", "41"],
|
||||||
|
["частотный", "8", "0,1", "95,91%", "97,22%", "94,37%", "84 590", "62"],
|
||||||
|
["частотный", "4", "0,1", "93,07%", "94,35%", "90,86%", "37 405", "47"],
|
||||||
|
]),
|
||||||
|
Text("Выбрана первая строка — по валидации, тест открыт после выбора. Два наблюдения из таблицы:"),
|
||||||
|
Steps([
|
||||||
|
"<b>Временной код выигрывает у частотного</b> при том же числе тактов: точнее и в 4 раза дешевле. Частотному коду на "
|
||||||
|
"8 тактах не хватает оттенков, временной передаёт яркость моментом.",
|
||||||
|
"<b>Штраф за скрытые спайки не равен экономии.</b> Почти вся работа — во входном слое: каждый из ≈143 входных спайков "
|
||||||
|
"задевает в среднем ≈139 скрытых нейронов, это ≈19,7 тыс. из 20 тыс. событий. Сильный штраф вдвое снижает "
|
||||||
|
"скрытые спайки, но сеть компенсирует это более плотными входными весами, и событий становится больше. Экономить "
|
||||||
|
"нужно на числе ненулевых входных весов.",
|
||||||
|
]),
|
||||||
|
H2("Что осталось до чипа"),
|
||||||
|
Text("Правила чипа соблюдены на бумаге: палитра {0, 1, 2, 4}, наибольший уровень 304 при пределе 32 767, смещение — "
|
||||||
|
"постоянный вход каждый такт, то есть тактовые линии, как у нативной сети. Но в KNP и GoldenModel сеть ещё не "
|
||||||
|
"запускалась, и для неё нет очистки между картинками. Это следующий шаг, после которого её можно ставить в один "
|
||||||
|
"ряд с нативной."),
|
||||||
|
Next("Сравнение", compare_page, "все сети рядом."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("5.5", "Сравнение")
|
||||||
|
def compare_page():
|
||||||
|
return [
|
||||||
|
H1("Сравнение сетей"),
|
||||||
|
Lead("Три сети, которые целиком состоят из нейронов AltAILIF, на одной задаче: тест MNIST, 10 000 цифр."),
|
||||||
|
Table(["", "Колонки STDP", "Нативная сеть", "Обучение по тактам"], [
|
||||||
|
["Точность, тест", "92,2%", "97,76%", "97,42%"],
|
||||||
|
["Точность, рисунки", "—", "94,1% (повтор рецепта)", "95,9%"],
|
||||||
|
["Тактов", "15", "32", "8"],
|
||||||
|
["Входных спайков", "≈870", "≈3200", "≈143"],
|
||||||
|
["Синаптических событий", "≈175 тыс.", "≈338 тыс.", "≈20 тыс."],
|
||||||
|
["Нейронов", "200", "416", "266"],
|
||||||
|
["Где учится", "в KNP", "на компьютере", "на компьютере"],
|
||||||
|
["Проверена в GoldenModel", "нет", "да, 1000 цифр", "нет"],
|
||||||
|
]),
|
||||||
|
H2("Одна цифра — три сети"),
|
||||||
|
Text("Нарисуйте цифру или выберите тестовую. Все три сети считаются здесь целочисленно, по тем же правилам, что "
|
||||||
|
"на чипе. Сдвиньте рисунок к краю или нарисуйте очень толстой линией — колонки ошибутся первыми."),
|
||||||
|
Widget("compare"),
|
||||||
|
H2("Точность против работы чипа"),
|
||||||
|
Text("Каждая точка — сеть или её режим. Левее — меньше сложений и, значит, энергии; выше — точнее. Нативная сеть, "
|
||||||
|
"обрезанная по тактам, ползёт влево и вниз; сеть, обученная по тактам, стоит в левом верхнем углу."),
|
||||||
|
Widget("tradeoff"),
|
||||||
|
H2("Что выбирать"),
|
||||||
|
Table(["Нужно", "Сеть", "Почему"], [
|
||||||
|
["доказательство «работает на Алтае» сегодня", "нативная", "единственная прошла GoldenModel"],
|
||||||
|
["минимум энергии и тактов", "обучение по тактам", "в 17 раз меньше событий при той же точности; осталось пройти GoldenModel"],
|
||||||
|
["обучение без компьютера, внутри платформы", "колонки или дофамин по ошибке", "единственные варианты; точность ниже"],
|
||||||
|
]),
|
||||||
|
Next("Вход: хост и рисунки", host_page, "почему на рисунках точность ниже и что с этим делает хост."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 6. Хост
|
||||||
|
|
||||||
|
@Page("6", "Вход: хост и рисунки")
|
||||||
|
def host_page():
|
||||||
|
return [
|
||||||
|
H1("Вход: хост и рисунки"),
|
||||||
|
Lead("Сеть видит только то, что подал хост. Рисунок мышью отличается от цифры MNIST сильнее, чем кажется, и этот "
|
||||||
|
"разрыв закрывает подготовка входа и обучение на похожих рисунках."),
|
||||||
|
H2("Почему рисунок — другая задача"),
|
||||||
|
Text("Цифры MNIST — отсканированный почерк: штрих переменной толщины с серыми краями, цифра вписана в квадрат 20 × 20 "
|
||||||
|
"и поставлена центром масс в центр поля 28 × 28. Рисунок мышью — перо одной толщины, жёсткие края, произвольные "
|
||||||
|
"размер и место. Сеть, выучившая MNIST, видит на рисунке незнакомый «почерк»."),
|
||||||
|
Text("Чтобы мерить это без людей, тестовые цифры MNIST перерисованы так, как их нарисовала бы мышь: из цифры "
|
||||||
|
"выделяется осевая линия и обводится пером постоянной толщины на поле 280 × 280, потом уменьшается. Доля "
|
||||||
|
"верных ответов на таких копиях — <b>точность на рисунках</b>."),
|
||||||
|
H2("Что делает хост"),
|
||||||
|
Widget("hostprep"),
|
||||||
|
Table(["Шаг", "Что делает", "Зачем"], [
|
||||||
|
["рамка", "вписывает рисунок в квадрат 20 × 20 и ставит центр масс в центр 28 × 28", "как в MNIST: размер и положение перестают мешать"],
|
||||||
|
["выпрямление наклона", "по моментам изображения находит наклон главной оси и сдвигает строки, чтобы она стала вертикальной", "наклонный почерк приводится к прямому"],
|
||||||
|
["уменьшение с усреднением", "каждый пиксель 28 × 28 — среднее своей области", "сохраняет серые края, как у сканов"],
|
||||||
|
["кодирование", "яркость → спайки", "частотный или временной код"],
|
||||||
|
]),
|
||||||
|
H2("Что помогает и насколько"),
|
||||||
|
Text("Опыты на рецепте нативной сети: один и тот же учитель, меняется только подготовка и обучающие данные."),
|
||||||
|
Table(["Вариант", "Тест MNIST", "Рисунки"], [
|
||||||
|
["как было", "97,66%", "94,08%"],
|
||||||
|
["+ обучение на перерисованных копиях", "97,15%", "95,20%"],
|
||||||
|
["+ выпрямление наклона", "98,01%", "94,96%"],
|
||||||
|
["+ выпрямление и перерисованные копии", "97,76%", "95,86%"],
|
||||||
|
["скелет: и тест, и рисунки сводятся к тонкой линии", "95,98%", "92,53%"],
|
||||||
|
]),
|
||||||
|
Text("Лучший вариант — выпрямление вместе с обучением на копиях: +1,8 п.п. на рисунках почти без потери на тесте. "
|
||||||
|
"Приведение всего к тонкой линии вредит: вместе с разницей толщины теряется и полезная информация о форме."),
|
||||||
|
Callout("Хост готовит вход и читает выход, но не вклинивается между слоями сети. Иначе часть распознавания уходит с чипа, "
|
||||||
|
"как у детекторов с суммированием на хосте.", title="Граница хоста", kind="note"),
|
||||||
|
Next("От цифр к мусору", trash_section, "что из этого переносится на робота."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 7. Мусор
|
||||||
|
|
||||||
|
@Page("7", "От цифр к мусору")
|
||||||
|
def trash_section():
|
||||||
|
return [
|
||||||
|
H1("От цифр к мусору"),
|
||||||
|
Lead("Цифры были полигоном. На роботе другой вход — цветной кадр камеры, другие классы и другой бюджет нейронов."),
|
||||||
|
H2("Что переносится"),
|
||||||
|
Table(["Приём", "Почему подходит мусору"], [
|
||||||
|
["обучение на компьютере сразу под палитру чипа", "чип не учится; точность выше, чем у правил внутри KNP"],
|
||||||
|
["временной код и обучение по тактам", "≈1 событие на значимый пиксель; меньше энергии на кадр"],
|
||||||
|
["хост только на входе: окно, центровка, кодирование", "вся сеть остаётся на кристалле"],
|
||||||
|
["проверка счётчиков в KNP и GoldenModel", "доказательство без платы"],
|
||||||
|
["обходы правил чипа: тактовые смещения, очистка, «за/против»", "не зависят от задачи"],
|
||||||
|
]),
|
||||||
|
H2("Что не переносится"),
|
||||||
|
Text("Сама сеть. Цифра — это чёрно-белый штрих в центре поля, мусор — цветной объект на пёстром фоне. "
|
||||||
|
"Полносвязного слоя на пикселях для объектов мало: нужны признаки — узоры, которые ищутся по всей картинке. "
|
||||||
|
f"Что известно о признаках и почему они плохо ложатся на чип — на странице {LocalLink('объектов', objects_page)}."),
|
||||||
|
Links([
|
||||||
|
LocalLink("Объекты: что уже известно", objects_page) + " — опыты на кошках и собаках CIFAR-10.",
|
||||||
|
LocalLink("План распознавателя мусора", trash_plan) + " — шаги и открытые вопросы.",
|
||||||
|
]),
|
||||||
|
Next("Объекты: что известно", objects_page, "что дали опыты с объектами."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("7.1", "Объекты: что известно")
|
||||||
|
def objects_page():
|
||||||
|
return [
|
||||||
|
H1("Объекты: что уже известно"),
|
||||||
|
Lead("Перед мусором техника проверялась на фотографиях кошек и собак из CIFAR-10 (32 × 32, цвет). Это близкая задача: "
|
||||||
|
"два класса, пёстрый фон, объект в разных позах."),
|
||||||
|
H2("Узкое место — признаки"),
|
||||||
|
Text("Первая сеть брала готовые признаки HOG (направления перепадов яркости) и колонки STDP — около 62%. Проверка "
|
||||||
|
"показала, что дело не в колонках: даже линейный классификатор на HOG даёт около 60%. В признаках просто мало "
|
||||||
|
"информации."),
|
||||||
|
Text("Признаки, выученные из самих картинок, дали больше. Из картинок вырезаются кусочки 6 × 6, и метод k-средних "
|
||||||
|
"находит в них 400–1600 типичных узоров, без меток. Нейрон узора стреляет, если кусочек похож на его узор, а "
|
||||||
|
"спайки суммируются по четвертям картинки, чтобы не зависеть от точного положения. Без спайков на таких признаках "
|
||||||
|
"получается 77% — это потолок для выбранных признаков. Выучить узоры можно и нейронным правилом «победитель "
|
||||||
|
"забирает всё» по одной картинке: не хуже пакетного k-средних."),
|
||||||
|
H2("Где теряется точность"),
|
||||||
|
Table(["Переход", "Потеря", "Почему"], [
|
||||||
|
["нормировка контраста убрана", "−0,7 п.п.", "одним нейроном её не сделать, но потеря мала"],
|
||||||
|
["признаки и выход на спайках", "−8 п.п.", "нейрон признака за картинку стреляет в среднем один раз: оттенки силы признака теряются"],
|
||||||
|
["колонки STDP вместо обученного выхода", "до −10 п.п.", "на плотных признаках колонки молчат; приходится оставлять 256 сильнейших из 2048"],
|
||||||
|
]),
|
||||||
|
Text("Итог на свежем тесте из 600 картинок: колонки STDP в KNP — 67,0%, полностью спайковая цепочка — 68,3% "
|
||||||
|
"(погрешность ±3,7 п.п.). Лучшая сеть — детекторы признаков с выходом, обученным на компьютере: 71,35% на "
|
||||||
|
"официальном тесте CIFAR, исполнение в KNP на GPU."),
|
||||||
|
H2("Почему детекторы не ложатся на чип"),
|
||||||
|
Text("Аудит сохранённых весов этой сети против правил «Алтая»:"),
|
||||||
|
Table(["Проверка", "Сеть детекторов", "Предел «Алтая-2»"], [
|
||||||
|
["нейронов", "39 458 (200 узоров × 196 позиций + 258)", "8192 в прототипе"],
|
||||||
|
["разных величин веса на нейрон", "до 55 у детекторов, до 110 в скрытом слое", "4"],
|
||||||
|
["наибольший уровень", "от −492 108 до 569 761", "−32 768…32 767"],
|
||||||
|
["хост между слоями", "суммирование по четвертям, нормировка, перепаковка", "сеть целиком на чипе"],
|
||||||
|
]),
|
||||||
|
Text("Главная причина — отсутствие общих весов. В свёрточной сети узор «вертикальная черта» хранится один раз и "
|
||||||
|
"прикладывается ко всем позициям. На чипе каждая позиция — отдельный нейрон со своей копией весов, и число "
|
||||||
|
"нейронов растёт как «узоры × позиции». Подберите параметры, при которых сеть влезет в прототип:"),
|
||||||
|
Widget("budget"),
|
||||||
|
Text("Влезть можно только за счёт меньшего окна, большего шага и малого числа узоров — то есть ценой точности. "
|
||||||
|
"Это главный вопрос для мусора."),
|
||||||
|
Next("План распознавателя мусора", trash_plan, "как из этого собрать распознаватель мусора."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("7.2", "План распознавателя мусора")
|
||||||
|
def trash_plan():
|
||||||
|
return [
|
||||||
|
H1("План распознавателя мусора"),
|
||||||
|
Lead("Порядок работ, при котором каждый шаг проверяем, а ошибка видна до того, как появится плата."),
|
||||||
|
Steps([
|
||||||
|
"<b>Постановка.</b> Начать с двух классов «мусор / не мусор» на окне кадра. Хост режет кадр на окна, чип отвечает "
|
||||||
|
"по каждому, место мусора видно по сработавшим окнам.",
|
||||||
|
"<b>Данные.</b> Открытые наборы с фотографиями мусора (например, TACO, TrashNet) для начала и снимки с камеры робота "
|
||||||
|
"для честного теста. Тест не трогать до выбора модели.",
|
||||||
|
"<b>Бюджет.</b> До обучения выбрать размер окна, число узоров и шаг так, чтобы сеть влезла в 8192 нейрона; "
|
||||||
|
"проверить три числа со страницы правил.",
|
||||||
|
"<b>Обучение.</b> Сеть по тактам с временным кодом, огрублением весов до палитры с середины обучения и штрафом за "
|
||||||
|
"спайки; узоры первого слоя — локальные окна без общих весов.",
|
||||||
|
"<b>Проверка.</b> Целочисленный эталон → KNP на CPU и GPU → GoldenModel, сравнение счётчиков всех нейронов.",
|
||||||
|
"<b>Плата.</b> Запуск на AltAI_HW и замер энергии на кадр.",
|
||||||
|
], ordered=True),
|
||||||
|
H2("Открытые вопросы"),
|
||||||
|
Table(["Вопрос", "От чего зависит"], [
|
||||||
|
["какая конфигурация «Алтая» будет на роботе и есть ли плата", "бюджет нейронов, возможность измерить энергию"],
|
||||||
|
["классы: «мусор / не мусор» или типы мусора", "размер выходного слоя и объём данных"],
|
||||||
|
["какая камера: обычная или событийная", "событийная камера сама выдаёт спайки, и кодирование на хосте не нужно"],
|
||||||
|
["сколько кадров в секунду нужно роботу", "сколько тактов можно потратить на окно"],
|
||||||
|
]),
|
||||||
|
Next("Справочник", reference_section, "словарь терминов, правила измерений и карта репозитория."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 8. Справочник
|
||||||
|
|
||||||
|
@Page("8", "Справочник")
|
||||||
|
def reference_section():
|
||||||
|
return [
|
||||||
|
H1("Справочник"),
|
||||||
|
Links([
|
||||||
|
LocalLink("Глоссарий", glossary) + " — термины вики в одном месте.",
|
||||||
|
LocalLink("Методика измерений", metrics_page) + " — как получены цифры и когда им верить.",
|
||||||
|
LocalLink("Где что лежит", repo_page) + " — код, модели и команды в репозитории проекта.",
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("8.1", "Глоссарий")
|
||||||
|
def glossary():
|
||||||
|
return [
|
||||||
|
H1("Глоссарий"),
|
||||||
|
Table(["Термин", "Определение"], [
|
||||||
|
["Спайк", "событие «нейрон выстрелил» в определённый такт; несёт один бит"],
|
||||||
|
["Такт", "шаг времени сети; за такт нейрон складывает пришедшее и решает, стрелять ли"],
|
||||||
|
["Уровень (потенциал)", "накопленный заряд нейрона; на «Алтае» — целое 16 бит"],
|
||||||
|
["Порог", "уровень, при котором нейрон выдаёт спайк"],
|
||||||
|
["Вес", "на сколько изменится уровень получателя от одного спайка по связи"],
|
||||||
|
["Палитра весов", "набор допустимых величин веса, например {0, ±1, ±2, ±4}"],
|
||||||
|
["Синаптическое событие", "одно сложение «спайк → вес» в получателе; мера работы и энергии сети"],
|
||||||
|
["Частотный код", "яркость передаётся числом спайков за картинку"],
|
||||||
|
["Временной код", "яркость передаётся моментом единственного спайка: ярче — раньше"],
|
||||||
|
["Тактовая линия", "входная линия, которая стреляет каждый такт; так на «Алтае» задаётся смещение нейрона"],
|
||||||
|
["Очистка", "пачка отрицательных спайков после картинки, обнуляющая уровни"],
|
||||||
|
["STDP", "правило: связь крепнет, если вход пришёл перед выстрелом, и слабеет, если после"],
|
||||||
|
["Дофамин", "сигнал награды в KNP, который закрепляет или отменяет недавние изменения связей"],
|
||||||
|
["Суррогатный градиент", "замена производной ступеньки-спайка гладкой функцией при обучении"],
|
||||||
|
["Квантование", "перевод дробных весов в целые из палитры"],
|
||||||
|
["Хост", "обычный процессор рядом с чипом: готовит вход и читает ответ"],
|
||||||
|
["GoldenModel", "программная копия «Алтая» из официального бэкенда, AltAI_GM"],
|
||||||
|
["Размещатель", "часть бэкенда «Алтая», раскладывающая нейроны и связи по ядрам"],
|
||||||
|
["Счётчики спайков", "сколько раз выстрелил каждый нейрон за картинку; по ним сравнивают исполнителей"],
|
||||||
|
["Точность на рисунках", "точность на тестовых цифрах, перерисованных пером постоянной толщины"],
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("8.2", "Методика измерений")
|
||||||
|
def metrics_page():
|
||||||
|
return [
|
||||||
|
H1("Методика измерений"),
|
||||||
|
Lead("Как получены цифры в вики и при каких условиях им можно верить."),
|
||||||
|
H2("Данные"),
|
||||||
|
Text("MNIST: 60 000 обучающих и 10 000 тестовых цифр. Из обучающих 5000 отложены на валидацию. Все настройки — пороги, "
|
||||||
|
"масштабы, число эпох, выбор лучшего варианта — выбираются по валидации. Тест открывается один раз, после "
|
||||||
|
"выбора: иначе оценка завышена."),
|
||||||
|
H2("Метрики"),
|
||||||
|
Table(["Метрика", "Определение"], [
|
||||||
|
["Точность", "доля верных ответов на тесте; молчание сети считается ошибкой"],
|
||||||
|
["Точность на рисунках", "то же на перерисованных тестовых цифрах"],
|
||||||
|
["Синаптические события", "сложений «спайк → вес» на одну картинку, среднее по тесту"],
|
||||||
|
["Входные спайки", "сколько событий хост отправляет на чип за картинку"],
|
||||||
|
["Такты", "длительность распознавания одной картинки"],
|
||||||
|
["Совпадение счётчиков", "счётчик каждого нейрона одинаков в эталоне, KNP и GoldenModel на каждой картинке"],
|
||||||
|
]),
|
||||||
|
H2("Шум"),
|
||||||
|
Text("Разница меньше 0,3 п.п. на тесте MNIST при одном прогоне — в пределах случайности. На 1000 картинках разброс "
|
||||||
|
"по seed ±1–2 п.п., на 600 картинках погрешность ±3,7 п.п. Выводы о разнице в 1–2 п.п. на малых выборках без "
|
||||||
|
"нескольких seed ненадёжны."),
|
||||||
|
H2("Источник"),
|
||||||
|
Text("Цифра без источника не считается результатом. У каждого прогона есть файл итогов: final-summary.json, "
|
||||||
|
"VERIFIED.json или result.json рядом с моделью. Результат, записанный в FAILED.json, не используется, даже "
|
||||||
|
"если точность выглядит правдоподобно."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@Page("8.3", "Где что лежит")
|
||||||
|
def repo_page():
|
||||||
|
return [
|
||||||
|
H1("Где что лежит"),
|
||||||
|
Lead("Репозиторий проекта — git.snw.su/int/our_knp. Пути ниже — от его корня."),
|
||||||
|
Table(["Что", "Где"], [
|
||||||
|
["нативная сеть: обучение, граф KNP, проверки", "mnist-altai-native/: prepare_native.py, native_digits.cpp, run_verify.py, run_sharded.py"],
|
||||||
|
["учитель нативной сети", "mnist-fast/: model_core.py, train_fast.py"],
|
||||||
|
["колонки STDP и переключатели KNP_*", "examples/examples/mnist-learn/models/altai/construct_network.cpp"],
|
||||||
|
["бэкенд для дофамина по ошибке", "readout/build_delta_cpu.py; запуски — readout/delta_runs.sh"],
|
||||||
|
["признаки штрихов", "features/stroke_features.py"],
|
||||||
|
["признаки объектов, спайковая цепочка", "objects/kmeans_features.py, objects/spiking_pipeline.py, objects/dopamine_readout.py"],
|
||||||
|
["симулятор колонок и страницы рисования", "viz/"],
|
||||||
|
["отчёты и журналы", "reports/; начинать с reports/Итоги_и_план.md"],
|
||||||
|
]),
|
||||||
|
H2("Нативная сеть: сборка и проверка"),
|
||||||
|
Code('''python3 mnist-altai-native/build_native.py # собрать native_digits
|
||||||
|
python mnist-altai-native/prepare_native.py # переобучить (перезаписывает модель)
|
||||||
|
python mnist-altai-native/run_verify.py cpu 10000 8 cpu-run # KNP CPU, 10 000 цифр
|
||||||
|
python mnist-altai-native/run_verify.py cuda 10000 8 cuda-run # наш CUDA-бэкенд
|
||||||
|
python mnist-altai-native/run_sharded.py gm-run # GoldenModel, первые 1000, 8 процессов''', "bash"),
|
||||||
|
Text("Каждый прогон пишет в новый каталог; VERIFIED.json появляется только при совпадении всех счётчиков."),
|
||||||
|
Callout("Код сети по тактам и опытов с рисунками пока не в репозитории: веса и симулятор лежат только в данных этой "
|
||||||
|
"вики (data/models.json, widgets.js).", title="Не в репозитории", kind="warning"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
HEADER = [
|
||||||
|
HeaderButton("Обзор", overview),
|
||||||
|
HeaderButton("Алтай", altai_section),
|
||||||
|
HeaderButton("Обучение", learning_section),
|
||||||
|
HeaderButton("Сравнение", compare_page),
|
||||||
|
HeaderButton("Мусор", trash_section),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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