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

О чём это

Играя против 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, с новой телеметрией — отдельно увидим и винрейт, и снижение зевков.

6.1. Результаты ретрена (26.07): кроссовер по ширине поиска

Ретрен выполнен на полном корпусе — 1.04 млн партий, 15.3 млн позиций (в 13 раз больше, чем видела oracle-3). Обучены две модели: rich_1m (те же 9 фич, только больше данных) и safe_1m (+6 safety-фич, веса по рейтингу, монотонные ограничения).

Probe-гейт сработал как задумано. На 92 тысячах реальных зевков ферзя, добытых из корпуса: oracle-3 и rich_1m ставят позицию с висящим ферзём выше безопасной в 87% пар (они её буквально предпочитают — мобильность вознаграждает центральную расстановку). У safe_1m знак переворачивается: 53% в пользу безопасной. Слепота вылечена.

Но сила за этим не последовала. Полная таблица, 400 партий на ячейку, против aggressive, сидировано:

МодельK=8K=24 (прод)
oracle-3 (kcp-дистиллят)56.5%69.3%
rich_1m (корпус 1.04M)60.5%65.5%
safe_1m (safety-фичи)56.5%

Знак меняется в зависимости от ширины поиска

Новая модель лучше на узком поиске (+4.0 п.п. при K=8) и хуже на широком (−3.8 п.п. при K=24). Обе разницы по отдельности в пределах шума (при n=400 стандартная ошибка разницы ≈3.4 п.п., то есть z≈1.1) — так что утверждать превосходство ни одной из них нельзя. Важно другое: точечная оценка на K=24, где бот реально работает, в пользу старой oracle-3.

Рабочая гипотеза: oracle-3 дистиллирована с kcp-учителя, её сглаженные метки лучше ранжируют почти равных кандидатов — ровно то, что нужно при K=24, где надо выбрать лучший из 24 близких ходов. rich_1m учена на сырых исходах большего, но более шумного корпуса: калибрована в среднем лучше, различает близкие ходы хуже.

Урок методологии: валидировать надо в той конфигурации, в которой деплоишь. Замер делался на K=8, а выкатывалось на K=24 — и знак эффекта оказался противоположным.

safe_1m в прямой дуэли против rich_1m при K=8 дал 49.0% — ничья, при том что зевает измеримо меньше (0.50 против 0.56 зевков ферзя за партию) и работает в 3 раза медленнее. Это согласуется с находкой из майнинга: винрейт зевнувшего ферзя растёт с рейтингом (32% у слабых → 47% у сильных). В dice chess зевок объективно дёшев — дисперсия костей его прощает, — поэтому аккуратность сама по себе в победы не конвертируется.

Оба бота выставлены на живой ладдер (gcp/expectimax-onnx-4 и lab/safe-1) — там разнообразные соперники, чего сидированная арена против одного aggressive не даёт.

6.2. Кривая по ширине поиска (26.07): потолок есть, но не такой чистый, как казалось на полпути

Раздел 6.1 сравнивал модели только на двух точках (K=8 и K=24, по 400 партий). Чтобы понять, не изменится ли вывод на других ширинах поиска, досняли ещё три точки — K=16, K=32, K=48 — для обеих моделей на отдельной Oracle-машине (арена без нагрузки от живого флота ботов).

Разный объём выборки — читать осторожно

Точки K=8/K=24 сделаны на 400 партиях (стандартная ошибка ≈2.5 п.п.), а новые K=16/K=32/K=48 — только на 100 партиях каждая (SE≈4.9 п.п., вдвое шумнее). Прямое сравнение чисел без поправки на это будет обманчивым.

Полная таблица, винрейт против aggressive:

Koracle-3 (kcp-дистиллят)rich_1m (корпус 1.04M)
8 (n=400)56.5%60.5%
16 (n=100)64.0%66.0%
24 (n=400, прод)69.3%65.5%
32 (n=100)66.0%66.0%
48 (n=100)60.0%66.0%

rich_1m действительно выходит на плато: 66.0 / 65.5 / 66.0 / 66.0 при K от 16 до 48 — все четыре точки в пределах шума друг друга. Дальнейшее расширение поиска этой модели ничего не даёт: ей хватает и K=16.

oracle-3 ведёт себя не так гладко: пик на K=24 (69.3%), затем просадка на K=32 (66.0%) и заметная просадка на K=48 (60.0%). При n=100 разница K=32→K=48 (6 п.п.) — это около 1.2 стандартной ошибки, то есть формально не значима, но и не выглядит как чистый шум вокруг одного числа: тренд направленный, не дребезжащий. Не исключено, что дистиллированные из kcp-учителя метки oracle-3 (глава 6.1: гипотеза «лучше ранжирует близких кандидатов, хуже — в среднем») теряют это преимущество, когда кандидатов становится действительно много, и сама модель на широком K начинает путать похожие продолжения сильнее, чем rich_1m. Это гипотеза, не подтверждённый факт — для проверки нужен повтор K=48 на 400 партиях, а это ещё ~4× время (уже ушло 6–8 часов на ячейку при n=100).

Практический вывод не меняется: обе модели вокруг K=24 (прод-конфигурация) дают 65–69% — и никакая доступная ширина поиска не поднимает результат заметно выше этого. Дальнейшее расширение K — не рычаг для победы над aggressive; рычаг, если он есть, лежит в качестве модели или в фичах, а не в поиске. Это подтверждает решение из главы 6.1 не гнаться за более широким K и держать текущий прод (K=24, oracle-3/rich_1m оба на ладдере как независимые боты) как есть.

⚠️ ПОПРАВКА 27.07 — вывод выше верен только БЕЗ ЧАСОВ

Вся таблица 6.2 снята на арене без контроля времени, где поиску дают досчитать до конца. Замеры следующего дня показали, что в бою под часами бот успевает не 24 кандидата, а один-два, — то есть работает в той области кривой, где сила ещё круто растёт с шириной. Фраза «рычаг лежит в качестве модели, а не в поиске» оказалась преждевременной: сначала надо вернуть ширину, которую съедает медленность. Подробный разбор и новый план — Bot-ne-glupyj-a-medlennyj.

7. Ссылки

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