Введение: когда выбор стоит денег

Представьте, что вы стоите в зале казино перед рядом игровых автоматов. У каждого — своя (неизвестная вам) вероятность выигрыша. У вас ограниченное количество попыток. Как действовать: всё время тянуть за один рычаг, который уже дал выигрыш? Или методично проверять остальные в надежде найти лучший?

Это и есть задача многорукого бандита (Multi-Armed Bandit, MAB) — один из фундаментальных вопросов теории принятия решений и обучения с подкреплением (Reinforcement Learning). За простой игровой метафорой скрывается проблема, которая встречается в A/B-тестировании, персонализации контента, клинических испытаниях, рекламных системах и управлении портфелем.

В этой статье мы разберём суть дилеммы, математику за ней и главные алгоритмические решения — от наивного ε-жадного до байесовского сэмплирования Томпсона.


Формализация задачи

Что такое «бандит»?

Термин «однорукий бандит» — жаргонное название игрового автомата (он «грабит» игроков). «Многорукий бандит» — это задача с K рычагами, каждый из которых при нажатии выдаёт случайное вознаграждение из некоторого (неизвестного) распределения.

Формально:

  • Есть K действий (рычагов) $a_1, a_2, \ldots, a_K$
  • Каждое действие $a_i$ имеет истинное среднее вознаграждение $Q^*(a_i)$, неизвестное агенту
  • На каждом шаге $t$ агент выбирает действие $A_t$ и получает вознаграждение $R_t$
  • Цель: максимизировать суммарное вознаграждение за $T$ шагов

Дилемма исследования и эксплуатации

«Эксплуатация использует лучшее из того, что вы уже знаете, но исследование может помочь найти нечто лучшее в будущем.»

Это ключевое противоречие в MAB и в обучении с подкреплением в целом:

  • Эксплуатация (Exploitation): выбирать действие с наибольшей известной на данный момент оценкой
  • Исследование (Exploration): пробовать менее изученные действия, чтобы уточнить их реальную ценность

Делать только одно — проигрывать. Чистая эксплуатация зацикливается на субоптимальном рычаге. Чистое исследование игнорирует накопленные знания.

ℹ Ключевая метрика: Regret

Качество стратегии оценивается через regret (сожаление) — разницу между тем, что мог заработать идеальный агент (всегда выбирающий лучший рычаг), и тем, что заработал реальный агент:

$$L_T = T \cdot Q^(a^) - \sum_{t=1}^{T} Q^*(A_t)$$

Хороший алгоритм должен добиваться сублинейного regret — то есть средние потери на шаг стремятся к нулю по мере роста $T$.


Алгоритм 1: ε-жадный (Epsilon-Greedy)

Самый простой и интуитивный подход. Идея: большую часть времени действуй жадно (выбирай лучший известный вариант), но с вероятностью ε — исследуй случайно.

Как работает

import numpy as np

class EpsilonGreedy:
    def __init__(self, n_arms, epsilon=0.1):
        self.n_arms = n_arms
        self.epsilon = epsilon
        self.counts = np.zeros(n_arms)    # сколько раз выбирали руку i
        self.values = np.zeros(n_arms)    # оценка Q(a)

    def select_arm(self):
        if np.random.random() < self.epsilon:
            return np.random.randint(self.n_arms)  # исследование
        return np.argmax(self.values)              # эксплуатация

    def update(self, arm, reward):
        self.counts[arm] += 1
        n = self.counts[arm]
        # инкрементальное среднее
        self.values[arm] += (reward - self.values[arm]) / n

Плюсы и минусы

Алгоритм прост в реализации и понятен. Однако у него есть врождённый недостаток: он исследует равномерно случайно — тратит одинаковое время на плохие и хорошие неизученные руки. Кроме того, фиксированное ε не адаптируется: в начале хочется больше исследовать, в конце — эксплуатировать.

Решение: убывающее ε — например, $\varepsilon_t = 1/t$ или $\varepsilon_t = \varepsilon_0 / \sqrt{t}$.

💡 Практический совет
Для A/B-тестирования с несколькими вариантами страниц ε = 0.1–0.2 — хороший стартовый выбор. Если вариантов больше 10, рассмотрите убывающее ε или UCB.

