🎓 Почему бот зевает ферзя — диагноз и первые лекарства

О чём это

Играя против gcp/expectimax-onnx-3 (те же мозги, что у house oracle-3), легко заметить: бот подставляет ферзя под пешку и вообще не любит держать фигуры защищёнными. Мы прошли по всему конвейеру — фичи → обучение → поиск → прод-обвязка — и нашли пять механизмов, которые вместе дают этот эффект (и один подозреваемый оказался невиновен). Здесь разбор «почему», два уже готовых лекарства (телеметрия зевков + KCP-рескоринг корня) и план ретрена. Первые разделы — концептуальные, можно читать без кода.

Связанные: 2-ply-Expectimax-poverkh-value-modeli · KCP-ficha-taktika-protiv-glubiny-poiska · Chto-takoe-LightGBM · 03-Idei-po-uluchsheniyu-algoritmov


1. Что видно за доской

Бот выводит ферзя на поле, которое бьёт пешка соперника, — и оставляет его там. Живой шахматист так почти никогда не делает: ферзь стоит девять пешек, и даже начинающий держит его подальше от дешёвых нападений, а фигуры старается расставлять так, чтобы они защищали друг друга.

Интуитивно кажется, что «бот просто слабый». Но у бота есть поиск на два полухода и обученная на десятках тысяч партий модель — почему же он делает ошибку, которую человек видит мгновенно? Ответ интереснее, чем «плохо обучили»: это системная слепота, а не случайный шум.

2. Как устроен мозг бота (короткое напоминание)

Подробно — в 2-ply-Expectimax-poverkh-value-modeli, здесь только скелет:

  1. Бот генерирует все свои легальные ходы под выпавшие кости.
  2. Быстрый пре-ранкер (чистый материал) сортирует их и оставляет top-K кандидатов (candidateLimit).
  3. Для каждого кандидата поиск перебирает все 56 вариантов броска соперника, для каждого броска — все ответы, и берёт худший для нас; итог — взвешенное по вероятностям бросков ожидание (chance node).
  4. Каждый «лист» дерева оценивает value-модель (LightGBM → ONNX): 9 чисел на входе → «вероятность выигрыша» на выходе.

Ключевое: дальше двух полуходов бот не видит ничего — там работает только оценка модели.

3. Пять механизмов зевка

3.1 Модель физически не видит висящего ферзя (главная причина)

Все 9 входов модели oracle-3:

#ФичаЧто это
1–5p/n/b/r/q_diffразница по количеству фигур каждого типа
6material_diffразница материала (1/3/3/5/9)
7total_materialсуммарный материал на доске
8mobility_diffразница в количестве псевдо-ходов
9king_safety_diffбинарный флаг «король под боем» (±2000)

Теперь мысленный эксперимент: ферзь стоит на d5 под ударом пешки c6 — и тот же ферзь на соседнем безопасном поле. Какая из девяти фич изменится? Материал тот же, мобильность почти та же, король не тронут. Векторы фич практически идентичны — а значит, модель не может научиться отличать эти позиции, сколько бы партий ей ни показали. Это не «модель плохо выучила» — ей буквально не из чего это выучить.

Единственная фича, которая видит опасность ферзю — queen_capture_danger из набора kcp-13 (KCP-ficha-taktika-protiv-glubiny-poiska) — в прод не задеплоена: её 216-DFS слишком дорог для тысяч листьев. Попытка «дистиллировать» знание kcp-модели в 9-фичную провалилась с показательным результатом: AUC 0.7023 при любой доле учителя — студенту некуда записать сигнал, которого нет в его входах.

Следствие про «защищённые фигуры»

Вторая привычка сильных игроков — расставлять фигуры под защитой — отсутствует у бота по той же причине: среди фич нет ни «атакован», ни «защищён своими». Обе привычки лечатся одним и тем же блоком safety-фич (раздел 6).

3.2 Обучение никогда не «штрафовало» зевки как следует

Метка при обучении — итог всей партии, приписанный каждой позиции. Фильтра по силе игроков нет: колонки с рейтингом и типом игрока экспортируются, но не используются. В корпусе слабых игроков висящий ферзь часто остаётся безнаказанным (соперник не заметил или не выпала нужная кость) — и партия всё равно выигрывается. Модель честно выучивает то, что видит в данных: «остаться без ферзя не так уж страшно». Штраф за −9 материала сжимается к уровню шума.

3.3 Кости разбавляют наказание

Дальше — чистая математика Dice Chess. Пусть ферзя атакует пешка. Чтобы её сыграть, сопернику нужна кость «пешка» хотя бы на одном из трёх кубиков:

Обобщённо, если ферзя бьют фигуры разных типов:

(типов атакующих) (накажут этим ходом)
142.1%
270.4%
387.5%

Поиск считает ожидание честно: штраф за зевок входит в оценку с весом ~0.42, а остальные ~58% массы — те самые «слепые» листья из 3.1, где висящий ферзь выглядит нормально. Итого: (сжатый штраф из 3.2) × 0.42 — и ход с зевком спокойно обходит тихий безопасный ход в рейтинге.

Важный нюанс

В Dice Chess подставить фигуру с шансом наказания 42% иногда корректно — если компенсация того стоит. Цель лечения — не «никогда не висеть», а правильно ценить риск. Сейчас риск оценивается почти нулём.

3.4 Материальный пре-ранкер хоронит тихие ходы

В прод-обвязке кандидатов сортирует чистый материал (проверено по коду: preRankWithModel не включён). Все тихие ходы по материалу равны — и «увести ферзя в безопасность» стоит в списке там, куда его поставил генератор ходов, часто за пределами top-K. Ходы, которых нет среди кандидатов, для поиска не существуют. Косвенное подтверждение силы этого эффекта: расширение K с 8 до 16 дало +4.8 п.п. винрейта — чисто за счёт «откопанных» ходов.

