🎓 Почему бот зевает ферзя — диагноз и первые лекарства
О чём это
Играя против
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, с новой телеметрией — отдельно увидим и винрейт, и снижение зевков.
7. Ссылки
- Диагноз и телеметрия: engine issue #494 → PR #495
- Рескоринг в боте: dicechess-bot-gcp-onnx PR #1
- Провал дистилляции и логи обучения: репозиторий
dicechess-ev(logs/distill.log,logs/kcp.log)