Привет, Хабр! Мы команда ЖЦК, занимаемся машинным обучением в ВТБ. Сегодня расскажем про алгоритмическую магию, которая творится прямо у нас под носом. Авторами проекта этой магии в ВТБ стали дата-сайентисты Дмитрий Тимохин, Василий Сизов, Александр Лукашевич и Егор Суравейкин. Речь пойдет не о хитрых нейросетях с их миллионами параметров, а о простом подходе, который помог им и команде сэкономить много времени на решении задач, в которых раньше использовались классические методы тестирования. 

Зачем дополнять классику

Представьте себе картину: огромный банк, сотни тысяч клиентов, десятки направлений для маркетинговых предложений. К сожалению, эффективность (или неэффективность) тех или иных предложений на старте ясна далеко не всегда. Идея, кажущаяся здравой на бумаге, может выстрелить, может совершенно не зайти аудитории. Убытки при этом могут измеряться не только большими суммами денег, но и репутационными потерями.

Обычно для проверки гипотез используется A/B-тестирование. Вы, конечно, знаете, что это: распределяем клиентов по группам, даём им разные предложения и смотрим на статистику отклика. Классический метод, давно себя зарекомендовавший.

Однако такие проверки требуют много времени и денег: мы ведь буквально последовательно тестируем один вариант за другим. И участвуют в тестировании порой совсем небольшие группы людей: бывает так, что из ста тысяч клиентов только двести двадцать человек как-то отреагируют на предложение. Такова реальность продукта. 

Конверсия в 0,22% может быть репрезентативной, даже из такой малой выборки участников можно получить ценные данные. Однако мы задались вопросом: можно ли повысить эту цифру или снизить число нерелевантных предложений для клиента? В нашей работе каждый перцентиль — это люди, их время, доверие к банку и, собственно, наши потенциальные доходы. Решение, которое помогло бы стабильно повысить участие, было бы очень полезно банку.

Знакомство с бандитами

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

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

Дилемма исследования и использования — это ключевая проблема, которую решают многорукие бандиты. Когда у вас ограниченный бюджет и время, вы не можете бесконечно экспериментировать. Нужно быстро находить баланс между пробой нового и применением того, что уже работает. Это особенно актуально в банковской среде, где каждое предложение — это не просто строчка в маркетинговой таблице, а потенциально взаимовыгодный и долгосрочный контракт с клиентом. 

Поэтому для нашего проекта метод многорукого бандита работал качественнее, чем классическое A/B-тестирование. Многорукие бандиты перераспределяют трафик динамически: в пользу более эффективных стратегий, вместо того, чтобы распределять нагрузку равномерно, как это делают в классическом тестировании. Такой алгоритм «учится на ходу»: чем больше данных получено,  тем «выигрышнее» становятся рекомендации.

Методология разработки

Итак, мы с командой решили внедрить многоруких бандитов. Давайте пройдёмся по нашему пути подготовки данных и разработки.

Мы подбирали библиотеку для многоруких бандитов в Open Source, но не нашли вариант, хорошо подходящий именно под банковскую среду: в open source много библиотек с разными фичами, но ни одна не сочетала все возможности, нужные нам. 

Дело в том, что существуют бандиты классические, а ещё есть контекстные. Нам как раз нужно было сделать так, чтобы алгоритм не просто считал статистику, но и учитывал контекст. А еще мог понять, что изменения в поведении клиентов — это не случайность, а устойчивый тренд. Для этого нам пришлось собирать решение самостоятельно и настраивать каждый параметр вручную.

Базовую выборку данных мы взяли из профилей клиентов* на июль 2024 года. Например, дату регистрации, ОКВЭД и другие данные.

Отбирали только из сегмента среднего и малого бизнеса: собирали признаки из внешнего профиля, например, данные СПАРК ИНТЕРФАКС, данные о транзакциях, балансах и остатках, финансовой отчетности. Данные предварительно нормализовали с помощью класса StandardScaler из модуля pyspark.ml.feature.

Целевой переменной для кластеризатора не было, поскольку кластеризация обучалась без учителя (unsupervised learning). Кластеризацию проводили на 15 кластерах. Чтобы выделить 15 кластеров, взяли модель K-Means из модуля pyspark.ml.clustering для кластеризатора. Мерой расстояния выбрали квадрат евклидова расстояния (squaredEuclidean). Качество классификатора проводили через оценку silhouette score и размер выделенных кластеров в выборке на основе того же июльского портфеля. Итоговый silhouette score — 0.65. 

