🎓 Ранжирование против регрессии: что такое lambdarank и зачем он нам

О чём это

Наши модели весь год учились предсказывать число — вероятность победы из позиции. А та часть поиска, которая принимает больше всего решений в реальной партии, потребляет не число, а порядок: расставить ходы-кандидаты и взять лучшие.

Это не одно и то же, и разница — не мелочь. Здесь разбирается, почему, и что такое lambdarank — способ учить модель сразу правильному порядку.


1. Кто такой пре-ранкер и почему он важнее, чем кажется

Напомним устройство бота. За свой ход в dice chess можно сделать 1–3 микрохода по выпавшим костям, и вариантов полного хода бывают сотни. Прогнать каждый через глубокий анализ невозможно, поэтому работа делится надвое:

  1. Пре-ранкер быстро прикидывает все варианты и оставляет лучшие K штук (в проде K=24).
  2. Поиск честно считает эти 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 (приватный репозиторий)