⚡ Что такое NNUE

О чём это

NNUE (Neural Network, Efficiently Updatable — «эффективно обновляемая нейросеть») — способ получить оценку позиции нейросетевого качества за микросекунды на обычном CPU. Придумана Ю Насу (Yu Nasu) в 2018 для сёги, в 2020 встроена в Stockfish 12 — и с тех пор это стандарт: все сильнейшие классические движки оценивают позиции именно так. Здесь — как она устроена (четыре идеи), как обучается, и что из этого нужно (и что НЕ нужно) нам.

Связанные: what-is-lightgbm · win-probability-for-sparse-positions · value-model-improvement-plan · model-improvement-results-and-oracle-bots-roster


Зачем она вообще появилась

В движке с перебором (alpha-beta, expectimax) функция оценки вызывается миллионы раз на один ход — на каждом листе дерева. Бюджет на один вызов — микросекунды. Отсюда две философии:

  • AlphaZero/Leela: огромная сеть (десятки миллионов весов), очень умная, но медленная (миллисекунды на GPU) → компенсируем тем, что перебираем мало узлов (тысячи, MCTS).
  • NNUE/Stockfish: крошечная сеть, оцененная за микросекунды на CPU → перебираем миллионы узлов, глубина добивает то, чего не хватает уму.

Практика классических шахмат показала: вторая философия на CPU сильнее. Для нас это знакомая дилемма — наш KCP-эвалуатор «умный, но 6,6 с/ход», а 2-ply rich «глупее, но успевает» (см. model-improvement-results-and-oracle-bots-roster). NNUE — это способ получить и умную оценку, и микросекунды.


Идея 1: сеть-«гантеля» — вся ёмкость в первом слое

NNUE — это не глубокая сеть. Классическая архитектура Stockfish 12:

вход: ~41 000 бинарных фич (почти все = 0)
        ↓  W1 — ЕДИНСТВЕННЫЙ большой слой весов
аккумулятор: 2 × 256 чисел          ← 99% всех весов сети живёт здесь
        ↓
крошечные плотные слои: 512 → 32 → 32 → 1
        ↓
скаляр (оценка позиции)

Почти все веса — в первом слое, а «голова» из двух-трёх слоёв по 32 нейрона тривиально дешёвая. Почему так — станет ясно из идей 2 и 3: первый слой особенный, его можно почти никогда не пересчитывать.

Идея 2: разреженные фичи «король × фигура × клетка»

Вход — не «доска как картинка», а огромный, но почти пустой бинарный вектор. Классический набор HalfKP: фича = тройка (клетка СВОЕГО короля, тип+цвет фигуры, клетка фигуры). Таких комбинаций 64 × 10 × 64 ≈ 41 тысяча, но в реальной позиции фигур ≤ 30 — значит, активны лишь ~30 фич из 41 000.

Интуиция, почему фичи привязаны к королю: ценность «ладья на d3» радикально зависит от того, где твой король. NNUE фактически выучивает piece-square-таблицы, обусловленные позицией короля — и это выразительнее любых ручных таблиц. Для Dice Chess это попадание в яблочко: вся игра — охота на короля, взятие короля = победа.

Идея 3: аккумулятор и инкрементальное обновление — главный трюк

Раз вход бинарный и разреженный, первый слой — это просто сумма ~30 колонок матрицы W1 (по одной колонке на активную фичу) плюс bias. Эту сумму (256 чисел) называют аккумулятором.

А теперь ключевое наблюдение: соседние позиции в дереве перебора отличаются одним ходом. Ход меняет 2–3 фичи: фигура ушла с e4 (− колонка), пришла на d6 (+ колонка), съела фигуру (− ещё колонка). Значит, вместо пересчёта суммы 30 колонок:

аккумулятор_после = аккумулятор_до − W1[фича_ушла] + W1[фича_пришла] (− W1[съеденная])

Это O(изменений), а не O(всей позиции) — пара сотен целочисленных сложений. При спуске/подъёме по дереву поиска аккумулятор просто корректируется, и «дорогая» часть сети не считается вообще никогда, кроме корня. Честная оговорка: ход своим королём инвалидирует все фичи своей половины (они же привязаны к клетке короля) — тогда своя половина аккумулятора пересчитывается целиком. Это редкий, заложенный в дизайн худший случай.

Пересчитываются на каждом листе только крошечные слои 512 → 32 → 32 → 1 — сотни умножений.

Идея 4: квантование — целые числа вместо дробных

Обучается сеть как обычно, в float32. А для игры веса квантуются: аккумулятор — int16, остальные слои — int8. Зачем:

  • SIMD-инструкции CPU (AVX2 на x86, NEON на ARM — т.е. и M3, и Raspberry Pi) обрабатывают 16–32 целых числа за такт — это в разы быстрее float;
  • целочисленная арифметика стабильнее по кэшу: вся сеть — сотни килобайт, влезает в L2.