3.5 На 1 vCPU дедлайн режет ширину

Cloud Run даёт боту 1 vCPU и ~2 секунды на ход. Дедлайн проверяется между кандидатами, а один кандидат — это 56 бросков × все ответы через одну ONNX-сессию. Если реально успевают посчитаться 2–3 кандидата из 24 — бот вырождается в «сыграй первый выбор материального пре-ранкера», то есть в жадину. До сегодняшнего дня это никто не мог измерить (см. раздел 4).

3.6 Кто оказался невиновен

Первая гипотеза — «наказывающий ход соперника отрезается прунингом» — не подтвердилась: candidateLimit режет только ходы самого бота, ответы соперника перебираются полностью для каждого броска. Прямой зевок «в один ход» поиск видит — он просто неправильно его ценит (3.1–3.3). А вот зевок с наказанием на третьем полуходе (соперник нападает, бьёт следующим ходом) не виден вообще: квиесценции нет, горизонт — два полухода.

4. Лекарство №1: телеметрия зевков (сначала измерь, потом лечи)

До сих пор арена выдавала только W/L/D — «стал ли бот меньше зевать» проверить было нечем. Теперь движок умеет считать (issue #494):

МетрикаСмысл
HangTходов, после которых своя фигура осталась «висеть» (атакована и не защищена)
QHangиз них — с висящим ферзём
HungMatсуммарный повисший материал (в пешках)
Punishedсколько повисших фигур соперник реально забрал следующим ходом
PunMatматериал наказанных зевков

Определение «висит» — намеренно простое (булево «атакована и не защищена», без разменной оценки) и живёт в одном месте (PieceSafety), чтобы метрика и будущие фичи считали зевок одинаково. Плюс поиск теперь умеет сообщать, сколько кандидатов реально успел посчитать до дедлайна (RootSearchStats) — это прямой тест механизма 3.5 на прод-железе.

5. Лекарство №2: KCP-рескоринг корня

Идея из KCP-ficha-taktika-protiv-glubiny-poiska, доведённая до прода: kcp-модель слишком дорога для листьев, но корневых кандидатов всего K ≤ 24. Рескоринг прогоняет kcp-модель один раз по позициям после наших ходов-кандидатов и смешивает оценки:

queen_capture_danger — вторая по важности фича kcp-модели, и именно она «видит» ферзя под боем. Стоимость — K × ~4 мс DFS ≈ 100–200 мс из 2-секундного бюджета. Движок это уже умел (RootRescoreModel); сегодня добавлена обвязка в Cloud Run-боте: переменные RESCORE_MODEL_PATH и RESCORE_WEIGHT, kcp-модель монтируется из того же приватного бакета рядом с основной.

Гарантия безопасности: кандидат, который хоть на одном броске теряет короля, рескорингом не спасается (сентинель проигрыша всегда ранжируется последним).

Результаты валидации (25.07): гипотеза не подтвердилась

Парная сидированная арена против aggressive, 400 партий на конфигурацию, движок с новой телеметрией:

КонфигурацияВинрейтЗевки ферзя/партиюНаказано (пешки/партию)
rich9, K=856.5%0.444.98
rich9 + rescore w=0.5, K=857.5%0.394.77
rich9, K=2469.3%0.465.17
rich9 + rescore w=0.5, K=2464.8%0.445.65

На K=8 рескоринг — шум (+1.0 п.п.); на прод-конфигурации K=24 он вредит (−4.5 п.п.). Урок: ожидание по 56 броскам × все ответы (2-ply) — уже более сильный сигнал, чем 1-ply kcp-модель, и смешивание 50/50 его разбавляет. Ранний +6 п.п. на 100 партиях оказался шумом малой выборки — ещё один аргумент, почему решения принимаются только по 400+ партиям. Второй урок: расширение K с 8 до 24 само по себе даёт +12.8 п.п., но зевки при этом не уменьшаются — сила растёт не за счёт аккуратности, что снова указывает на слепоту фич как первопричину. В прод рескоринг с w=0.5 не выкатываем; обвязка в боте остаётся выключенной способностью (возможен свип малых весов w ≤ 0.2 под контролем телеметрии).

6. Что дальше: ретрен с safety-фичами

После валидации картина ясна: телеметрия — рабочий инструмент, рескоринг — тупик, а лечение первопричины (3.1) — ретрен-раунд:

  1. Блок safety-фич в rich-набор: повисший материал с учётом костей ( по формуле из 3.3), максимальная висящая ценность, счётчики «защищён пешкой/фигурой», плюс быстрые замкнутые формулы вместо 216-DFS.
  2. Гигиена меток: веса по рейтингу игроков (колонки уже в экспорте — надо начать их читать).
  3. Монотонные ограничения LightGBM: оценка обязана расти по материалу и падать по висящему материалу — дёшево страхует от шума в метках.
  4. Гейт перед деплоем: probe-suite из позиций «ферзь en prise vs на клетку левее» — у oracle-3 оценка этих пар почти не отличается, у новой модели обязана.

План проверки: сидированная арена ×400 партий, rich9 против rich9+rescore при одинаковом K, с новой телеметрией — отдельно увидим и винрейт, и снижение зевков.

7. Ссылки

  • Диагноз и телеметрия: engine issue #494PR #495
  • Рескоринг в боте: dicechess-bot-gcp-onnx PR #1
  • Провал дистилляции и логи обучения: репозиторий dicechess-ev (logs/distill.log, logs/kcp.log)