Drop the ON/OFF aside, the 8-replica anecdote and "next step" wording; explain the per-line spike rule, define drawn-digit accuracy, separate GoldenModel accuracy from counter verification, and phrase backend and neuron-flag notes without project-internal references. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFVgZ8Nct8BEh9k7bjuRdw
637 lines
52 KiB
Python
637 lines
52 KiB
Python
"""Вики «Нейроморфное распознавание на Алтае» на SharedDocsLib.
|
||
|
||
Сборка статического комплекта: python wiki/knp_wiki.py build
|
||
Live-preview на порту 1234: python wiki/knp_wiki.py
|
||
|
||
Интерактивные виджеты лежат в assets/widgets.js и подключаются через custom_js: страница
|
||
вставляется через innerHTML, поэтому скрипты внутри неё не выполняются. Данные моделей
|
||
(data/models.json) дописываются в начало custom_js при сборке.
|
||
"""
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from docslib import HeaderButton, build, run
|
||
from docslib.components import Callout, Code, H1, H2, H3, List, LocalLink, P, Table
|
||
from docslib.hooks import Component, Page
|
||
|
||
HERE = Path(__file__).resolve().parent
|
||
ASSETS = HERE / "assets"
|
||
TITLE = "Нейроморфное распознавание на Алтае"
|
||
|
||
|
||
@Component
|
||
def Lead(text):
|
||
return f'<p class="lead">{text}</p>'
|
||
|
||
|
||
@Component
|
||
def Def(text):
|
||
return f'<p class="def">{text}</p>'
|
||
|
||
|
||
@Component
|
||
def Widget(name):
|
||
return f'<div data-widget="{name}"></div>'
|
||
|
||
|
||
@Component
|
||
def Links(items):
|
||
"""Список со ссылками: пункты уже содержат доверенный HTML LocalLink."""
|
||
return "<ul>" + "".join(f"<li>{item}</li>" for item in items) + "</ul>"
|
||
|
||
|
||
@Component
|
||
def Facts(items):
|
||
cells = "".join(f"<div><b>{value}</b><span>{label}</span></div>" for value, label in items)
|
||
return f'<div class="facts">{cells}</div>'
|
||
|
||
|
||
@Component
|
||
def Pipeline():
|
||
"""Схема системы: хост → чип → хост. Цвета берутся из темы через currentColor."""
|
||
boxes = [
|
||
(10, "Камера", "кадр"), (165, "Хост", "окно и центровка"), (320, "Кодирование", "яркость → такт"),
|
||
(475, "Алтай", "импульсная сеть"), (630, "Хост", "счёт спайков"),
|
||
]
|
||
parts = []
|
||
for i, (x, title, sub) in enumerate(boxes):
|
||
chip = title == "Алтай"
|
||
parts.append(
|
||
f'<rect x="{x}" y="30" width="140" height="70" rx="16" fill="{"currentColor" if chip else "none"}" '
|
||
f'stroke="currentColor" stroke-width="1.5" opacity="{1 if chip else .9}"/>'
|
||
f'<text x="{x + 70}" y="60" text-anchor="middle" font-size="15" font-weight="600" '
|
||
f'fill="{"var(--shell)" if chip else "currentColor"}">{title}</text>'
|
||
f'<text x="{x + 70}" y="82" text-anchor="middle" font-size="11" '
|
||
f'fill="{"var(--shell)" if chip else "currentColor"}" opacity=".75">{sub}</text>'
|
||
)
|
||
if i < len(boxes) - 1:
|
||
parts.append(f'<path d="M{x + 143},65 L{x + 162},65" stroke="currentColor" stroke-width="1.5" marker-end="url(#a)"/>')
|
||
return (
|
||
'<figure><div class="w-scroll"><svg viewBox="0 0 780 130" style="width:100%;min-width:620px;color:var(--text)" role="img" '
|
||
'aria-label="Камера, хост, кодирование, процессор Алтай, хост">'
|
||
'<defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">'
|
||
'<path d="M0,0 L8,4 L0,8" fill="none" stroke="currentColor"/></marker></defs>'
|
||
+ "".join(parts)
|
||
+ '<text x="390" y="122" text-anchor="middle" font-size="12" fill="currentColor" opacity=".7">'
|
||
"обучение — на обычном компьютере; на чип загружаются готовые целочисленные веса</text>"
|
||
"</svg></div><figcaption>Состав системы распознавания</figcaption></figure>"
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------- 1. Обзор
|
||
|
||
@Page("1", "Обзор")
|
||
def overview():
|
||
return [
|
||
H1("Нейроморфное распознавание на Алтае"),
|
||
Lead("Система, в которой распознавание изображений выполняет импульсная нейронная сеть "
|
||
"на нейроморфном процессоре «Алтай», а подготовка входа и чтение ответа — обычный компьютер робота."),
|
||
Def("<b>Нейроморфное распознавание</b> — распознавание образов сетью из импульсных нейронов, "
|
||
"которые обмениваются короткими событиями-спайками и тратят энергию только в момент события. "
|
||
"В отличие от обычной нейросети, вычисление идёт по тактам и разреженно: молчащий нейрон ничего не считает."),
|
||
Pipeline(),
|
||
H2("Из чего состоит"),
|
||
Table(["Часть", "Что это", "Роль в системе"], [
|
||
["Алтай", "нейроморфный процессор Мотив НТ: сетка ядер с целочисленными нейронами", "исполняет импульсную сеть"],
|
||
["KNP", "Kaspersky Neuromorphic Platform: программная платформа импульсных сетей", "описывает сеть, моделирует её на CPU/GPU, загружает на Алтай"],
|
||
["GoldenModel", "программная копия Алтая из бэкенда Мотив НТ", "доказывает, что сеть заработает на чипе"],
|
||
["Хост", "обычный процессор робота", "готовит вход, кодирует его спайками, читает ответ"],
|
||
["Обучение", "обычный компьютер, PyTorch / NumPy", "находит веса с учётом ограничений чипа"],
|
||
]),
|
||
H2("Ключевые свойства"),
|
||
Facts([
|
||
("97,4%", "точность импульсной сети на цифрах MNIST"),
|
||
("8 тактов", "от первого спайка до ответа"),
|
||
("≈143", "входных спайка на картинку — по одному на пиксель"),
|
||
("≈20 тыс.", "синаптических событий на картинку"),
|
||
("8192", "нейрона в 16-ядерном прототипе Алтая"),
|
||
("4", "разных величины веса на нейрон"),
|
||
]),
|
||
Callout("Чип не обучается: в официальном бэкенде Алтая пластичность выключена. Поэтому сеть учат на компьютере "
|
||
"сразу под рамки чипа, а на Алтай загружают готовые целые веса.", title="Главное ограничение", kind="note"),
|
||
H2("Как читать вики"),
|
||
Links([
|
||
LocalLink("Импульсные сети", snn_section) + " — что такое импульсный нейрон, как кодируется вход, как сети учатся.",
|
||
LocalLink("Платформа KNP", knp_section) + " — программная модель сети и способы её исполнения.",
|
||
LocalLink("Процессор Алтай", altai_section) + " — устройство чипа и правила, которым должна подчиняться сеть.",
|
||
LocalLink("Архитектуры распознавания", arch_section) + " — четыре способа построить распознаватель и их сравнение.",
|
||
LocalLink("Распознавание мусора", trash) + " — как всё это ложится на задачу робота.",
|
||
]),
|
||
]
|
||
|
||
|
||
@Page("1.1", "Глоссарий")
|
||
def glossary():
|
||
return [
|
||
H1("Глоссарий"),
|
||
Table(["Термин", "Определение"], [
|
||
["Спайк", "Короткое событие «нейрон выстрелил». Несёт один бит и момент времени."],
|
||
["Такт", "Шаг дискретного времени. За такт нейрон суммирует пришедшие спайки и решает, стрелять ли."],
|
||
["Потенциал (уровень)", "Накопленный заряд нейрона. Когда он достигает порога, нейрон стреляет."],
|
||
["Порог", "Уровень, при котором нейрон выдаёт спайк. На Алтае — целое число."],
|
||
["Синапс, вес", "Связь между нейронами. Вес — на сколько изменится потенциал получателя от одного спайка."],
|
||
["Синаптическое событие", "Одно сложение «спайк × вес». Основная мера работы и энергии импульсной сети."],
|
||
["Популяция", "Группа однотипных нейронов в KNP."],
|
||
["Проекция", "Пучок связей из одной популяции в другую, с весами и задержками."],
|
||
["Частотный код", "Яркость пикселя передаётся частотой спайков."],
|
||
["Временной код", "Яркость передаётся моментом единственного спайка: ярче — раньше."],
|
||
["STDP", "Правило обучения: связь крепнет, если входной спайк пришёл незадолго до выстрела, и слабеет, если после."],
|
||
["Дофамин", "Третий сигнал обучения: награда или наказание, превращающие недавние изменения связей в постоянные."],
|
||
["Суррогатный градиент", "Способ обучать импульсную сеть обратным распространением: ступенька спайка заменяется гладкой функцией."],
|
||
["Квантование", "Перевод дробных весов в небольшой набор целых, который понимает чип."],
|
||
["Кроссбар", "Таблица связей внутри ядра Алтая: строки — входящие линии, столбцы — нейроны."],
|
||
["Размещатель", "Часть бэкенда Алтая, раскладывающая нейроны и связи по ядрам."],
|
||
["GoldenModel", "Эталонная программная модель Алтая, совпадающая с чипом по счётчикам спайков."],
|
||
["Хост", "Обычный процессор, который готовит вход для чипа и читает его ответ."],
|
||
["Валидация и тест", "На валидации выбирают настройки; тест смотрят один раз, иначе оценка завышена."],
|
||
]),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 2. Импульсные сети
|
||
|
||
@Page("2", "Импульсные сети")
|
||
def snn_section():
|
||
return [
|
||
H1("Импульсные нейронные сети"),
|
||
Def("<b>Импульсная нейронная сеть</b> (spiking neural network, SNN) — сеть, в которой нейроны хранят внутреннее "
|
||
"состояние и обмениваются не числами, а событиями-спайками во времени."),
|
||
H2("Чем отличается от обычной нейросети"),
|
||
Table(["", "Обычная нейросеть", "Импульсная сеть"], [
|
||
["Сигнал между нейронами", "вещественное число", "спайк: есть или нет, и когда"],
|
||
["Время", "нет: один проход", "дискретные такты"],
|
||
["Состояние нейрона", "нет", "потенциал, копится между тактами"],
|
||
["Объём вычислений", "все нейроны на каждом входе", "только там, где пришли спайки"],
|
||
["Обучение", "обратное распространение", "STDP, дофамин или суррогатный градиент"],
|
||
["Где выгодна", "сервер, видеокарта", "нейроморфный чип: дрон, робот, датчик"],
|
||
]),
|
||
P("Выигрыш импульсной сети — в разреженности. Если на картинке 150 закрашенных пикселей из 784, "
|
||
"то при временном коде на чип приходит около 150 событий, и каждое затрагивает только свои связи."),
|
||
Links([
|
||
LocalLink("Импульсный нейрон", neuron_page),
|
||
LocalLink("Кодирование входа", coding_page),
|
||
LocalLink("Связи и сложение сигналов", links_page),
|
||
LocalLink("Обучение", learning_page),
|
||
]),
|
||
]
|
||
|
||
|
||
@Page("2.1", "Импульсный нейрон")
|
||
def neuron_page():
|
||
return [
|
||
H1("Импульсный нейрон AltAILIF"),
|
||
Def("<b>AltAILIF</b> — модель нейрона «накопление–утечка–выстрел», повторяющая нейрон процессора «Алтай»: "
|
||
"целочисленный 16-битный потенциал, целые порог, утечка и значение сброса."),
|
||
P("Удобная аналогия — стакан. Каждый входной спайк доливает в него воду (вес связи), вода понемногу "
|
||
"испаряется (утечка), а когда уровень доходит до черты (порог), нейрон выдаёт спайк и стакан частично или полностью опустошается."),
|
||
Widget("neuron"),
|
||
H2("Шаг нейрона за такт"),
|
||
List([
|
||
"к потенциалу прибавляются веса всех пришедших спайков (тормозящие — со знаком минус);",
|
||
"прибавляется утечка; при флаге leak_rev её знак следует за знаком потенциала;",
|
||
"если потенциал ≥ порога — спайк; при is_diff из потенциала вычитается порог, при is_reset он сбрасывается;",
|
||
"если потенциал ниже отрицательного порога — при saturate он упирается в этот порог.",
|
||
], ordered=True),
|
||
Table(["Флаг", "Что делает", "Как используется"], [
|
||
["is_diff", "после выстрела вычитает порог, остаток сохраняется", "заряд сверх порога не теряется"],
|
||
["is_reset", "после выстрела сбрасывает потенциал в значение сброса", "штатный пример KNP"],
|
||
["saturate", "не даёт потенциалу уйти ниже отрицательного порога", "отрицательный порог 0: уровень не ниже нуля"],
|
||
["leak_rev", "утечка всегда тянет потенциал к нулю", "при нулевой утечке нейрон копит заряд всю картинку"],
|
||
]),
|
||
Callout("Все числа нейрона целые, потенциал — 16 бит со знаком (от −32 768 до 32 767). "
|
||
"Сеть, у которой сумма входов за такт выходит за этот диапазон, на чипе не заработает.", title="Ограничение", kind="note"),
|
||
]
|
||
|
||
|
||
@Page("2.2", "Кодирование входа")
|
||
def coding_page():
|
||
return [
|
||
H1("Кодирование входа"),
|
||
Def("<b>Кодирование</b> — правило, по которому хост превращает яркость пикселей в спайки на входе сети."),
|
||
Widget("coding"),
|
||
Table(["", "Частотный код", "Временной код"], [
|
||
["Что несёт информацию", "сколько раз пиксель выстрелил", "в какой такт пиксель выстрелил"],
|
||
["Спайков на пиксель", "до T (числа тактов)", "не больше одного"],
|
||
["Входных спайков на цифру MNIST", "≈730 за 8 тактов, ≈3200 за 32", "≈143 при любом T"],
|
||
["Сколько нужно тактов", "много: точность оттенков растёт с T", "мало: 4–8"],
|
||
["Как обучать", "можно перенести обычную сеть", "нужно обучать по тактам"],
|
||
]),
|
||
P("Частотный код проще: число спайков приближённо равно активации обычной нейросети, поэтому её веса можно перенести. "
|
||
"Временной код экономнее, но нейроны должны реагировать на порядок прихода спайков, поэтому сеть учат прямо на динамике."),
|
||
]
|
||
|
||
|
||
@Page("2.3", "Связи и сложение сигналов")
|
||
def links_page():
|
||
return [
|
||
H1("Связи и сложение сигналов"),
|
||
Def("<b>Проекция</b> — набор связей из одной группы нейронов в другую. Схему соединения выбирает конструктор сети, "
|
||
"обучение меняет только веса."),
|
||
Table(["Схема в KNP", "Кто с кем связан", "Где применяется"], [
|
||
["all_to_all", "каждый с каждым", "полносвязные слои: 784 → 256"],
|
||
["one_to_one", "i-й с i-м", "передача без изменений"],
|
||
["index_based", "по своему правилу, например окно", "детекторы признаков: каждый смотрит в свой кусок"],
|
||
["fixed_probability", "случайно с заданной вероятностью", "разреженные сети"],
|
||
["exclusive", "каждый с каждым, кроме себя", "взаимное торможение, «победитель забирает всё»"],
|
||
]),
|
||
H2("Сложение"),
|
||
P("Всё, что пришло в нейрон за один такт, складывается. У каждой связи есть задержка: спайк, отправленный на такте N "
|
||
"с задержкой D, учитывается на такте N + D."),
|
||
Widget("synsum"),
|
||
H2("Соревнование"),
|
||
P("Нейроны, которые видят одно и то же и учатся по одному правилу, выучивают одно и то же. Взаимное торможение "
|
||
"заставляет их делить работу: учится только тот, кто выстрелил первым."),
|
||
Widget("wta"),
|
||
]
|
||
|
||
|
||
@Page("2.4", "Обучение")
|
||
def learning_page():
|
||
return [
|
||
H1("Обучение импульсных сетей"),
|
||
Def("Спайк — это ступенька, у неё нет производной. Поэтому обычное обратное распространение к импульсной сети напрямую "
|
||
"не применимо, и существует несколько семейств правил обучения."),
|
||
H2("STDP: локальное правило без учителя"),
|
||
P("После выстрела каждый вход разбирается отдельно. Спайк, пришедший незадолго до выстрела, помог — связь крепнет. "
|
||
"Пришедший после — нейрон обошёлся без него, связь слабеет."),
|
||
Widget("stdp"),
|
||
H2("Сравнение способов"),
|
||
Table(["Способ", "Где идёт", "Чему учит", "Цифры MNIST"], [
|
||
["STDP (классический)", "в KNP", "частые узоры, без меток", "—"],
|
||
["STDP с синаптическим ресурсом и дофамином: вес растёт с насыщением, награда закрепляет изменения", "в KNP", "«похоже на мою цифру»", "≈92%"],
|
||
["Дофамин по ошибке: связь меняется на разницу «должен был выстрелить» минус «выстрелил»", "в KNP", "один слой по ошибке ответа", "≈94,8% (валидация)"],
|
||
["Перенос обычной сети", "на компьютере", "всю сеть обратным распространением по аналоговой копии", "97,8%"],
|
||
["Суррогатный градиент по тактам", "на компьютере", "всю сеть прямо на динамике спайков", "97,4% за 8 тактов"],
|
||
]),
|
||
Callout("Чем больше обучения уходит на компьютер, тем выше точность. Алтай не обучается вообще, поэтому для него "
|
||
"естественный путь — обучение на компьютере с учётом ограничений чипа.", title="Вывод", kind="note"),
|
||
H3("Суррогатный градиент"),
|
||
P("Прямой проход идёт по тактам с настоящими спайками. При обратном проходе производная ступеньки заменяется "
|
||
"гладкой функцией 1/(1+4|v|)². Сеть видит ту же арифметику, что и чип: целые веса, вычитание порога, уровень не ниже нуля, задержку в такт."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 3. KNP
|
||
|
||
@Page("3", "Платформа KNP")
|
||
def knp_section():
|
||
return [
|
||
H1("Kaspersky Neuromorphic Platform"),
|
||
Def("<b>KNP</b> — открытая программная платформа (C++ и Python, Apache 2.0) для описания, обучения и исполнения "
|
||
"импульсных сетей на CPU, GPU и нейроморфном процессоре «Алтай»."),
|
||
H2("Модель сети"),
|
||
Table(["Уровень", "Что это"], [
|
||
["Популяция", "группа однотипных нейронов; параметры задаёт функция-генератор"],
|
||
["Проекция", "связи из популяции в популяцию: вес, задержка, тип, номера нейронов"],
|
||
["Network", "популяции и проекции"],
|
||
["Model", "сеть плюс входные и выходные каналы"],
|
||
["ModelExecutor", "прогоняет модель по тактам на выбранном бэкенде"],
|
||
]),
|
||
P("Каждый такт: популяции обновляют нейроны → шина доставляет сообщения → проекции превращают спайки в воздействия."),
|
||
Code('''P = knp::core::Population<AltAILIF>
|
||
R = knp::core::Projection<DeltaSynapse>
|
||
|
||
P hidden(make_neuron, 256); // 256 нейронов AltAILIF
|
||
R input(input_uid, hidden.get_uid(), gen, n); // связи вход → скрытый слой
|
||
Network net; net.add_population(hidden); net.add_projection(input);
|
||
Model model(net); model.add_input_channel(...); model.add_output_channel(...);
|
||
ModelExecutor exec(model, backend, inputs); exec.start(until_step);''', "cpp"),
|
||
Links([LocalLink("Бэкенды и модели нейронов", backends_page)]),
|
||
]
|
||
|
||
|
||
@Page("3.1", "Бэкенды и модели нейронов")
|
||
def backends_page():
|
||
return [
|
||
H1("Бэкенды и модели нейронов"),
|
||
Def("<b>Бэкенд</b> — исполнитель сети. Сеть описывается один раз, а запускается на любом бэкенде, который поддерживает её типы нейронов."),
|
||
Table(["Бэкенд", "Нейроны", "Обучение", "Замечание"], [
|
||
["CPU, один поток", "LIF, BLIFAT, AltAILIF и STDP-варианты", "да", "эталон"],
|
||
["CPU, много потоков", "LIF, BLIFAT", "частично", "AltAILIF не поддерживает"],
|
||
["CUDA (официальный)", "BLIFAT", "нет", "AltAILIF не поддерживает"],
|
||
["CUDA для AltAILIF (разработан в проекте)", "AltAILIF, Delta", "нет", "результат побайтно совпадает с CPU, в 5 раз быстрее на больших наборах"],
|
||
["Алтай: GoldenModel и чип", "AltAILIF, Delta", "нет", "закрытая библиотека Мотив НТ, deb-пакет 1.0.2"],
|
||
]),
|
||
Table(["Модель нейрона", "Что это", "Где исполняется"], [
|
||
["LIF", "классический нейрон с утечкой", "CPU"],
|
||
["BLIFAT", "расширенная модель Kaspersky: адаптивный порог, торможение", "CPU, CUDA"],
|
||
["AltAILIF", "ровно возможности нейрона Алтая", "CPU, CUDA проекта, Алтай"],
|
||
]),
|
||
P("Для распознавателя на Алтае имеет значение только AltAILIF с синапсами Delta: остальные модели на чип не переносятся."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 4. Алтай
|
||
|
||
@Page("4", "Процессор Алтай")
|
||
def altai_section():
|
||
return [
|
||
H1("Процессор «Алтай»"),
|
||
Def("<b>Алтай</b> — нейроморфный процессор компании Мотив НТ: сетка одинаковых ядер, каждое из которых хранит нейроны "
|
||
"и таблицу их связей и обменивается спайками с соседями пакетами."),
|
||
Facts([("16", "ядер в прототипе"), ("8192", "нейрона в прототипе"), ("131 072", "нейрона в максимальной конфигурации"),
|
||
("16 бит", "потенциал со знаком"), ("4", "величины веса на нейрон"), ("нет", "обучения на кристалле")]),
|
||
Links([LocalLink("Сетка ядер", grid_page), LocalLink("Ядро и кроссбар", crossbar_page),
|
||
LocalLink("Ограничения и квантование", rules_page), LocalLink("Размещение и проверка", placement_page)]),
|
||
]
|
||
|
||
|
||
@Page("4.1", "Сетка ядер")
|
||
def grid_page():
|
||
return [
|
||
H1("Сетка ядер"),
|
||
Def("Ядра соединены с соседями; внешний мир подключается через <b>порт</b> — сейчас только с северной стороны сетки, стандартной длины 4 ядра."),
|
||
Widget("chipgrid"),
|
||
P("В эмуляторе по умолчанию сетка 4×4. Число нейронов в ядре — около 512 (8192 нейрона прототипа на 16 ядер; расчётная величина)."),
|
||
]
|
||
|
||
|
||
@Page("4.2", "Ядро и кроссбар")
|
||
def crossbar_page():
|
||
return [
|
||
H1("Ядро и кроссбар"),
|
||
Def("<b>Кроссбар</b> — таблица связей ядра: строки — входящие линии (аксоны), столбцы — нейроны, в клетке — вес. "
|
||
"Пришёл спайк по строке — каждый нейрон-столбец прибавляет вес из этой строки."),
|
||
Widget("crossbar"),
|
||
P("Отсюда главное ограничение: у нейрона не может быть больше 4 разных величин веса. Знак задаётся отдельно и в счёт не идёт."),
|
||
]
|
||
|
||
|
||
@Page("4.3", "Ограничения и квантование")
|
||
def rules_page():
|
||
return [
|
||
H1("Ограничения и квантование"),
|
||
Table(["Правило", "Следствие для сети"], [
|
||
["вес — целое, |w| ≤ 255", "веса после обучения переводятся в целые"],
|
||
["не больше 4 разных величин веса на нейрон", "учить сразу с палитрой, например {0, ±1, ±2, ±4}"],
|
||
["нулевых связей нет", "связь с весом 0 не создаётся"],
|
||
["один тип связи, знак в весе", "торможение — отрицательный вес"],
|
||
["потенциал 16 бит со знаком", "сумма входов за такт ограничена"],
|
||
["начальный потенциал игнорируется", "смещение подаётся отдельными входными спайками"],
|
||
["два спайка по одной входной линии в одном такте считаются одним", "если нужно подать несколько событий за такт, у каждого своя линия"],
|
||
["обучения нет", "веса загружаются готовыми"],
|
||
]),
|
||
H2("Квантование"),
|
||
Def("<b>Квантование</b> — замена дробных весов ступеньками из небольшого набора. Чем меньше ступенек, тем грубее нейрон."),
|
||
Widget("quant"),
|
||
H2("Влезет ли сеть"),
|
||
Widget("fit"),
|
||
]
|
||
|
||
|
||
@Page("4.4", "Размещение и проверка")
|
||
def placement_page():
|
||
return [
|
||
H1("Размещение и проверка"),
|
||
Def("<b>Размещатель</b> — часть бэкенда Алтая, которая оценивает число нужных ядер, раскладывает нейроны и связи и отказывается, "
|
||
"если сеть не помещается. Нейрон, которому нужно больше входов, чем строк в ядре, он делит на несколько."),
|
||
Table(["Ступень проверки", "Что доказывает"], [
|
||
["Целочисленный эталон (NumPy)", "арифметика сети записана верно"],
|
||
["KNP на CPU", "граф собран правильно, счётчики спайков совпадают с эталоном"],
|
||
["KNP на GPU", "то же на большом объёме данных, побайтно"],
|
||
["GoldenModel", "размещение и правила чипа соблюдены, счётчики совпадают"],
|
||
["Чип AltAI_HW", "работа на железе"],
|
||
]),
|
||
Callout("То, что размещатель принял сеть, ещё не доказывает её правильность: размещение может пройти, а счёт — разойтись "
|
||
"с эталоном. Критерий — совпадение счётчика спайков каждого нейрона во всех ступенях.",
|
||
title="Критерий", kind="note"),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 5. Архитектуры
|
||
|
||
@Page("5", "Архитектуры распознавания")
|
||
def arch_section():
|
||
return [
|
||
H1("Архитектуры распознавания"),
|
||
Def("<b>Архитектура</b> распознавателя — устройство слоёв, способ кодирования входа и способ обучения. "
|
||
"Все четыре архитектуры ниже построены на нейронах AltAILIF."),
|
||
Widget("arch"),
|
||
Table(["Архитектура", "Устройство", "Обучение", "Ложится на чип"], [
|
||
["Колонки STDP", "пиксели → 10 колонок × 20 нейронов", "в KNP, STDP + дофамин", "да"],
|
||
["Детекторы + выход", "200 типов детекторов × 196 позиций → хост → 256 → 2", "детекторы k-средними, выход на компьютере", "нет"],
|
||
["Нативная сеть", "784 → 256 → 160 «за/против», 32 такта", "перенос обычной сети", "да, проверено"],
|
||
["Импульсная по тактам", "784 → 256 → 10, 8 тактов, временной код", "суррогатный градиент", "да, по рамкам"],
|
||
]),
|
||
Links([LocalLink("Колонки STDP", columns_page), LocalLink("Детекторы признаков", detectors_page),
|
||
LocalLink("Нативная сеть", native_page), LocalLink("Импульсная сеть по тактам", ticks_page),
|
||
LocalLink("Сравнение архитектур", compare_page)]),
|
||
]
|
||
|
||
|
||
@Page("5.1", "Колонки STDP")
|
||
def columns_page():
|
||
return [
|
||
H1("Колонки STDP"),
|
||
Def("<b>Колонки</b> — архитектура штатного примера KNP: на каждый класс отводится колонка из 20 нейронов, "
|
||
"которые учатся по STDP с дофамином; ответ — колонка, выстрелившая больше всех."),
|
||
H2("Как работает"),
|
||
List(["картинка кодируется частотно за 10 тактов;", "спайки идут во все колонки;",
|
||
"при обучении метка через вспомогательную популяцию будит колонку правильного класса, и её связи с активными пикселями крепнут;",
|
||
"при распознавании соседи в колонке тормозят друг друга, чтобы нейроны специализировались."], ordered=True),
|
||
H2("Свойства"),
|
||
Table(["Свойство", "Значение"], [
|
||
["Точность на MNIST", "≈92%"], ["Обучение на платформе", "да"],
|
||
["Сдвиг цифры на 2 пикселя", "точность падает до ≈64–67%"],
|
||
["Типичная ошибка", "4 → 9: трафарет девятки целиком накрывает четвёрку"],
|
||
]),
|
||
P("Колонка учит «похоже на мою цифру», но не «не путай с соседней», и не получает сигнала «здесь линии нет»: "
|
||
"пустой пиксель молчит. Это потолок архитектуры, а не настроек."),
|
||
]
|
||
|
||
|
||
@Page("5.2", "Детекторы признаков")
|
||
def detectors_page():
|
||
return [
|
||
H1("Детекторы признаков с хостом"),
|
||
Def("Архитектура для фотографий: слой нейронов-детекторов ищет выученные узоры по всей картинке, хост суммирует "
|
||
"их отклики по областям и нормирует, затем небольшая сеть выдаёт класс."),
|
||
Table(["Ступень", "Где исполняется", "Размер"], [
|
||
["пиксели → спайки, 10 тактов", "хост", "32×32×3"],
|
||
["детекторы узоров 6×6×3, шаг 2", "KNP", "200 типов × 196 позиций = 39 200 нейронов"],
|
||
["суммирование по четвертям, нормировка", "хост", "800 чисел"],
|
||
["скрытый слой и выход", "KNP", "256 + 2 нейрона"],
|
||
]),
|
||
P("Точность на кошках и собаках CIFAR-10 — 71,35%, на транспорте — 71,25% (валидация). На Алтай архитектура не ложится: "
|
||
"нейронов в 5 раз больше прототипа, у нейронов 55–110 разных весов вместо 4, потенциалы доходят до ±570 000, "
|
||
"а между слоями работает хост."),
|
||
Callout("На чипе нет общих весов: детектор «вертикальная черта» в каждой позиции — отдельный нейрон. "
|
||
"Поэтому число нейронов растёт как «типы × позиции».", title="Почему так много нейронов", kind="note"),
|
||
]
|
||
|
||
|
||
@Page("5.3", "Нативная сеть")
|
||
def native_page():
|
||
return [
|
||
H1("Нативная сеть"),
|
||
Def("<b>Нативная сеть</b> — двухслойная сеть, целиком составленная из нейронов AltAILIF и уложенная в рамки чипа. "
|
||
"Её веса найдены обучением обычной сети и перенесены."),
|
||
Table(["Слой", "Размер", "Параметры"], [
|
||
["вход", "784", "частотный код, 32 такта"],
|
||
["скрытый", "256 AltAILIF", "веса {0, ±1, ±2, ±4}, порог 32, вычитание порога"],
|
||
["выход", "160 AltAILIF", "10 классов × 8 групп «за» и 8 групп «против», порог 4"],
|
||
["хост", "—", "«за» минус «против», максимум"],
|
||
]),
|
||
H2("Почему выход «за/против»"),
|
||
P("Потенциал не опускается ниже нуля, а у нейрона мало разных весов. Поэтому отрицательные доводы собираются отдельными "
|
||
"нейронами «против», а вычитание делает хост. Так сеть учитывает «здесь есть крыша — значит не 4»."),
|
||
H2("Обучение"),
|
||
List(["обычная сеть 784 → 256 → 10 учится обратным распространением; активация ограничена 0…4 и округляется, как счёт спайков;",
|
||
"веса скрытого слоя переводятся в целые {0, ±1, ±2, ±4};",
|
||
"выход заново обучается по настоящим счётчикам спайков скрытого слоя и раскладывается на группы."], ordered=True),
|
||
Table(["Свойство", "Значение"], [
|
||
["Точность на MNIST", "97,8%; с выпрямлением наклона на хосте — 98,0%"],
|
||
["Проверка", "KNP на CPU и CUDA и GoldenModel: счётчики спайков всех нейронов совпали с эталоном (в GoldenModel — на 1000 картинках)"],
|
||
["Цена", "≈3200 входных спайков и ≈338 тыс. синаптических событий на картинку"],
|
||
]),
|
||
]
|
||
|
||
|
||
@Page("5.4", "Импульсная сеть по тактам")
|
||
def ticks_page():
|
||
return [
|
||
H1("Импульсная сеть по тактам"),
|
||
Def("<b>Импульсная сеть по тактам</b> — сеть 784 → 256 → 10 нейронов AltAILIF с временным кодом входа, "
|
||
"обученная суррогатным градиентом прямо на целочисленной динамике чипа."),
|
||
Widget("ticks"),
|
||
H2("Устройство"),
|
||
Table(["Элемент", "Значение"], [
|
||
["Вход", "каждый пиксель ярче 32 стреляет один раз; такт = ⌊(255 − яркость)·8/224⌋"],
|
||
["Скрытый слой", "256 нейронов, порог 8, веса и смещения {0, ±1, ±2, ±4}"],
|
||
["Выход", "10 нейронов, порог 8, задержка 1 такт"],
|
||
["Ответ", "больше спайков; при равенстве — раньше первый спайк"],
|
||
["Ранний ответ", "такт, после которого лидер больше не меняется"],
|
||
]),
|
||
H2("Обучение"),
|
||
List(["прямой проход идёт по 8 тактам с настоящими спайками и вычитанием порога;",
|
||
"с 9-й эпохи веса в прямом проходе огрубляются до палитры чипа, градиент проходит «насквозь»;",
|
||
"к ошибке добавлен штраф за спайки скрытого слоя — сеть учится молчать, когда может;",
|
||
"настройки выбираются по валидации, итог считается отдельным целочисленным симулятором."], ordered=True),
|
||
Table(["Свойство", "Значение"], [
|
||
["Точность на MNIST", "97,4%"], ["Точность на рисунках", "95,9%"], ["Тактов", "8"],
|
||
["Входных спайков", "≈143"], ["Синаптических событий", "≈20 тыс. — в 17 раз меньше нативной сети"],
|
||
["Проверка", "целочисленный эталон; в KNP и GoldenModel не проверялась"],
|
||
]),
|
||
]
|
||
|
||
|
||
@Page("5.5", "Сравнение архитектур")
|
||
def compare_page():
|
||
return [
|
||
H1("Сравнение архитектур"),
|
||
Def("Сравнение на одной задаче — цифры MNIST, тест 10 000 изображений — по точности, числу тактов и работе чипа."),
|
||
Table(["", "Колонки STDP", "Нативная сеть", "Импульсная по тактам"], [
|
||
["Точность", "92,2%", "97,8%", "97,4%"],
|
||
["Тактов", "15", "32", "8"],
|
||
["Входных спайков", "≈870", "≈3200", "≈143"],
|
||
["Синаптических событий", "≈175 тыс.", "≈338 тыс.", "≈20 тыс."],
|
||
["Нейронов", "200", "416", "266"],
|
||
["Учится на платформе", "да", "нет", "нет"],
|
||
["Проверено в GoldenModel", "—", "да", "ещё нет"],
|
||
]),
|
||
H2("Одна цифра — три сети"),
|
||
P("Нарисуйте цифру или выберите тестовую. Все три сети считаются здесь целочисленно, как на чипе."),
|
||
Widget("compare"),
|
||
H2("Точность против работы чипа"),
|
||
P("Каждая точка — сеть или режим. Чем левее, тем меньше сложений «спайк × вес» и энергии; чем выше, тем точнее."),
|
||
Widget("tradeoff"),
|
||
P("Обрезать нативную сеть по тактам можно до 8 — дальше точность обваливается (84,6% на 4 тактах). "
|
||
"Сеть, обученная по тактам с временным кодом, держит 97% даже на 4 тактах."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 6–8
|
||
|
||
@Page("6", "Обработка на хосте")
|
||
def host_page():
|
||
return [
|
||
H1("Обработка на хосте"),
|
||
Def("<b>Хост</b> — обычный процессор рядом с чипом. Он делает то, что дёшево сделать один раз на картинку: "
|
||
"вырезает и центрирует объект, выпрямляет наклон и превращает яркость в моменты спайков."),
|
||
Widget("hostprep"),
|
||
Table(["Шаг", "Что делает", "Зачем"], [
|
||
["рамка 20×20", "вписывает объект в квадрат 20×20 и ставит центр масс в центр 28×28", "размер и положение не мешают"],
|
||
["выпрямление", "сдвигает строки так, чтобы главная ось стала вертикальной", "наклонный почерк узнаётся лучше: +0,35 п.п. на MNIST"],
|
||
["усреднение", "уменьшает с усреднением по площади", "сохраняет серые края, как в обучающих данных"],
|
||
["временной код", "яркость → номер такта", "один спайк на пиксель"],
|
||
]),
|
||
Callout("Обработка на хосте не должна вклиниваться между слоями сети. Тогда вся сеть живёт на чипе, а хост только готовит вход и читает выход.",
|
||
title="Граница хоста", kind="note"),
|
||
]
|
||
|
||
|
||
@Page("7", "Метрики и методика")
|
||
def metrics_page():
|
||
return [
|
||
H1("Метрики и методика"),
|
||
Table(["Метрика", "Определение"], [
|
||
["Точность", "доля правильных ответов на тесте"],
|
||
["Точность на рисунках", "та же доля, но на тестовых цифрах, перерисованных пером постоянной толщины, как при рисовании мышью"],
|
||
["Синаптические события", "число сложений «спайк × вес» на одну картинку; главная мера энергии на нейроморфном чипе"],
|
||
["Входные спайки", "сколько событий хост отправляет на чип"],
|
||
["Такты", "длительность распознавания в шагах чипа"],
|
||
["Совпадение счётчиков", "счётчик спайков каждого нейрона одинаков в эталоне, KNP и GoldenModel"],
|
||
]),
|
||
H2("Правила"),
|
||
List(["настройки выбираются только на валидации; тест смотрится один раз;",
|
||
"цифра без источника (файл метрик или журнал проверки) не считается результатом;",
|
||
"разница меньше 0,3 п.п. при одном прогоне — в пределах шума;",
|
||
"точность на тесте MNIST не равна точности на рисунках мышью: это другой «почерк»."]),
|
||
P("Рисунок мышью отличается от рукописной цифры: перо одной толщины, жёсткие края, другая толщина линии. Поэтому точность "
|
||
"на рисунках обычно ниже точности на тесте; её сокращают обработкой на хосте и обучением на перерисованных копиях."),
|
||
]
|
||
|
||
|
||
@Page("8", "Распознавание мусора")
|
||
def trash():
|
||
return [
|
||
H1("Распознавание мусора"),
|
||
Def("Целевая задача: робот с процессором «Алтай» распознаёт мусор в кадре своей камеры."),
|
||
H2("Постановка"),
|
||
List(["хост режет кадр на окна и для каждого окна готовит вход сети;",
|
||
"чип отвечает по окну: «мусор / не мусор» или тип;",
|
||
"где находится мусор, видно по сработавшим окнам."]),
|
||
H2("Бюджет нейронов"),
|
||
P("Детектор узоров на чипе стоит нейрон на каждую позицию. Подберите размер окна, шаг и число типов детекторов так, чтобы сеть влезла в прототип."),
|
||
Widget("budget"),
|
||
H2("Что переносится из распознавания цифр"),
|
||
Table(["Приём", "Почему подходит"], [
|
||
["обучение на компьютере под рамки чипа", "чип не учится; точность выше, чем у правил STDP"],
|
||
["временной код и обучение по тактам", "мало спайков и тактов — меньше энергии на кадр"],
|
||
["обработка на хосте только на входе", "вся сеть остаётся на кристалле"],
|
||
["проверка счётчиков в KNP и GoldenModel", "доказательство без платы"],
|
||
]),
|
||
H2("Открытые вопросы"),
|
||
List(["конфигурация чипа и наличие платы;", "классы: «мусор / не мусор» или типы;",
|
||
"камера робота, в том числе событийная;", "данные: открытые наборы и снимки с камеры робота."]),
|
||
]
|
||
|
||
|
||
HEADER = [
|
||
HeaderButton("Обзор", overview),
|
||
HeaderButton("Импульсные сети", snn_section),
|
||
HeaderButton("Алтай", altai_section),
|
||
HeaderButton("Архитектуры", arch_section),
|
||
HeaderButton("Сравнение", compare_page),
|
||
]
|
||
|
||
|
||
def custom_js() -> Path:
|
||
"""Данные моделей + виджеты в одном файле custom_js."""
|
||
out = ASSETS / "inject.generated.js"
|
||
data = json.loads((HERE / "data" / "models.json").read_text(encoding="utf-8"))
|
||
out.write_text("globalThis.WIKI_DATA = " + json.dumps(data, ensure_ascii=False, separators=(",", ":")) + ";\n"
|
||
+ (ASSETS / "widgets.js").read_text(encoding="utf-8"), encoding="utf-8")
|
||
return out
|
||
|
||
|
||
OPTIONS = dict(title=TITLE, assets_dir=str(ASSETS), custom_css=str(ASSETS / "widgets.css"), header_buttons=HEADER)
|
||
|
||
if __name__ == "__main__":
|
||
js = custom_js()
|
||
if len(sys.argv) > 1 and sys.argv[1] == "build":
|
||
result = build(build_directory=str(HERE / "build"), custom_js=str(js), manifest_path="./manifest.json", **OPTIONS)
|
||
print("built", HERE / "build")
|
||
else:
|
||
run(port=1234, host="0.0.0.0", custom_js=str(js), live_preview=True, **OPTIONS)
|