diff --git a/wiki/assets/widgets.css b/wiki/assets/widgets.css
index a22a8e9..f55495c 100644
--- a/wiki/assets/widgets.css
+++ b/wiki/assets/widgets.css
@@ -82,11 +82,8 @@
.content .w-bar::after { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; background: var(--text); opacity: .5; }
.content .w-bar i { position: absolute; inset: 0 auto 0 0; border-radius: 99px; background: var(--w-good); }
.content .w-bar i.no { background: var(--w-bad); }
-.content .def { border-left: 2px solid var(--text); padding: .2rem 0 .2rem 1rem; margin: 1rem 0 1.4rem; font-size: 1.08em; }
-.content .facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; margin: 1.2rem 0 1.6rem; }
-.content .facts div { border: 1px solid var(--border); border-radius: 16px; padding: 12px 14px; background: var(--shell); display: flex; flex-direction: column; gap: 2px; }
-.content .facts b { font-size: 1.5em; font-weight: 600; font-variant-numeric: tabular-nums; }
-.content .facts span { color: var(--muted); font-size: .88em; }
@media (max-width: 640px) { .content .w-px { width: 96px; height: 96px; } .content .w-kv { grid-template-columns: 1fr; } }
@media print { .content .w-card { display: none; } }
@media (prefers-reduced-motion: reduce) { .content * { transition: none !important; } }
+.content .next { margin-top: 2.4rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--muted); }
+.content .next a { font-weight: 600; }
diff --git a/wiki/assets/widgets.js b/wiki/assets/widgets.js
index 6481f5c..4f6b5a9 100644
--- a/wiki/assets/widgets.js
+++ b/wiki/assets/widgets.js
@@ -270,29 +270,12 @@
init(); draw();
};
- /* 6. Сетка ядер Алтая */
- W.chipgrid = (el) => {
- card(el); const svg = s("svg", { viewBox: "0 0 420 420", class: "w-svg", role: "img", "aria-label": "Сетка 4×4 ядра" }), say = note("Нажмите на ядро: пакет со спайком войдёт через порт и пройдёт по соседям."); let path = [], step = 0, target = null, timer = null;
- const gx = (i) => 40 + i * 90, gy = (j) => 80 + j * 82;
- function draw() { const c = C(); svg.replaceChildren(); svg.append(s("rect", { x: 34, y: 18, width: 352, height: 30, rx: 10, fill: c.spike, opacity: .18 }), s("text", { x: 210, y: 38, "text-anchor": "middle", fill: c.text, "font-size": 13 }, "порт: вход с камеры и выход ответа"));
- for (let i = 0; i < 4; i++) { svg.append(s("line", { x1: gx(i) + 35, y1: 48, x2: gx(i) + 35, y2: gy(0), stroke: c.border, "stroke-width": 3 })); for (let j = 0; j < 3; j++) svg.append(s("line", { x1: gx(i) + 35, y1: gy(j) + 70, x2: gx(i) + 35, y2: gy(j + 1), stroke: c.border, "stroke-width": 3 }), s("line", { x1: gx(j) + 70, y1: gy(i) + 35, x2: gx(j + 1), y2: gy(i) + 35, stroke: c.border, "stroke-width": 3 })); }
- const vis = new Set(path.slice(0, step + 1).map((p) => p.join()));
- for (let j = 0; j < 4; j++) for (let i = 0; i < 4; i++) { const isT = target && target[0] === i && target[1] === j, v = vis.has(i + "," + j);
- const r = s("rect", { x: gx(i), y: gy(j), width: 70, height: 70, rx: 14, fill: isT ? c.spike : v ? c.control : c.shell, stroke: v ? c.spike : c.text, "stroke-width": 1.5, tabindex: 0, role: "button", "aria-label": "ядро " + (j * 4 + i + 1), style: "cursor:pointer" });
- r.addEventListener("click", () => send(i, j)); r.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); send(i, j); } });
- svg.append(r, s("text", { x: gx(i) + 35, y: gy(j) + 33, "text-anchor": "middle", fill: isT ? "#fff" : c.text, "font-size": 13, "pointer-events": "none" }, "ядро " + (j * 4 + i + 1)), s("text", { x: gx(i) + 35, y: gy(j) + 51, "text-anchor": "middle", fill: isT ? "#fff" : c.muted, "font-size": 11, "pointer-events": "none" }, "≈512 нейр.")); }
- if (path.length) { const p = path[Math.min(step, path.length - 1)]; svg.append(s("circle", { cx: gx(p[0]) + 35, cy: p[1] < 0 ? 33 : gy(p[1]) + 35, r: 8, fill: c.spike, stroke: c.shell, "stroke-width": 2 })); } }
- function send(i, j) { target = [i, j]; path = [[i, -1]]; for (let y = 0; y <= j; y++) path.push([i, y]); step = 0; clearInterval(timer);
- timer = setInterval(() => { if (!el.isConnected) return clearInterval(timer); step++; draw(); if (step >= path.length - 1) { clearInterval(timer); say.textContent = `Пакет вошёл через порт и дошёл до ядра ${j * 4 + i + 1} за ${j + 1} переход(а). Чем дальше ядро и чем больше пакетов, тем дольше путь — поэтому размещатель кладёт связанные нейроны рядом.`; } }, 260); draw(); }
- el.append(svg, say); draw(); alive(el, draw);
- };
-
/* 7. Кроссбар ядра */
W.crossbar = (el) => {
card(el); const cyc = [0, 2, 3, 4, 5, 6, 8, -3, -5]; let Wt = [[4, 2, 0, 3], [4, 0, 5, 3], [3, 4, 0, 0], [0, 4, 6, 2], [0, 0, 3, 5], [-3, 0, 0, -5]], on = [true, true, false, false, false, false], sums = null;
const tb = h("div", { class: "w-xb" }), say = note("");
- function draw() { tb.replaceChildren(); tb.append(h("span", { class: "w-cap", text: "аксон ↓ / нейрон →" })); for (let n2 = 0; n2 < 4; n2++) tb.append(h("span", { class: "w-cap", text: "нейрон " + (n2 + 1) }));
- Wt.forEach((r, a) => { tb.append(h("button", { type: "button", class: "w-btn", "aria-pressed": String(on[a]), text: "аксон " + (a + 1), onclick: () => { on[a] = !on[a]; sums = null; draw(); } }));
+ 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" })); }
@@ -336,29 +319,7 @@
};
/* 10. Схема трёх архитектур */
- W.arch = (el) => {
- card(el); let mode = 0; const svg = s("svg", { viewBox: "0 0 700 300", class: "w-svg w-wide", role: "img", "aria-label": "Схема архитектуры" }), tab = h("div", { class: "w-kv" });
- const ROWS = [
- [["Слои", "пиксели → 10 колонок × 20 нейронов"], ["Ответ", "колонка, которая выстрелила больше"], ["Обучение", "в KNP: STDP и дофамин, без учителя ошибок"], ["Слабость", "учит трафареты, не видит отсутствие линий"]],
- [["Слои", "пиксели → 256 скрытых → 160 «за/против»"], ["Ответ", "«за» минус «против», максимум"], ["Обучение", "обычная сеть на компьютере → перенос весов"], ["Цена", "32 такта частотного кода, сотни тысяч синаптических событий"]],
- [["Слои", "пиксели → 256 скрытых → 10 выходов"], ["Ответ", "больше спайков, при равенстве — раньше"], ["Обучение", "градиент прямо по тактам динамики Алтая"], ["Выигрыш", "каждый пиксель стреляет один раз, ответ за несколько тактов"]]];
- const txt = (x, y, t, size = 12, color, weight = 400, anchor = "middle") => svg.append(s("text", { x, y, "text-anchor": anchor, fill: color || C().muted, "font-size": size, "font-weight": weight }, t));
- function draw() { const c = C(); svg.replaceChildren(); const lines = [];
- const D7 = [[1, 0], [2, 0], [3, 0], [4, 0], [4, 1], [3, 2], [2, 3], [2, 4], [2, 5]];
- for (let y = 0; y < 6; y++) for (let x = 0; x < 6; x++) svg.append(s("rect", { x: 16 + x * 13, y: 110 + y * 13, width: 11, height: 11, rx: 2, fill: D7.some(([a, b]) => a === x && b === y) ? c.text : c.control }));
- txt(55, 96, mode === 2 ? "пиксели → один спайк" : "пиксели → спайки");
- if (mode === 0) { for (let k = 0; k < 10; k++) { const cx = 190 + k * 36, win = k === 7; svg.append(s("rect", { x: cx - 14, y: 56, width: 28, height: 176, rx: 9, fill: win ? c.control : "none", stroke: win ? c.spike : c.border, "stroke-width": win ? 2 : 1 })); for (let n2 = 0; n2 < 10; n2++) svg.append(s("circle", { cx: cx - 5 + (n2 % 2) * 10, cy: 70 + Math.floor(n2 / 2) * 34, r: 4, fill: win && n2 < 6 ? c.spike : c.pot })); txt(cx, 252, String(k), 13, win ? c.spike : c.text, 600); lines.push([96, 145, cx - 14, 90 + (k * 37) % 120]); }
- txt(352, 40, "10 колонок по 20 нейронов: каждая колонка — трафарет своей цифры", 12, c.text, 600); txt(352, 280, "ответ — колонка «7»: выстрелила больше всех", 13, c.text, 600); }
- else { const hx = 210, hy = 70; for (let i = 0; i < 64; i++) svg.append(s("circle", { cx: hx + (i % 8) * 16, cy: hy + Math.floor(i / 8) * 20, r: 4.5, fill: (i * 29) % 7 < 2 ? c.spike : c.pot })); txt(hx + 56, 50, "256 скрытых нейронов (показана четверть)", 12, c.text, 600); for (let k = 0; k < 8; k++) lines.push([96, 145, hx - 6, hy + k * 20]);
- const ox = 420, oy = 64;
- if (mode === 1) { for (let d = 0; d < 10; d++) { const y = oy + d * 19; txt(ox - 10, y + 5, String(d), 12, d === 7 ? c.spike : c.text, 600, "end"); for (let g2 = 0; g2 < 8; g2++) svg.append(s("rect", { x: ox + g2 * 9, y: y - 5, width: 7, height: 10, rx: 2, fill: c.good, opacity: d === 7 ? 1 : .3 }), s("rect", { x: ox + 80 + g2 * 9, y: y - 5, width: 7, height: 10, rx: 2, fill: c.bad, opacity: d === 4 || d === 9 ? .9 : .25 })); lines.push([hx + 118, hy + (d * 31) % 140, ox - 2, y]); }
- txt(ox + 32, 48, "за ×8", 11, c.good, 600); txt(ox + 112, 48, "против ×8", 11, c.bad, 600); svg.append(s("rect", { x: 600, y: 140, width: 90, height: 54, rx: 12, fill: c.text })); txt(645, 163, "хост:", 11, c.shell, 600); txt(645, 181, "за − против", 11, c.shell); txt(330, 282, "32 такта · каждый пиксель стреляет до 32 раз", 13, c.text, 600); }
- else { for (let d = 0; d < 10; d++) { const y = oy + d * 19; txt(ox - 10, y + 5, String(d), 12, d === 7 ? c.spike : c.text, 600, "end"); svg.append(s("circle", { cx: ox + 10, cy: y, r: 6, fill: d === 7 ? c.spike : c.pot })); for (let t = 0; t < 8; t++) svg.append(s("rect", { x: ox + 26 + t * 12, y: y - 4, width: 9, height: 8, rx: 2, fill: d === 7 && t >= 3 && t % 2 ? c.spike : c.control })); lines.push([hx + 118, hy + (d * 31) % 140, ox + 4, y]); }
- txt(ox + 74, 48, "такты 0…7", 11, c.text, 600); txt(330, 282, "8 тактов · каждый пиксель стреляет один раз", 13, c.text, 600); } }
- lines.forEach(([x1, y1, x2, y2]) => svg.prepend(s("line", { x1, y1, x2, y2, stroke: c.border })));
- tab.replaceChildren(...ROWS[mode].flatMap(([k, v]) => [h("b", { text: k }), h("span", { text: v })])); }
- el.append(seg(["Колонки STDP", "Нативная сеть", "Импульсная по тактам"], (i) => { mode = i; draw(); }), h("div", { class: "w-scroll" }, svg), tab); draw(); alive(el, draw);
- };
+
/* 11. Одна цифра — три сети */
W.compare = (el) => {
diff --git a/wiki/knp_wiki.py b/wiki/knp_wiki.py
index 3d392f8..827702f 100644
--- a/wiki/knp_wiki.py
+++ b/wiki/knp_wiki.py
@@ -12,7 +12,7 @@ import sys
from pathlib import Path
from docslib import HeaderButton, build, run
-from docslib.components import Callout, Code, H1, H2, H3, List, LocalLink, P, Table
+from docslib.components import Callout, Code, H1, H2, H3, List, LocalLink, Table
from docslib.hooks import Component, Page
HERE = Path(__file__).resolve().parent
@@ -26,8 +26,9 @@ def Lead(text):
@Component
-def Def(text):
- return f'
{text}
'
+def Text(html):
+ """Абзац с доверенной разметкой: жирный текст и ссылки LocalLink."""
+ return f"{html}
"
@Component
@@ -42,9 +43,15 @@ def Links(items):
@Component
-def Facts(items):
- cells = "".join(f"{value}{label}
" for value, label in items)
- return f'{cells}
'
+def Steps(items, ordered=False):
+ """Список с доверенной разметкой в пунктах."""
+ tag = "ol" if ordered else "ul"
+ return f"<{tag}>" + "".join(f"{item}" for item in items) + f"{tag}>"
+
+
+@Component
+def Next(title, page, why):
+ return f'Дальше: {LocalLink(title, page)} — {why}
'
@Component
@@ -75,7 +82,7 @@ def Pipeline():
+ "".join(parts)
+ ''
"обучение — на обычном компьютере; на чип загружаются готовые целочисленные веса"
- "Состав системы распознавания"
+ "Путь кадра через систему"
)
@@ -85,338 +92,440 @@ def Pipeline():
def overview():
return [
H1("Нейроморфное распознавание на Алтае"),
- Lead("Система, в которой распознавание изображений выполняет импульсная нейронная сеть "
- "на нейроморфном процессоре «Алтай», а подготовка входа и чтение ответа — обычный компьютер робота."),
- Def("Нейроморфное распознавание — распознавание образов сетью из импульсных нейронов, "
- "которые обмениваются короткими событиями-спайками и тратят энергию только в момент события. "
- "В отличие от обычной нейросети, вычисление идёт по тактам и разреженно: молчащий нейрон ничего не считает."),
+ Lead("Как заставить нейроморфный процессор «Алтай» распознавать изображения: что умеет чип, какие сети на него ложатся, "
+ "как их обучать и как доказать, что сеть заработает на кристалле."),
+ H2("Задача"),
+ Text("Конечная цель — робот, который сам находит мусор в кадре своей камеры, а распознавание выполняет «Алтай». "
+ "Пока цель недостижима напрямую: нет ни данных с робота, ни платы с чипом, а сам путь от картинки до ответа на "
+ "чипе нужно было сначала построить. Поэтому вся техника отработана на рукописных цифрах MNIST — "
+ "задаче, где ответ известен, а ошибку легко увидеть глазами. Всё, что описано в вики, переносится на мусор, "
+ "кроме самих весов сети."),
+ H2("Зачем нейроморфный чип"),
+ Text("Обычная нейросеть на каждую картинку перемножает все входы на все веса. Для сети 784 → 256 → 10 это "
+ "203 264 умножения со сложением, сколько бы пикселей ни было закрашено. Импульсная сеть считает только там, "
+ "где что-то произошло: нейрон, получивший спайк, прибавляет к своему уровню вес связи, и всё. Лучшая сеть из этой "
+ "вики распознаёт цифру за ≈20 тысяч таких сложений — в 10 раз меньше операций, и это сложения, а не умножения. "
+ "На роботе с батареей это и есть довод в пользу чипа."),
+ Callout("Энергию самого «Алтая» на этих сетях никто не измерял: платы в проекте нет. Сравнение идёт по числу операций — "
+ "синаптических событий, а не по джоулям.", title="Оговорка", kind="note"),
+ H2("Как устроена система"),
Pipeline(),
- H2("Из чего состоит"),
- Table(["Часть", "Что это", "Роль в системе"], [
- ["Алтай", "нейроморфный процессор Мотив НТ: сетка ядер с целочисленными нейронами", "исполняет импульсную сеть"],
- ["KNP", "Kaspersky Neuromorphic Platform: программная платформа импульсных сетей", "описывает сеть, моделирует её на CPU/GPU, загружает на Алтай"],
- ["GoldenModel", "программная копия Алтая из бэкенда Мотив НТ", "доказывает, что сеть заработает на чипе"],
- ["Хост", "обычный процессор робота", "готовит вход, кодирует его спайками, читает ответ"],
- ["Обучение", "обычный компьютер, PyTorch / NumPy", "находит веса с учётом ограничений чипа"],
- ]),
- H2("Ключевые свойства"),
- Facts([
- ("97,4%", "точность импульсной сети на цифрах MNIST"),
- ("8 тактов", "от первого спайка до ответа"),
- ("≈143", "входных спайка на картинку — по одному на пиксель"),
- ("≈20 тыс.", "синаптических событий на картинку"),
- ("8192", "нейрона в 16-ядерном прототипе Алтая"),
- ("4", "разных величины веса на нейрон"),
- ]),
- Callout("Чип не обучается: в официальном бэкенде Алтая пластичность выключена. Поэтому сеть учат на компьютере "
- "сразу под рамки чипа, а на Алтай загружают готовые целые веса.", title="Главное ограничение", kind="note"),
- H2("Как читать вики"),
- Links([
- LocalLink("Импульсные сети", snn_section) + " — что такое импульсный нейрон, как кодируется вход, как сети учатся.",
- LocalLink("Платформа KNP", knp_section) + " — программная модель сети и способы её исполнения.",
- LocalLink("Процессор Алтай", altai_section) + " — устройство чипа и правила, которым должна подчиняться сеть.",
- LocalLink("Архитектуры распознавания", arch_section) + " — четыре способа построить распознаватель и их сравнение.",
- LocalLink("Распознавание мусора", trash) + " — как всё это ложится на задачу робота.",
+ 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, "с чего начинается любая сеть на «Алтае»."),
]
-@Page("1.1", "Глоссарий")
-def glossary():
- return [
- H1("Глоссарий"),
- Table(["Термин", "Определение"], [
- ["Спайк", "Короткое событие «нейрон выстрелил». Несёт один бит и момент времени."],
- ["Такт", "Шаг дискретного времени. За такт нейрон суммирует пришедшие спайки и решает, стрелять ли."],
- ["Потенциал (уровень)", "Накопленный заряд нейрона. Когда он достигает порога, нейрон стреляет."],
- ["Порог", "Уровень, при котором нейрон выдаёт спайк. На Алтае — целое число."],
- ["Синапс, вес", "Связь между нейронами. Вес — на сколько изменится потенциал получателя от одного спайка."],
- ["Синаптическое событие", "Одно сложение «спайк × вес». Основная мера работы и энергии импульсной сети."],
- ["Популяция", "Группа однотипных нейронов в KNP."],
- ["Проекция", "Пучок связей из одной популяции в другую, с весами и задержками."],
- ["Частотный код", "Яркость пикселя передаётся частотой спайков."],
- ["Временной код", "Яркость передаётся моментом единственного спайка: ярче — раньше."],
- ["STDP", "Правило обучения: связь крепнет, если входной спайк пришёл незадолго до выстрела, и слабеет, если после."],
- ["Дофамин", "Третий сигнал обучения: награда или наказание, превращающие недавние изменения связей в постоянные."],
- ["Суррогатный градиент", "Способ обучать импульсную сеть обратным распространением: ступенька спайка заменяется гладкой функцией."],
- ["Квантование", "Перевод дробных весов в небольшой набор целых, который понимает чип."],
- ["Кроссбар", "Таблица связей внутри ядра Алтая: строки — входящие линии, столбцы — нейроны."],
- ["Размещатель", "Часть бэкенда Алтая, раскладывающая нейроны и связи по ядрам."],
- ["GoldenModel", "Эталонная программная модель Алтая, совпадающая с чипом по счётчикам спайков."],
- ["Хост", "Обычный процессор, который готовит вход для чипа и читает его ответ."],
- ["Валидация и тест", "На валидации выбирают настройки; тест смотрят один раз, иначе оценка завышена."],
- ]),
- ]
+# ---------------------------------------------------------------- 2. Импульсная сеть
-
-# ---------------------------------------------------------------- 2. Импульсные сети
-
-@Page("2", "Импульсные сети")
+@Page("2", "Импульсная сеть")
def snn_section():
return [
- H1("Импульсные нейронные сети"),
- Def("Импульсная нейронная сеть (spiking neural network, SNN) — сеть, в которой нейроны хранят внутреннее "
- "состояние и обмениваются не числами, а событиями-спайками во времени."),
- H2("Чем отличается от обычной нейросети"),
+ H1("Импульсная сеть"),
+ Lead("Сеть, в которой нейроны хранят заряд между тактами и обмениваются не числами, а событиями-спайками."),
+ Text("Обычная нейросеть — это формула: картинка на входе, числа на выходе, один проход. Импульсная сеть — это процесс во "
+ "времени. На каждом такте нейроны получают спайки, копят заряд и, дойдя до порога, сами выдают спайк. "
+ "Информация живёт в том, сколько спайков пришло и когда."),
Table(["", "Обычная нейросеть", "Импульсная сеть"], [
- ["Сигнал между нейронами", "вещественное число", "спайк: есть или нет, и когда"],
- ["Время", "нет: один проход", "дискретные такты"],
- ["Состояние нейрона", "нет", "потенциал, копится между тактами"],
- ["Объём вычислений", "все нейроны на каждом входе", "только там, где пришли спайки"],
- ["Обучение", "обратное распространение", "STDP, дофамин или суррогатный градиент"],
- ["Где выгодна", "сервер, видеокарта", "нейроморфный чип: дрон, робот, датчик"],
- ]),
- P("Выигрыш импульсной сети — в разреженности. Если на картинке 150 закрашенных пикселей из 784, "
- "то при временном коде на чип приходит около 150 событий, и каждое затрагивает только свои связи."),
- Links([
- LocalLink("Импульсный нейрон", neuron_page),
- LocalLink("Кодирование входа", coding_page),
- LocalLink("Связи и сложение сигналов", links_page),
- LocalLink("Обучение", learning_page),
+ ["Сигнал между нейронами", "вещественное число", "спайк: есть или нет"],
+ ["Время", "нет, один проход", "такты; нейрон помнит заряд между ними"],
+ ["Работа на картинку", "все веса, всегда", "только веса связей, по которым пришёл спайк"],
+ ["Арифметика", "умножение и сложение", "сложение веса"],
+ ["Обучение", "обратное распространение ошибки", "спайк не дифференцируется — нужны обходные пути"],
]),
+ Text("Три страницы раздела идут снизу вверх: "
+ f"{LocalLink('нейрон', neuron_page)}, {LocalLink('вход', coding_page)}, {LocalLink('слой', layer_page)}. "
+ "Все примеры — на нейроне AltAILIF, том самом, что стоит на «Алтае»."),
+ Next("Нейрон «Алтая»", neuron_page, "из чего всё собирается."),
]
-@Page("2.1", "Импульсный нейрон")
+@Page("2.1", "Нейрон «Алтая»")
def neuron_page():
return [
- H1("Импульсный нейрон AltAILIF"),
- Def("AltAILIF — модель нейрона «накопление–утечка–выстрел», повторяющая нейрон процессора «Алтай»: "
- "целочисленный 16-битный потенциал, целые порог, утечка и значение сброса."),
- P("Удобная аналогия — стакан. Каждый входной спайк доливает в него воду (вес связи), вода понемногу "
- "испаряется (утечка), а когда уровень доходит до черты (порог), нейрон выдаёт спайк и стакан частично или полностью опустошается."),
- Widget("neuron"),
- H2("Шаг нейрона за такт"),
+ H1("Нейрон «Алтая»: AltAILIF"),
+ Lead("Целочисленный нейрон «накопление — утечка — выстрел». Всё, что умеет сеть на чипе, сводится к правилам этого нейрона."),
+ H2("Правило такта"),
List([
- "к потенциалу прибавляются веса всех пришедших спайков (тормозящие — со знаком минус);",
- "прибавляется утечка; при флаге leak_rev её знак следует за знаком потенциала;",
- "если потенциал ≥ порога — спайк; при is_diff из потенциала вычитается порог, при is_reset он сбрасывается;",
- "если потенциал ниже отрицательного порога — при saturate он упирается в этот порог.",
+ "к уровню (потенциалу) прибавляются веса всех связей, по которым в этот такт пришёл спайк;",
+ "прибавляется утечка — постоянная добавка, тянущая уровень вверх или вниз;",
+ "если уровень ≥ порога — нейрон выдаёт спайк, и уровень либо уменьшается на порог (флаг is_diff), "
+ "либо сбрасывается в заданное значение (флаг is_reset);",
+ "если уровень ниже отрицательного порога — при флаге saturate он упирается в этот порог.",
], ordered=True),
- Table(["Флаг", "Что делает", "Как используется"], [
- ["is_diff", "после выстрела вычитает порог, остаток сохраняется", "заряд сверх порога не теряется"],
- ["is_reset", "после выстрела сбрасывает потенциал в значение сброса", "штатный пример KNP"],
- ["saturate", "не даёт потенциалу уйти ниже отрицательного порога", "отрицательный порог 0: уровень не ниже нуля"],
- ["leak_rev", "утечка всегда тянет потенциал к нулю", "при нулевой утечке нейрон копит заряд всю картинку"],
+ 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"],
]),
- Callout("Все числа нейрона целые, потенциал — 16 бит со знаком (от −32 768 до 32 767). "
- "Сеть, у которой сумма входов за такт выходит за этот диапазон, на чипе не заработает.", title="Ограничение", kind="note"),
+ Widget("neuron"),
+ Text("Поставьте вес 3 и порог 9 и посмотрите, как часто нейрон стреляет при вычитании порога и при сбросе в ноль. "
+ "С вычитанием спайков ровно ⌊заряд / порог⌋: заряд 21 при пороге 3 даёт 7 спайков. Это проверено на официальной "
+ "программной копии чипа, а не только в описании."),
+ H2("Почему такие флаги"),
+ Text("Вычитание порога превращает нейрон в счётчик: число спайков пропорционально полученному заряду. "
+ "Поэтому частоту спайков можно приравнять к выходу обычного нейрона и переносить в импульсную сеть веса обычной — "
+ f"на этом построена {LocalLink('нативная сеть', native_page)}. При сбросе в ноль излишек сгорает, и счёт искажается."),
+ Text("Пол на нуле защищает от переполнения и упрощает проверку, но у него есть цена: нейрон не помнит "
+ "«отрицательных доводов». Если на такте 0 пришло −8, а на такте 1 +8, нейрон всё равно выстрелит, хотя в сумме "
+ "заряд нулевой. Отсюда две особенности сетей ниже: выход «за/против» у нативной сети и важность порядка спайков "
+ "у сети по тактам."),
+ Table(["Флаг", "Что делает", "Как выставлен в сетях вики"], [
+ ["is_diff", "после выстрела вычитает порог", "включён: остаток не теряется"],
+ ["is_reset", "после выстрела сбрасывает уровень", "выключен"],
+ ["saturate + отрицательный порог", "не пускает уровень ниже отрицательного порога", "включён, порог 0: уровень ≥ 0"],
+ ["утечка", "добавка каждый такт; при leak_rev тянет к нулю", "0: нейрон копит заряд всю картинку"],
+ ]),
+ Next("Вход: яркость в спайки", coding_page, "как в нейрон попадает картинка."),
]
-@Page("2.2", "Кодирование входа")
+@Page("2.2", "Вход: яркость в спайки")
def coding_page():
return [
- H1("Кодирование входа"),
- Def("Кодирование — правило, по которому хост превращает яркость пикселей в спайки на входе сети."),
+ H1("Вход: как яркость становится спайками"),
+ Lead("Чип принимает только спайки. Хост решает, каким правилом превратить яркость пикселя в события, — и от этого "
+ "правила зависят и точность, и число тактов, и работа чипа."),
+ H2("Частотный код"),
+ Text("У каждого пикселя есть накопитель. На каждом такте к нему прибавляется яркость (0…255); как только накопитель "
+ "дошёл до 255, пиксель выдаёт спайк, и 255 вычитается. Белый пиксель стреляет каждый такт, серый 128 — через такт, "
+ "чёрный молчит. За T тактов пиксель даёт примерно яркость × T / 255 спайков."),
+ H2("Временной код"),
+ Text("Каждый пиксель ярче 32 стреляет один раз, и яркость записана в моменте: такт = ⌊(255 − яркость) · T / 224⌋. "
+ "При T = 8 белый пиксель стреляет на такте 0, яркость 200 — на такте 1, 128 — на такте 4, 40 — на такте 7. "
+ "Тусклые края штриха приходят последними."),
Widget("coding"),
Table(["", "Частотный код", "Временной код"], [
- ["Что несёт информацию", "сколько раз пиксель выстрелил", "в какой такт пиксель выстрелил"],
- ["Спайков на пиксель", "до T (числа тактов)", "не больше одного"],
- ["Входных спайков на цифру MNIST", "≈730 за 8 тактов, ≈3200 за 32", "≈143 при любом T"],
- ["Сколько нужно тактов", "много: точность оттенков растёт с T", "мало: 4–8"],
- ["Как обучать", "можно перенести обычную сеть", "нужно обучать по тактам"],
+ ["Чем несёт яркость", "числом спайков", "моментом единственного спайка"],
+ ["Входных спайков на цифру MNIST", "≈730 за 8 тактов, ≈3200 за 32", "≈143 при любом числе тактов"],
+ ["Что нужно для точности", "много тактов: оттенки различаются только при большом T", "4–8 тактов"],
+ ["Как получить веса", "можно перенести обычную сеть: число спайков ≈ активация", "сеть надо учить прямо на тактах"],
]),
- P("Частотный код проще: число спайков приближённо равно активации обычной нейросети, поэтому её веса можно перенести. "
- "Временной код экономнее, но нейроны должны реагировать на порядок прихода спайков, поэтому сеть учат прямо на динамике."),
+ Text("Частотный код удобен для переноса, но дорог: картинка из 143 закрашенных пикселей превращается в 3200 событий. "
+ "Временной код отправляет на чип по одному событию на закрашенный пиксель — меньше уже нельзя."),
+ Callout("Подать яркость пачкой спайков в одном такте нельзя: если в одном такте по одной входной линии пришло несколько "
+ "спайков, «Алтай» засчитывает один. Это проверено на GoldenModel: 25 одинаковых спайков дали эффект одного. "
+ "Поэтому сила сигнала разворачивается во времени или раскладывается по разным линиям.",
+ title="Ограничение чипа", kind="warning"),
+ Next("Слой: сложение и торможение", layer_page, "что происходит, когда спайки доходят до слоя нейронов."),
]
-@Page("2.3", "Связи и сложение сигналов")
-def links_page():
+@Page("2.3", "Слой: сложение и торможение")
+def layer_page():
return [
- H1("Связи и сложение сигналов"),
- Def("Проекция — набор связей из одной группы нейронов в другую. Схему соединения выбирает конструктор сети, "
- "обучение меняет только веса."),
- Table(["Схема в KNP", "Кто с кем связан", "Где применяется"], [
- ["all_to_all", "каждый с каждым", "полносвязные слои: 784 → 256"],
- ["one_to_one", "i-й с i-м", "передача без изменений"],
- ["index_based", "по своему правилу, например окно", "детекторы признаков: каждый смотрит в свой кусок"],
- ["fixed_probability", "случайно с заданной вероятностью", "разреженные сети"],
- ["exclusive", "каждый с каждым, кроме себя", "взаимное торможение, «победитель забирает всё»"],
- ]),
- H2("Сложение"),
- P("Всё, что пришло в нейрон за один такт, складывается. У каждой связи есть задержка: спайк, отправленный на такте N "
- "с задержкой D, учитывается на такте N + D."),
+ H1("Слой: сложение, задержка, торможение"),
+ Lead("Слой — это группа нейронов и набор связей к ней. Связи только складывают; всё остальное делает порог."),
+ H2("Сложение за такт"),
+ Text("Всё, что пришло в нейрон за такт, складывается со своими весами, включая отрицательные. У каждой связи есть задержка "
+ "не меньше одного такта: спайк, выпущенный на такте N, доходит на такт N + 1. Поэтому в сети из двух слоёв ответ "
+ "отстаёт от входа на такт, а после последнего входного такта нужен ещё один, чтобы выход получил всё."),
Widget("synsum"),
- H2("Соревнование"),
- P("Нейроны, которые видят одно и то же и учатся по одному правилу, выучивают одно и то же. Взаимное торможение "
- "заставляет их делить работу: учится только тот, кто выстрелил первым."),
+ H2("Торможение и «победитель забирает всё»"),
+ Text("Отрицательный вес — это торможение. Если связать нейроны слоя друг с другом отрицательными связями, первый "
+ "выстреливший гасит остальных. Так делают, когда нейроны видят одно и то же и должны поделить работу: без торможения "
+ "одинаковые нейроны с одинаковым правилом обучения выучивают одно и то же. На этом построено соревнование в "
+ f"{LocalLink('колонках STDP', columns_page)}."),
Widget("wta"),
+ H2("Кто с кем связан"),
+ Text("Схему связей задаёт конструктор сети, обучение меняет только веса. В сетях этой вики встречаются две схемы: "
+ "полносвязная (каждый вход с каждым нейроном слоя, 784 × 256) и локальная, когда нейрон смотрит в своё окно "
+ f"картинки. Локальная — основа {LocalLink('детекторов признаков', objects_page)}, и у неё на чипе есть цена: "
+ "общих весов нет, каждый детектор в каждой позиции — отдельный нейрон."),
+ Next("Процессор «Алтай»", altai_section, "какие из этих возможностей есть у чипа и с какими ограничениями."),
]
-@Page("2.4", "Обучение")
-def learning_page():
- return [
- H1("Обучение импульсных сетей"),
- Def("Спайк — это ступенька, у неё нет производной. Поэтому обычное обратное распространение к импульсной сети напрямую "
- "не применимо, и существует несколько семейств правил обучения."),
- H2("STDP: локальное правило без учителя"),
- P("После выстрела каждый вход разбирается отдельно. Спайк, пришедший незадолго до выстрела, помог — связь крепнет. "
- "Пришедший после — нейрон обошёлся без него, связь слабеет."),
- Widget("stdp"),
- H2("Сравнение способов"),
- Table(["Способ", "Где идёт", "Чему учит", "Цифры MNIST"], [
- ["STDP (классический)", "в KNP", "частые узоры, без меток", "—"],
- ["STDP с синаптическим ресурсом и дофамином: вес растёт с насыщением, награда закрепляет изменения", "в KNP", "«похоже на мою цифру»", "≈92%"],
- ["Дофамин по ошибке: связь меняется на разницу «должен был выстрелить» минус «выстрелил»", "в KNP", "один слой по ошибке ответа", "≈94,8% (валидация)"],
- ["Перенос обычной сети", "на компьютере", "всю сеть обратным распространением по аналоговой копии", "97,8%"],
- ["Суррогатный градиент по тактам", "на компьютере", "всю сеть прямо на динамике спайков", "97,4% за 8 тактов"],
- ]),
- Callout("Чем больше обучения уходит на компьютер, тем выше точность. Алтай не обучается вообще, поэтому для него "
- "естественный путь — обучение на компьютере с учётом ограничений чипа.", title="Вывод", kind="note"),
- H3("Суррогатный градиент"),
- P("Прямой проход идёт по тактам с настоящими спайками. При обратном проходе производная ступеньки заменяется "
- "гладкой функцией 1/(1+4|v|)². Сеть видит ту же арифметику, что и чип: целые веса, вычитание порога, уровень не ниже нуля, задержку в такт."),
- ]
+# ---------------------------------------------------------------- 3. Алтай
-
-# ---------------------------------------------------------------- 3. KNP
-
-@Page("3", "Платформа KNP")
-def knp_section():
- return [
- H1("Kaspersky Neuromorphic Platform"),
- Def("KNP — открытая программная платформа (C++ и Python, Apache 2.0) для описания, обучения и исполнения "
- "импульсных сетей на CPU, GPU и нейроморфном процессоре «Алтай»."),
- H2("Модель сети"),
- Table(["Уровень", "Что это"], [
- ["Популяция", "группа однотипных нейронов; параметры задаёт функция-генератор"],
- ["Проекция", "связи из популяции в популяцию: вес, задержка, тип, номера нейронов"],
- ["Network", "популяции и проекции"],
- ["Model", "сеть плюс входные и выходные каналы"],
- ["ModelExecutor", "прогоняет модель по тактам на выбранном бэкенде"],
- ]),
- P("Каждый такт: популяции обновляют нейроны → шина доставляет сообщения → проекции превращают спайки в воздействия."),
- Code('''P = knp::core::Population
-R = knp::core::Projection
-
-P hidden(make_neuron, 256); // 256 нейронов AltAILIF
-R input(input_uid, hidden.get_uid(), gen, n); // связи вход → скрытый слой
-Network net; net.add_population(hidden); net.add_projection(input);
-Model model(net); model.add_input_channel(...); model.add_output_channel(...);
-ModelExecutor exec(model, backend, inputs); exec.start(until_step);''', "cpp"),
- Links([LocalLink("Бэкенды и модели нейронов", backends_page)]),
- ]
-
-
-@Page("3.1", "Бэкенды и модели нейронов")
-def backends_page():
- return [
- H1("Бэкенды и модели нейронов"),
- Def("Бэкенд — исполнитель сети. Сеть описывается один раз, а запускается на любом бэкенде, который поддерживает её типы нейронов."),
- Table(["Бэкенд", "Нейроны", "Обучение", "Замечание"], [
- ["CPU, один поток", "LIF, BLIFAT, AltAILIF и STDP-варианты", "да", "эталон"],
- ["CPU, много потоков", "LIF, BLIFAT", "частично", "AltAILIF не поддерживает"],
- ["CUDA (официальный)", "BLIFAT", "нет", "AltAILIF не поддерживает"],
- ["CUDA для AltAILIF (разработан в проекте)", "AltAILIF, Delta", "нет", "результат побайтно совпадает с CPU, в 5 раз быстрее на больших наборах"],
- ["Алтай: GoldenModel и чип", "AltAILIF, Delta", "нет", "закрытая библиотека Мотив НТ, deb-пакет 1.0.2"],
- ]),
- Table(["Модель нейрона", "Что это", "Где исполняется"], [
- ["LIF", "классический нейрон с утечкой", "CPU"],
- ["BLIFAT", "расширенная модель Kaspersky: адаптивный порог, торможение", "CPU, CUDA"],
- ["AltAILIF", "ровно возможности нейрона Алтая", "CPU, CUDA проекта, Алтай"],
- ]),
- P("Для распознавателя на Алтае имеет значение только AltAILIF с синапсами Delta: остальные модели на чип не переносятся."),
- ]
-
-
-# ---------------------------------------------------------------- 4. Алтай
-
-@Page("4", "Процессор Алтай")
+@Page("3", "Процессор «Алтай»")
def altai_section():
return [
H1("Процессор «Алтай»"),
- Def("Алтай — нейроморфный процессор компании Мотив НТ: сетка одинаковых ядер, каждое из которых хранит нейроны "
- "и таблицу их связей и обменивается спайками с соседями пакетами."),
- Facts([("16", "ядер в прототипе"), ("8192", "нейрона в прототипе"), ("131 072", "нейрона в максимальной конфигурации"),
- ("16 бит", "потенциал со знаком"), ("4", "величины веса на нейрон"), ("нет", "обучения на кристалле")]),
- Links([LocalLink("Сетка ядер", grid_page), LocalLink("Ядро и кроссбар", crossbar_page),
- LocalLink("Ограничения и квантование", rules_page), LocalLink("Размещение и проверка", placement_page)]),
+ 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 — GoldenModel, программная копия, "
+ "которая считает так же, как кристалл, и раскладывает сеть по ядрам тем же размещателем. По умолчанию это "
+ "сетка 4 × 4 ядра. Библиотека закрытая: её поведение изучено только опытами, без дизассемблирования. "
+ "В самом пакете устройство называется AltAIv1_GoldenModel, хотя руководство говорит об «Алтае-2»."),
+ Links([
+ LocalLink("Ядро и веса", core_page) + " — откуда правило «4 величины веса на нейрон».",
+ LocalLink("Правила для сети и как их обходить", rules_page) + " — полный список ограничений и решения, проверенные на GoldenModel.",
+ LocalLink("Проверка в GoldenModel", verify_page) + " — как доказать, что сеть заработает, и на чём легко обмануться.",
+ ]),
+ Next("Ядро и веса", core_page, "почему у нейрона так мало разных весов."),
]
-@Page("4.1", "Сетка ядер")
-def grid_page():
+@Page("3.1", "Ядро и веса")
+def core_page():
return [
- H1("Сетка ядер"),
- Def("Ядра соединены с соседями; внешний мир подключается через порт — сейчас только с северной стороны сетки, стандартной длины 4 ядра."),
- Widget("chipgrid"),
- P("В эмуляторе по умолчанию сетка 4×4. Число нейронов в ядре — около 512 (8192 нейрона прототипа на 16 ядер; расчётная величина)."),
- ]
-
-
-@Page("4.2", "Ядро и кроссбар")
-def crossbar_page():
- return [
- H1("Ядро и кроссбар"),
- Def("Кроссбар — таблица связей ядра: строки — входящие линии (аксоны), столбцы — нейроны, в клетке — вес. "
- "Пришёл спайк по строке — каждый нейрон-столбец прибавляет вес из этой строки."),
+ H1("Ядро и веса"),
+ Lead("Ядро хранит своих нейронов и связи к ним. Входящий спайк приходит по линии, и каждый нейрон ядра прибавляет вес, "
+ "записанный для этой линии."),
+ Text("Удобно представлять ядро таблицей: строки — входящие линии, столбцы — нейроны, в клетке — вес. Спайк по строке "
+ "заставляет каждый нейрон-столбец прибавить свою клетку. Руководство KNP формулирует главное ограничение так: "
+ "у нейрона не больше четырёх разных величин веса, знак выбирается отдельно и в счёт не идёт. "
+ "Вес 3 и вес −3 — одна величина; веса 1, 2, 4 и 255 — уже четыре."),
Widget("crossbar"),
- P("Отсюда главное ограничение: у нейрона не может быть больше 4 разных величин веса. Знак задаётся отдельно и в счёт не идёт."),
+ Text("Поменяйте веса в таблице: когда у столбца появляется пятая величина, нейрон перестаёт помещаться в правило. "
+ "Обычная сеть после обучения имеет у каждого нейрона сотни разных весов — поэтому без квантования её на чип не положить."),
+ H2("Что занимает «величину»"),
+ Text("Величины расходуют не только обученные веса. В нативной сети у скрытого нейрона веса {1, 2, 4} и ещё −255 для "
+ "очистки уровня между картинками — ровно четыре. Обученным весам остаётся три уровня, и это определило палитру "
+ f"{{0, ±1, ±2, ±4}} во всех сетях вики. Подробно — на странице {LocalLink('правил', rules_page)}."),
+ Next("Правила для сети и обходы", rules_page, "все ограничения чипа сразу и обход каждого."),
]
-@Page("4.3", "Ограничения и квантование")
+@Page("3.2", "Правила для сети и обходы")
def rules_page():
return [
- H1("Ограничения и квантование"),
- Table(["Правило", "Следствие для сети"], [
- ["вес — целое, |w| ≤ 255", "веса после обучения переводятся в целые"],
- ["не больше 4 разных величин веса на нейрон", "учить сразу с палитрой, например {0, ±1, ±2, ±4}"],
- ["нулевых связей нет", "связь с весом 0 не создаётся"],
- ["один тип связи, знак в весе", "торможение — отрицательный вес"],
- ["потенциал 16 бит со знаком", "сумма входов за такт ограничена"],
- ["начальный потенциал игнорируется", "смещение подаётся отдельными входными спайками"],
- ["два спайка по одной входной линии в одном такте считаются одним", "если нужно подать несколько событий за такт, у каждого своя линия"],
- ["обучения нет", "веса загружаются готовыми"],
+ 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("Квантование"),
- Def("Квантование — замена дробных весов ступеньками из небольшого набора. Чем меньше ступенек, тем грубее нейрон."),
+ Text("Квантование заменяет дробный вес ближайшим из палитры. Чем меньше ступенек, тем грубее нейрон: из тонкого "
+ "профиля весов остаются три уровня. Ошибку квантования сеть переживает, если учится уже с огрублёнными весами — "
+ f"так сделано в {LocalLink('сети по тактам', ticks_page)}."),
Widget("quant"),
H2("Влезет ли сеть"),
+ Text("Три проверки, которые можно сделать до размещателя: число нейронов, число разных весов у самого «разнообразного» "
+ "нейрона и наибольший уровень. Переключитесь на сеть детекторов — она проваливает все три."),
Widget("fit"),
+ Callout("Эти три числа необходимы, но недостаточны. Размещатель проверяет ещё входящие линии ядер и маршруты, а "
+ "правильность счёта доказывает только сравнение спайков с эталоном.", title="Предел оценки", kind="note"),
+ Next("Проверка в GoldenModel", verify_page, "как это сравнение устроено."),
]
-@Page("4.4", "Размещение и проверка")
-def placement_page():
+@Page("3.3", "Проверка в GoldenModel")
+def verify_page():
return [
- H1("Размещение и проверка"),
- Def("Размещатель — часть бэкенда Алтая, которая оценивает число нужных ядер, раскладывает нейроны и связи и отказывается, "
- "если сеть не помещается. Нейрон, которому нужно больше входов, чем строк в ядре, он делит на несколько."),
- Table(["Ступень проверки", "Что доказывает"], [
- ["Целочисленный эталон (NumPy)", "арифметика сети записана верно"],
- ["KNP на CPU", "граф собран правильно, счётчики спайков совпадают с эталоном"],
- ["KNP на GPU", "то же на большом объёме данных, побайтно"],
- ["GoldenModel", "размещение и правила чипа соблюдены, счётчики совпадают"],
- ["Чип AltAI_HW", "работа на железе"],
+ 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", "работа на железе", "не проверялось"],
]),
- Callout("То, что размещатель принял сеть, ещё не доказывает её правильность: размещение может пройти, а счёт — разойтись "
- "с эталоном. Критерий — совпадение счётчика спайков каждого нейрона во всех ступенях.",
- title="Критерий", kind="note"),
+ 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, "в чём эту сеть описывают и чем запускают."),
]
-# ---------------------------------------------------------------- 5. Архитектуры
+# ---------------------------------------------------------------- 4. KNP
-@Page("5", "Архитектуры распознавания")
-def arch_section():
+@Page("4", "Платформа KNP")
+def knp_section():
return [
- H1("Архитектуры распознавания"),
- Def("Архитектура распознавателя — устройство слоёв, способ кодирования входа и способ обучения. "
- "Все четыре архитектуры ниже построены на нейронах AltAILIF."),
- Widget("arch"),
- Table(["Архитектура", "Устройство", "Обучение", "Ложится на чип"], [
- ["Колонки STDP", "пиксели → 10 колонок × 20 нейронов", "в KNP, STDP + дофамин", "да"],
- ["Детекторы + выход", "200 типов детекторов × 196 позиций → хост → 256 → 2", "детекторы k-средними, выход на компьютере", "нет"],
- ["Нативная сеть", "784 → 256 → 160 «за/против», 32 такта", "перенос обычной сети", "да, проверено"],
- ["Импульсная по тактам", "784 → 256 → 10, 8 тактов, временной код", "суррогатный градиент", "да, по рамкам"],
+ H1("Платформа KNP"),
+ Lead("Kaspersky Neuromorphic Platform — открытая библиотека (C++ и Python, Apache 2.0): сеть описывается один раз и "
+ "исполняется на CPU, GPU или «Алтае»."),
+ H2("Из чего состоит модель"),
+ Table(["Объект", "Что это"], [
+ ["Population", "группа однотипных нейронов; параметры каждого задаёт функция-генератор"],
+ ["Projection", "связи в популяцию: для каждой — вес, задержка, тип, номер источника и получателя"],
+ ["Network", "набор популяций и проекций"],
+ ["Model", "сеть плюс входные каналы (куда хост шлёт спайки) и выходные (откуда читает)"],
+ ["ModelExecutor", "гоняет модель по тактам на выбранном бэкенде и вызывает код хоста каждый такт"],
]),
- Links([LocalLink("Колонки STDP", columns_page), LocalLink("Детекторы признаков", detectors_page),
- LocalLink("Нативная сеть", native_page), LocalLink("Импульсная сеть по тактам", ticks_page),
- LocalLink("Сравнение архитектур", compare_page)]),
+ Text("Каждый такт исполнитель берёт входные спайки из каналов, бэкенд обновляет нейроны и доставляет спайки по проекциям, "
+ "выходные спайки складываются в каналы. Хост между тактами решает, что подать дальше."),
+ H2("Нативная сеть в коде"),
+ Text("Сокращённый фрагмент из mnist-altai-native/native_digits.cpp. Видны все обходы со страницы "
+ f"{LocalLink('правил', rules_page)}: тактовые линии смещения, очистка по 128 линиям весом −255, только тип EXCITATORY."),
+ Code('''using P = knp::core::Population;
+using R = knp::core::Projection;
+
+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("Обычную сеть учат обратным распространением ошибки: считают, как ответ зависит от каждого веса, и сдвигают веса. "
+ "Для импульсной сети это напрямую не работает: выход нейрона меняется скачком, производная почти везде равна нулю. "
+ "Отсюда два пути: учиться локальными правилами, которым производная не нужна, или учиться на компьютере, "
+ "заменив ступеньку чем-то гладким."),
+ 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."),
]
@@ -424,196 +533,446 @@ def arch_section():
def columns_page():
return [
H1("Колонки STDP"),
- Def("Колонки — архитектура штатного примера KNP: на каждый класс отводится колонка из 20 нейронов, "
- "которые учатся по STDP с дофамином; ответ — колонка, выстрелившая больше всех."),
- H2("Как работает"),
- List(["картинка кодируется частотно за 10 тактов;", "спайки идут во все колонки;",
- "при обучении метка через вспомогательную популяцию будит колонку правильного класса, и её связи с активными пикселями крепнут;",
- "при распознавании соседи в колонке тормозят друг друга, чтобы нейроны специализировались."], ordered=True),
- H2("Свойства"),
- Table(["Свойство", "Значение"], [
- ["Точность на MNIST", "≈92%"], ["Обучение на платформе", "да"],
- ["Сдвиг цифры на 2 пикселя", "точность падает до ≈64–67%"],
- ["Типичная ошибка", "4 → 9: трафарет девятки целиком накрывает четвёрку"],
+ Lead("Штатный пример KNP mnist-learn: на каждую цифру — колонка из 20 нейронов, которые учатся по STDP с дофамином. "
+ "Ответ — колонка, которая стреляет больше всех."),
+ H2("Как учится"),
+ Text("Картинка подаётся частотным кодом за 10 тактов во все колонки сразу. Во время обучения метка через вспомогательную "
+ "популяцию будит колонку правильной цифры, и у её нейронов крепнут связи с пикселями, которые стреляли перед "
+ "выстрелом нейрона. Это STDP — правило, зависящее от порядка спайков: вход за мгновение до выстрела помог, "
+ "связь растёт; вход после выстрела не понадобился, связь слабеет."),
+ Widget("stdp"),
+ Text("В KNP используется вариант с синаптическим ресурсом: вес растёт тем медленнее, чем ближе к пределу, и изменения "
+ "закрепляются наградой — дофамином. Внутри колонки нейроны тормозят друг друга, чтобы каждый выучил свой стиль "
+ "написания."),
+ H2("Что пришлось исправить"),
+ Text("В исходном примере все 20 нейронов колонки стартовали с одинаковых весов и получали один и тот же сигнал метки — "
+ "колонка работала как один нейрон, 82,5%. Небольшой случайный разброс стартовых весов дал нейронам разные "
+ "стили, а торможение −255 между всеми нейронами на проверке оставило голос самому похожему: 92,2% на полном "
+ f"MNIST. Попутно найдена ошибка загрузчика KNP — см. {LocalLink('подводные камни', knp_pitfalls)}."),
+ H2("Почему дальше 92% не идёт"),
+ Text("Колонка выучивает трафарет: «где у моей цифры обычно есть линия». Отсюда четыре следствия, и все они "
+ "измерены:"),
+ List([
+ "сдвиг цифры на 2 пикселя роняет точность до 64–67%, утолщение или утончение линии — до 75–81%: трафарет перестаёт совпадать;",
+ "типичная ошибка 4 → 9: трафарет девятки накрывает четвёрку целиком, а «не путай с девяткой» колонку никто не учил — "
+ "награда бывает только за свою цифру; наказание чужих колонок дофамином даёт +2…4 п.п., но не снимает потолок;",
+ "пустой пиксель молчит, поэтому колонка не видит довода «здесь линии нет». Попытка добавить такие доводы отдельными "
+ "входами «пиксель тёмный» (кодирование ON/OFF) утроила число входных спайков, и сеть замолчала;",
+ "на плотном входе колонки молчат вообще: после STDP большинство весов отрицательные, сумма уходит в минус.",
]),
- P("Колонка учит «похоже на мою цифру», но не «не путай с соседней», и не получает сигнала «здесь линии нет»: "
- "пустой пиксель молчит. Это потолок архитектуры, а не настроек."),
+ Text("Это ограничение архитектуры, а не настроек: колонки сравнивают с образцом, но не учатся различать классы."),
+ Next("Дофамин по ошибке", delta_page, "как научить сеть именно различать, не выходя из KNP."),
]
-@Page("5.2", "Детекторы признаков")
-def detectors_page():
+@Page("5.2", "Дофамин по ошибке")
+def delta_page():
return [
- H1("Детекторы признаков с хостом"),
- Def("Архитектура для фотографий: слой нейронов-детекторов ищет выученные узоры по всей картинке, хост суммирует "
- "их отклики по областям и нормирует, затем небольшая сеть выдаёт класс."),
- Table(["Ступень", "Где исполняется", "Размер"], [
- ["пиксели → спайки, 10 тактов", "хост", "32×32×3"],
- ["детекторы узоров 6×6×3, шаг 2", "KNP", "200 типов × 196 позиций = 39 200 нейронов"],
- ["суммирование по четвертям, нормировка", "хост", "800 чисел"],
- ["скрытый слой и выход", "KNP", "256 + 2 нейрона"],
+ 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%"],
]),
- P("Точность на кошках и собаках CIFAR-10 — 71,35%, на транспорте — 71,25% (валидация). На Алтай архитектура не ложится: "
- "нейронов в 5 раз больше прототипа, у нейронов 55–110 разных весов вместо 4, потенциалы доходят до ±570 000, "
- "а между слоями работает хост."),
- Callout("На чипе нет общих весов: детектор «вертикальная черта» в каждой позиции — отдельный нейрон. "
- "Поэтому число нейронов растёт как «типы × позиции».", title="Почему так много нейронов", kind="note"),
+ Text("На цифрах обучение внутри платформы догнало обучение на компьютере. На объектах разрыв 5–9 п.п. не объяснён: "
+ "диапазон весов и число проходов не помогают."),
+ H2("Ограничение"),
+ Text("Правило учит один слой, а один слой на пикселях — это линейный классификатор, около 92% на MNIST. Поэтому вход "
+ "у него — признаки штрихов, которые считает хост: 8 направлений линии × 2 толщины, сложенные по клеткам 4 × 4. "
+ "Получается, что половина распознавания идёт не на чипе. Чтобы на чипе была вся сеть, нужны веса для нескольких "
+ "слоёв — а их даёт только обучение на компьютере."),
+ Next("Перенос обычной сети", native_page, "первая сеть, целиком живущая на «Алтае»."),
]
-@Page("5.3", "Нативная сеть")
+@Page("5.3", "Перенос обычной сети")
def native_page():
return [
- H1("Нативная сеть"),
- Def("Нативная сеть — двухслойная сеть, целиком составленная из нейронов AltAILIF и уложенная в рамки чипа. "
- "Её веса найдены обучением обычной сети и перенесены."),
- Table(["Слой", "Размер", "Параметры"], [
- ["вход", "784", "частотный код, 32 такта"],
- ["скрытый", "256 AltAILIF", "веса {0, ±1, ±2, ±4}, порог 32, вычитание порога"],
- ["выход", "160 AltAILIF", "10 классов × 8 групп «за» и 8 групп «против», порог 4"],
- ["хост", "—", "«за» минус «против», максимум"],
+ 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("Почему выход «за/против»"),
- P("Потенциал не опускается ниже нуля, а у нейрона мало разных весов. Поэтому отрицательные доводы собираются отдельными "
- "нейронами «против», а вычитание делает хост. Так сеть учитывает «здесь есть крыша — значит не 4»."),
- H2("Обучение"),
- List(["обычная сеть 784 → 256 → 10 учится обратным распространением; активация ограничена 0…4 и округляется, как счёт спайков;",
- "веса скрытого слоя переводятся в целые {0, ±1, ±2, ±4};",
- "выход заново обучается по настоящим счётчикам спайков скрытого слоя и раскладывается на группы."], ordered=True),
+ Text("Выходной вес бывает и отрицательным: «здесь есть верхняя перекладина — значит, не 4». Но уровень нейрона не "
+ "опускается ниже нуля, и отрицательный довод, пришедший раньше положительного, теряется. Поэтому положительные "
+ "веса идут в нейрон «за», отрицательные — со знаком плюс в нейрон «против», а вычитание делает хост. Деление на 8 "
+ "групп нужно уже из-за правила четырёх величин: у каждой группы свои три уровня."),
+ H2("Результаты"),
Table(["Свойство", "Значение"], [
- ["Точность на MNIST", "97,8%; с выпрямлением наклона на хосте — 98,0%"],
- ["Проверка", "KNP на CPU и CUDA и GoldenModel: счётчики спайков всех нейронов совпали с эталоном (в GoldenModel — на 1000 картинках)"],
- ["Цена", "≈3200 входных спайков и ≈338 тыс. синаптических событий на картинку"],
+ ["Точность на тесте 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", "Импульсная сеть по тактам")
+@Page("5.4", "Обучение по тактам")
def ticks_page():
return [
- H1("Импульсная сеть по тактам"),
- Def("Импульсная сеть по тактам — сеть 784 → 256 → 10 нейронов AltAILIF с временным кодом входа, "
- "обученная суррогатным градиентом прямо на целочисленной динамике чипа."),
+ H1("Обучение по тактам"),
+ Lead("Сеть 784 → 256 → 10 из AltAILIF, обученная прямо на спайковой целочисленной динамике чипа. Вход — временной код: "
+ "один спайк на пиксель, ответ за 8 тактов."),
Widget("ticks"),
- H2("Устройство"),
- Table(["Элемент", "Значение"], [
- ["Вход", "каждый пиксель ярче 32 стреляет один раз; такт = ⌊(255 − яркость)·8/224⌋"],
- ["Скрытый слой", "256 нейронов, порог 8, веса и смещения {0, ±1, ±2, ±4}"],
- ["Выход", "10 нейронов, порог 8, задержка 1 такт"],
- ["Ответ", "больше спайков; при равенстве — раньше первый спайк"],
- ["Ранний ответ", "такт, после которого лидер больше не меняется"],
+ Text("Нарисуйте цифру и прокрутите такты. Яркие пиксели стреляют первыми, скрытые нейроны начинают отвечать с первого "
+ "такта, и выход обычно определяется задолго до восьмого: такт, после которого лидер больше не меняется, показан "
+ "как ранний ответ."),
+ H2("Как обучить ступеньку"),
+ Text("Прямой проход идёт по настоящим тактам: спайки, вычитание порога, пол на нуле, задержка в такт между слоями — та же "
+ "арифметика, что на чипе. При обратном проходе производная ступеньки заменяется гладким «колоколом» "
+ "1 / (1 + 4|v|)², где v — расстояние уровня до порога. Это суррогатный градиент: он говорит весам, "
+ "в какую сторону сдвинуть уровень, чтобы нейрон стрельнул или промолчал."),
+ 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"],
]),
- H2("Обучение"),
- List(["прямой проход идёт по 8 тактам с настоящими спайками и вычитанием порога;",
- "с 9-й эпохи веса в прямом проходе огрубляются до палитры чипа, градиент проходит «насквозь»;",
- "к ошибке добавлен штраф за спайки скрытого слоя — сеть учится молчать, когда может;",
- "настройки выбираются по валидации, итог считается отдельным целочисленным симулятором."], ordered=True),
- Table(["Свойство", "Значение"], [
- ["Точность на MNIST", "97,4%"], ["Точность на рисунках", "95,9%"], ["Тактов", "8"],
- ["Входных спайков", "≈143"], ["Синаптических событий", "≈20 тыс. — в 17 раз меньше нативной сети"],
- ["Проверка", "целочисленный эталон; в KNP и GoldenModel не проверялась"],
+ Text("Выбрана первая строка — по валидации, тест открыт после выбора. Два наблюдения из таблицы:"),
+ Steps([
+ "Временной код выигрывает у частотного при том же числе тактов: точнее и в 4 раза дешевле. Частотному коду на "
+ "8 тактах не хватает оттенков, временной передаёт яркость моментом.",
+ "Штраф за скрытые спайки не равен экономии. Почти вся работа — во входном слое: каждый из ≈143 входных спайков "
+ "задевает в среднем ≈139 скрытых нейронов, это ≈19,7 тыс. из 20 тыс. событий. Сильный штраф вдвое снижает "
+ "скрытые спайки, но сеть компенсирует это более плотными входными весами, и событий становится больше. Экономить "
+ "нужно на числе ненулевых входных весов.",
]),
+ H2("Что осталось до чипа"),
+ Text("Правила чипа соблюдены на бумаге: палитра {0, 1, 2, 4}, наибольший уровень 304 при пределе 32 767, смещение — "
+ "постоянный вход каждый такт, то есть тактовые линии, как у нативной сети. Но в KNP и GoldenModel сеть ещё не "
+ "запускалась, и для неё нет очистки между картинками. Это следующий шаг, после которого её можно ставить в один "
+ "ряд с нативной."),
+ Next("Сравнение", compare_page, "все сети рядом."),
]
-@Page("5.5", "Сравнение архитектур")
+@Page("5.5", "Сравнение")
def compare_page():
return [
- H1("Сравнение архитектур"),
- Def("Сравнение на одной задаче — цифры MNIST, тест 10 000 изображений — по точности, числу тактов и работе чипа."),
- Table(["", "Колонки STDP", "Нативная сеть", "Импульсная по тактам"], [
- ["Точность", "92,2%", "97,8%", "97,4%"],
+ 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"],
- ["Учится на платформе", "да", "нет", "нет"],
- ["Проверено в GoldenModel", "—", "да", "ещё нет"],
+ ["Где учится", "в KNP", "на компьютере", "на компьютере"],
+ ["Проверена в GoldenModel", "нет", "да, 1000 цифр", "нет"],
]),
H2("Одна цифра — три сети"),
- P("Нарисуйте цифру или выберите тестовую. Все три сети считаются здесь целочисленно, как на чипе."),
+ Text("Нарисуйте цифру или выберите тестовую. Все три сети считаются здесь целочисленно, по тем же правилам, что "
+ "на чипе. Сдвиньте рисунок к краю или нарисуйте очень толстой линией — колонки ошибутся первыми."),
Widget("compare"),
H2("Точность против работы чипа"),
- P("Каждая точка — сеть или режим. Чем левее, тем меньше сложений «спайк × вес» и энергии; чем выше, тем точнее."),
+ Text("Каждая точка — сеть или её режим. Левее — меньше сложений и, значит, энергии; выше — точнее. Нативная сеть, "
+ "обрезанная по тактам, ползёт влево и вниз; сеть, обученная по тактам, стоит в левом верхнем углу."),
Widget("tradeoff"),
- P("Обрезать нативную сеть по тактам можно до 8 — дальше точность обваливается (84,6% на 4 тактах). "
- "Сеть, обученная по тактам с временным кодом, держит 97% даже на 4 тактах."),
+ H2("Что выбирать"),
+ Table(["Нужно", "Сеть", "Почему"], [
+ ["доказательство «работает на Алтае» сегодня", "нативная", "единственная прошла GoldenModel"],
+ ["минимум энергии и тактов", "обучение по тактам", "в 17 раз меньше событий при той же точности; осталось пройти GoldenModel"],
+ ["обучение без компьютера, внутри платформы", "колонки или дофамин по ошибке", "единственные варианты; точность ниже"],
+ ]),
+ Next("Вход: хост и рисунки", host_page, "почему на рисунках точность ниже и что с этим делает хост."),
]
-# ---------------------------------------------------------------- 6–8
+# ---------------------------------------------------------------- 6. Хост
-@Page("6", "Обработка на хосте")
+@Page("6", "Вход: хост и рисунки")
def host_page():
return [
- H1("Обработка на хосте"),
- Def("Хост — обычный процессор рядом с чипом. Он делает то, что дёшево сделать один раз на картинку: "
- "вырезает и центрирует объект, выпрямляет наклон и превращает яркость в моменты спайков."),
+ H1("Вход: хост и рисунки"),
+ Lead("Сеть видит только то, что подал хост. Рисунок мышью отличается от цифры MNIST сильнее, чем кажется, и этот "
+ "разрыв закрывает подготовка входа и обучение на похожих рисунках."),
+ H2("Почему рисунок — другая задача"),
+ Text("Цифры MNIST — отсканированный почерк: штрих переменной толщины с серыми краями, цифра вписана в квадрат 20 × 20 "
+ "и поставлена центром масс в центр поля 28 × 28. Рисунок мышью — перо одной толщины, жёсткие края, произвольные "
+ "размер и место. Сеть, выучившая MNIST, видит на рисунке незнакомый «почерк»."),
+ Text("Чтобы мерить это без людей, тестовые цифры MNIST перерисованы так, как их нарисовала бы мышь: из цифры "
+ "выделяется осевая линия и обводится пером постоянной толщины на поле 280 × 280, потом уменьшается. Доля "
+ "верных ответов на таких копиях — точность на рисунках."),
+ H2("Что делает хост"),
Widget("hostprep"),
Table(["Шаг", "Что делает", "Зачем"], [
- ["рамка 20×20", "вписывает объект в квадрат 20×20 и ставит центр масс в центр 28×28", "размер и положение не мешают"],
- ["выпрямление", "сдвигает строки так, чтобы главная ось стала вертикальной", "наклонный почерк узнаётся лучше: +0,35 п.п. на MNIST"],
- ["усреднение", "уменьшает с усреднением по площади", "сохраняет серые края, как в обучающих данных"],
- ["временной код", "яркость → номер такта", "один спайк на пиксель"],
+ ["рамка", "вписывает рисунок в квадрат 20 × 20 и ставит центр масс в центр 28 × 28", "как в MNIST: размер и положение перестают мешать"],
+ ["выпрямление наклона", "по моментам изображения находит наклон главной оси и сдвигает строки, чтобы она стала вертикальной", "наклонный почерк приводится к прямому"],
+ ["уменьшение с усреднением", "каждый пиксель 28 × 28 — среднее своей области", "сохраняет серые края, как у сканов"],
+ ["кодирование", "яркость → спайки", "частотный или временной код"],
]),
- Callout("Обработка на хосте не должна вклиниваться между слоями сети. Тогда вся сеть живёт на чипе, а хост только готовит вход и читает выход.",
- title="Граница хоста", kind="note"),
+ 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, "что из этого переносится на робота."),
]
-@Page("7", "Метрики и методика")
+# ---------------------------------------------------------------- 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([
+ "Постановка. Начать с двух классов «мусор / не мусор» на окне кадра. Хост режет кадр на окна, чип отвечает "
+ "по каждому, место мусора видно по сработавшим окнам.",
+ "Данные. Открытые наборы с фотографиями мусора (например, TACO, TrashNet) для начала и снимки с камеры робота "
+ "для честного теста. Тест не трогать до выбора модели.",
+ "Бюджет. До обучения выбрать размер окна, число узоров и шаг так, чтобы сеть влезла в 8192 нейрона; "
+ "проверить три числа со страницы правил.",
+ "Обучение. Сеть по тактам с временным кодом, огрублением весов до палитры с середины обучения и штрафом за "
+ "спайки; узоры первого слоя — локальные окна без общих весов.",
+ "Проверка. Целочисленный эталон → KNP на CPU и GPU → GoldenModel, сравнение счётчиков всех нейронов.",
+ "Плата. Запуск на 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("Метрики и методика"),
+ H1("Методика измерений"),
+ Lead("Как получены цифры в вики и при каких условиях им можно верить."),
+ H2("Данные"),
+ Text("MNIST: 60 000 обучающих и 10 000 тестовых цифр. Из обучающих 5000 отложены на валидацию. Все настройки — пороги, "
+ "масштабы, число эпох, выбор лучшего варианта — выбираются по валидации. Тест открывается один раз, после "
+ "выбора: иначе оценка завышена."),
+ H2("Метрики"),
Table(["Метрика", "Определение"], [
- ["Точность", "доля правильных ответов на тесте"],
- ["Точность на рисунках", "та же доля, но на тестовых цифрах, перерисованных пером постоянной толщины, как при рисовании мышью"],
- ["Синаптические события", "число сложений «спайк × вес» на одну картинку; главная мера энергии на нейроморфном чипе"],
- ["Входные спайки", "сколько событий хост отправляет на чип"],
- ["Такты", "длительность распознавания в шагах чипа"],
- ["Совпадение счётчиков", "счётчик спайков каждого нейрона одинаков в эталоне, KNP и GoldenModel"],
+ ["Точность", "доля верных ответов на тесте; молчание сети считается ошибкой"],
+ ["Точность на рисунках", "то же на перерисованных тестовых цифрах"],
+ ["Синаптические события", "сложений «спайк → вес» на одну картинку, среднее по тесту"],
+ ["Входные спайки", "сколько событий хост отправляет на чип за картинку"],
+ ["Такты", "длительность распознавания одной картинки"],
+ ["Совпадение счётчиков", "счётчик каждого нейрона одинаков в эталоне, KNP и GoldenModel на каждой картинке"],
]),
- H2("Правила"),
- List(["настройки выбираются только на валидации; тест смотрится один раз;",
- "цифра без источника (файл метрик или журнал проверки) не считается результатом;",
- "разница меньше 0,3 п.п. при одном прогоне — в пределах шума;",
- "точность на тесте MNIST не равна точности на рисунках мышью: это другой «почерк»."]),
- P("Рисунок мышью отличается от рукописной цифры: перо одной толщины, жёсткие края, другая толщина линии. Поэтому точность "
- "на рисунках обычно ниже точности на тесте; её сокращают обработкой на хосте и обучением на перерисованных копиях."),
+ 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", "Распознавание мусора")
-def trash():
+@Page("8.3", "Где что лежит")
+def repo_page():
return [
- H1("Распознавание мусора"),
- Def("Целевая задача: робот с процессором «Алтай» распознаёт мусор в кадре своей камеры."),
- H2("Постановка"),
- List(["хост режет кадр на окна и для каждого окна готовит вход сети;",
- "чип отвечает по окну: «мусор / не мусор» или тип;",
- "где находится мусор, видно по сработавшим окнам."]),
- H2("Бюджет нейронов"),
- P("Детектор узоров на чипе стоит нейрон на каждую позицию. Подберите размер окна, шаг и число типов детекторов так, чтобы сеть влезла в прототип."),
- Widget("budget"),
- H2("Что переносится из распознавания цифр"),
- Table(["Приём", "Почему подходит"], [
- ["обучение на компьютере под рамки чипа", "чип не учится; точность выше, чем у правил STDP"],
- ["временной код и обучение по тактам", "мало спайков и тактов — меньше энергии на кадр"],
- ["обработка на хосте только на входе", "вся сеть остаётся на кристалле"],
- ["проверка счётчиков в KNP и GoldenModel", "доказательство без платы"],
+ 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("Открытые вопросы"),
- List(["конфигурация чипа и наличие платы;", "классы: «мусор / не мусор» или типы;",
- "камера робота, в том числе событийная;", "данные: открытые наборы и снимки с камеры робота."]),
+ 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("Импульсные сети", snn_section),
HeaderButton("Алтай", altai_section),
- HeaderButton("Архитектуры", arch_section),
+ HeaderButton("Обучение", learning_section),
HeaderButton("Сравнение", compare_page),
+ HeaderButton("Мусор", trash_section),
]