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"" + + +@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), ]