Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFVgZ8Nct8BEh9k7bjuRdw
996 lines
100 KiB
Python
996 lines
100 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, Table
|
||
from docslib.hooks import Component, Page
|
||
|
||
HERE = Path(__file__).resolve().parent
|
||
ASSETS = HERE / "assets"
|
||
TITLE = "Нейроморфное распознавание на Алтае"
|
||
|
||
|
||
@Component
|
||
def Lead(text):
|
||
return f'<p class="lead">{text}</p>'
|
||
|
||
|
||
@Component
|
||
def Text(html):
|
||
"""Абзац с доверенной разметкой: жирный текст и ссылки LocalLink."""
|
||
return f"<p>{html}</p>"
|
||
|
||
|
||
@Component
|
||
def Widget(name):
|
||
return f'<div data-widget="{name}"></div>'
|
||
|
||
|
||
@Component
|
||
def Links(items):
|
||
"""Список со ссылками: пункты уже содержат доверенный HTML LocalLink."""
|
||
return "<ul>" + "".join(f"<li>{item}</li>" for item in items) + "</ul>"
|
||
|
||
|
||
@Component
|
||
def Steps(items, ordered=False):
|
||
"""Список с доверенной разметкой в пунктах."""
|
||
tag = "ol" if ordered else "ul"
|
||
return f"<{tag}>" + "".join(f"<li>{item}</li>" for item in items) + f"</{tag}>"
|
||
|
||
|
||
@Component
|
||
def Next(title, page, why):
|
||
return f'<p class="next">Дальше: {LocalLink(title, page)} — {why}</p>'
|
||
|
||
|
||
@Component
|
||
def Pipeline():
|
||
"""Схема системы: хост → чип → хост. Цвета берутся из темы через currentColor."""
|
||
boxes = [
|
||
(10, "Камера", "кадр"), (165, "Хост", "окно и центровка"), (320, "Кодирование", "яркость → такт"),
|
||
(475, "Алтай", "импульсная сеть"), (630, "Хост", "счёт спайков"),
|
||
]
|
||
parts = []
|
||
for i, (x, title, sub) in enumerate(boxes):
|
||
chip = title == "Алтай"
|
||
parts.append(
|
||
f'<rect x="{x}" y="30" width="140" height="70" rx="16" fill="{"currentColor" if chip else "none"}" '
|
||
f'stroke="currentColor" stroke-width="1.5" opacity="{1 if chip else .9}"/>'
|
||
f'<text x="{x + 70}" y="60" text-anchor="middle" font-size="15" font-weight="600" '
|
||
f'fill="{"var(--shell)" if chip else "currentColor"}">{title}</text>'
|
||
f'<text x="{x + 70}" y="82" text-anchor="middle" font-size="11" '
|
||
f'fill="{"var(--shell)" if chip else "currentColor"}" opacity=".75">{sub}</text>'
|
||
)
|
||
if i < len(boxes) - 1:
|
||
parts.append(f'<path d="M{x + 143},65 L{x + 162},65" stroke="currentColor" stroke-width="1.5" marker-end="url(#a)"/>')
|
||
return (
|
||
'<figure><div class="w-scroll"><svg viewBox="0 0 780 130" style="width:100%;min-width:620px;color:var(--text)" role="img" '
|
||
'aria-label="Камера, хост, кодирование, процессор Алтай, хост">'
|
||
'<defs><marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">'
|
||
'<path d="M0,0 L8,4 L0,8" fill="none" stroke="currentColor"/></marker></defs>'
|
||
+ "".join(parts)
|
||
+ '<text x="390" y="122" text-anchor="middle" font-size="12" fill="currentColor" opacity=".7">'
|
||
"обучение — на обычном компьютере; на чип загружаются готовые целочисленные веса</text>"
|
||
"</svg></div><figcaption>Путь кадра через систему</figcaption></figure>"
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------- 1. Обзор
|
||
|
||
@Page("1", "Обзор")
|
||
def overview():
|
||
return [
|
||
H1("Нейроморфное распознавание на Алтае"),
|
||
Lead("Как заставить нейроморфный процессор «Алтай» распознавать изображения: что умеет чип, какие сети на него ложатся, "
|
||
"как их обучать и как доказать, что сеть заработает на кристалле."),
|
||
H2("Задача"),
|
||
Text("Конечная цель — робот, который сам находит мусор в кадре своей камеры, а распознавание выполняет «Алтай». "
|
||
"Пока цель недостижима напрямую: нет ни данных с робота, ни платы с чипом, а сам путь от картинки до ответа на "
|
||
"чипе нужно было сначала построить. Поэтому вся техника отработана на рукописных цифрах MNIST — "
|
||
"задаче, где ответ известен, а ошибку легко увидеть глазами. Всё, что описано в вики, переносится на мусор, "
|
||
"кроме самих весов сети."),
|
||
H2("Зачем нейроморфный чип"),
|
||
Text("Обычная нейросеть на каждую картинку перемножает все входы на все веса. Для сети 784 → 256 → 10 это "
|
||
"<b>203 264</b> умножения со сложением, сколько бы пикселей ни было закрашено. Импульсная сеть считает только там, "
|
||
"где что-то произошло: нейрон, получивший спайк, прибавляет к своему уровню вес связи, и всё. Лучшая сеть из этой "
|
||
"вики распознаёт цифру за <b>≈20 тысяч</b> таких сложений — в 10 раз меньше операций, и это сложения, а не умножения. "
|
||
"На роботе с батареей это и есть довод в пользу чипа."),
|
||
Callout("Энергию самого «Алтая» на этих сетях никто не измерял: платы в проекте нет. Сравнение идёт по числу операций — "
|
||
"синаптических событий, а не по джоулям.", title="Оговорка", kind="note"),
|
||
H2("Как устроена система"),
|
||
Pipeline(),
|
||
Text("Камера даёт кадр. Хост — обычный процессор робота — вырезает окно с объектом, выравнивает его и превращает яркость "
|
||
"пикселей в спайки. Чип прогоняет спайки через сеть по тактам. Хост считает выходные спайки и выбирает ответ. "
|
||
"Обучение в эту цепочку не входит: «Алтай» не умеет менять веса, поэтому сеть учат на компьютере и загружают готовой."),
|
||
H2("Что уже доказано, а что нет"),
|
||
Table(["Сеть", "Точность на MNIST", "Где проверена", "Статус"], [
|
||
["Нативная сеть, 784 → 256 → 160", "97,76%", "KNP на CPU и GPU — 10 000 цифр; GoldenModel — 1000 цифр (97,50%)",
|
||
"счётчики спайков всех нейронов совпали с эталоном; ближе всего к чипу"],
|
||
["Импульсная сеть по тактам, 784 → 256 → 10", "97,42%", "целочисленный эталон на Python и в браузере",
|
||
"в 17 раз экономнее нативной; в KNP и GoldenModel ещё не запускалась"],
|
||
["Колонки STDP (пример KNP)", "92,2%", "KNP на CPU", "единственная сеть, которая учится в самой платформе; держится за положение цифры"],
|
||
["Дофамин по ошибке", "94,8% (валидация)", "KNP на CPU с доработанным бэкендом", "учится в платформе, но признаки штрихов считает хост"],
|
||
["Детекторы признаков, кошки/собаки", "71,35% на CIFAR-10", "KNP на GPU", "на «Алтай» не ложится: в 5 раз больше нейронов и десятки весов на нейрон"],
|
||
]),
|
||
Text("Физический чип не проверялся ни разу. Самое сильное доказательство — программная копия чипа GoldenModel из "
|
||
"официального бэкенда: на ней нативная сеть дала те же спайки, что и эталон, нейрон в нейрон."),
|
||
H2("Порядок чтения"),
|
||
Text("Вики построена как цепочка вопросов. "
|
||
f"{LocalLink('Импульсная сеть', snn_section)} — как считает сеть из спайков. "
|
||
f"{LocalLink('Процессор «Алтай»', altai_section)} — что из этого умеет чип и какие правила он навязывает. "
|
||
f"{LocalLink('Платформа KNP', knp_section)} — чем сеть описывают и запускают. "
|
||
f"{LocalLink('Обучение под «Алтай»', learning_section)} — главная часть: четыре способа получить веса и их сравнение. "
|
||
f"{LocalLink('Вход: хост и рисунки', host_page)} — почему рисунок мышью распознаётся хуже теста и как это лечить. "
|
||
f"{LocalLink('От цифр к мусору', trash_section)} — что из этого переносится на робота и чего не хватает."),
|
||
Next("Импульсная сеть", snn_section, "с чего начинается любая сеть на «Алтае»."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 2. Импульсная сеть
|
||
|
||
@Page("2", "Импульсная сеть")
|
||
def snn_section():
|
||
return [
|
||
H1("Импульсная сеть"),
|
||
Lead("Сеть, в которой нейроны хранят заряд между тактами и обмениваются не числами, а событиями-спайками."),
|
||
Text("Обычная нейросеть — это формула: картинка на входе, числа на выходе, один проход. Импульсная сеть — это процесс во "
|
||
"времени. На каждом такте нейроны получают спайки, копят заряд и, дойдя до порога, сами выдают спайк. "
|
||
"Информация живёт в том, <b>сколько</b> спайков пришло и <b>когда</b>."),
|
||
Table(["", "Обычная нейросеть", "Импульсная сеть"], [
|
||
["Сигнал между нейронами", "вещественное число", "спайк: есть или нет"],
|
||
["Время", "нет, один проход", "такты; нейрон помнит заряд между ними"],
|
||
["Работа на картинку", "все веса, всегда", "только веса связей, по которым пришёл спайк"],
|
||
["Арифметика", "умножение и сложение", "сложение веса"],
|
||
["Обучение", "обратное распространение ошибки", "спайк не дифференцируется — нужны обходные пути"],
|
||
]),
|
||
Text("Три страницы раздела идут снизу вверх: "
|
||
f"{LocalLink('нейрон', neuron_page)}, {LocalLink('вход', coding_page)}, {LocalLink('слой', layer_page)}. "
|
||
"Все примеры — на нейроне AltAILIF, том самом, что стоит на «Алтае»."),
|
||
Next("Нейрон «Алтая»", neuron_page, "из чего всё собирается."),
|
||
]
|
||
|
||
|
||
@Page("2.1", "Нейрон «Алтая»")
|
||
def neuron_page():
|
||
return [
|
||
H1("Нейрон «Алтая»: AltAILIF"),
|
||
Lead("Целочисленный нейрон «накопление — утечка — выстрел». Всё, что умеет сеть на чипе, сводится к правилам этого нейрона."),
|
||
H2("Правило такта"),
|
||
List([
|
||
"к уровню (потенциалу) прибавляются веса всех связей, по которым в этот такт пришёл спайк;",
|
||
"прибавляется утечка — постоянная добавка, тянущая уровень вверх или вниз;",
|
||
"если уровень ≥ порога — нейрон выдаёт спайк, и уровень либо уменьшается на порог (флаг is_diff), "
|
||
"либо сбрасывается в заданное значение (флаг is_reset);",
|
||
"если уровень ниже отрицательного порога — при флаге saturate он упирается в этот порог.",
|
||
], ordered=True),
|
||
Text("Все числа целые: веса, порог, утечка. Уровень хранится в 16 битах со знаком, от −32 768 до 32 767."),
|
||
H2("Пример по тактам"),
|
||
Text("Порог 8, после выстрела порог вычитается, ниже нуля уровень не опускается — ровно так настроены скрытые нейроны "
|
||
"сетей в этой вики."),
|
||
Table(["Такт", "Пришло", "Уровень после сложения", "Спайк", "Уровень в конце такта"], [
|
||
["0", "+4, +4, +2", "10", "да", "2 — остаток сохранён"],
|
||
["1", "+4", "6", "нет", "6"],
|
||
["2", "+2, +4", "12", "да", "4"],
|
||
["3", "−4, −4", "−4", "нет", "0 — ниже нуля нельзя"],
|
||
["4", "+4", "4", "нет", "4"],
|
||
]),
|
||
Widget("neuron"),
|
||
Text("Поставьте вес 3 и порог 9 и посмотрите, как часто нейрон стреляет при вычитании порога и при сбросе в ноль. "
|
||
"С вычитанием спайков ровно ⌊заряд / порог⌋: заряд 21 при пороге 3 даёт 7 спайков. Это проверено на официальной "
|
||
"программной копии чипа, а не только в описании."),
|
||
H2("Почему такие флаги"),
|
||
Text("<b>Вычитание порога</b> превращает нейрон в счётчик: число спайков пропорционально полученному заряду. "
|
||
"Поэтому частоту спайков можно приравнять к выходу обычного нейрона и переносить в импульсную сеть веса обычной — "
|
||
f"на этом построена {LocalLink('нативная сеть', native_page)}. При сбросе в ноль излишек сгорает, и счёт искажается."),
|
||
Text("<b>Пол на нуле</b> защищает от переполнения и упрощает проверку, но у него есть цена: нейрон не помнит "
|
||
"«отрицательных доводов». Если на такте 0 пришло −8, а на такте 1 +8, нейрон всё равно выстрелит, хотя в сумме "
|
||
"заряд нулевой. Отсюда две особенности сетей ниже: выход «за/против» у нативной сети и важность порядка спайков "
|
||
"у сети по тактам."),
|
||
Table(["Флаг", "Что делает", "Как выставлен в сетях вики"], [
|
||
["is_diff", "после выстрела вычитает порог", "включён: остаток не теряется"],
|
||
["is_reset", "после выстрела сбрасывает уровень", "выключен"],
|
||
["saturate + отрицательный порог", "не пускает уровень ниже отрицательного порога", "включён, порог 0: уровень ≥ 0"],
|
||
["утечка", "добавка каждый такт; при leak_rev тянет к нулю", "0: нейрон копит заряд всю картинку"],
|
||
]),
|
||
Next("Вход: яркость в спайки", coding_page, "как в нейрон попадает картинка."),
|
||
]
|
||
|
||
|
||
@Page("2.2", "Вход: яркость в спайки")
|
||
def coding_page():
|
||
return [
|
||
H1("Вход: как яркость становится спайками"),
|
||
Lead("Чип принимает только спайки. Хост решает, каким правилом превратить яркость пикселя в события, — и от этого "
|
||
"правила зависят и точность, и число тактов, и работа чипа."),
|
||
H2("Частотный код"),
|
||
Text("У каждого пикселя есть накопитель. На каждом такте к нему прибавляется яркость (0…255); как только накопитель "
|
||
"дошёл до 255, пиксель выдаёт спайк, и 255 вычитается. Белый пиксель стреляет каждый такт, серый 128 — через такт, "
|
||
"чёрный молчит. За T тактов пиксель даёт примерно яркость × T / 255 спайков."),
|
||
H2("Временной код"),
|
||
Text("Каждый пиксель ярче 32 стреляет <b>один раз</b>, и яркость записана в моменте: такт = ⌊(255 − яркость) · T / 224⌋. "
|
||
"При T = 8 белый пиксель стреляет на такте 0, яркость 200 — на такте 1, 128 — на такте 4, 40 — на такте 7. "
|
||
"Тусклые края штриха приходят последними."),
|
||
Widget("coding"),
|
||
Table(["", "Частотный код", "Временной код"], [
|
||
["Чем несёт яркость", "числом спайков", "моментом единственного спайка"],
|
||
["Входных спайков на цифру MNIST", "≈730 за 8 тактов, ≈3200 за 32", "≈143 при любом числе тактов"],
|
||
["Что нужно для точности", "много тактов: оттенки различаются только при большом T", "4–8 тактов"],
|
||
["Как получить веса", "можно перенести обычную сеть: число спайков ≈ активация", "сеть надо учить прямо на тактах"],
|
||
]),
|
||
Text("Частотный код удобен для переноса, но дорог: картинка из 143 закрашенных пикселей превращается в 3200 событий. "
|
||
"Временной код отправляет на чип по одному событию на закрашенный пиксель — меньше уже нельзя."),
|
||
Callout("Подать яркость пачкой спайков в одном такте нельзя: если в одном такте по одной входной линии пришло несколько "
|
||
"спайков, «Алтай» засчитывает один. Это проверено на GoldenModel: 25 одинаковых спайков дали эффект одного. "
|
||
"Поэтому сила сигнала разворачивается во времени или раскладывается по разным линиям.",
|
||
title="Ограничение чипа", kind="warning"),
|
||
Next("Слой: сложение и торможение", layer_page, "что происходит, когда спайки доходят до слоя нейронов."),
|
||
]
|
||
|
||
|
||
@Page("2.3", "Слой: сложение и торможение")
|
||
def layer_page():
|
||
return [
|
||
H1("Слой: сложение, задержка, торможение"),
|
||
Lead("Слой — это группа нейронов и набор связей к ней. Связи только складывают; всё остальное делает порог."),
|
||
H2("Сложение за такт"),
|
||
Text("Всё, что пришло в нейрон за такт, складывается со своими весами, включая отрицательные. У каждой связи есть задержка "
|
||
"не меньше одного такта: спайк, выпущенный на такте N, доходит на такт N + 1. Поэтому в сети из двух слоёв ответ "
|
||
"отстаёт от входа на такт, а после последнего входного такта нужен ещё один, чтобы выход получил всё."),
|
||
Widget("synsum"),
|
||
H2("Торможение и «победитель забирает всё»"),
|
||
Text("Отрицательный вес — это торможение. Если связать нейроны слоя друг с другом отрицательными связями, первый "
|
||
"выстреливший гасит остальных. Так делают, когда нейроны видят одно и то же и должны поделить работу: без торможения "
|
||
"одинаковые нейроны с одинаковым правилом обучения выучивают одно и то же. На этом построено соревнование в "
|
||
f"{LocalLink('колонках STDP', columns_page)}."),
|
||
Widget("wta"),
|
||
H2("Кто с кем связан"),
|
||
Text("Схему связей задаёт конструктор сети, обучение меняет только веса. В сетях этой вики встречаются две схемы: "
|
||
"полносвязная (каждый вход с каждым нейроном слоя, 784 × 256) и локальная, когда нейрон смотрит в своё окно "
|
||
f"картинки. Локальная — основа {LocalLink('детекторов признаков', objects_page)}, и у неё на чипе есть цена: "
|
||
"общих весов нет, каждый детектор в каждой позиции — отдельный нейрон."),
|
||
Next("Процессор «Алтай»", altai_section, "какие из этих возможностей есть у чипа и с какими ограничениями."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 3. Алтай
|
||
|
||
@Page("3", "Процессор «Алтай»")
|
||
def altai_section():
|
||
return [
|
||
H1("Процессор «Алтай»"),
|
||
Lead("Нейроморфный процессор компании Мотив НТ: сетка одинаковых ядер, в каждом — нейроны AltAILIF и таблица их связей."),
|
||
Table(["Характеристика", "Значение"], [
|
||
["Прототип «Алтай-2»", "16 ядер, 8192 нейрона"],
|
||
["Масштабируемая конфигурация", "до 131 072 нейронов"],
|
||
["Нейрон", "AltAILIF: целые числа, уровень 16 бит со знаком"],
|
||
["Связь", "целый вес, |w| ≤ 255; не больше 4 разных величин веса на нейрон"],
|
||
["Обучение на кристалле", "нет: в официальном бэкенде пластичность выключена"],
|
||
["Ввод и вывод", "через порт с северной стороны сетки ядер"],
|
||
]),
|
||
Text("Источник цифр — руководство KNP 2.0. В нём описан «Алтай-2»; переносить эти числа на другие поколения чипа нельзя."),
|
||
H2("Чем мы пользуемся вместо чипа"),
|
||
Text("Платы нет, поэтому вместо чипа — официальный бэкенд Мотив НТ для KNP, пакет knp-altai-backend 1.0.2 из релиза "
|
||
"KNP 2.0.0. В нём два устройства: AltAI_HW — сам чип, и AltAI_GM — <b>GoldenModel</b>, программная копия, "
|
||
"которая считает так же, как кристалл, и раскладывает сеть по ядрам тем же размещателем. По умолчанию это "
|
||
"сетка 4 × 4 ядра. Библиотека закрытая: её поведение изучено только опытами, без дизассемблирования. "
|
||
"В самом пакете устройство называется AltAIv1_GoldenModel, хотя руководство говорит об «Алтае-2»."),
|
||
Links([
|
||
LocalLink("Ядро и веса", core_page) + " — откуда правило «4 величины веса на нейрон».",
|
||
LocalLink("Правила для сети и как их обходить", rules_page) + " — полный список ограничений и решения, проверенные на GoldenModel.",
|
||
LocalLink("Проверка в GoldenModel", verify_page) + " — как доказать, что сеть заработает, и на чём легко обмануться.",
|
||
]),
|
||
Next("Ядро и веса", core_page, "почему у нейрона так мало разных весов."),
|
||
]
|
||
|
||
|
||
@Page("3.1", "Ядро и веса")
|
||
def core_page():
|
||
return [
|
||
H1("Ядро и веса"),
|
||
Lead("Ядро хранит своих нейронов и связи к ним. Входящий спайк приходит по линии, и каждый нейрон ядра прибавляет вес, "
|
||
"записанный для этой линии."),
|
||
Text("Удобно представлять ядро таблицей: строки — входящие линии, столбцы — нейроны, в клетке — вес. Спайк по строке "
|
||
"заставляет каждый нейрон-столбец прибавить свою клетку. Руководство KNP формулирует главное ограничение так: "
|
||
"у нейрона не больше <b>четырёх разных величин веса</b>, знак выбирается отдельно и в счёт не идёт. "
|
||
"Вес 3 и вес −3 — одна величина; веса 1, 2, 4 и 255 — уже четыре."),
|
||
Widget("crossbar"),
|
||
Text("Поменяйте веса в таблице: когда у столбца появляется пятая величина, нейрон перестаёт помещаться в правило. "
|
||
"Обычная сеть после обучения имеет у каждого нейрона сотни разных весов — поэтому без квантования её на чип не положить."),
|
||
H2("Что занимает «величину»"),
|
||
Text("Величины расходуют не только обученные веса. В нативной сети у скрытого нейрона веса {1, 2, 4} и ещё −255 для "
|
||
"очистки уровня между картинками — ровно четыре. Обученным весам остаётся три уровня, и это определило палитру "
|
||
f"{{0, ±1, ±2, ±4}} во всех сетях вики. Подробно — на странице {LocalLink('правил', rules_page)}."),
|
||
Next("Правила для сети и обходы", rules_page, "все ограничения чипа сразу и обход каждого."),
|
||
]
|
||
|
||
|
||
@Page("3.2", "Правила для сети и обходы")
|
||
def rules_page():
|
||
return [
|
||
H1("Правила для сети и как их обходить"),
|
||
Lead("Сеть, которая нарушает хоть одно правило, на «Алтай» не загрузится или посчитает не то. Ниже — все ограничения, "
|
||
"найденные в руководстве и опытах на GoldenModel, и решения, которыми нативная сеть их обходит."),
|
||
Table(["Правило чипа", "Чем мешает", "Решение"], [
|
||
["Вес — целое, |w| ≤ 255", "обученные веса дробные", "квантование: вес × масштаб, округление до палитры"],
|
||
["Не больше 4 величин веса на нейрон", "у обученного нейрона сотни разных весов",
|
||
"палитра {0, ±1, ±2, ±4}; у выходных нейронов вход делится на группы по 32 со своими уровнями"],
|
||
["Связи с весом 0 не бывает", "нулевые веса после квантования",
|
||
"нулевая связь просто не создаётся; входные линии без связей не отправляются, иначе ошибка «неизвестный вход»"],
|
||
["Один тип связи, знак в весе", "тормозящая связь типа INHIBITORY не прошла в GoldenModel",
|
||
"торможение — связь EXCITATORY с отрицательным весом"],
|
||
["Начальный уровень нейрона не учитывается", "смещение (bias) нельзя задать стартовым зарядом",
|
||
"смещение +b — это b «тактовых» входных линий с весом +1, которые стреляют каждый такт"],
|
||
["Повтор одной линии в такте = один спайк", "нельзя подать сильный сигнал пачкой",
|
||
"сила разворачивается во времени; очистка идёт по 128 разным линиям"],
|
||
["Уровень — 16 бит со знаком", "сумма за картинку может переполниться",
|
||
"до загрузки считается верхняя граница: T × сумма положительных весов; у нативной сети 12 864 и 1 120"],
|
||
["Уровень не сбрасывается между картинками сам", "заряд прошлой картинки портит следующую",
|
||
"после картинки — пачка −255 по 128 линиям, всего −32 640: сначала чистится скрытый слой, потом выход"],
|
||
["Обучения нет", "веса нельзя подстроить на месте", "обучение на компьютере, загрузка готовых весов"],
|
||
]),
|
||
H2("Квантование"),
|
||
Text("Квантование заменяет дробный вес ближайшим из палитры. Чем меньше ступенек, тем грубее нейрон: из тонкого "
|
||
"профиля весов остаются три уровня. Ошибку квантования сеть переживает, если учится уже с огрублёнными весами — "
|
||
f"так сделано в {LocalLink('сети по тактам', ticks_page)}."),
|
||
Widget("quant"),
|
||
H2("Влезет ли сеть"),
|
||
Text("Три проверки, которые можно сделать до размещателя: число нейронов, число разных весов у самого «разнообразного» "
|
||
"нейрона и наибольший уровень. Переключитесь на сеть детекторов — она проваливает все три."),
|
||
Widget("fit"),
|
||
Callout("Эти три числа необходимы, но недостаточны. Размещатель проверяет ещё входящие линии ядер и маршруты, а "
|
||
"правильность счёта доказывает только сравнение спайков с эталоном.", title="Предел оценки", kind="note"),
|
||
Next("Проверка в GoldenModel", verify_page, "как это сравнение устроено."),
|
||
]
|
||
|
||
|
||
@Page("3.3", "Проверка в GoldenModel")
|
||
def verify_page():
|
||
return [
|
||
H1("Проверка в GoldenModel"),
|
||
Lead("Сеть считается рабочей на «Алтае», когда счётчик спайков каждого нейрона на каждой картинке совпал с "
|
||
"эталоном. Совпадения точности недостаточно."),
|
||
H2("Лестница проверки"),
|
||
Table(["Ступень", "Что доказывает", "Время на нативной сети"], [
|
||
["Целочисленный эталон (NumPy)", "арифметика сети записана верно", "—"],
|
||
["KNP, однопоточный CPU", "граф собран правильно; счётчики равны эталону", "65 с на 10 000 цифр"],
|
||
["KNP, наш CUDA-бэкенд", "то же на больших объёмах, побайтно равно CPU", "11,6 с на 10 000 цифр"],
|
||
["GoldenModel", "сеть размещается по ядрам и считает по правилам чипа", "≈6 с на одну цифру; 1000 цифр — 143 с в 8 процессах"],
|
||
["Чип AltAI_HW", "работа на железе", "не проверялось"],
|
||
]),
|
||
Text("Почему счётчики, а не точность: две сети с разными ошибками могут дать одинаковый процент. Совпадение 256 скрытых "
|
||
"и 160 выходных счётчиков на каждой картинке исключает и ошибку в весах, и потерянный спайк, и неочищенный уровень."),
|
||
H2("Размещатель может принять сеть и всё равно посчитать не то"),
|
||
Text("Чтобы ускорить проверку, в GoldenModel загрузили 8 копий нативной сети сразу. Размещатель граф принял и выделил "
|
||
"24 ядра при стандартной сетке 4 × 4. Счёт разошёлся: 26 495 расхождений скрытых счётчиков на 319 картинках из 1000, "
|
||
"точность 93,6% вместо 97,5%. Явная сетка 8 × 8 тоже не помогла. Причина не установлена."),
|
||
Text("Вывод: принятие графа размещателем ничего не доказывает. Рабочий режим — не больше двух копий сети на процесс "
|
||
"в стандартной сетке 4 × 4, а параллельность — отдельными процессами эмулятора. Исполнитель нативной сети "
|
||
"отказывается запускать GoldenModel с большим числом копий."),
|
||
H2("Что проверено опытами"),
|
||
Text("Поведение закрытой библиотеки установлено крошечными сетями на GoldenModel:"),
|
||
Table(["Опыт", "Результат", "Вывод"], [
|
||
["заряд 21, порог 3, вычитание порога", "7 спайков", "остаток сохраняется"],
|
||
["25 одинаковых входных спайков в такте, вес 1, порог 21", "0 спайков", "повторы схлопываются в один"],
|
||
["начальный уровень 21, порог 3, без входа", "0 спайков", "начальный уровень не учитывается"],
|
||
["утечка +1, порог 3", "спайки на тактах 3, 6, 9, 12, 15", "утечка работает как постоянный вход"],
|
||
["24 / 128 / 512 тактов крошечной сети", "0,69 / 3,3 / 13 с", "у эмулятора большие накладные расходы на такт"],
|
||
]),
|
||
H2("Правила прогонов"),
|
||
List([
|
||
"каждый прогон — в новый пустой каталог; старый результат не может замаскировать новый сбой;",
|
||
"до и после прогона сверяются хеши модели и исполняемого файла;",
|
||
"проверяется точный список номеров картинок, а не только их количество;",
|
||
"при первом расхождении счётчиков прогон останавливается и пишет FAILED.json; VERIFIED.json появляется только при полном совпадении.",
|
||
]),
|
||
Text("Итог для нативной сети: 975 из 1000 первых тестовых цифр в GoldenModel, все счётчики совпали. "
|
||
"Полные 10 000 в GoldenModel не запускались."),
|
||
Next("Платформа KNP", knp_section, "в чём эту сеть описывают и чем запускают."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 4. KNP
|
||
|
||
@Page("4", "Платформа KNP")
|
||
def knp_section():
|
||
return [
|
||
H1("Платформа KNP"),
|
||
Lead("Kaspersky Neuromorphic Platform — открытая библиотека (C++ и Python, Apache 2.0): сеть описывается один раз и "
|
||
"исполняется на CPU, GPU или «Алтае»."),
|
||
H2("Из чего состоит модель"),
|
||
Table(["Объект", "Что это"], [
|
||
["Population", "группа однотипных нейронов; параметры каждого задаёт функция-генератор"],
|
||
["Projection", "связи в популяцию: для каждой — вес, задержка, тип, номер источника и получателя"],
|
||
["Network", "набор популяций и проекций"],
|
||
["Model", "сеть плюс входные каналы (куда хост шлёт спайки) и выходные (откуда читает)"],
|
||
["ModelExecutor", "гоняет модель по тактам на выбранном бэкенде и вызывает код хоста каждый такт"],
|
||
]),
|
||
Text("Каждый такт исполнитель берёт входные спайки из каналов, бэкенд обновляет нейроны и доставляет спайки по проекциям, "
|
||
"выходные спайки складываются в каналы. Хост между тактами решает, что подать дальше."),
|
||
H2("Нативная сеть в коде"),
|
||
Text("Сокращённый фрагмент из mnist-altai-native/native_digits.cpp. Видны все обходы со страницы "
|
||
f"{LocalLink('правил', rules_page)}: тактовые линии смещения, очистка по 128 линиям весом −255, только тип EXCITATORY."),
|
||
Code('''using P = knp::core::Population<knp::neuron_traits::AltAILIF>;
|
||
using R = knp::core::Projection<knp::synapse_traits::DeltaSynapse>;
|
||
|
||
auto make = [&](int thr) { // нейрон: вычитание порога, уровень не ниже 0, без утечки
|
||
N n; n.activation_threshold_ = thr; n.negative_activation_threshold_ = 0;
|
||
n.is_diff_ = true; n.is_reset_ = false; n.saturate_ = true; n.potential_leak_ = 0; return n;
|
||
};
|
||
P hidden([&](size_t) { return make(32); }, 256);
|
||
P output([&](size_t) { return make(4); }, 160);
|
||
|
||
auto add = [](auto& v, int pre, int post, int w) { // нулевых связей нет, знак — в весе
|
||
if (w) v.push_back(R::Synapse{{float(w), 1, OutputType::EXCITATORY}, size_t(pre), size_t(post)});
|
||
};
|
||
for (int h = 0; h < 256; h++) {
|
||
for (int p = 0; p < 784; p++) add(hin, p, h, w1[p * 256 + h]); // пиксели
|
||
for (int c = 0; c < abs(b1[h]); c++) add(hin, 784 + c, h, b1[h] > 0 ? 1 : -1); // смещение
|
||
for (int c = 0; c < 128; c++) add(hin, 784 + clock + c, h, -255); // очистка
|
||
}
|
||
Network net; net.add_population(hidden); net.add_population(output);
|
||
net.add_projection(R(input_uid, hidden.get_uid(), from(hin), hin.size())); /* … */
|
||
Model model(std::move(net));
|
||
model.add_input_channel(in_ch, input_projection_uid);
|
||
model.add_output_channel(out_ch, output.get_uid());
|
||
ModelExecutor exec(model, backend, {{in_ch, send_spikes}});
|
||
exec.start([&](size_t step) { return !done; });''', "cpp"),
|
||
H2("Бэкенды"),
|
||
Table(["Бэкенд", "Нейроны", "Обучение", "Замечание"], [
|
||
["CPU, однопоточный", "LIF, BLIFAT, AltAILIF и их STDP-варианты", "да", "эталон KNP"],
|
||
["CPU, многопоточный", "LIF, BLIFAT", "частично", "AltAILIF не поддерживает"],
|
||
["CUDA официальный", "BLIFAT", "нет", "AltAILIF не поддерживает"],
|
||
["CUDA для AltAILIF", "AltAILIF + Delta", "нет", "написан в проекте; побайтно совпадает с CPU, в 5,6 раза быстрее на пачке из 8 картинок"],
|
||
["AltAI: GoldenModel и чип", "AltAILIF + Delta", "нет", "закрытая библиотека Мотив НТ, пакет 1.0.2"],
|
||
]),
|
||
Text("Для «Алтая» имеет смысл только связка AltAILIF + DeltaSynapse: модели LIF и BLIFAT на чип не переносятся, "
|
||
"а STDP работает только на CPU."),
|
||
Next("Подводные камни KNP", knp_pitfalls, "где KNP ведёт себя не так, как ожидаешь."),
|
||
]
|
||
|
||
|
||
@Page("4.1", "Подводные камни KNP")
|
||
def knp_pitfalls():
|
||
return [
|
||
H1("Подводные камни KNP"),
|
||
Lead("Ошибки и неочевидное поведение, найденные при работе с KNP 2.0.0. Каждое стоило дней, поэтому записано."),
|
||
Table(["Что", "Симптом", "Причина и решение"], [
|
||
["ModelLoader::init_channels", "примерно каждое 8–12-е обучение молча проваливается: веса остаются стартовыми, точность случайная",
|
||
"вход картинки иногда подписывается на канал меток. Исправлено в репозитории проекта, в KasperskyLab/knp не отправлено"],
|
||
["Порог BLIFAT после обучения", "пример mnist-learn молчит на 68% тестовых цифр (30,9% верных)",
|
||
"добавка к порогу растёт при обучении и застывает. Порог на проверке задаётся отдельно: 75,5%"],
|
||
["Одинаковые нейроны колонки", "20 нейронов колонки имеют одинаковые веса и работают как один",
|
||
"одинаковый старт и общий сигнал метки. Разброс стартовых весов σ = 20: 82,5% → 92,2%"],
|
||
["Дофамин опаздывает на такт", "обучение по ошибке не сходится",
|
||
"дофамин приходит на такт позже входных спайков; нужна поправка KNP_DELTA_LAG=1"],
|
||
["Обучение молчащего нейрона", "штатный STDP не меняет связи нейрона, который не выстрелил",
|
||
"для обучения по ошибке собрана отдельная копия CPU-бэкенда (readout/build_delta_cpu.py)"],
|
||
["stochastic_stimulation", "параметр задан, но ни на что не влияет", "CPU-бэкенд его не читает"],
|
||
["Ничья в «победитель забирает всё»", "один прогон из пяти схлопывает все веса к нулю",
|
||
"предположительно случайный выбор победителя с seed из std::random_device; не подтверждено"],
|
||
["Сохранение сети", "обученную сеть примера нельзя загрузить для проверки",
|
||
"пример не вызывает save_network, режим inference_only не загружает модель"],
|
||
["get_grid_cols_rows", "ошибка компоновки с бэкендом AltAI", "функция объявлена в заголовке, но отсутствует в пакете 1.0.2"],
|
||
]),
|
||
H2("Переключатели примера mnist-learn"),
|
||
Text("Изменения в примере сделаны переменными окружения: без них пример работает как в KNP."),
|
||
Table(["Переменная", "Что делает"], [
|
||
["KNP_MNIST_THR, KNP_MNIST_ADD_THR_SCALE", "порог входных нейронов на проверке и множитель добавки к нему"],
|
||
["KNP_COL_JITTER", "случайный разброс стартовых весов колонок"],
|
||
["KNP_COL_PUNISH", "отрицательный дофамин колонкам чужих классов; лучшее 0,012–0,02"],
|
||
["KNP_DELTA, KNP_DELTA_LAG", "обучение выхода по ошибке и сдвиг дофамина на такт"],
|
||
["KNP_W_RANGE", "диапазон весов при обучении по ошибке"],
|
||
["KNP_MNIST_INPUT_SIZE", "размер входа: признаки вместо 784 пикселей"],
|
||
["KNP_MNIST_DUMP", "выгрузка весов и порогов после обучения"],
|
||
]),
|
||
Next("Обучение под «Алтай»", learning_section, "главный вопрос: откуда взять веса."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 5. Обучение
|
||
|
||
@Page("5", "Обучение под «Алтай»")
|
||
def learning_section():
|
||
return [
|
||
H1("Обучение под «Алтай»"),
|
||
Lead("«Алтай» не учится, а спайк — ступенька без производной. Поэтому вопрос «как обучить сеть» здесь главный, и ответов "
|
||
"на него четыре."),
|
||
Text("Обычную сеть учат обратным распространением ошибки: считают, как ответ зависит от каждого веса, и сдвигают веса. "
|
||
"Для импульсной сети это напрямую не работает: выход нейрона меняется скачком, производная почти везде равна нулю. "
|
||
"Отсюда два пути: учиться <b>локальными правилами</b>, которым производная не нужна, или учиться <b>на компьютере</b>, "
|
||
"заменив ступеньку чем-то гладким."),
|
||
Table(["Способ", "Где учится", "Чему учит", "Цифры MNIST"], [
|
||
["Колонки STDP", "в KNP", "«похоже на мою цифру», без сравнения с чужими", "92,2%"],
|
||
["Дофамин по ошибке", "в KNP", "один слой по ошибке ответа; признаки готовит хост", "94,8% (валидация)"],
|
||
["Перенос обычной сети", "на компьютере", "всю сеть через её аналоговую копию", "97,76%"],
|
||
["Обучение по тактам", "на компьютере", "всю сеть прямо на спайковой динамике чипа", "97,42% за 8 тактов"],
|
||
]),
|
||
Text("Закономерность видна сразу: чем больше обучения уходит на компьютер, тем выше точность. Для «Алтая», который "
|
||
"учиться не умеет, это не уступка, а естественный путь. Обучение внутри KNP ценно другим — оно показывает, что "
|
||
"платформа может учиться сама, если когда-нибудь появится чип с пластичностью."),
|
||
Links([
|
||
LocalLink("Колонки STDP", columns_page) + " — как учится штатный пример KNP и почему он упирается в 92%.",
|
||
LocalLink("Дофамин по ошибке", delta_page) + " — правило перцептрона, собранное из дофамина KNP.",
|
||
LocalLink("Перенос обычной сети", native_page) + " — нативная сеть, проверенная в GoldenModel.",
|
||
LocalLink("Обучение по тактам", ticks_page) + " — та же точность в 17 раз дешевле.",
|
||
LocalLink("Сравнение", compare_page) + " — все сети на одной цифре и на одном графике.",
|
||
]),
|
||
Next("Колонки STDP", columns_page, "с чего начинал KNP."),
|
||
]
|
||
|
||
|
||
@Page("5.1", "Колонки STDP")
|
||
def columns_page():
|
||
return [
|
||
H1("Колонки STDP"),
|
||
Lead("Штатный пример KNP mnist-learn: на каждую цифру — колонка из 20 нейронов, которые учатся по STDP с дофамином. "
|
||
"Ответ — колонка, которая стреляет больше всех."),
|
||
H2("Как учится"),
|
||
Text("Картинка подаётся частотным кодом за 10 тактов во все колонки сразу. Во время обучения метка через вспомогательную "
|
||
"популяцию будит колонку правильной цифры, и у её нейронов крепнут связи с пикселями, которые стреляли перед "
|
||
"выстрелом нейрона. Это <b>STDP</b> — правило, зависящее от порядка спайков: вход за мгновение до выстрела помог, "
|
||
"связь растёт; вход после выстрела не понадобился, связь слабеет."),
|
||
Widget("stdp"),
|
||
Text("В KNP используется вариант с синаптическим ресурсом: вес растёт тем медленнее, чем ближе к пределу, и изменения "
|
||
"закрепляются наградой — дофамином. Внутри колонки нейроны тормозят друг друга, чтобы каждый выучил свой стиль "
|
||
"написания."),
|
||
H2("Что пришлось исправить"),
|
||
Text("В исходном примере все 20 нейронов колонки стартовали с одинаковых весов и получали один и тот же сигнал метки — "
|
||
"колонка работала как один нейрон, 82,5%. Небольшой случайный разброс стартовых весов дал нейронам разные "
|
||
"стили, а торможение −255 между всеми нейронами на проверке оставило голос самому похожему: <b>92,2%</b> на полном "
|
||
f"MNIST. Попутно найдена ошибка загрузчика KNP — см. {LocalLink('подводные камни', knp_pitfalls)}."),
|
||
H2("Почему дальше 92% не идёт"),
|
||
Text("Колонка выучивает <b>трафарет</b>: «где у моей цифры обычно есть линия». Отсюда четыре следствия, и все они "
|
||
"измерены:"),
|
||
List([
|
||
"сдвиг цифры на 2 пикселя роняет точность до 64–67%, утолщение или утончение линии — до 75–81%: трафарет перестаёт совпадать;",
|
||
"типичная ошибка 4 → 9: трафарет девятки накрывает четвёрку целиком, а «не путай с девяткой» колонку никто не учил — "
|
||
"награда бывает только за свою цифру; наказание чужих колонок дофамином даёт +2…4 п.п., но не снимает потолок;",
|
||
"пустой пиксель молчит, поэтому колонка не видит довода «здесь линии нет». Попытка добавить такие доводы отдельными "
|
||
"входами «пиксель тёмный» (кодирование ON/OFF) утроила число входных спайков, и сеть замолчала;",
|
||
"на плотном входе колонки молчат вообще: после STDP большинство весов отрицательные, сумма уходит в минус.",
|
||
]),
|
||
Text("Это ограничение архитектуры, а не настроек: колонки сравнивают с образцом, но не учатся различать классы."),
|
||
Next("Дофамин по ошибке", delta_page, "как научить сеть именно различать, не выходя из KNP."),
|
||
]
|
||
|
||
|
||
@Page("5.2", "Дофамин по ошибке")
|
||
def delta_page():
|
||
return [
|
||
H1("Дофамин по ошибке"),
|
||
Lead("Правило перцептрона, собранное из механизмов KNP: нейрон получает дофамин «цель минус ответ», и связь меняется "
|
||
"на эту величину, если по ней только что пришёл спайк."),
|
||
H2("Правило"),
|
||
Text("Метка даёт нейрону своей цифры положительный дофамин на каждом такте, собственный спайк нейрона — отрицательный. "
|
||
"Если нейрон должен был стрелять и стрелял, сумма около нуля, ничего не меняется. Если должен, но молчал — связи с "
|
||
"активными входами крепнут. Если стрелял зря — слабеют. В отличие от колонок, сеть учится и на чужих цифрах: "
|
||
"«не стреляй на девятку» получается само."),
|
||
Text("В штатном KNP так не выйдет: STDP меняет только связи выстрелившего нейрона, а здесь нужно учить и молчащего. "
|
||
"Для этого собрана отдельная копия CPU-бэкенда (readout/build_delta_cpu.py, режим KNP_DELTA). Оригинальные "
|
||
"библиотеки не менялись. Дофамин в KNP приходит на такт позже входа, поэтому обязателен KNP_DELTA_LAG=1."),
|
||
H2("Результаты"),
|
||
Table(["Задача (валидация)", "Колонки STDP", "Дофамин по ошибке в KNP", "Тот же слой, обученный на компьютере"], [
|
||
["Цифры, признаки штрихов", "83,5% (200 нейронов)", "94,8% (10 нейронов)", "94,9%"],
|
||
["Цифры со сдвигом на 2 пикселя, обучение со сдвигами", "—", "91,3%", "89,9%"],
|
||
["Кошки/собаки, выученные признаки", "64–65%", "65–69%", "74%"],
|
||
]),
|
||
Text("На цифрах обучение внутри платформы догнало обучение на компьютере. На объектах разрыв 5–9 п.п. не объяснён: "
|
||
"диапазон весов и число проходов не помогают."),
|
||
H2("Ограничение"),
|
||
Text("Правило учит один слой, а один слой на пикселях — это линейный классификатор, около 92% на MNIST. Поэтому вход "
|
||
"у него — <b>признаки штрихов</b>, которые считает хост: 8 направлений линии × 2 толщины, сложенные по клеткам 4 × 4. "
|
||
"Получается, что половина распознавания идёт не на чипе. Чтобы на чипе была вся сеть, нужны веса для нескольких "
|
||
"слоёв — а их даёт только обучение на компьютере."),
|
||
Next("Перенос обычной сети", native_page, "первая сеть, целиком живущая на «Алтае»."),
|
||
]
|
||
|
||
|
||
@Page("5.3", "Перенос обычной сети")
|
||
def native_page():
|
||
return [
|
||
H1("Перенос обычной сети: нативная сеть"),
|
||
Lead("Двухслойная сеть из нейронов AltAILIF, уложенная во все правила чипа. Веса взяты у обычной сети, обученной так, "
|
||
"чтобы её числа можно было заменить счётом спайков."),
|
||
Table(["Слой", "Размер", "Устройство"], [
|
||
["вход", "784 линии + тактовые + 128 линий очистки", "частотный код, 32 такта"],
|
||
["скрытый", "256 AltAILIF", "порог 32, вычитание порога; веса {0, ±1, ±2, ±4}; смещение тактовыми линиями"],
|
||
["выход", "160 AltAILIF", "10 цифр × 8 групп × «за» и «против»; порог 4"],
|
||
["хост", "—", "по каждой цифре: спайки «за» минус спайки «против», затем максимум"],
|
||
]),
|
||
H2("Почему это работает"),
|
||
Text("Скрытый нейрон с вычитанием порога за 32 такта выдаёт ⌊заряд / 32⌋ спайков, но не больше 32. Это ступенчатая "
|
||
"функция с насыщением, почти та же, что ReLU с ограничением у обычной сети. Значит, если обычную сеть учить с "
|
||
"активацией, ограниченной и округлённой так же, её веса подойдут импульсной. Учитель — сеть 784 → 256 → 10 "
|
||
"(mnist-fast), обученная со сдвигами, поворотами и изменением толщины."),
|
||
H2("Рецепт"),
|
||
List([
|
||
"веса скрытого слоя учителя × 8 округляются до {0, ±1, ±2, ±4}, смещения — до целых;",
|
||
"через импульсный скрытый слой прогоняются все 70 000 картинок и записываются настоящие счётчики спайков;",
|
||
"выходной слой обучается заново уже на этих счётчиках — так он учитывает ошибки квантования скрытого слоя;",
|
||
"выходные веса квантуются отдельно для каждой группы из 32 скрытых нейронов: уровни {0, 1, ≈0,45·max, max}, "
|
||
"чтобы у выходного нейрона было не больше трёх величин плюс −255 очистки;",
|
||
"масштаб и порог выхода выбираются по валидации (5000 картинок): масштаб 4, порог 4; тест открывается после выбора.",
|
||
], ordered=True),
|
||
H2("Почему выход «за/против»"),
|
||
Text("Выходной вес бывает и отрицательным: «здесь есть верхняя перекладина — значит, не 4». Но уровень нейрона не "
|
||
"опускается ниже нуля, и отрицательный довод, пришедший раньше положительного, теряется. Поэтому положительные "
|
||
"веса идут в нейрон «за», отрицательные — со знаком плюс в нейрон «против», а вычитание делает хост. Деление на 8 "
|
||
"групп нужно уже из-за правила четырёх величин: у каждой группы свои три уровня."),
|
||
H2("Результаты"),
|
||
Table(["Свойство", "Значение"], [
|
||
["Точность на тесте MNIST", "97,76% — эталон, KNP на CPU и наш CUDA дают одинаковые счётчики на всех 10 000"],
|
||
["GoldenModel", "975 из 1000 первых тестовых цифр, все счётчики совпали"],
|
||
["Цена", "≈3200 входных спайков и ≈338 тыс. синаптических событий на цифру"],
|
||
["Наибольший уровень", "оценка сверху 12 864 (скрытый) и 1 120 (выход) — в 16 бит помещается"],
|
||
]),
|
||
H2("Можно ли короче"),
|
||
Text("32 такта частотного кода — дорого. Если просто обрезать ту же сеть по тактам, точность держится до 8, а потом "
|
||
"обваливается: сеть училась на 32 оттенках, и при 4 тактах серые пиксели неотличимы от чёрных."),
|
||
Table(["Тактов", "Точность", "Синаптических событий"], [
|
||
["32", "97,76%", "338 тыс."], ["16", "97,66%", "164 тыс."], ["8", "97,19%", "77 тыс."], ["4", "84,63%", "33 тыс."],
|
||
]),
|
||
Next("Обучение по тактам", ticks_page, "как сделать сеть, которой мало тактов не мешает."),
|
||
]
|
||
|
||
|
||
@Page("5.4", "Обучение по тактам")
|
||
def ticks_page():
|
||
return [
|
||
H1("Обучение по тактам"),
|
||
Lead("Сеть 784 → 256 → 10 из AltAILIF, обученная прямо на спайковой целочисленной динамике чипа. Вход — временной код: "
|
||
"один спайк на пиксель, ответ за 8 тактов."),
|
||
Widget("ticks"),
|
||
Text("Нарисуйте цифру и прокрутите такты. Яркие пиксели стреляют первыми, скрытые нейроны начинают отвечать с первого "
|
||
"такта, и выход обычно определяется задолго до восьмого: такт, после которого лидер больше не меняется, показан "
|
||
"как ранний ответ."),
|
||
H2("Как обучить ступеньку"),
|
||
Text("Прямой проход идёт по настоящим тактам: спайки, вычитание порога, пол на нуле, задержка в такт между слоями — та же "
|
||
"арифметика, что на чипе. При обратном проходе производная ступеньки заменяется гладким «колоколом» "
|
||
"1 / (1 + 4|v|)², где v — расстояние уровня до порога. Это <b>суррогатный градиент</b>: он говорит весам, "
|
||
"в какую сторону сдвинуть уровень, чтобы нейрон стрельнул или промолчал."),
|
||
H2("Рецепт"),
|
||
List([
|
||
"30 эпох, Adam, шаг 0,002 с косинусным спадом, пачки по 256; половина каждой пачки — те же цифры, перерисованные "
|
||
"пером постоянной толщины, плюс сдвиги и повороты;",
|
||
"первые 8 эпох веса дробные; с 9-й в прямом проходе они огрубляются до {0, ±1, ±2, ±4}, а градиент проходит мимо "
|
||
"огрубления. Сеть доучивается уже с теми весами, что будут на чипе;",
|
||
"ответ сети при обучении — число выходных спайков плюс половина остатка уровня, чтобы ошибка была видна и при равенстве счёта;",
|
||
"к ошибке добавлен штраф λ за спайки скрытого слоя: сеть учится молчать, когда может;",
|
||
"после каждой эпохи веса переводятся в целые и проверяются отдельным целочисленным симулятором на валидации; "
|
||
"лучшая эпоха выбирается по нему.",
|
||
], ordered=True),
|
||
H2("Варианты"),
|
||
Table(["Код", "Тактов", "λ", "Валидация", "Тест", "Тест, рисунки", "Синапт. событий", "Скрытых спайков"], [
|
||
["временной", "8", "0,1", "96,66%", "97,42%", "95,85%", "20 150", "45"],
|
||
["временной", "8", "0,5", "96,13%", "97,11%", "95,10%", "25 175", "21"],
|
||
["временной", "6", "0,5", "96,10%", "97,12%", "95,07%", "24 927", "18"],
|
||
["временной", "4", "0,5", "95,88%", "97,08%", "95,05%", "24 129", "17"],
|
||
["временной", "4", "0,1", "95,17%", "96,11%", "93,93%", "18 142", "41"],
|
||
["частотный", "8", "0,1", "95,91%", "97,22%", "94,37%", "84 590", "62"],
|
||
["частотный", "4", "0,1", "93,07%", "94,35%", "90,86%", "37 405", "47"],
|
||
]),
|
||
Text("Выбрана первая строка — по валидации, тест открыт после выбора. Два наблюдения из таблицы:"),
|
||
Steps([
|
||
"<b>Временной код выигрывает у частотного</b> при том же числе тактов: точнее и в 4 раза дешевле. Частотному коду на "
|
||
"8 тактах не хватает оттенков, временной передаёт яркость моментом.",
|
||
"<b>Штраф за скрытые спайки не равен экономии.</b> Почти вся работа — во входном слое: каждый из ≈143 входных спайков "
|
||
"задевает в среднем ≈139 скрытых нейронов, это ≈19,7 тыс. из 20 тыс. событий. Сильный штраф вдвое снижает "
|
||
"скрытые спайки, но сеть компенсирует это более плотными входными весами, и событий становится больше. Экономить "
|
||
"нужно на числе ненулевых входных весов.",
|
||
]),
|
||
H2("Что осталось до чипа"),
|
||
Text("Правила чипа соблюдены на бумаге: палитра {0, 1, 2, 4}, наибольший уровень 304 при пределе 32 767, смещение — "
|
||
"постоянный вход каждый такт, то есть тактовые линии, как у нативной сети. Но в KNP и GoldenModel сеть ещё не "
|
||
"запускалась, и для неё нет очистки между картинками. Это следующий шаг, после которого её можно ставить в один "
|
||
"ряд с нативной."),
|
||
Next("Сравнение", compare_page, "все сети рядом."),
|
||
]
|
||
|
||
|
||
@Page("5.5", "Сравнение")
|
||
def compare_page():
|
||
return [
|
||
H1("Сравнение сетей"),
|
||
Lead("Три сети, которые целиком состоят из нейронов AltAILIF, на одной задаче: тест MNIST, 10 000 цифр."),
|
||
Table(["", "Колонки STDP", "Нативная сеть", "Обучение по тактам"], [
|
||
["Точность, тест", "92,2%", "97,76%", "97,42%"],
|
||
["Точность, рисунки", "—", "94,1% (повтор рецепта)", "95,9%"],
|
||
["Тактов", "15", "32", "8"],
|
||
["Входных спайков", "≈870", "≈3200", "≈143"],
|
||
["Синаптических событий", "≈175 тыс.", "≈338 тыс.", "≈20 тыс."],
|
||
["Нейронов", "200", "416", "266"],
|
||
["Где учится", "в KNP", "на компьютере", "на компьютере"],
|
||
["Проверена в GoldenModel", "нет", "да, 1000 цифр", "нет"],
|
||
]),
|
||
H2("Одна цифра — три сети"),
|
||
Text("Нарисуйте цифру или выберите тестовую. Все три сети считаются здесь целочисленно, по тем же правилам, что "
|
||
"на чипе. Сдвиньте рисунок к краю или нарисуйте очень толстой линией — колонки ошибутся первыми."),
|
||
Widget("compare"),
|
||
H2("Точность против работы чипа"),
|
||
Text("Каждая точка — сеть или её режим. Левее — меньше сложений и, значит, энергии; выше — точнее. Нативная сеть, "
|
||
"обрезанная по тактам, ползёт влево и вниз; сеть, обученная по тактам, стоит в левом верхнем углу."),
|
||
Widget("tradeoff"),
|
||
H2("Что выбирать"),
|
||
Table(["Нужно", "Сеть", "Почему"], [
|
||
["доказательство «работает на Алтае» сегодня", "нативная", "единственная прошла GoldenModel"],
|
||
["минимум энергии и тактов", "обучение по тактам", "в 17 раз меньше событий при той же точности; осталось пройти GoldenModel"],
|
||
["обучение без компьютера, внутри платформы", "колонки или дофамин по ошибке", "единственные варианты; точность ниже"],
|
||
]),
|
||
Next("Вход: хост и рисунки", host_page, "почему на рисунках точность ниже и что с этим делает хост."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 6. Хост
|
||
|
||
@Page("6", "Вход: хост и рисунки")
|
||
def host_page():
|
||
return [
|
||
H1("Вход: хост и рисунки"),
|
||
Lead("Сеть видит только то, что подал хост. Рисунок мышью отличается от цифры MNIST сильнее, чем кажется, и этот "
|
||
"разрыв закрывает подготовка входа и обучение на похожих рисунках."),
|
||
H2("Почему рисунок — другая задача"),
|
||
Text("Цифры MNIST — отсканированный почерк: штрих переменной толщины с серыми краями, цифра вписана в квадрат 20 × 20 "
|
||
"и поставлена центром масс в центр поля 28 × 28. Рисунок мышью — перо одной толщины, жёсткие края, произвольные "
|
||
"размер и место. Сеть, выучившая MNIST, видит на рисунке незнакомый «почерк»."),
|
||
Text("Чтобы мерить это без людей, тестовые цифры MNIST перерисованы так, как их нарисовала бы мышь: из цифры "
|
||
"выделяется осевая линия и обводится пером постоянной толщины на поле 280 × 280, потом уменьшается. Доля "
|
||
"верных ответов на таких копиях — <b>точность на рисунках</b>."),
|
||
H2("Что делает хост"),
|
||
Widget("hostprep"),
|
||
Table(["Шаг", "Что делает", "Зачем"], [
|
||
["рамка", "вписывает рисунок в квадрат 20 × 20 и ставит центр масс в центр 28 × 28", "как в MNIST: размер и положение перестают мешать"],
|
||
["выпрямление наклона", "по моментам изображения находит наклон главной оси и сдвигает строки, чтобы она стала вертикальной", "наклонный почерк приводится к прямому"],
|
||
["уменьшение с усреднением", "каждый пиксель 28 × 28 — среднее своей области", "сохраняет серые края, как у сканов"],
|
||
["кодирование", "яркость → спайки", "частотный или временной код"],
|
||
]),
|
||
H2("Что помогает и насколько"),
|
||
Text("Опыты на рецепте нативной сети: один и тот же учитель, меняется только подготовка и обучающие данные."),
|
||
Table(["Вариант", "Тест MNIST", "Рисунки"], [
|
||
["как было", "97,66%", "94,08%"],
|
||
["+ обучение на перерисованных копиях", "97,15%", "95,20%"],
|
||
["+ выпрямление наклона", "98,01%", "94,96%"],
|
||
["+ выпрямление и перерисованные копии", "97,76%", "95,86%"],
|
||
["скелет: и тест, и рисунки сводятся к тонкой линии", "95,98%", "92,53%"],
|
||
]),
|
||
Text("Лучший вариант — выпрямление вместе с обучением на копиях: +1,8 п.п. на рисунках почти без потери на тесте. "
|
||
"Приведение всего к тонкой линии вредит: вместе с разницей толщины теряется и полезная информация о форме."),
|
||
Callout("Хост готовит вход и читает выход, но не вклинивается между слоями сети. Иначе часть распознавания уходит с чипа, "
|
||
"как у детекторов с суммированием на хосте.", title="Граница хоста", kind="note"),
|
||
Next("От цифр к мусору", trash_section, "что из этого переносится на робота."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 7. Мусор
|
||
|
||
@Page("7", "От цифр к мусору")
|
||
def trash_section():
|
||
return [
|
||
H1("От цифр к мусору"),
|
||
Lead("Цифры были полигоном. На роботе другой вход — цветной кадр камеры, другие классы и другой бюджет нейронов."),
|
||
H2("Что переносится"),
|
||
Table(["Приём", "Почему подходит мусору"], [
|
||
["обучение на компьютере сразу под палитру чипа", "чип не учится; точность выше, чем у правил внутри KNP"],
|
||
["временной код и обучение по тактам", "≈1 событие на значимый пиксель; меньше энергии на кадр"],
|
||
["хост только на входе: окно, центровка, кодирование", "вся сеть остаётся на кристалле"],
|
||
["проверка счётчиков в KNP и GoldenModel", "доказательство без платы"],
|
||
["обходы правил чипа: тактовые смещения, очистка, «за/против»", "не зависят от задачи"],
|
||
]),
|
||
H2("Что не переносится"),
|
||
Text("Сама сеть. Цифра — это чёрно-белый штрих в центре поля, мусор — цветной объект на пёстром фоне. "
|
||
"Полносвязного слоя на пикселях для объектов мало: нужны признаки — узоры, которые ищутся по всей картинке. "
|
||
f"Что известно о признаках и почему они плохо ложатся на чип — на странице {LocalLink('объектов', objects_page)}."),
|
||
Links([
|
||
LocalLink("Объекты: что уже известно", objects_page) + " — опыты на кошках и собаках CIFAR-10.",
|
||
LocalLink("План распознавателя мусора", trash_plan) + " — шаги и открытые вопросы.",
|
||
]),
|
||
Next("Объекты: что известно", objects_page, "что дали опыты с объектами."),
|
||
]
|
||
|
||
|
||
@Page("7.1", "Объекты: что известно")
|
||
def objects_page():
|
||
return [
|
||
H1("Объекты: что уже известно"),
|
||
Lead("Перед мусором техника проверялась на фотографиях кошек и собак из CIFAR-10 (32 × 32, цвет). Это близкая задача: "
|
||
"два класса, пёстрый фон, объект в разных позах."),
|
||
H2("Узкое место — признаки"),
|
||
Text("Первая сеть брала готовые признаки HOG (направления перепадов яркости) и колонки STDP — около 62%. Проверка "
|
||
"показала, что дело не в колонках: даже линейный классификатор на HOG даёт около 60%. В признаках просто мало "
|
||
"информации."),
|
||
Text("Признаки, выученные из самих картинок, дали больше. Из картинок вырезаются кусочки 6 × 6, и метод k-средних "
|
||
"находит в них 400–1600 типичных узоров, без меток. Нейрон узора стреляет, если кусочек похож на его узор, а "
|
||
"спайки суммируются по четвертям картинки, чтобы не зависеть от точного положения. Без спайков на таких признаках "
|
||
"получается 77% — это потолок для выбранных признаков. Выучить узоры можно и нейронным правилом «победитель "
|
||
"забирает всё» по одной картинке: не хуже пакетного k-средних."),
|
||
H2("Где теряется точность"),
|
||
Table(["Переход", "Потеря", "Почему"], [
|
||
["нормировка контраста убрана", "−0,7 п.п.", "одним нейроном её не сделать, но потеря мала"],
|
||
["признаки и выход на спайках", "−8 п.п.", "нейрон признака за картинку стреляет в среднем один раз: оттенки силы признака теряются"],
|
||
["колонки STDP вместо обученного выхода", "до −10 п.п.", "на плотных признаках колонки молчат; приходится оставлять 256 сильнейших из 2048"],
|
||
]),
|
||
Text("Итог на свежем тесте из 600 картинок: колонки STDP в KNP — 67,0%, полностью спайковая цепочка — 68,3% "
|
||
"(погрешность ±3,7 п.п.). Лучшая сеть — детекторы признаков с выходом, обученным на компьютере: 71,35% на "
|
||
"официальном тесте CIFAR, исполнение в KNP на GPU."),
|
||
H2("Почему детекторы не ложатся на чип"),
|
||
Text("Аудит сохранённых весов этой сети против правил «Алтая»:"),
|
||
Table(["Проверка", "Сеть детекторов", "Предел «Алтая-2»"], [
|
||
["нейронов", "39 458 (200 узоров × 196 позиций + 258)", "8192 в прототипе"],
|
||
["разных величин веса на нейрон", "до 55 у детекторов, до 110 в скрытом слое", "4"],
|
||
["наибольший уровень", "от −492 108 до 569 761", "−32 768…32 767"],
|
||
["хост между слоями", "суммирование по четвертям, нормировка, перепаковка", "сеть целиком на чипе"],
|
||
]),
|
||
Text("Главная причина — отсутствие общих весов. В свёрточной сети узор «вертикальная черта» хранится один раз и "
|
||
"прикладывается ко всем позициям. На чипе каждая позиция — отдельный нейрон со своей копией весов, и число "
|
||
"нейронов растёт как «узоры × позиции». Подберите параметры, при которых сеть влезет в прототип:"),
|
||
Widget("budget"),
|
||
Text("Влезть можно только за счёт меньшего окна, большего шага и малого числа узоров — то есть ценой точности. "
|
||
"Это главный вопрос для мусора."),
|
||
Next("План распознавателя мусора", trash_plan, "как из этого собрать распознаватель мусора."),
|
||
]
|
||
|
||
|
||
@Page("7.2", "План распознавателя мусора")
|
||
def trash_plan():
|
||
return [
|
||
H1("План распознавателя мусора"),
|
||
Lead("Порядок работ, при котором каждый шаг проверяем, а ошибка видна до того, как появится плата."),
|
||
Steps([
|
||
"<b>Постановка.</b> Начать с двух классов «мусор / не мусор» на окне кадра. Хост режет кадр на окна, чип отвечает "
|
||
"по каждому, место мусора видно по сработавшим окнам.",
|
||
"<b>Данные.</b> Открытые наборы с фотографиями мусора (например, TACO, TrashNet) для начала и снимки с камеры робота "
|
||
"для честного теста. Тест не трогать до выбора модели.",
|
||
"<b>Бюджет.</b> До обучения выбрать размер окна, число узоров и шаг так, чтобы сеть влезла в 8192 нейрона; "
|
||
"проверить три числа со страницы правил.",
|
||
"<b>Обучение.</b> Сеть по тактам с временным кодом, огрублением весов до палитры с середины обучения и штрафом за "
|
||
"спайки; узоры первого слоя — локальные окна без общих весов.",
|
||
"<b>Проверка.</b> Целочисленный эталон → KNP на CPU и GPU → GoldenModel, сравнение счётчиков всех нейронов.",
|
||
"<b>Плата.</b> Запуск на AltAI_HW и замер энергии на кадр.",
|
||
], ordered=True),
|
||
H2("Открытые вопросы"),
|
||
Table(["Вопрос", "От чего зависит"], [
|
||
["какая конфигурация «Алтая» будет на роботе и есть ли плата", "бюджет нейронов, возможность измерить энергию"],
|
||
["классы: «мусор / не мусор» или типы мусора", "размер выходного слоя и объём данных"],
|
||
["какая камера: обычная или событийная", "событийная камера сама выдаёт спайки, и кодирование на хосте не нужно"],
|
||
["сколько кадров в секунду нужно роботу", "сколько тактов можно потратить на окно"],
|
||
]),
|
||
Next("Справочник", reference_section, "словарь терминов, правила измерений и карта репозитория."),
|
||
]
|
||
|
||
|
||
# ---------------------------------------------------------------- 8. Справочник
|
||
|
||
@Page("8", "Справочник")
|
||
def reference_section():
|
||
return [
|
||
H1("Справочник"),
|
||
Links([
|
||
LocalLink("Глоссарий", glossary) + " — термины вики в одном месте.",
|
||
LocalLink("Методика измерений", metrics_page) + " — как получены цифры и когда им верить.",
|
||
LocalLink("Где что лежит", repo_page) + " — код, модели и команды в репозитории проекта.",
|
||
]),
|
||
]
|
||
|
||
|
||
@Page("8.1", "Глоссарий")
|
||
def glossary():
|
||
return [
|
||
H1("Глоссарий"),
|
||
Table(["Термин", "Определение"], [
|
||
["Спайк", "событие «нейрон выстрелил» в определённый такт; несёт один бит"],
|
||
["Такт", "шаг времени сети; за такт нейрон складывает пришедшее и решает, стрелять ли"],
|
||
["Уровень (потенциал)", "накопленный заряд нейрона; на «Алтае» — целое 16 бит"],
|
||
["Порог", "уровень, при котором нейрон выдаёт спайк"],
|
||
["Вес", "на сколько изменится уровень получателя от одного спайка по связи"],
|
||
["Палитра весов", "набор допустимых величин веса, например {0, ±1, ±2, ±4}"],
|
||
["Синаптическое событие", "одно сложение «спайк → вес» в получателе; мера работы и энергии сети"],
|
||
["Частотный код", "яркость передаётся числом спайков за картинку"],
|
||
["Временной код", "яркость передаётся моментом единственного спайка: ярче — раньше"],
|
||
["Тактовая линия", "входная линия, которая стреляет каждый такт; так на «Алтае» задаётся смещение нейрона"],
|
||
["Очистка", "пачка отрицательных спайков после картинки, обнуляющая уровни"],
|
||
["STDP", "правило: связь крепнет, если вход пришёл перед выстрелом, и слабеет, если после"],
|
||
["Дофамин", "сигнал награды в KNP, который закрепляет или отменяет недавние изменения связей"],
|
||
["Суррогатный градиент", "замена производной ступеньки-спайка гладкой функцией при обучении"],
|
||
["Квантование", "перевод дробных весов в целые из палитры"],
|
||
["Хост", "обычный процессор рядом с чипом: готовит вход и читает ответ"],
|
||
["GoldenModel", "программная копия «Алтая» из официального бэкенда, AltAI_GM"],
|
||
["Размещатель", "часть бэкенда «Алтая», раскладывающая нейроны и связи по ядрам"],
|
||
["Счётчики спайков", "сколько раз выстрелил каждый нейрон за картинку; по ним сравнивают исполнителей"],
|
||
["Точность на рисунках", "точность на тестовых цифрах, перерисованных пером постоянной толщины"],
|
||
]),
|
||
]
|
||
|
||
|
||
@Page("8.2", "Методика измерений")
|
||
def metrics_page():
|
||
return [
|
||
H1("Методика измерений"),
|
||
Lead("Как получены цифры в вики и при каких условиях им можно верить."),
|
||
H2("Данные"),
|
||
Text("MNIST: 60 000 обучающих и 10 000 тестовых цифр. Из обучающих 5000 отложены на валидацию. Все настройки — пороги, "
|
||
"масштабы, число эпох, выбор лучшего варианта — выбираются по валидации. Тест открывается один раз, после "
|
||
"выбора: иначе оценка завышена."),
|
||
H2("Метрики"),
|
||
Table(["Метрика", "Определение"], [
|
||
["Точность", "доля верных ответов на тесте; молчание сети считается ошибкой"],
|
||
["Точность на рисунках", "то же на перерисованных тестовых цифрах"],
|
||
["Синаптические события", "сложений «спайк → вес» на одну картинку, среднее по тесту"],
|
||
["Входные спайки", "сколько событий хост отправляет на чип за картинку"],
|
||
["Такты", "длительность распознавания одной картинки"],
|
||
["Совпадение счётчиков", "счётчик каждого нейрона одинаков в эталоне, KNP и GoldenModel на каждой картинке"],
|
||
]),
|
||
H2("Шум"),
|
||
Text("Разница меньше 0,3 п.п. на тесте MNIST при одном прогоне — в пределах случайности. На 1000 картинках разброс "
|
||
"по seed ±1–2 п.п., на 600 картинках погрешность ±3,7 п.п. Выводы о разнице в 1–2 п.п. на малых выборках без "
|
||
"нескольких seed ненадёжны."),
|
||
H2("Источник"),
|
||
Text("Цифра без источника не считается результатом. У каждого прогона есть файл итогов: final-summary.json, "
|
||
"VERIFIED.json или result.json рядом с моделью. Результат, записанный в FAILED.json, не используется, даже "
|
||
"если точность выглядит правдоподобно."),
|
||
]
|
||
|
||
|
||
@Page("8.3", "Где что лежит")
|
||
def repo_page():
|
||
return [
|
||
H1("Где что лежит"),
|
||
Lead("Репозиторий проекта — git.snw.su/int/our_knp. Пути ниже — от его корня."),
|
||
Table(["Что", "Где"], [
|
||
["нативная сеть: обучение, граф KNP, проверки", "mnist-altai-native/: prepare_native.py, native_digits.cpp, run_verify.py, run_sharded.py"],
|
||
["учитель нативной сети", "mnist-fast/: model_core.py, train_fast.py"],
|
||
["колонки STDP и переключатели KNP_*", "examples/examples/mnist-learn/models/altai/construct_network.cpp"],
|
||
["бэкенд для дофамина по ошибке", "readout/build_delta_cpu.py; запуски — readout/delta_runs.sh"],
|
||
["признаки штрихов", "features/stroke_features.py"],
|
||
["признаки объектов, спайковая цепочка", "objects/kmeans_features.py, objects/spiking_pipeline.py, objects/dopamine_readout.py"],
|
||
["симулятор колонок и страницы рисования", "viz/"],
|
||
["отчёты и журналы", "reports/; начинать с reports/Итоги_и_план.md"],
|
||
]),
|
||
H2("Нативная сеть: сборка и проверка"),
|
||
Code('''python3 mnist-altai-native/build_native.py # собрать native_digits
|
||
python mnist-altai-native/prepare_native.py # переобучить (перезаписывает модель)
|
||
python mnist-altai-native/run_verify.py cpu 10000 8 cpu-run # KNP CPU, 10 000 цифр
|
||
python mnist-altai-native/run_verify.py cuda 10000 8 cuda-run # наш CUDA-бэкенд
|
||
python mnist-altai-native/run_sharded.py gm-run # GoldenModel, первые 1000, 8 процессов''', "bash"),
|
||
Text("Каждый прогон пишет в новый каталог; VERIFIED.json появляется только при совпадении всех счётчиков."),
|
||
Callout("Код сети по тактам и опытов с рисунками пока не в репозитории: веса и симулятор лежат только в данных этой "
|
||
"вики (data/models.json, widgets.js).", title="Не в репозитории", kind="warning"),
|
||
]
|
||
|
||
|
||
HEADER = [
|
||
HeaderButton("Обзор", overview),
|
||
HeaderButton("Алтай", altai_section),
|
||
HeaderButton("Обучение", learning_section),
|
||
HeaderButton("Сравнение", compare_page),
|
||
HeaderButton("Мусор", trash_section),
|
||
]
|
||
|
||
|
||
def custom_js() -> Path:
|
||
"""Данные моделей + виджеты в одном файле custom_js."""
|
||
out = ASSETS / "inject.generated.js"
|
||
data = json.loads((HERE / "data" / "models.json").read_text(encoding="utf-8"))
|
||
out.write_text("globalThis.WIKI_DATA = " + json.dumps(data, ensure_ascii=False, separators=(",", ":")) + ";\n"
|
||
+ (ASSETS / "widgets.js").read_text(encoding="utf-8"), encoding="utf-8")
|
||
return out
|
||
|
||
|
||
OPTIONS = dict(title=TITLE, assets_dir=str(ASSETS), custom_css=str(ASSETS / "widgets.css"), header_buttons=HEADER)
|
||
|
||
if __name__ == "__main__":
|
||
js = custom_js()
|
||
if len(sys.argv) > 1 and sys.argv[1] == "build":
|
||
result = build(build_directory=str(HERE / "build"), custom_js=str(js), manifest_path="./manifest.json", **OPTIONS)
|
||
print("built", HERE / "build")
|
||
else:
|
||
run(port=1234, host="0.0.0.0", custom_js=str(js), live_preview=True, **OPTIONS)
|