Мы применили кластеризатор и провели слияние кластеров, чтобы сегментировать июльскую бизнес-выборку. Если среди выделенных в бизнес-выборке кластеров определялись кластеры размером менее 10% от общего размера выборки, каждый элемент этого малого кластера переопределяли в другой кластер, с ближайшим по Евклидовой метрике центроидом. Слияние делали, чтобы не было кластеров, в которых ни один клиент не принял ни одну руку бандита. Иначе обучить классификатор принятия руки было бы невозможно.

Бинарные классификаторы принятия рук оценивают, с какой вероятностью конкретный клиент выберет конкретную руку из бизнес-выборки. То есть классификатор делает предсказание на объекте, среди факторов которого есть описание клиента и предлагаемая рука. Предлагаемая рука в факторах имеет название "mehanika_(variant_skidki_na_paket_uslug)".

Целевая переменная бинарная, определяется по атрибуту "fakt_podkljuchenija_paketa_(ot_prochitannyh)", который принимает значения 0 или 1, что означает подключение предложенного пакета услуг после коммуникации с клиентом. 

Каждый выделенный кластер разбивался на обучающую-тестовую подвыборку и отложенную подвыборку: мы отслеживали, чтобы половина положительных объектов находилась в отложенной подвыборке, чтобы набралась статистика для оценки качества каждой модели.

Обучение классификаторов мы начали с синтетического увеличения представителей положительного класса: чтобы хватило объектов для выполнения кросс-валидации. Синтетические объекты генерили с помощью класса SMOTENC из модуля imblearn.over_sampling внутри каждого выделенного кластера. Это помогло избежать генерации нереалистичных объектов. Целевое соотношение минорого класса к мажорному — 5% (изначальное соотношение — около 1%), количество ближайших соседей -3.

После синтетического увеличения минорого класса, внутри каждого кластера производились анализ и отбор факторов, за ними следовало обучение алгоритма машинного обучения «Случайный лес». Все действия ниже выполнялись средствами внутреннего инструмента autobinary версии 2.1.2: это внутренняя библиотека для обучения моделей на основе "деревянных" алгоритмов. О ней мы уже подробно писали на Хабре в нескольких частях: 1, 2, 3. «Случайный лес» задавался со следующими параметрами:

TASK = 'classification'

    params = {

        "criterion": "gini",

        "max_depth": 30,

        "random_state": 42,

        "n_estimators": 1000,

        "min_samples_split": 10,

        "min_samples_leaf": 10,

        "class_weight": "balanced",

        "max_features":"sqrt"

    }

Далее обучающую-тестовую подвыборку мы разбивали на обучающую и тестовую с использованием стратификации. Обучающая составляла 80% от всей подвыборки. Применялся стандартный подготовочный пайплайн PrepPipe, заменяющий отсутствующие значения и бесконечности. 

После мы проводили анализ на отсутствующие значения, корреляционный анализ, анализ признаков по методом FeaturePermutation (kib), анализ относительно глубины деревьев в ансамбле и, наконец, прямой отбор признаков (forward selection) по ROC_AUC.На отобранных признаках обучался финальный «Случайный лес».

Качество классификаторов оценивалось на отложенной подвыборке, в которой не производили синтетического увеличения минорного класса с помощью SMOTENC. Каждый классификатор оценивался на соответствующем кластере отложенной подвыборке. 

Промежуточная таблица, которую далее использовали для обучения бандита имеет следующую структуру. Классификатор оценивает вероятность принятия всех возможных рук, представленных в бизнес-выборке. На предоставленных данных это «1 мес.», «2 мес.». «3 мес.». Результат в виде таблицы сохраняется на распределенное хранилище hadoop.

В рамках проекта мы рассматривали варианты policy для наших бандитов. Перечислю несколько наиболее заметных.

  • Эпсилон жадный алгоритм: с вероятностью ε выбирается случайная рука, это исследование, с вероятностью 1 - ε выбирается наилучшая рука из уже известных, это уже эксплуатация.

  • Softmax: вводит специальные параметры, позволяющие выбирать руку пропорционально вознаграждению. 

  • Upper Confidence Bound: использует руку, имеющую максимальную верхнюю границу доверительного интервала для ожидаемого вознаграждения.

  • Thompson Sampling: считает вероятностное распределение вознаграждения для каждой из рук, выбирает руку согласно распределению.