Почему точность не страдает: активации зажаты clipped-ReLU в диапазон [0, 127], цепочки вычислений короткие (2–3 слоя), ошибки округления не успевают накопиться. Потеря качества от квантования — доли процента, ускорение — на порядок.

Итог четырёх идей: оценка позиции внутри поиска стоит порядка микросекунды, миллионы оценок в секунду на одном ядре — против ~40–60 мс нашей точной KCP-фичи.


Как её обучают

Два источника сигнала, обычно в смеси:

  1. Дистилляция от учителя: позиции размечаются оценкой поиска сильного движка (у Stockfish — миллиарды позиций из self-play). Сеть учится предсказывать «что скажет глубокий поиск», получая знание глубины за цену статики.
  2. Исходы партий: метка 0 / 0.5 / 1 — та же, что у нашего LightGBM-бейзлайна (см. what-is-lightgbm).

Для нас важно: если на выход поставить сигмоиду и учить кросс-энтропией, сеть выдаёт сразу вероятность выигрыша — именно то, что нужно аналитике как prior (win-probability-for-sparse-positions), без пересчёта centipawn’ов.


Что это значит для Dice Chess

Кости. NNUE оценивает позицию; наши модели и так dice-free (оценка до броска — так их зовёт поиск на листе). Если захочется roll-aware оценку, состав броска (6 счётчиков) добавляется входом в маленькие плотные слои после аккумулятора — дёшево и не ломает инкрементальность (кости меняются каждый ход, но эта часть сети и так пересчитывается).

Король в центре модели фич. HalfKP-фичи, обусловленные клеткой короля, идеально совпадают с целью игры — взятием короля.

Сеть может «выучить KCP». Наш точный KingCaptureProbability — 4 DFS по 216 исходов, ~40–60 мс, из-за чего KCP-бот не влез в live-бюджет, а на rpi4 немыслим. NNUE-класса сеть, обученная на данных (или дистиллированная от KCP/Hunter-учителя), аппроксимирует эти паттерны за микросекунды. Это снимает главный компромисс «умно ИЛИ быстро» и для live-ботов на слабом железе (house-боты на rpi4!) — единственный реалистичный путь к KCP-качеству.

Данных теперь хватает. 1,5 млн партий ≈ десятки миллионов позиций; плюс дистилляция позволяет разметить сколько угодно дополнительных. Год назад NNUE был бы для нас фантазией, сегодня — нет.

Когда NNUE НЕ нужен — честная часть

Суперсила NNUE — инкрементальность внутри поиска. Она окупается, когда оценка зовётся на миллионах соседних позиций (2-ply expectimax, Hunter-поиск, любой live-бот). Аналитике же нужен поштучный/батчевый скоринг независимых позиций — там инкремент не даёт ничего, и обычного MLP через ONNX Runtime (как rawboard_l000.onnx) достаточно. Не заводить NNUE «для аналитики» — заводить его, если решим усиливать живых ботов; аналитика тогда получит ту же сеть бесплатно как prior.

Цена внедрения. Обучение — PyTorch (MacBook/MPS для прототипа, Azure GPU в окно кредита для свипов). А вот инференс: настоящая NNUE-скорость требует рукописного целочисленного инференса, встроенного в поиск (аккумулятор в Int-массивах, обновление в makeMove/undo движка) — ONNX Runtime с его пооперационными вызовами инкрементальность не умеет. Это заметная Scala-работа; браться за неё стоит только после того, как сеть докажет силу в офлайн-бенчмарке.


Шпаргалка: три семейства рядом

LightGBM (наш бейзлайн)MLP/raw-board (ONNX)NNUE
Входинженерные фичидоска как тензорразреженные king-relative фичи
Цена оценки~мкс + цена фичей (KCP до 60 мс)~мс~мкс (в поиске)
Инкрементальностьнетнетда — в этом суть
Данных нужносотни тысяч строкдесятки млндесятки млн (+дистилляция)
Потолокфичи придумываем мыучит фичи самаучит фичи сама
Где уместенprior аналитики сейчасprior-кандидат, батч-скорингlive-боты с поиском, rpi4
Статус✅ обучен (df_rich, kcp_nodice)✅ есть (rawboard_l000)❌ идея

Резюме: для prior’а аналитики сегодня NNUE не нужен — хватает LightGBM/MLP. NNUE — это план на следующий уровень live-ботов: KCP-качество оценки за микросекунды, включая слабое железо. Если пойдём туда — начинать с офлайн-прототипа на PyTorch и бенчмарка против df_rich/kcp_nodice, а рукописный инференс в движке делать последним шагом.