🎓 Почему бот зевает ферзя — диагноз и первые лекарства
О чём это
Играя против
gcp/expectimax-onnx-3(те же мозги, что у houseoracle-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, здесь только скелет:
- Бот генерирует все свои легальные ходы под выпавшие кости.
- Быстрый пре-ранкер (чистый материал) сортирует их и оставляет top-K кандидатов (
candidateLimit). - Для каждого кандидата поиск перебирает все 56 вариантов броска соперника, для каждого броска — все ответы, и берёт худший для нас; итог — взвешенное по вероятностям бросков ожидание (chance node).
- Каждый «лист» дерева оценивает value-модель (LightGBM → ONNX): 9 чисел на входе → «вероятность выигрыша» на выходе.
Ключевое: дальше двух полуходов бот не видит ничего — там работает только оценка модели.
3. Пять механизмов зевка
3.1 Модель физически не видит висящего ферзя (главная причина)
Все 9 входов модели oracle-3:
| # | Фича | Что это |
|---|---|---|
| 1–5 | p/n/b/r/q_diff | разница по количеству фигур каждого типа |
| 6 | material_diff | разница материала (1/3/3/5/9) |
| 7 | total_material | суммарный материал на доске |
| 8 | mobility_diff | разница в количестве псевдо-ходов |
| 9 | king_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. Пусть ферзя атакует пешка. Чтобы её сыграть, сопернику нужна кость «пешка» хотя бы на одном из трёх кубиков:
Обобщённо, если ферзя бьют фигуры разных типов:
| (типов атакующих) | (накажут этим ходом) |
|---|---|
| 1 | 42.1% |
| 2 | 70.4% |
| 3 | 87.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=8 56.5% 0.44 4.98 rich9 + rescore w=0.5, K=8 57.5% 0.39 4.77 rich9, K=24 69.3% 0.46 5.17 rich9 + rescore w=0.5, K=24 64.8% 0.44 5.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) — ретрен-раунд:
- Блок safety-фич в rich-набор: повисший материал с учётом костей ( по формуле из 3.3), максимальная висящая ценность, счётчики «защищён пешкой/фигурой», плюс быстрые замкнутые формулы вместо 216-DFS.
- Гигиена меток: веса по рейтингу игроков (колонки уже в экспорте — надо начать их читать).
- Монотонные ограничения LightGBM: оценка обязана расти по материалу и падать по висящему материалу — дёшево страхует от шума в метках.
- Гейт перед деплоем: 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=8 | K=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:
| K | oracle-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 #494 → PR #495
- Рескоринг в боте: dicechess-bot-gcp-onnx PR #1
- Провал дистилляции и логи обучения: репозиторий
dicechess-ev(logs/distill.log,logs/kcp.log)