В итоге, для реализации мы взяли библиотеку space_bandints, которая использует модели, разработанные и протестированные в исследовательской работе Deep Bayesian Bandits Showdown: An Empirical Comparison of Bayesian Deep Networks for Thompson Sampling. Эта библиотека подкупила своей гибкой реализацией в нескольких вариантах (Linear, NeuralLinear, Neural), хорошей документацией и системой вознаграждений {R}.

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

Далее предсказаниями модели наград соединялись с нашей промежуточной таблицей в Hadoop. Это реализовали так: для каждой уникальной тройки inn, slxid, report_date подтягиваются все необходимые признаки для модели ЧОД, делается предсказание моделью, приписывается предсказание бейзлайном. Результат совмещается с исходной таблицей: мы LEFT JOIN ее к таблице с предсказаниями.

Финальную таблицу переформатируем для работы со space_bandits и другими пакетами. Атрибут reward, вектор наград за принятие соответствующей руки (по индексу) формируется так: для каждой строки финальной таблицы симулируется распределение Бернулли с параметром p = p_arm в количестве одной реализации. Реализация (0 или 1) домножается на значение predict из этой же строки – обозначим полученное значение как reward_arm. Далее результат группируется по inn, slxid, report_date, а атрибуты reward_arm конкатенируются в виде списка, это и есть атрибут reward. Атрибуты target и best action вычисляются как факт наличия хотя бы одного ненулевого элемента в reward и аргмаксимум reward соответственно.

В рамках проекта мы рассматривали варианты policy для наших бандитов и проводили их сравнение. Перечислю несколько наиболее заметных.

  • Эпсилон жадный алгоритм: с вероятностью ε выбирается случайная рука, это исследование, с вероятностью 1 - ε выбирается наилучшая рука из уже известных, это уже эксплуатация.

  • Softmax: вводит специальные параметры, позволяющие выбирать руку пропорционально вознаграждению. 

  • Upper Confidence Bound: использует руку, имеющую максимальную верхнюю границу доверительного интервала для ожидаемого вознаграждения.

  • Thompson Sampling: считает вероятностное распределение вознаграждения для каждой из рук, выбирает руку согласно распределению.

Это бандит, работавший с Random Policy

А это гораздо лучшие показатели UCB Bandit, больше наград, меньше клиентских разочарований

В процессе разработки разобрались. Теперь давайте расскажу, как мы тестировали наших бандитов.

Симуляция реальности

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

Зачем вообще нужна такая сложная симуляция? В реальной жизни использовать алгоритмы многорукого бандита довольно просто: передаем клиентов в бандита, получаем руки (предложения) и эти конкретные руки предлагаем клиентам и получаем какой-либо ответ.

А теперь представьте, что вы хотите протестировать алгоритмы многоруких бандитов и у вас даже есть выборка с клиентами, руками и ответами. Возникает следующая проблема: в какой-то момент бандит выдаст руку, ответа по которой нет. Фактически, получили базовую проблему из области casual inference: чтобы получить точную оценку качества алгоритмов, необходимо знать ответы клиента по каждой из рук. То есть одновременно предложить клиенту сразу все руки (каждую руку отдельно), а это невозможно.

В самом базовом варианте мы моделировали тестовую систему распределениями Бернулли: смотрели конверсии по рукам на истории и передавали эти параметры в распределения. Для каждого клиента в симуляции мы создавали ряд сценариев развития действий: вероятность отклика на предложение №1, №2, №3. Также рассчитывали финансовые показатели относительно клиентской истории: к примеру, чистый доход, который он может принести банку. Далее мы комбинировали эти показатели и получали разные сценарии, максимально приближенные к реальности.

Результаты и выводы

После нескольких месяцев экспериментов мы получили результаты, которых даже не ожидали. Контекстные многорукие бандиты не просто работали лучше классического тестирования — они сильно персонализировали наш подход к тестам. Бандиты учитывали подключённые клиентом фичи при показе предложений-«рук». Рекомендации также зависели от потенциальной доходности клиента. Наконец, бандиты работали гибко, меняли пространство доступных действий в ходе тестирования: наш Python-скрипт быстро реагировал на изменения в поведении клиентов. И это было его основное преимущество перед классическим А/B-методом, который не мог быстро отследить, как меняются предпочтения аудитории. 

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

P.S. А какие методы и подходы вы используете для решения подобных задач?

Комментарии (0)