Часть IV · Последовательности и выдача · глава 16 из 19
Exploration и бандиты
Все предыдущие главы улучшали модель при фиксированной политике показа. Но модель учится на том, что показала предыдущая модель, и хороший айтем, которого никогда не показывали, так и останется неизвестным. Эта глава — про то, как разорвать круг, сколько это стоит и почему стохастичность выдачи нужна не только ради исследования.
- Круг замкнут буквально: чтобы узнать CTR, надо показать; чтобы показать, надо знать CTR. Жадная политика берёт 0.834 от идеальной выдачи и не находит спрятанный самородок ни в одном из восьми каталогов.
- Бонус за незнание даёт +9% перестановкой того же пула кандидатов — без новой модели и без новых признаков. Но зависимость от \(\alpha\) — горб, а не лестница.
- Главная ловушка — не всякая неопределённость полезна. \(\sqrt{p(1-p)}\) не убывает с числом показов вовсе; это шум события, а не незнание модели.
- Thompson выбирают не за качество, а за измеримость. Детерминированная политика даёт склонности 0 и 1 — и вся офлайн-оценка превращается в деление на ноль.
1. Замкнутый круг
Проблема сформулирована ещё в главе про данные, здесь мы её решаем. Ранжирующая модель обучена на логах; логи собраны предыдущей версией той же модели; всё, что она не показывала, в обучающих данных отсутствует.
Каталог из 40 айтемов, показываем топ-10, у шести айтемов истории нет вовсе. Среди новых спрятан самородок с истинным CTR 0.145 — лучший в каталоге, где у ближайшего конкурента 0.097.
Модель про него не знает ничего: его оценка равна априорной, 0.06. А значит он не попадёт в топ-10. А значит показов не получит. А значит оценка не изменится.
Числа воспроизводятся скриптом _tools/bandit_demo.py в этом репозитории.
Чтобы узнать CTR, надо показать; чтобы показать, надо знать CTR. Это не патология данных и не ошибка обучения — это устойчивое состояние системы, в которое она приходит сама.
2. Два дешёвых механизма
Прежде чем строить что-то сложное, стоит знать: первые 95% выгоды от exploration обычно забираются двумя простыми приёмами. Всё остальное в этой главе — про оставшиеся проценты.
На доле \(\varepsilon\) позиций показываем случайные рекомендации, в остальных — выдачу модели.
Плюс: сильно разнообразит обучающий пул, причём именно тем, чего модель никогда бы не показала. Можно усложнить, сделав «случайную» часть не случайной, а выученной под отдельный эксплоративный таргет.
Минус: делает это топорно. Случайный айтем из миллионного каталога почти наверняка плох, и сетап приходится подбирать аккуратно, чтобы не уронить бизнес-метрики.
Считаем софтмакс от скоров с температурой и сэмплируем из полученного распределения без возвращения:
$$ P(i) \;\propto\; \exp\bigl(f(u,i)/T\bigr) $$Плюс: бережно к бизнес-метрикам — мы не показываем случайный мусор, а слегка перемешиваем хорошее. Легко подобрать сетап, и есть быстрый способ сэмплировать без возвращения через Gumbel-top-\(k\): прибавить к логитам гумбелевский шум и взять топ.
Минус, и он принципиален: софтмакс-сэмплинг переранжирует только существующий топ. Он исследует внутри того, что уже отобрала воронка, а воронка отбирала жадно. Чтобы исследовать по-настоящему, механизм нужен на всех стадиях, включая retrieval.
3. Алгоритмы
Теперь собственно бандитская теория — ровно в том объёме, который спрашивают.
Выбираем действие с наибольшей верхней оценкой матожидания награды:
$$ a_t = \arg\max_a \bigl[\,q_t(a) + u_t(a)\,\bigr] $$Красота в том, что бонус \(u_t\) не выдумывается, а выводится.
Неравенство Хёфдинга. Пусть \(X_1 \dots X_n\) — независимая выборка из распределения на \([0,1]\) с истинным средним \(\mu\), а \(\hat\mu\) — выборочное среднее. Тогда для любого \(u > 0\):
$$ \mathbb{P}\bigl(\mu \ge \hat\mu + u\bigr) \le e^{-2nu^2} $$Приравняем правую часть к \(\delta\) и разрешим относительно \(u\). С вероятностью \(1-\delta\) истинное \(Q(a)\) не превосходит \(Q_k(a) + U_k(a)\), где
$$ U_k(a) = \sqrt{\frac{-\ln \delta}{2\,n_k(a)}} $$Осталось выбрать \(\delta\). Берём \(\delta = 1/k^{c}\) — то есть с ростом времени требуем всё более надёжную границу — и получаем каноническую формулу:
$$ a_k = \arg\max_a \Bigl[\, Q_k(a) + c\sqrt{\frac{\log k}{n_k(a)}} \,\Bigr] $$Читается прозрачно: оценка средней награды плюс ширина доверительного интервала.
| Показов у ручки | Ширина при \(\delta = 0.05\) | Бонус \(\sqrt{\log k / n}\) при \(k = 10^4\) |
|---|---|---|
| 1 | 1.2239 | 3.0349 |
| 10 | 0.3870 | 0.9597 |
| 100 | 0.1224 | 0.3035 |
| 1000 | 0.0387 | 0.0960 |
| 10000 | 0.0122 | — |
Числа воспроизводятся скриптом _tools/bandit_demo.py.
Бонус убывает как \(1/\sqrt{n}\), и отсюда главное свойство: отдельной «ручки исследования» не нужно. Ручка, которую дёргали мало, всплывает сама; по мере накопления данных исследование сходит на нет автоматически.
Байесовская альтернатива. Для каждого действия задаём модель награды и распределение над её параметрами, затем на каждом шаге:
- сэмплируем параметры из апостериора: \(\theta \sim p(\theta \mid \text{данные})\);
- действуем жадно относительно сэмпла: \(a_t = \arg\max_a \mathbb{E}[r \mid a, \theta]\).
Неопределённость в параметрах и есть механизм исследования: чем меньше данных о ручке, тем шире её апостериор и тем чаще сэмпл выпадет большим.
Beta-Bernoulli — рабочая лошадка. Награда бинарная, \(r \sim \mathrm{Bernoulli}(\theta_a)\), априор \(\theta_a \sim \mathrm{Beta}(\alpha_a, \beta_a)\). Бета сопряжена с бернуллиевским, поэтому обновление тривиально:
$$ r_t = 1: \ \alpha_a \mathrel{+}= 1, \qquad r_t = 0: \ \beta_a \mathrel{+}= 1 $$Сэмплируем \(\theta_a\) для каждой ручки, берём максимум. Три строки кода и ни одного гиперпараметра, кроме априора.
Три ручки с CTR 0.30, 0.10, 0.05; горизонт 20 000 шагов. Regret — суммарная упущенная награда.
| Политика | t = 1000 | t = 5000 | t = 10000 | t = 20000 |
|---|---|---|---|---|
| ε-greedy 0.10 | 25.9 | 91.0 | 164.9 | 307.5 |
| UCB, c = 1 | 28.0 | 46.3 | 57.2 | 67.1 |
| Thompson | 14.1 | 19.4 | 21.0 | 23.8 |
Смотреть надо не на высоту в конце, а на прирост при удвоении отрезка:
| Политика | 5000 → 10000 | 10000 → 20000 | Отношение |
|---|---|---|---|
| ε-greedy 0.10 | 73.9 | 142.5 | 1.93 |
| UCB, c = 1 | 10.9 | 9.9 | 0.91 |
| Thompson | 1.6 | 2.8 | 1.70 |
Числа воспроизводятся скриптом _tools/bandit_demo.py.
У ε-greedy отрезок вдвое длиннее даёт вдвое больше сожаления — regret линейный. Фиксированная доля случайного трафика тратится вечно: и через миллион шагов алгоритм отдаёт 10% показов заведомо плохим ручкам.
У UCB прирост даже слегка падает — кривая загибается. У Thompson отношение 1.70 смотреть бессмысленно: приросты 1.6 и 2.8 при общем regret 23.8 — это шум, важно, что абсолютная величина в 13 раз меньше, чем у ε-greedy.
Поиграть с горизонтом и разрывом между ручками можно в тренажёре: чем ближе CTR ручек, тем дольше путаются все три алгоритма — сложность задачи задаётся разрывом, а не числом ручек.
4. Бандит как слой переранжирования
Теория выше говорит про «ручки». Инженерный вопрос — как вставить её в готовую воронку, у которой уже есть кандидатогенерация, ранкер и блендер.
Что считать ручкой
В классической постановке ручек единицы и каждую дёргают тысячи раз. В рекомендациях каталог — миллионы айтемов, а показов на айтем в хвосте единицы или ноль. Прямой перенос не работает: ручка, которую дёрнули ноль раз, ничему не научит. Отсюда первое проектное решение.
| Ручка | Сколько их | Что получаем | Чего не получаем |
|---|---|---|---|
| Айтем | миллионы | настоящее исследование каталога: холодный старт закрывается сам собой | статистику — работает только если неопределённость даёт модель, а не счётчик показов |
| Источник кандидатов | единицы–десятки | динамические квоты: сколько слотов отдать ANN, сколько подпискам, сколько трендам | ничего про конкретный айтем — внутри источника по-прежнему жадно |
| Политика целиком | единицы | автоматический выбор между версиями ранкера | это уже не рекомендации, а самонастраивающийся A/B |
| Группа айтемов | тысячи | компромисс: статистика копится на группу и переносится на новый айтем внутри неё | точности внутри группы — хороший айтем в плохой категории останется незамеченным |
Аргумент против айтема как ручки был в том, что у него нет статистики. Но счётчик показов — не единственный источник неопределённости.
У нас уже есть обученная модель, и она умеет отвечать «я не знаю» про айтем, который видит впервые: незнакомые признаки, пустая история, эмбеддинг из инициализации. Неопределённость берётся из модели, а не из счётчика — и тогда миллионы ручек перестают быть проблемой.
Отсюда правильная формулировка того, чем является этот слой: бандит здесь — надстройка над ранкером, а не замена ему. Ранкер отвечает за среднее, бандит — за то, что делать с разбросом вокруг него.
Почему слой встаёт именно на переранжировании
Исследовать можно на любой стадии, но переранжирование удобно сразу по четырём причинам, и все они инженерные, а не математические.
- Цена ошибки ограничена и известна заранее. Слой переставляет сотню кандидатов, каждый из которых уже прошёл фильтры и ранкер. Худшее, что может случиться, — показать девятого вместо второго. На стадии retrieval цена ошибки не ограничена ничем.
- Здесь есть все признаки. Оценка неопределённости нужна не «про айтем вообще», а про пару (пользователь, айтем) в текущем контексте. Полный вектор признаков существует только на стадии ранжирования.
- Кандидатов мало. Ансамбль из пяти моделей на сотне кандидатов — пятьсот вычислений, посильно. На десяти тысячах — уже нет.
- Это точка, где решение и так принимается. Порядок выдачи всё равно формируется здесь; добавить к скору слагаемое дешевле, чем встроить исследование внутрь ANN-индекса.
Слой исследует только то, что отобрала воронка. Если retrieval тоже жадный — ANN по тому же скору, — бандит исследует внутри уже смещённого пула.
Это тот же ограничитель, что у софтмакс-сэмплинга, и он никуда не девается от смены алгоритма. Настоящее исследование каталога требует механизма и на retrieval — например, отдельного источника кандидатов под холодные айтемы.
Что слой делает со скором
Ранкер отдаёт число \(\hat f(u,i)\). Слой считает его средним распределения награды и добавляет второй параметр — разброс \(\sigma(u,i)\).
$$ \text{UCB: } \ s_i = \mu_i + \alpha\,\sigma_i \qquad\qquad \text{Thompson: } \ s_i = \mu_i + \alpha\,\sigma_i \cdot \xi_i, \ \ \xi_i \sim \mathcal{N}(0,1) $$И в обоих случаях — сортировка по \(s_i\). Здесь спрятан шаг, который в теории обычно проговаривают вскользь: классический бандит выбирает одно действие через \(\arg\max\), а выдача — это \(k\) действий сразу. Переход от «взять максимум» к «отсортировать и взять топ-\(k\)» ломает три предпосылки теории.
- Награда наблюдается не за все \(k\). До десятой позиции доходит меньше половины пользователей. Отсутствие клика на позиции 10 — почти всегда «не посмотрел», а не «не понравилось», и обновлять апостериор так, будто это отказ, значит систематически занижать оценки внизу выдачи.
- Награда приходит с задержкой. Между показом и кликом — секунды, между показом и возвратом на следующий день — сутки. Пока награда не пришла, апостериор не обновился, и одному айтему успевают выдать сотни показов авансом.
- Обновление батчевое. Апостериор пересчитывается раз в \(N\) минут, а не после каждого запроса, — значит внутри окна политика фиксирована и все гарантии, доказанные для пошагового обновления, работают лишь приближённо.
Тот же каталог из раздела 1, усреднение по восьми каталогам, 300 запросов. Доля от идеальной выдачи:
| \(\alpha\) | Жадный | UCB | Thompson | UCB нашёл самородок | UCB вывел в топ-1 |
|---|---|---|---|---|---|
| 0.00 | 0.834 | 0.834 | 0.834 | 3/8 | 0/8 |
| 0.50 | 0.834 | 0.858 | 0.858 | 7/8 | 2/8 |
| 1.00 | 0.834 | 0.866 | 0.837 | 8/8 | 4/8 |
| 3.00 | 0.834 | 0.909 | 0.756 | 8/8 | 8/8 |
| 5.00 | 0.834 | 0.891 | 0.723 | 8/8 | 8/8 |
Числа воспроизводятся скриптом _tools/bandit_demo.py; он же независимо повторяет вычисления виджета.
Четыре наблюдения, и третье — самое интересное.
- \(\alpha = 3\) даёт 0.909 против 0.834 — это +9% к выдаче, полученные перестановкой того же самого пула кандидатов. Ни новой модели, ни новых признаков.
- \(\alpha = 5\) даёт уже 0.891. Исследование перестало окупаться: мы поднимаем наверх и тех, про кого всё понятно. Зависимость от \(\alpha\) — горб, а не лестница, и это первое, что стоит проверить, настраивая такой слой.
- При \(\alpha = 0.5\) самородок заглядывает в топ в 7 каталогах из 8, а закрепляется в топ-1 только в 2. Айтем получает несколько показов, ловит неудачную серию, его оценка проседает, а бонус \(\propto 1/\sqrt{n}\) уже подсох — и он выпадает обратно. Малый бонус хуже, чем никакой: он тратит показы, но не доводит дело до конца.
- Thompson по чистой награде проигрывает UCB и при больших \(\alpha\) уходит ниже жадного: он шумит там, где всё и так понятно. Почему в проде всё равно выбирают его — в разделе 6.
- Поставьте \(\alpha = 0\): самородок не попадает в топ-1 ни разу. Круг замкнут.
- Поднимите до 3: он первый во всех восьми каталогах, доля растёт до 0.909.
- Поднимите до 5 — доля падает. Найдите вершину горба.
- Переключитесь на Thompson и опустите \(\alpha\) до 1: он возвращается к отвергнутым, потому что сэмпл иногда выпадает большим, — но платит за это шумом постоянно.
Что сказать на собесе: «Бандит на переранжировании — это сортировка не по предикту, а по предикту плюс мера незнания. Главное решение — откуда берётся σ: из ансамбля или MC-dropout, но не из \(p(1-p)\), потому что это алеаторический шум, который от показов не убывает».
5. Откуда берётся σ
| Источник | Как | Цена |
|---|---|---|
| Ансамбль | \(N\) моделей, берём выборочную дисперсию предсказаний | \(N\) моделей в обучении и в рантайме |
| MC-dropout | включаем дропаут на инференсе, делаем несколько проходов | \(N\) прогонов головы; эмбеддинги считаются один раз |
| Neural linear | байесовская линейная регрессия поверх последнего скрытого слоя; сеть даёт представление, линейный слой — честную ковариацию и \(\sqrt{x^\top A^{-1} x}\) | поддержка матрицы \(A\), зато интервал точный |
Первые два стоят почти ничего по разработке, и это делает их стандартным первым шагом. Третий — то, как устроен ранкер YouTube.
Разброс бывает двух сортов, и складывать их нельзя.
- Эпистемическая — «модель не знает». Уменьшается от новых данных. Исследовать имеет смысл именно её.
- Алеаторическая — «событие само по себе случайно». Пользователь кликает с вероятностью 0.3, и определённее это не станет ни от какого объёма данных.
Опасность конкретная. Модель предсказывает вероятность \(p\); у бернуллиевской величины дисперсия равна \(p(1-p)\), и заманчиво взять \(\sigma = \sqrt{p(1-p)}\) — формула под рукой, считать нечего.
| Показов у айтема | \(\sqrt{p(1-p)}\) | \(\sqrt{p(1-p)/n}\) |
|---|---|---|
| 1 | 0.4583 | 0.4583 |
| 10 | 0.4583 | 0.1449 |
| 100 | 0.4583 | 0.0458 |
| 1000 | 0.4583 | 0.0145 |
| 10000 | 0.4583 | 0.0046 |
Числа воспроизводятся скриптом _tools/bandit_demo.py.
Левый столбец не меняется вовсе. Это чистая алеаторика: такой «бонус» максимален при \(p = 0.5\) (значение 0.5000) и просто тянет наверх середнячков, никогда не затухая.
Как проверить, что вы измеряете нужное: постройте \(\sigma\) как функцию числа показов айтема. Если зависимости нет — вы измеряете не то.
И второй способ убить эпистемическую оценку: собрать ансамбль из моделей, делящих одну таблицу эмбеддингов. Предсказания окажутся почти одинаковыми, \(\sigma\) выйдет крошечной, и слой не будет делать ничего. Различаться должны и инициализация, и порядок данных.
6. UCB или Thompson: аргумент, который решает
По чистой награде UCB обычно выигрывает — таблица выше это показывает. В проде тем не менее выбирают Thompson, и причина лежит за пределами бандитской задачи.
UCB детерминирован: при данном состоянии он выдаёт один и тот же порядок. Значит вероятность показа \(\pi(i \mid u)\) равна нулю или единице, и всё, что построено на обратных склонностях — IPS, SNIPS, doubly robust, — превращается в деление на ноль. Оценить офлайн новую модель по таким логам нельзя.
Thompson задаёт распределение над перестановками. Склонности положительны и оцениваются повторным сэмплированием при тех же \((\mu, \sigma)\), а значит логи годятся для off-policy оценки. Один и тот же слой закрывает обе задачи: и исследует, и делает данные пригодными для оценки.
Практический вывод: если склонности всё равно нужны — а они нужны, как только появляется желание сравнивать модели офлайн, — то стохастическая политика не роскошь, а условие. И сохранять надо не только факт показа, но и \(\mu\), \(\sigma\) и версию модели на момент решения.
Оценка IPS формально несмещённая, но её дисперсия определяется разбросом весов \(w = \pi_{\text{new}}/\pi_{\text{log}}\). Эффективный размер выборки: \(\mathrm{ESS} = (\sum w)^2 / \sum w^2\).
Пусть у 99% наблюдений вес 1, а у оставшегося процента — вес \(W\):
| \(W\) | 1 | 10 | 100 | 1000 | 10000 |
|---|---|---|---|---|---|
| ESS из 1000 | 1000.0 | 597.0 | 39.2 | 12.1 | 10.2 |
| доля | 100% | 59.7% | 3.9% | 1.2% | 1.0% |
Числа воспроизводятся скриптом _tools/bandit_demo.py.
Один процент наблюдений с большим весом съедает 99% эффективной выборки: тысяча логов работает как десять. Отсюда обрезка весов, SNIPS и doubly robust — и отсюда же требование, чтобы политика не только была стохастической, но и не уходила слишком далеко от логирующей.
Посмотреть, как расходятся IPS и SNIPS при росте разброса весов, можно в тренажёре.
7. Что ломает слой в реальной системе
- Бизнес-правила поверх бандита. Слой выдал стохастический порядок, а правило «не более двух айтемов одного автора подряд» его переставило. Фактическая политика — уже не та, склонности посчитаны для не той выдачи, и off-policy оценка тихо смещается. Либо правила учитываются при расчёте склонностей, либо оценке нельзя верить.
- Обновление страницы. Пользователь перезагрузил ленту и увидел другой порядок — при стохастической политике это происходит само собой и читается как сбой. Результат рандомизации кэшируют на сессию, а вместе с ним кэшируют и склонность.
- Дрейф. Апостериор с накопленными счётчиками помнит всё. Айтем, который был хорош в декабре, к марту таким быть перестал, а бонус у него давно нулевой, и слой этого не проверит. Лечится экспоненциальным забыванием: старые наблюдения обесцениваются, \(\sigma\) снова растёт.
- Двойной учёт неопределённости. Если ранкер обучен на логах, где холодные айтемы уже поднимались отдельным правилом, модель это выучила, и бонус добавится поверх — новые айтемы поедут наверх вдвое сильнее ожидаемого.
8. Когда всё это не нужно
Тот же холодный старт решается и жёстким правилом: детерминированной подстановкой — ровно один пост малоизвестного автора поднимается на скор, соответствующий условной позиции 15–16, независимо от того, насколько модель в нём не уверена. Именно так это сделано в открытом коде ленты X.
Размен ровно тот, что в таблице про ручки. Правило предсказуемо, объяснимо и стоит десять строк. Бонус за неопределённость сам решает, кому и сколько показов дать, но требует честной \(\sigma\), стохастичности и логирования склонностей.
Если единственная задача — не дать новому автору умереть в безвестности, правило справится и обойдётся дешевле.
Двух приёмов из раздела 2 — случайной квоты и софтмакс-сэмплинга — с большой вероятностью хватит, чтобы забрать первые 95% выгоды. Строить бандитный слой имеет смысл, когда выполнены три условия сразу: каталог быстро обновляется, у вас есть честная оценка неопределённости и вам нужны склонности для офлайн-оценки.
И отдельная методическая сложность. Выигрыш exploration проявляется в качестве будущих моделей, а меряют его обычно недельным A/B по сегодняшним метрикам. На этом горизонте исследование почти всегда выглядит проигрышем — ровно как MMR в главе про разнообразие, и по той же причине: измеряется не то, ради чего это делается.
Стоит помнить и про границы теории: классические теоремы доказываются для неконтекстных бандитов, где награда не зависит ни от пользователя, ни от времени суток. Для рекомендаций это слишком сильное ограничение — весь смысл в том, что \(\mu\) и \(\sigma\) считаются для пары (пользователь, айтем). Контекстные варианты — linUCB, linTS, neural linear — гарантии сохраняют, но при более сильных предположениях.
Вопросы с собеседований
Что такое петля обратной связи и как из неё выбраться?
Модель обучается на логах, собранных предыдущей версией той же модели. Всё, что она не показывала, в данных отсутствует, и хороший айтем без показов останется неизвестным: чтобы узнать CTR, надо показать; чтобы показать, надо знать CTR.
Выход — менять логирующую политику. Два дешёвых способа: случайная квота (ε-greedy) и софтмакс-сэмплинг с температурой. Ими обычно забирают первые 95% выгоды. Дальше — бандитный слой с оценкой неопределённости.
Важная оговорка про софтмакс-сэмплинг: он переранжирует только то, что уже отобрала воронка, а воронка отбирала жадно. Настоящее исследование требует механизма и на retrieval.
Выведите бонус UCB.
Из неравенства Хёфдинга: для выборки на [0,1] с истинным средним μ и выборочным \(\hat\mu\) верно \(\mathbb{P}(\mu \ge \hat\mu + u) \le e^{-2nu^2}\). Приравниваем правую часть к δ и разрешаем относительно u: \(U = \sqrt{-\ln\delta / 2n}\).
Дальше выбираем δ = 1/k^c — с ростом времени требуем всё более надёжную границу — и получаем \(a_k = \arg\max [Q_k(a) + c\sqrt{\log k / n_k(a)}]\).
Читается как «оценка средней награды плюс ширина доверительного интервала». Бонус убывает как \(1/\sqrt{n}\): при 1 показе он 3.03, при 1000 уже 0.096. Поэтому отдельной ручки исследования не нужно — алгоритм сам сокращает его по мере накопления данных.
Чем Thompson отличается от UCB?
UCB детерминирован и оптимистичен: берёт верхнюю границу интервала. Thompson байесовский: сэмплирует параметры из апостериора и действует жадно относительно сэмпла. Для бинарной награды это Beta-Bernoulli — априор Beta сопряжён с бернуллиевским, обновление тривиально (успех → α+1, неуспех → β+1), три строки кода и никаких гиперпараметров кроме априора.
По regret оба сублинейны, в отличие от ε-greedy: у того при удвоении отрезка прирост удваивается (73.9 → 142.5), потому что фиксированная доля случайного трафика тратится вечно.
Главное отличие на практике — не в награде, а в измеримости, см. следующий вопрос.
Почему в проде чаще берут Thompson, если UCB даёт больше награды?
Из-за склонностей. UCB детерминирован: при данном состоянии порядок один и тот же, значит π(i|u) равна нулю или единице, и IPS, SNIPS, doubly robust превращаются в деление на ноль. Офлайн оценить новую модель по таким логам нельзя.
Thompson задаёт распределение над перестановками: склонности положительны и оцениваются повторным сэмплированием при тех же μ и σ. Один слой закрывает обе задачи — и исследует, и делает данные пригодными для оценки.
Оговорка: положительных склонностей мало, важен ещё их разброс. Если у 1% наблюдений вес 100, эффективный размер выборки падает с 1000 до 39 — оценка формально несмещённая, но бесполезная. Отсюда обрезка весов и требование, чтобы политика не уходила далеко от логирующей.
Что брать за ручку в рекомендациях?
Прямой перенос не работает: в классической постановке ручек единицы и каждую дёргают тысячи раз, а тут миллионы айтемов и ноль показов в хвосте. Варианты — айтем, источник кандидатов, политика целиком, группа айтемов.
Почти всегда выбирают айтем, и это возможно потому, что счётчик показов — не единственный источник неопределённости. Обученная модель умеет отвечать «я не знаю» про айтем с незнакомыми признаками и пустой историей. Неопределённость берётся из модели, а не из счётчика.
Отсюда правильная формулировка: бандит — надстройка над ранкером, а не замена. Ранкер отвечает за среднее, бандит — за то, что делать с разбросом вокруг него.
Почему бандитный слой ставят на переранжирование?
Четыре инженерные причины. Цена ошибки ограничена: слой переставляет сотню уже отфильтрованных кандидатов, худшее — показать девятого вместо второго; на retrieval цена не ограничена ничем. Здесь есть полный вектор признаков, а σ нужна про пару (пользователь, айтем) в контексте. Кандидатов мало: ансамбль из пяти моделей на сотне — посильно, на десяти тысячах — нет. И это точка, где решение и так принимается.
Ограничение: слой исследует только то, что отобрала воронка. Если retrieval жадный, бандит исследует внутри уже смещённого пула.
Откуда брать σ и какая именно σ нужна?
Три источника: ансамбль моделей (выборочная дисперсия предсказаний), MC-dropout (дропаут на инференсе, несколько проходов головы) и neural linear (байесовская линейная регрессия поверх последнего слоя, даёт честную \(\sqrt{x^\top A^{-1}x}\)). Первые два почти ничего не стоят по разработке.
Главное — не перепутать эпистемическую неопределённость («модель не знает», убывает от данных) с алеаторической («событие случайно», не убывает никогда). Соблазн взять \(\sigma = \sqrt{p(1-p)}\) велик — формула под рукой. Но это чистая алеаторика: при p = 0.3 она равна 0.4583 и при одном показе, и при десяти тысячах. Такой бонус максимален при p = 0.5 и просто тянет наверх середнячков.
Проверка: постройте σ как функцию числа показов. Нет зависимости — измеряете не то. И не собирайте ансамбль из моделей с общей таблицей эмбеддингов: предсказания совпадут, σ схлопнется.
Как настраивать силу бонуса?
Помнить, что зависимость — горб, а не лестница. В симуляции α = 3 даёт 0.909 от идеальной выдачи против 0.834 у жадного (плюс 9% перестановкой того же пула), а α = 5 уже 0.891: мы начинаем поднимать наверх и тех, про кого всё понятно.
И отдельно — про малые α. При α = 0.5 самородок заглядывает в топ в 7 каталогах из 8, а закрепляется в топ-1 только в 2: он получает несколько показов, ловит неудачную серию, оценка проседает, а бонус \(\propto 1/\sqrt{n}\) уже подсох. Малый бонус хуже, чем никакой — тратит показы, но не доводит дело до конца.
Когда бандитный слой не нужен?
Когда хватает случайной квоты и софтмакс-сэмплинга — а хватает их, чтобы забрать первые 95% выгоды. Слой окупается при трёх условиях сразу: каталог быстро обновляется, есть честная оценка неопределённости, нужны склонности для офлайн-оценки.
Плюс есть детерминированная альтернатива: жёстко поднимать ровно один пост малоизвестного автора на фиксированную позицию. Так сделано в открытом коде ленты X. Предсказуемо, объяснимо, десять строк — и если задача только в том, чтобы новый автор не умер в безвестности, этого достаточно.
И методическая ловушка: выигрыш exploration проявляется в качестве будущих моделей, а меряют его недельным A/B по сегодняшним метрикам. На этом горизонте он почти всегда выглядит проигрышем.
Шпаргалка одним экраном
Круг
Чтобы узнать CTR, надо показать; чтобы показать — знать CTR. Жадный: 0.834 и 0/8 самородков.
Дёшево
Случайная квота и софтмакс-сэмплинг дают первые 95%. Второй исследует только внутри топа.
UCB
Из Хёфдинга: \(c\sqrt{\log k / n}\). Убывает как \(1/\sqrt n\) — исследование гаснет само.
Thompson
Beta-Bernoulli: успех → α+1. Три строки, гиперпараметров нет.
Regret
ε-greedy линеен (73.9 → 142.5), UCB и Thompson — нет.
Ручка = айтем
σ берём из модели, не из счётчика. Бандит — надстройка над ранкером, не замена.
Ловушка σ
\(\sqrt{p(1-p)}\) = 0.4583 при любом n. Нужна \(\sqrt{p(1-p)/n}\) или ансамбль.
Зачем стохастика
Ради склонностей. UCB даёт π ∈ {0,1} и убивает IPS. Разброс весов тоже убивает: ESS 39 из 1000.
Первоисточники
- P. Auer, N. Cesa-Bianchi, P. Fischer. Finite-time Analysis of the Multiarmed Bandit Problem, Machine Learning 2002 — UCB1 и вывод из Хёфдинга.
- W. R. Thompson. On the Likelihood that One Unknown Probability Exceeds Another, Biometrika 1933.
- O. Chapelle, L. Li. An Empirical Evaluation of Thompson Sampling, NeurIPS 2011.
- L. Li, W. Chu, J. Langford, R. Schapire. A Contextual-Bandit Approach to Personalized News Article Recommendation, WWW 2010 — LinUCB.
- C. Riquelme, G. Tucker, J. Snoek. Deep Bayesian Bandits Showdown, ICLR 2018 — сравнение источников \(\sigma\), включая neural linear.
- A. Swaminathan, T. Joachims. The Self-Normalized Estimator for Counterfactual Learning, NeurIPS 2015 — SNIPS.
- M. Chen et al. Top-K Off-Policy Correction for a REINFORCE Recommender System, WSDM 2019 — переход от одного действия к слейту.
- Числа главы:
_tools/bandit_demo.pyв этом репозитории.