Как мы развернули трансформер на событиях интерфейса операторов поддержки
Привет всем! Я Артем Карасюк, руковожу ML-командой в RecSys-отделе AI Центра Т-Банка, которая занимается рекомендательными системами для автоматизации обслуживания клиентов. Расскажу о том, как мы развернули кастомизированную модель на базе трансформера и по каким граблям прогулялись.
Однажды на митапе в Новосибирске я читал доклад о внедрении машинного обучения в эксплуатацию. Лейтмотив был такой: «Давайте двигаться от простого к сложному, чтобы покрыть все этапы сборки модели, знать обо всем, что вокруг нее происходит, и только потом уже внедрять и усложнять». Подходить к внедрению трансформера лучше итеративно. Это обусловлено сложностью модели, она требует много знаний о своей внутренней структуре. Кроме того, нужно быть готовыми к трудностям отладки.
Наш проект — подсказки в интерфейсе
Проект, над которым мы трудимся и по сей день, — это подсказки в интерфейсе поиска для десятков тысяч операторов. У них есть бизнес-процессы (интенты), по которым они проводят клиентов в процессе обслуживания, и есть строка поиска с рекомендациями под ней. Подсказки не статичны, часто меняются вслед за контекстом происходящего. Сейчас рекомендации пересчитываются минимум раз в 4 секунды. При этом каждый третий интент пользователя открывается из рекомендаций — это хороший бизнес-результат. Все остальные интенты выбираются из поиска или других источников.
Пример рекомендаций в интерфейсе оператора
У каждого оператора свой набор навыков, в рамках которого он проводит пользователей по определенным бизнес-процессам. Эти процессы подразумевают запрос разной дополнительной информации или ее предоставление. Допустим, пользователь написал в чате: «Сумма задолженности». После этого может быть несколько развилок:
Пример графа перехода в наиболее вероятные процедуры оператора после процедуры «Сумма задолженности»
Например, человек хочет полностью погасить кредит. Оператор открывает следующий интент и проводит клиента по этому бизнес-процессу. Как работает оператор:
Выслушивает клиента, собирая всю необходимую информацию.
Задает уточняющие вопросы.
Собирает внешнюю информацию: проверяет новости, просматривает базу знаний.
Открывает интенты и проходит по бизнес-процессу.
Завершает беседу или переходит к следующей проблеме клиента.
Задачу на этот проект нам поставили так: в течение месяца создать процедуру рекомендации максимум шести процедур, при этом не создавая поисковый движок, потому что он уже был и достаточно развит. Поиск нужно было улучшить другими способами.
Моделирование
В начале проекта аналитики принесли данные:
Процедуры: история изменений, какие процедуры активны и валидны для рекомендаций на текущий момент.
Поиск процедур: на что кликнули в поиске, по каким запросам искали.
Информация по коммуникациям.
Информация по операторам.
TopPop + марковская цепь. Для начала мы взяли все сессии обслуживания, в которых есть хотя бы один открытый интент, не завершившийся ошибкой, и выбрали самые популярные интенты операторов. Таким образом была построена TopPop-модель для «холодного запуска» — Cold Start. В данной задаче это означает, что у нас еще нет информации по специфике обращения (на данном этапе это только другие открытые интенты) и мы можем только на основе информации об операторе предположить, что необходимо открыть первым.
Как строится модель Top Popular по операторам
Так как у каждого оператора есть навыки, в рамках которых он может обслуживать, мы выбираем строить Top Popular по операторам, а не по клиентам.
Реализация марковской цепи (предсказание следующего интента на основе контекста — Next-Intent-модель)
Считаем все открытые интенты после текущей цепочки и принимаем, что в рамках этой цепочки другого быть не может. Но бизнес-процессы проходят через разные развилки, поэтому мы все равно рекомендуем что-нибудь релевантное.
В результате А/Б-теста получили хорошие приросты бизнес-метрик.
Только Cold-Start-модель:
Итоги А/Б-теста для Cold-Start-модели. Сокращение времени поиска — на сколько процентов мы сократили общее время пользования поиском в интерфейсе. Сокращение количества поисков — сколько раз воспользовались поиском для открытия интентов, т. е. в данном случае воспользовались рекомендациями
Cold-Start-модель вместе с Next-Intent-моделью:
Итоги А/Б-теста для Cold-Start-модели вместе с Next-Intent-моделью
Марковская цепь дала лишь небольшой прирост. Причина в том, что мы добились основного улучшения метрик через базовую модель Cold Start — и последующего повышения результатов добиться труднее.
Ко второму этапу мы подошли с такими задачами:
Необходимо чаще обновлять подсказки. Во время коммуникации контекст может сильно меняться, при этом интенты не будут открываться.
Повысить качество подсказок.
Использовать больше доступных данных.
Событийный подход заключается вот в чем. В интерфейсе есть много элементов, которые генерируют событие с множеством данных, на которые кликают операторы. Они сохраняются в базу данных, откуда мы их формируем в последовательности для обучения. Давайте для данной последовательности в качестве примера для разбора возьмем событие «Проход по процедуре». На изображении ниже приводится пример данных, имитирующий реальные данные. Текст кнопки, тип элемента, дополнительную информацию — все это мы учитываем в предсказаниях.
Пример структуры события
При сборе данных мы берем все сессии операторов и складываем их в JSON. Здесь я подписал именно названия событий:
Пример набора событий для каждой из сессий обслуживания клиента оператором. Последовательности одинаковы для примера, на самом деле они сильно разнообразны.
Токенизируем события, которые описаны выше. Стоит обратить внимание, что данные негомогенные: в цепочках всегда есть два вида токенов — открытые интенты и события из интерфейса. Открытые интенты являются подмножеством событий из интерфейса, однако в них нас интересует только идентификатор. В остальных нам необходима дополнительная информация для разделения между собой.
События являются вспомогательными входными данными для модели, которые служат некоторым дополнительным контекстом для разнообразия рекомендаций.
Какие данные учитываются в токенизации в зависимости от типа события. События, произошедшие в интерфейсе, показаны в правой ветке. А открытый интент (из любого источника) показан в левой ветке
Пример перевода сессии в набор токенов, где каждый токен — отдельное событие. В данном случае названия одинаковые, однако могут быть переведены в другой токен, так как по другим полям информация различается
Получили неоднородные цепочки для предсказания интентов по предыдущим данным. При построении модели мы выделяем в цепочке целевые действия (вызванные интенты), которые нам необходимо предсказывать. Они выделены красным:
Итоговый набор токенов, с которым работает модель. Красным выделены целевые события, которые предсказываются моделью в результате.
Вариантов цепочек может быть очень много, и возможны ситуации, когда оператор выбрал продукт, открыл транзакцию, а в обучающем наборе этого не было. Или было один раз и неправда. Для решения этой проблемы мы удаляем справа по одному событию и генерируем семплы.
Как мы разбиваем одну сессию на примеры для обучения
Затем собираем матрицу со встречаемостью — делаем марковскую цепь.
Пример построенной матрицы совстречаемости на основе собранных примеров из сессий
В процессе оценки модели каждый день пересчитываем эту матрицу и сохраняем в Redis только топ-15, чтобы не хранить все подряд.
Как храним цепочки в Redis для рекомендаций в эксплуатации
Как мы собираем рекомендации:
Пример сбора рекомендаций для оператора. Красным выделены процедуры, которые попадают в итоговую выдачу. Серым выделены процедуры, которые были отфильтрованы, т. к. уже встречались в последовательности
Есть ключ [7, 2, 3, 4], по нему достаем одну рекомендацию. Так как количество рекомендаций меньше 10, справа убираем токен и достаем следующий ключ. Делаем так, пока не наберем необходимое количество.
Если в конце ничего не остается, можно добавить стандартные подсказки для бизнес-линии.
Когда набралось 10 штук (из них оператору могут быть доступны не все), цепочка собрана.
В данном случае мы собрали цепочку и отфильтровали результат по доступам. Серые токены были убраны из цепочки, т. к. при запуске они закончатся у оператора ошибкой
Мы поменяли не только модель и входные данные для нее, но и взаимодействие. Теперь фронтенд запрашивает конфигурацию, в которой мы объявляем: «Давайте вы будете запрашивать рекомендации, только когда появляется какое-то из этих событий, и будете складывать в контекст только те события, что мы перечислили здесь»:
Пример конфига для фронтенда, от которого зависит логика его взаимодействия с нашим сервисом. В данном случае они полностью совпадают.
Теперь при запросе рекомендаций мы входим в бесконечный цикл:
Как устроено взаимодействие фронтенда с нашим сервисом
На стороне сервиса токенизируем: по цепочке конструируем ключ и запрашиваем в Redis.
Пример перевода последовательности событий в цепочку — логика такая же, как и в офлайне
Результаты использования TopPop и марковских цепей
Результаты А/Б-теста в процентном соотношении по сравнению с предыдущим подходом. Желтым выделено не статистически значимое улучшение
Нас очень удивило, насколько сильно в А/Б-тесте поднялась конверсия (уменьшение частоты поиска): теперь у нас стало на 40% больше кликов по рекомендациям, а не поиском по сравнению с предыдущей моделью. Видимо, сыграло роль то, что рекомендации теперь обновляются чаще. Здесь под конверсией имеется в виду, на самом деле, буквально то же самое, что и уменьшение количества поисков.
Недостатки этого решения:
Рекомендации меняются слишком кардинально. Ввиду особенностей процесса оператор открывает интенты последовательно, и уже хотя бы один открытый интент может много сказать о проблеме пользователя. А мы учитывали только последние пять событий, среди которых могут быть открытые интенты, и если их открывается больше, «окно» сдвигалось и модель «забывала» о контексте коммуникации.
Крайне сложно добавлять домены в модель. А мы хотим внедрить сообщения, интенты с предыдущих этапов, клики из приложения, информацию о пользователе.
Бывали случаи, когда оператор промахивался по рекомендациям и рекомендации сильно менялись (см. пункт 1), из-за чего приходилось пользоваться поиском.
Нужно многократное открытие одного интента. Для нас данный запрос был в новинку. Поскольку мы пересчитываем рекомендации на каждый клик, но не чаще чем раз в 3 секунды, операторы стали использовали рекомендации для открытия нескольких интентов. Эффект очень интересный и неожиданный.
Сложно масштабировать. Словарь состоял из 30 тысяч токенов, поэтому вариаций цепочек длиной в 5 интентов может быть очень много и в Redis это помещалось с трудом.
Решение с использованием EvGen
Архитектура 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 понадобится еще больше.
Не экспериментируйте, пока этот процесс не отлажен и вы не уверены в метриках. Не всегда офлайн-метрики дают такой же прирост в онлайне.
Не выкатывайте новую модель, пока не проверите все оказывающие основной эффект разрезы пользователей.
Ни в коем случае не выкатывайте, пока не настроите онлайн-мониторинг модели.