Алгоритм 2: Upper Confidence Bound (UCB)

ε-жадный алгоритм исследует случайно. UCB исследует умно: он явно учитывает неопределённость — чем реже выбиралась рука, тем выше её «оптимистичная» оценка.

Принцип оптимизма в условиях неопределённости

Идея UCB1 (Auer et al., 2002):

$$A_t = \arg\max_{a} \left[ Q_t(a) + c \cdot \sqrt{\frac{\ln t}{N_t(a)}} \right]$$

где:

  • $Q_t(a)$ — текущая оценка ценности руки $a$
  • $N_t(a)$ — сколько раз рука $a$ выбиралась до момента $t$
  • $c$ — параметр уверенности (обычно 1 или 2)
  • $\sqrt{\ln t / N_t(a)}$ — «бонус за неизученность»

Чем реже рука выбиралась относительно других, тем выше её UCB-оценка — и тем больше вероятность, что агент её исследует.

class UCB1:
    def __init__(self, n_arms, c=2.0):
        self.n_arms = n_arms
        self.c = c
        self.counts = np.zeros(n_arms)
        self.values = np.zeros(n_arms)
        self.total_steps = 0

    def select_arm(self):
        # сначала попробуем каждую руку хотя бы раз
        for arm in range(self.n_arms):
            if self.counts[arm] == 0:
                return arm
        ucb_scores = self.values + self.c * np.sqrt(
            np.log(self.total_steps) / self.counts
        )
        return np.argmax(ucb_scores)

    def update(self, arm, reward):
        self.total_steps += 1
        self.counts[arm] += 1
        n = self.counts[arm]
        self.values[arm] += (reward - self.values[arm]) / n

UCB1 достигает логарифмического regret $O(\ln T)$ — теоретически оптимального для стохастических бандитов.

⚠ Ограничение UCB
UCB предполагает стационарное распределение наград. Если реальные вознаграждения меняются со временем (например, CTR рекламы зависит от сезона), UCB потребует модификаций: скользящего окна или дисконтирования старых наблюдений.

Алгоритм 3: Сэмплирование Томпсона (Thompson Sampling)

Томпсон-сэмплирование — байесовский подход, предложенный ещё в 1933 году, но широко применяемый лишь с 2010-х. Здесь агент поддерживает вероятностное распределение над истинными ценностями каждой руки и обновляет его по наблюдениям.

Алгоритм для бернуллиевских наград (0/1)

Предположим, что вознаграждение каждой руки — бинарное (клик/не клик). Используем бета-распределение $\text{Beta}(\alpha, \beta)$ как сопряжённый приор:

  • $\alpha_i$ — число успехов (наград 1) для руки $i$
  • $\beta_i$ — число неудач (наград 0) для руки $i$
class ThompsonSampling:
    def __init__(self, n_arms):
        self.n_arms = n_arms
        self.alpha = np.ones(n_arms)  # successes + 1
        self.beta  = np.ones(n_arms)  # failures + 1

    def select_arm(self):
        # сэмплируем θ из Beta-распределения для каждой руки
        theta = np.random.beta(self.alpha, self.beta)
        return np.argmax(theta)

    def update(self, arm, reward):
        # reward ∈ {0, 1}
        self.alpha[arm] += reward
        self.beta[arm]  += (1 - reward)

Почему это работает?

По мере накопления данных бета-распределение «сужается» вокруг истинного значения. Рука, которая редко выигрывала, будет иметь широкое распределение — и иногда случайно получит высокий сэмпл, стимулируя исследование. Успешные руки — узкое распределение с высоким центром, что стимулирует эксплуатацию.

Томпсон-сэмплирование:

  • Асимптотически оптимально (достигает нижней границы Lai-Robbins)
  • Хорошо работает на практике, особенно при малом числе шагов
  • Естественно расширяется на контекстуальные бандиты

Сравнение алгоритмов

АлгоритмТип исследованияRegretСложность реализацииАдаптивность
ε-GreedyСлучайное$O(T^{2/3})$⭐ НизкаяУмеренная
Убывающий εСлучайное$O(\ln T)$⭐⭐ СредняяХорошая
UCB1Оптимистичное$O(\ln T)$⭐⭐ СредняяУмеренная
Thompson SamplingБайесовское$O(\ln T)$⭐⭐⭐ ВышеОтличная
Gradient BanditSoftmax$O(\sqrt{T})$⭐⭐ СредняяХорошая

