"""Вики «Нейроморфное распознавание на Алтае» на 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'

{text}

' @Component def Def(text): return f'

{text}

' @Component def Widget(name): return f'
' @Component def Links(items): """Список со ссылками: пункты уже содержат доверенный HTML LocalLink.""" return "" @Component def Facts(items): cells = "".join(f"
{value}{label}
" for value, label in items) return f'
{cells}
' @Component def Pipeline(): """Схема системы: хост → чип → хост. Цвета берутся из темы через currentColor.""" boxes = [ (10, "Камера", "кадр"), (165, "Хост", "окно и центровка"), (320, "Кодирование", "яркость → такт"), (475, "Алтай", "импульсная сеть"), (630, "Хост", "счёт спайков"), ] parts = [] for i, (x, title, sub) in enumerate(boxes): chip = title == "Алтай" parts.append( f'' f'{title}' f'{sub}' ) if i < len(boxes) - 1: parts.append(f'') return ( '
' '' '' + "".join(parts) + '' "обучение — на обычном компьютере; на чип загружаются готовые целочисленные веса" "
Состав системы распознавания
" ) # ---------------------------------------------------------------- 1. Обзор @Page("1", "Обзор") def overview(): return [ H1("Нейроморфное распознавание на Алтае"), Lead("Система, в которой распознавание изображений выполняет импульсная нейронная сеть " "на нейроморфном процессоре «Алтай», а подготовка входа и чтение ответа — обычный компьютер робота."), Def("Нейроморфное распознавание — распознавание образов сетью из импульсных нейронов, " "которые обмениваются короткими событиями-спайками и тратят энергию только в момент события. " "В отличие от обычной нейросети, вычисление идёт по тактам и разреженно: молчащий нейрон ничего не считает."), 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("Импульсная нейронная сеть (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("AltAILIF — модель нейрона «накопление–утечка–выстрел», повторяющая нейрон процессора «Алтай»: " "целочисленный 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("Кодирование — правило, по которому хост превращает яркость пикселей в спайки на входе сети."), Widget("coding"), Table(["", "Частотный код", "Временной код"], [ ["Что несёт информацию", "сколько раз пиксель выстрелил", "в какой такт пиксель выстрелил"], ["Спайков на пиксель", "до T (числа тактов)", "не больше одного"], ["Входных спайков на цифру MNIST", "≈730 за 8 тактов, ≈3200 за 32", "≈143 при любом T"], ["Сколько нужно тактов", "много: точность оттенков растёт с T", "мало: 4–8"], ["Как обучать", "можно перенести обычную сеть", "нужно обучать по тактам"], ]), P("Частотный код проще: число спайков приближённо равно активации обычной нейросети, поэтому её веса можно перенести. " "Временной код экономнее, но нейроны должны реагировать на порядок прихода спайков, поэтому сеть учат прямо на динамике."), ] @Page("2.3", "Связи и сложение сигналов") def links_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."), 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("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", "Процессор Алтай") 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)]), ] @Page("4.1", "Сетка ядер") def grid_page(): return [ H1("Сетка ядер"), Def("Ядра соединены с соседями; внешний мир подключается через порт — сейчас только с северной стороны сетки, стандартной длины 4 ядра."), Widget("chipgrid"), P("В эмуляторе по умолчанию сетка 4×4. Число нейронов в ядре — около 512 (8192 нейрона прототипа на 16 ядер; расчётная величина)."), ] @Page("4.2", "Ядро и кроссбар") def crossbar_page(): return [ H1("Ядро и кроссбар"), Def("Кроссбар — таблица связей ядра: строки — входящие линии (аксоны), столбцы — нейроны, в клетке — вес. " "Пришёл спайк по строке — каждый нейрон-столбец прибавляет вес из этой строки."), Widget("crossbar"), P("Отсюда главное ограничение: у нейрона не может быть больше 4 разных величин веса. Знак задаётся отдельно и в счёт не идёт."), ] @Page("4.3", "Ограничения и квантование") def rules_page(): return [ H1("Ограничения и квантование"), Table(["Правило", "Следствие для сети"], [ ["вес — целое, |w| ≤ 255", "веса после обучения переводятся в целые"], ["не больше 4 разных величин веса на нейрон", "учить сразу с палитрой, например {0, ±1, ±2, ±4}"], ["нулевых связей нет", "связь с весом 0 не создаётся"], ["один тип связи, знак в весе", "торможение — отрицательный вес"], ["потенциал 16 бит со знаком", "сумма входов за такт ограничена"], ["начальный потенциал игнорируется", "смещение подаётся отдельными входными спайками"], ["два спайка по одной входной линии в одном такте считаются одним", "если нужно подать несколько событий за такт, у каждого своя линия"], ["обучения нет", "веса загружаются готовыми"], ]), H2("Квантование"), Def("Квантование — замена дробных весов ступеньками из небольшого набора. Чем меньше ступенек, тем грубее нейрон."), Widget("quant"), H2("Влезет ли сеть"), Widget("fit"), ] @Page("4.4", "Размещение и проверка") def placement_page(): return [ H1("Размещение и проверка"), Def("Размещатель — часть бэкенда Алтая, которая оценивает число нужных ядер, раскладывает нейроны и связи и отказывается, " "если сеть не помещается. Нейрон, которому нужно больше входов, чем строк в ядре, он делит на несколько."), Table(["Ступень проверки", "Что доказывает"], [ ["Целочисленный эталон (NumPy)", "арифметика сети записана верно"], ["KNP на CPU", "граф собран правильно, счётчики спайков совпадают с эталоном"], ["KNP на GPU", "то же на большом объёме данных, побайтно"], ["GoldenModel", "размещение и правила чипа соблюдены, счётчики совпадают"], ["Чип AltAI_HW", "работа на железе"], ]), Callout("То, что размещатель принял сеть, ещё не доказывает её правильность: размещение может пройти, а счёт — разойтись " "с эталоном. Критерий — совпадение счётчика спайков каждого нейрона во всех ступенях.", title="Критерий", kind="note"), ] # ---------------------------------------------------------------- 5. Архитектуры @Page("5", "Архитектуры распознавания") def arch_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 тактов, временной код", "суррогатный градиент", "да, по рамкам"], ]), 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("Колонки — архитектура штатного примера 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("Нативная сеть — двухслойная сеть, целиком составленная из нейронов 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("Импульсная сеть по тактам — сеть 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("Хост — обычный процессор рядом с чипом. Он делает то, что дёшево сделать один раз на картинку: " "вырезает и центрирует объект, выпрямляет наклон и превращает яркость в моменты спайков."), 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)