🎓 Ранжирование против регрессии: что такое lambdarank и зачем он нам
О чём это
Наши модели весь год учились предсказывать число — вероятность победы из позиции. А та часть поиска, которая принимает больше всего решений в реальной партии, потребляет не число, а порядок: расставить ходы-кандидаты и взять лучшие.
Это не одно и то же, и разница — не мелочь. Здесь разбирается, почему, и что такое lambdarank — способ учить модель сразу правильному порядку.
1. Кто такой пре-ранкер и почему он важнее, чем кажется
Напомним устройство бота. За свой ход в dice chess можно сделать 1–3 микрохода по выпавшим костям, и вариантов полного хода бывают сотни. Прогнать каждый через глубокий анализ невозможно, поэтому работа делится надвое:
- Пре-ранкер быстро прикидывает все варианты и оставляет лучшие K штук (в проде K=24).
- Поиск честно считает эти K кандидатов на два полухода вперёд — дорого, зато видит ответ соперника.
Обычно считается, что вся сила — во втором шаге, а первый просто «отсеивает мусор». Замеры конца июля показали, что это не так (см. Bot-ne-glupyj-a-medlennyj):
Под часами пре-ранкер — это почти и есть бот
В реальной партии бот успевает досчитать один-два кандидата из двадцати четырёх. А когда время выходит раньше, чем закончился хотя бы один, он играет первый ход из списка пре-ранкера — просто потому, что ничего лучше посчитать не успел.
То есть решение принимает не обученная модель, а быстрая прикидка. И вот эта прикидка у нас до сих пор — чистый материал.
2. Что делает материальный пре-ранкер
Буквально следующее: для каждого хода-кандидата берём позицию, которая получится, складываем стоимость фигур (ферзь 9, ладья 5, слон и конь 3, пешка 1), сортируем по убыванию, берём верхние K. Никакого обучения — арифметика.
Беда в том, что материал слеп к безопасности. Возьмём позицию, где есть три хода:
| Ход | Что делает | Оценка материалом |
|---|---|---|
| X | тихое развитие, всё защищено | 0 |
| Y | съедает пешку, но ферзь встаёт под бой | +1 |
| Z | нейтральный | 0 |
Материальный пре-ранкер ставит Y на первое место: он видит съеденную пешку и не видит, что следующим ходом заберут ферзя. Ход X оказывается ниже, и в густой позиции с сотнями кандидатов он может вообще не попасть в top-24 — то есть поиск его никогда даже не рассмотрит. Умный двухполуходовый анализ не спасёт ход, который до него не дошёл.
Отсюда и вся история про зевки ферзя (Pochemu-bot-zevaet-ferzya).
3. Ложная развилка: «руками» против «обученного»
Напрашивается вывод: заменить арифметику на нейросеть. Но это мы уже пробовали — в движке есть параметр preRankWithModel, который ранжирует кандидатов той же самой обученной моделью rich_1m. Замерено +1.5 п.п. Полировка, не рычаг.
Значит, дело не в том, «обученная модель или нет». Дело в том, какой задаче эта модель обучена.
4. Главное различие: регрессия против ранжирования
Регрессия — то, что мы делали весь год
Модель учится предсказывать число: вероятность победы из данной позиции. Функция потерь наказывает за отклонение предсказания от факта. Сказала 0.62, а партия выиграна — штраф маленький; сказала 0.90 — штраф большой.
Модель вознаграждается за точность величины.
Ранжирование — то, что предлагается
Модель учится, чтобы внутри одной группы кандидатов хорошие стояли выше плохих. Абсолютные значения не важны совсем:
- выдала 0.001 и 0.002, порядок верный → идеально;
- выдала 0.62 и 0.61, порядок неверный → штраф.
Аналогия
Градусник против медсестры в приёмном покое.
Градусник даёт точное число — 38.4°. Медсестра числа не называет, но безошибочно расставляет десять пациентов по срочности. Если вам всегда нужно только «кого лечить первым», идеально откалиброванный градусник избыточен — и вполне может проиграть тому, кого учили именно расставлять.
take(K) и «взять лучший» потребляют только порядок. Всё, что модель потратила на точность величины, для этой работы выброшено.
Хуже того: регрессия тратит ёмкость на то, чтобы число было верным в среднем по 15 миллионам позиций корпуса. А нам нужно различить двести почти одинаковых кандидатов внутри одной позиции. Это разные задачи, и вторая из первой не следует.
5. Что такое собственно lambdarank
Это способ обучать модель порядку. Механика по шагам.
Группы. Данные бьются на группы. Одна группа = одна точка принятия решения: конкретная позиция плюс конкретный бросок костей. Элементы группы = легальные ходы-кандидаты в этой позиции.
Метки. Каждому кандидату ставится «релевантность». Самый дешёвый вариант — 1 у хода, который реально сделал сильный игрок, 0 у всех остальных. Движок для этого запускать не надо: и сделанные ходы, и рейтинги игроков уже лежат в выгрузке корпуса.
Пары. Функция потерь смотрит на пары внутри группы: если модель поставила плохой ход выше хорошего — это нарушение, за него штраф.
И теперь самое интересное — сами «лямбды». Лямбда, давшая имя методу, — это вклад каждой пары в градиент, взвешенный по тому, насколько перестановка этой пары сдвинет итоговое качество списка. Ошибка в хвосте списка почти ничего не весит. Ошибка на первых местах весит много.
Почему это ложится на нашу задачу как влитое
У нас ровно та же экономика ошибок. Мы берём top-24, а под часами часто играем вообще top-1.
Ошибка на 150-м месте не стоит ничего — этот кандидат всё равно не был бы просчитан. Ошибка на первом месте — это буквально ход, который сделает бот.
Lambdarank сам, по устройству функции потерь, направляет обучение туда, где оно нам нужно.
6. Как меняются обучающие данные
| Регрессия (сейчас) | Lambdarank | |
|---|---|---|
| Что такое строка | позиция → выиграли ли (0/1) | кандидат внутри группы → релевантность |
| Группировка | нет, строки независимы | группа = позиция + бросок костей |
| Чему учится | «какова вероятность победы» | «какой ход выбрал бы сильный игрок» |
| Что важно в ответе | величина | порядок |
Практическая сторона: LightGBM умеет lambdarank из коробки, экспорт такой модели в ONNX поддерживается, а в движке уже есть куда её воткнуть — параметр preRank. Недостающий кусок ровно один: генератор групп кандидатов, которого пока нет.
7. Честные ограничения
Это не лекарство от слепоты фич. Если признаки физически не различают два хода, никакая функция потерь этого не исправит. Lambdarank — способ выжать из тех же признаков сигнал порядка, который регрессия выбрасывает, а не способ увидеть невидимое.
Ранкер не может быть листовым эвалюатором. Узел случайности берёт минимум по ответам соперника и матожидание по 56 броскам — для этого нужна осмысленная шкала величин. У ранкера её нет по определению: он даёт порядок, а не измерение. Его место только в шве пре-ранкера.
У этого шва смешанная репутация. Два предыдущих жильца дали +1.5 п.п. и −4.5 п.п. Аргумент, почему на этот раз может быть иначе, ровно один: обе те модели учились на калибровку, а использовались для упорядочивания.
8. Непроверенная клетка
Если свести весь наш цикл в таблицу, видно, где дырка:
| Попытка | Признаки видят опасность? | Задача обучения | Где применялось | Результат |
|---|---|---|---|---|
rich_1m | нет | регрессия | листовой эвалюатор | 65–66% |
safe_1m | да | регрессия | листовой эвалюатор | probe-гейт починился, побед ноль |
| материальный пре-ранкер | нет | обучения нет | пре-ранкер | слепота к зевкам |
preRankWithModel | нет | регрессия | пре-ранкер | +1.5 п.п. |
| lambdarank | (можно взять safe-признаки) | ранжирование | пре-ранкер | не проверено |
Мы чинили признаки, но оставляли регрессию. Мы ставили обученную модель в пре-ранкер, но она была обучена не тому. Клетка «правильные признаки + ранжирующая задача + место пре-ранкера» так и осталась пустой — и это единственный ретрен, который сейчас выглядит осмысленным.
С оговоркой, которую стоит держать в голове: эффект такого изменения, скорее всего, будет в пределах нескольких процентных пунктов, а наш нынешний измерительный инструмент меньше ~3 п.п. просто не различает. Поэтому сначала — инструмент, и лишь потом ретрен.
9. Ссылки
- Почему пре-ранкер оказался важнее, чем думали: Bot-ne-glupyj-a-medlennyj
- История про зевки ферзя и слепоту признаков: Pochemu-bot-zevaet-ferzya
- Что вообще такое LightGBM: Chto-takoe-LightGBM
- Каталог рассмотренных вариантов усиления: Katalog-variantov-usileniya-bota
- Задача на измерительный инструмент: engine #508
- Задача на сам ретрен:
dicechess-ev#14 (приватный репозиторий)