graph TD
    A[Задача MAB] --> B{Есть контекст?}
    B -- Нет --> C[Стохастический бандит]
    B -- Да --> D[Контекстуальный бандит]
    C --> E{Стационарное окружение?}
    E -- Да --> F{Нужна простота?}
    E -- Нет --> G[Sliding Window UCB / Discounted]
    F -- Да --> H[ε-Greedy]
    F -- Нет --> I{Байесовский подход?}
    I -- Да --> J[Thompson Sampling]
    I -- Нет --> K[UCB1]
    D --> L[LinUCB / Neural Bandit]


Практические применения

A/B/n-тестирование

Классическое A/B-тестирование делит трафик поровну на фиксированный срок — и в итоге долго отправляет пользователей на плохой вариант. MAB-алгоритмы (особенно Томпсон-сэмплирование) адаптивно перераспределяют трафик в пользу лучших вариантов, минимизируя потери на этапе теста.

Microsoft, Google, Netflix используют MAB-подходы для тестирования интерфейсов и рекламных баннеров.

Рекомендательные системы

Рекомендации контента — классический контекстуальный бандит: у нас есть профиль пользователя (контекст), набор возможных рекомендаций (руки) и обратная связь (клик/просмотр). LinUCB и нейронные бандиты активно применяются в этой области.

Клинические испытания

В медицинских исследованиях MAB позволяет адаптивно направлять больше пациентов на более эффективное лечение — это Response-Adaptive Randomization. Это этически важнее фиксированного разбиения.

📝 Пример: оптимизация push-уведомлений
Представьте мобильное приложение с 5 шаблонами push-уведомлений. Каждый день алгоритм UCB1 выбирает, какой шаблон отправить пользователю, и обновляет оценки по кликам. Через 2–3 недели система автоматически сходится к 1–2 лучшим вариантам без ручного анализа A/B-тестов.

Автонастройка гиперпараметров

Bayesian Optimization — по сути бандит: каждый набор гиперпараметров — это «рука», а качество модели — вознаграждение. Инструменты вроде Optuna и Ray Tune используют похожие принципы.


Расширения: за пределами базового MAB

Контекстуальные бандиты (Contextual Bandits)

В реальных системах у нас всегда есть контекст — информация о пользователе, времени суток, устройстве. Контекстуальный бандит выбирает действие на основе контекста $x_t$:

$$A_t = \arg\max_a Q(x_t, a)$$

Популярный алгоритм — LinUCB, где ценность параметризована линейной функцией от контекста.

Адверсариальные бандиты

Что если среда враждебна и намеренно подбирает плохие вознаграждения? Для таких случаев разработан алгоритм EXP3 (Exponential-weight algorithm for Exploration and Exploitation), гарантирующий sublinear regret даже против адверсариального оппонента.

Бандиты с задержкой наград

В рекламе между показом и конверсией может пройти несколько дней. Алгоритмы с задержкой (Delayed Feedback Bandits) учитывают эту временну́ю асимметрию.

💡 Когда использовать MAB, а когда — полное RL?
MAB подходит, когда нет состояния — каждый выбор независим от предыдущих. Если ваши действия влияют на будущее состояние среды (например, обучение персонажа в игре, управление роботом), нужен полноценный Reinforcement Learning с Q-learning или Policy Gradient методами.

Заключение

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

Главные выводы:

  1. ε-жадный — отправная точка, прост и работает, но исследует нецеленаправленно
  2. UCB1 — теоретически обоснован, логарифмический regret, отличный выбор для стационарных сред
  3. Thompson Sampling — практический фаворит, особенно при малых данных и бинарных наградах
  4. Для реальных систем почти всегда нужен контекстуальный бандит или полноценное RL
  5. Метрика regret — главный инструмент сравнения алгоритмов

МАB-алгоритмы — это не просто академическая задача. Они работают прямо сейчас в рекомендательных системах, рекламных платформах и медицинских испытаниях. Понимание их принципов даёт разработчику мощный инструмент для построения адаптивных, самооптимизирующихся систем.