
Привет всем! Я Артем Карасюк, руковожу ML-командой в RecSys-отделе AI Центра Т-Банка, которая занимается рекомендательными системами для автоматизации обслуживания клиентов. Расскажу о том, как мы развернули кастомизированную модель на базе трансформера и по каким граблям прогулялись.
Однажды на митапе в Новосибирске я читал доклад о внедрении машинного обучения в эксплуатацию. Лейтмотив был такой: «Давайте двигаться от простого к сложному, чтобы покрыть все этапы сборки модели, знать обо всем, что вокруг нее происходит, и только потом уже внедрять и усложнять». Подходить к внедрению трансформера лучше итеративно. Это обусловлено сложностью модели, она требует много знаний о своей внутренней структуре. Кроме того, нужно быть готовыми к трудностям отладки.
Наш проект — подсказки в интерфейсе
Проект, над которым мы трудимся и по сей день, — это подсказки в интерфейсе поиска для де��ятков тысяч операторов. У них есть бизнес-процессы (интенты), по которым они проводят клиентов в процессе обслуживания, и есть строка поиска с рекомендациями под ней. Подсказки не статичны, часто меняются вслед за контекстом происходящего. Сейчас рекомендации пересчитываются минимум раз в 4 секунды. При этом каждый третий интент пользователя открывается из рекомендаций — это хороший бизнес-результат. Все остальные интенты выбираются из поиска или других источников.

У каждого оператора свой набор навыков, в рамках которого он проводит пользователей по определенным бизнес-процессам. Эти процессы подразумевают запрос разной дополнительной информации или ее предоставление. Допустим, пользователь написал в чате: «Сумма задолженности». После этого может быть несколько развилок:

Например, человек хочет полностью погасить кредит. Оператор открывает следующий интент и проводит клиента по этому бизнес-процессу. Как работает оператор:
Выслушивает клиента, собирая всю необходимую информацию.
Задает уточняющие вопросы.
Собирает внешнюю информацию: проверяет новости, просматривает базу знаний.
Открывает интенты и проходит по бизнес-процессу.
Завершает беседу или переходит к следующей проблеме клиента.
Задачу на этот проект нам поставили так: в течение месяца создать процедуру рекомендации максимум шести процедур, при этом не создавая поисковый движок, потому что он уже был и достаточно развит. Поиск нужно было улучшить другими способами.
Моделирование
В начале проекта аналитики принесли данные:
Процедуры: история изменений, какие процедуры активны и валидны для рекомендаций на текущий момент.
Поиск процедур: на что кликнули в поиске, по каким запросам искали.
Информация по коммуникациям.
Информация по операторам.
TopPop + марковская цепь. Для начала мы взяли все сессии обслуживания, в которых есть хотя бы один открытый интент, не завершившийся ошибкой, и выбрали самые популярные интенты операторов. Таким образом была построена TopPop-модель для «холодного запуска» — Cold Start. В данной задаче это означает, что у нас еще нет информации по специфике обращения (на данном этапе это только другие открытые интенты) и мы можем только на основе информации об операторе предположить, что необходимо открыть первым.

Так как у каждого оператора есть навыки, в рамках которых он может обслуживать, мы выбираем строить Top Popular по операторам, а не по клиентам.

Считаем все открытые интенты после текущей цепочки и принимаем, что в рамках этой цепочки другого быть не может. Но бизнес-процессы проходят через разные развилки, поэтому мы все равно рекомендуем что-нибудь релевантное.
В результате А/Б-теста получили хорошие приросты бизнес-метрик.
Только Cold-Start-модель:

Cold-Start-модель вместе с Next-Intent-моделью:

Марковская цепь дала лишь небольшой прирост. Причина в том, что мы добились основного улучшения метрик через базовую модель Cold Start — и последующего повышения результатов добиться труднее.
Ко второму этапу мы подошли с такими задачами:
Необходимо чаще обновлять подсказки. Во время коммуникации контекст может сильно меняться, при этом интенты не будут открываться.
Повысить качество подсказок.
Использовать больше доступных данных.
Событийный подход заключается вот в чем. В интерфейсе есть много элементов, которые генерируют событие с множеством данных, на которые кликают операторы. Они сохраняются в базу данных, откуда мы их формируем в последовательности для обучения. Давайте для данной последовательности в качестве примера для разбора возьмем собы��ие «Проход по процедуре». На изображении ниже приводится пример данных, имитирующий реальные данные. Текст кнопки, тип элемента, дополнительную информацию — все это мы учитываем в предсказаниях.

При сборе данных мы берем все сессии операторов и складываем их в JSON. Здесь я подписал именно названия событий:

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


Получили неоднородные цепочки для предсказания интентов по предыдущим данным. При построении модели мы выделяем в цепочке целевые действия (вызванные интенты), которые нам необходимо предсказывать. Они выделены красным:

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

Затем собираем матрицу со встречаемостью — делаем марковскую цепь.

В процессе оценки модели каждый день пересчитываем эту матрицу и сохраняем в Redis только топ-15, чтобы не хранить все подряд.

Как мы собираем рекомендации:

Есть ключ [7, 2, 3, 4], по нему достаем одну рекомендацию. Так как количество рекомендаций меньше 10, справа убираем токен и достаем следующий ключ. Делаем так, пока не наберем необходимое количество.
Если в конце ничего не остается, можно добавить стандартные подсказки для бизнес-линии.
Когда набралось 10 штук (из них оператору могут быть доступны не все), цепочка собрана.

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

