🔬 Дизайн: анализ партий
Что это
Продуктовое применение обеих serving-моделей: динамическая шкала оценки по ходу партии, разложение каждого свинга на удачу и решение (фича, которой нет у шахматных баз — она возможна только в игре с костями) и оценка каждого хода человека против выбора модели. Спроектировано 10.08.2026; issues: analytics#276 (roll-aware эндпоинт), analytics#277 (грейдер ходов), analytics-ui#206 (сама фича, три фазы).
Связанные: ml-lab-journal · model-artifacts-storage-and-deploy · what-is-lightgbm · kcp-feature-tactics-vs-search-depth
Почему это вообще возможно: экономика перевернулась
Та же модель, что не годится для бота, идеально годится для анализа — из-за того, когда платится цена оценки:
| Бот (live) | Анализ партии | |
|---|---|---|
| Оценок на действие | сотни (1-ply) … сотни тысяч (2-ply) | одна на ход партии |
| Бюджет | секунды на ход, всегда | минута на партию, один раз |
| Повторное использование | нет | кэш навсегда (позиции дебюта шарятся между партиями) |
Стоимость одной оценки ~40–60 мс, и она сидит в фичах (4 DFS по 216 исходов в KcpFeatures), а не в модели (микросекунды). Для анализа это приемлемо; для листа поиска — нет. Подробно: запись журнала от 10.08.
Какие модели и в каких ролях
Обе — LightGBM objective=regression на полном корпусе 19 991 736 строк (только king_captured), версионированы с манифестами в приватном ev (ev#24):
| Модель | Входы | Раскладка | Роль в фиче | Статус |
|---|---|---|---|---|
kcp_nodice_serving_20260809.onnx | 13 | ровно KcpFeatures.extract | шкала (pre-roll точки); оценка позиций после хода в грейдере | ✅ в проде (EV_MODEL_PATH, analytics v0.6.0) |
kcpdice_serving_20260809.onnx | 19 | KcpFeatures.extract ++ 6 счётчиков костей (--dice-last) | компонента удачи: P(win | позиция, бросок) | экспортирована, ждёт #276 (EV_ROLL_MODEL_PATH) |
Ловушка двух 19-входовых раскладок
В ev существуют ДВЕ раскладки шириной 19: обучающая (кости в середине) и серверная (кости в конце). По ширине они неразличимы, а скоринг «не своей» раскладкой не падает — он возвращает правдоподобные неверные числа. Поэтому serving-модели обучены
--dice-last(сервер дописывает 6 чисел к массиву движка, а не вклеивает в середину), загрузчики проверяют ширину и называют раскладку в ошибке, а происхождение каждого файла закреплено манифестом.
Механика и формулы
Все вероятности — с точки зрения ходящего (как в моделях); UI нормализует к белым для графика: P_white = side=='w' ? p : 1−p.
Шкала. Точка на каждой границе хода (pre-roll позиция — ровно те моменты, где сейчас показывается панель equity): P_pre(pos_t) от 13-входовой модели. Середины ходов не оцениваем — это не начала ходов, pre-roll число там врало бы (та же логика, по которой панель на них скрывается). Терминальная позиция — 0/1 из результата, не из модели.
Удача броска. На ходе t выпали кости dice_t:
luck(t) = P_roll(pos_t, dice_t) − P_pre(pos_t)— насколько сам бросок улучшил или ухудшил положение ходящего, до всяких решений. Требует 19-входовую модель (#276).
Качество решения. Все легальные пути хода перечисляет TurnGenerator (движок), каждая результирующая позиция оценивается 13-входовой моделью; вероятность ходящего после пути = 1 − model(результат), потому что в результирующей позиции ход соперника; путь со взятием короля терминален — ровно 1.0 без модели:
decision(t) = P_after(сыгранный путь) − P_after(лучший путь) // ≤ 0Свинг между соседними точками шкалы ≈ luck(t) + decision(t) + невязка модели — разложение показывается пользователю как две полоски под графиком: «что дали кости» и «что сделал игрок».
Агрегаты на партию: суммарная удача каждой стороны («кому везло»), accuracy = средний winrate-loss, маркеры зевков при −decision(t) выше порога. Порог не выдумывается: калибруется по ground-truth набору наказанных зевков из mine_blunders.py (ev) по рейтинг-бендам — отдельная задача в ev.
Стоимости
| Операция | Цена | Когда платится |
|---|---|---|
| точка шкалы | 30–70 мс | ~30–40 на партию ≈ 2–4 с первый просмотр, дальше кэш по FEN |
| компонента удачи | +40–60 мс на ход | вместе со шкалой (фаза 2) |
| грейдинг одного хода | до ~6,6 с (сотни путей × KCP) | только по явной кнопке «Analyze game», с прогрессом; дедуп транспозиций и параллелизм внутри запроса срезают типичный случай до ~1–3 с |
| грейдинг партии | ~1 мин на 4 ядрах | один раз, кэш по (normalized_fen, dice_sorted) — дебютные ходы шарятся между всеми партиями |
Фазы поставки (каждая самостоятельно полезна)
- Шкала — чистый UI, бэкенд уже готов (существующий
/win-probability+ существующий кэшfetchWinProbability). Фан-аут с ограниченной конкуренцией — как 56-вызовная матрица. - Полоски удачи — после #276; деградирует до фазы 1, если roll-эндпоинт отвечает 503.
- Оценка ходов — после #277; кнопка «Analyze game», winrate-loss на ход, зевки, accuracy.
На деплое без моделей (503 всюду) фича просто отсутствует с одним тихим уведомлением — не ошибка.
Честные пределы (обязаны дойти до текстов UI)
- Грейдер — 1-ply оценщик силы ~сильного клубного игрока (60.8% против
aggressive), не оракул: расхождение с моделью — улика, не приговор. Тексты: «Model suggests», не «Correct was». Глубокую тактику на 2+ хода вперёд он не видит. - Шкала дрожит ±1–2 пп между спокойными ходами (MAE модели ~0.9 пп на пробах) — сглаживать или квантовать подачу, не изображать точность.
- Разложение свинга содержит невязку (модель несовершенна в обеих точках) — полоски удачи/решения подавать как оценку, суммы за партию честнее одиночных ходов.
Открытые вопросы
- Порог «зевка»: калибровка по
mine_blunders.pyпо рейтинг-бендам (задача в ev). - Персист кэша грейдинга: v1 — in-memory LRU; таблица в БД (game_id, model_version → JSONB) только если перезапуски заметно бьют по повторным анализам.
- Показывать ли «лучший ход» явно или только дельту (спойлер-эффект в тренажёрном сценарии).
- Тренажёрный режим поверх грейдера («найди лучший ход при этих костях») — естественное продолжение, отдельный дизайн.