Теперь при запросе рекомендаций мы входим в бесконечный цикл:

На стороне сервиса токенизируем: по цепочке конструируем ключ и запрашиваем в Redis.

Результаты использования TopPop и марковских цепей

Нас очень удивило, насколько сильно в А/Б-тесте поднялась конверсия (уменьшение частоты поиска): теперь у нас стало на 40% больше клик��в по рекомендациям, а не поиском по сравнению с предыдущей моделью. Видимо, сыграло роль то, что рекомендации теперь обновляются чаще. Здесь под конверсией имеется в виду, на самом деле, буквально то же самое, что и уменьшение количества поисков.
Недостатки этого решения:
Рекомендации меняются слишком кардинально. Ввиду особенностей процесса оператор открывает интенты последовательно, и уже хотя бы один открытый интент может много сказать о проблеме пользователя. А мы учитывали только последние пять событий, среди которых могут быть открытые интенты, и если их открывается больше, «окно» сдвигалось и модель «забывала» о контексте коммуникации.
Крайне сложно добавлять домены в модель. А мы хотим внедрить сообщения, интенты с предыдущих этапов, клики из приложения, информацию о пользователе.
Бывали случаи, когда оператор промахивался по рекомендациям и рекомендации сильно менялись (см. пункт 1), из-за чего приходилось пользоваться поиском.
Нужно многократное открытие одного интента. Для нас данный запрос был в новинку. Поскольку мы пересчитываем рекомендации на каждый клик, но не чаще чем раз в 3 секунды, операторы стали использовали рекомендации для открытия нескольких интентов. Эффект очень интересный и неожиданный.
Сложно масштабировать. Словарь состоял из 30 тысяч токенов, поэтому вариаций цепочек длиной в 5 интентов может быть очень много и в Redis это помещалось с трудом.
Решение с использованием EvGen

Название расшифровывается как Event Generator. Модель устроена очень просто, но работает хорошо. Masked self-attention — это блок от трансформера. В данной архитектуре вы не видите positional encoding перед подачей в блок. Подозреваем, что attention выучил positional encoding, потому что тот не дает никакого эффекта. Но мы заметили, что если добавить день недели и время начала беседы, заметно повышается diversity.
В данном случае в цепочки в начало добавляется токен BOS (Begin of sequence), который содержит информацию о бизнес-линии оператора, в котором происходит обслуживание, и логин оператора — для каждой такой пары обучаем вектор.
Также мы убираем все события, которые не входят в окно длины 5 (интенты и BOS-токен оставляем). Мы избавляемся от событий, так как они вносят много шума на длинных цепочках и предсказания становятся сильно хуже. Также мы заметили, что при большом количестве открытых интентов значительно падает diversity.
Один forward условно выглядит так: мы идем скользящим окном (выделено зеленым) и маскируем все события, которые в него не попадают:

В таргет мы подставляем либо padding token, либо следующий интент. Это сильно сокращало количество обучающих семплов (очевидно, мы не учились предсказывать PAD-токен). Но правильно должно быть так:

По сути, в предыдущем подходе мы боролись с out of vocabulary. Также мы хотим, чтобы оператор открыл интент раньше, чем накликает всю цепочку и ему попадется релевантная подсказка. EOS-токен можно предсказывать для консистентности модели и использовать его для другой фичи, но в рамках рекомендаций он, по сути, бесполезен.
Для инференса мы вновь токенизируем все в сервисе и убираем события, которые не помещаются в окно.

При А/Б-тесте мы не получили статистически значимых изменений.
Однако у нас появилось больше возможностей по интеграции новых доменов; мы можем учитывать время между событиями, повышая тем самым метрики; можем как угодно оптимизировать модель.
Какие трудности возникли:
Неправильно маскировали loss. Узнали об этом уже при эксплуатации. Когда маскировали сервисные токены перед Cross Entropy, были проблемы с Inf.
Не заметили проблем с разнообразием (diversity), когда прогоняли эксперимент. С метриками все было хорошо, но на А/Б-тесте старая модель работала гораздо лучше.
Поняли, что на самом деле обучали только на полных цепочках и это было неверно. Как уже упоминал, в предыдущем решении таким образом мы боролись с out of vocabulary, и оказалось, что это крайне важно и в нейронной сети.
Рост офлайн-метрик был значительным, однако в эксплуатации это не проявилось, что может говорить о проблемах в методике подсчета метрик.
Из-за изменения взаимодействия наши представления о работе оператора устарели. Ввиду этого необходимо менять и способ подсчета метрик.
Как внедрить трансформер в эксплуатацию
Вот несколько советов:
Исследуйте и опробуйте данные в бою. Как они к вам попадают? Насколько они похожи в онлайне и офлайне? И если они различаются, то почему и как?
Не бойтесь менять взаимодействие. Для нас этот пункт был ключевым. Теперь мы можем точнее влиять на пересчет рекомендаций, оптимизировать модель.
Убедитесь в правильности подсчета метрик, если меняете взаимодействие.
Готовьте много GPU. Трансформер обучается долго. Рекомендую использовать готовые реализации на PyTorch. Старайтесь не писать свои реализации, когда есть готовые CUDA-kernels, а то GPU понадобится еще больше.
Не экспериментируйте, пока этот процесс не отлажен и вы не уверены в метриках. Не всегда офлайн-метрики дают такой же прирост в онлайне.
Не выкатывайте новую модель, пока не проверите все оказывающие основной эффект разрезы пользователей.
Ни в коем случае не выкатывайте, пока не настроите онлайн-мониторинг модели.
