В этой статье мы поговорим о истории развития машинного обучения (ML) и обработки естественного языка искусственным интеллектом (NLP), расскажем, что такое AutoML и какими они бывают, а также о том, как Voximplant сделал эти технологии доступными каждому при помощи собственного AutoML компонента. Начнём с небольшого обзора технологии NLP и её истории: как она эволюционировала со временем и почему именно сейчас мы в компании Voximplant решили, что это наиболее подходящее время зайти на конкурентный рынок со своим продуктом. Историю NLP можно разделить на три этапа. Первый — это применение классических методов машинного обучения к задачам обработки естественного языка. Характерной чертой этого этапа был метод векторизации — превращение текста в представление, которое можно давать на вход ML моделям. Главный минус векторного подхода был в том, что слова “хороший” и “замечательный” были настолько же далеки друг от друга как слова “хороший” и “делать”, что было неэффективно, ведь стоит перефразировать текст синонимами, и мы получали совершенно другое векторное представление. Помимо этого, сами вектора получались огромными, и засунуть их в нейросетевые архитектуры было практически невозможно. Однако уже этого было достаточно чтобы добиться прогресса в областях вроде полнотекстового поиска. Второй этап в развитии NLP — это появление ворд-эмбеддингов. Это семейство методов позволило каждому слову найти компактное векторное представление, причём похожие слова имели схожие векторы — такие представления можно было легко подавать в нейросетевые архитектуры, и NLP-решения заметно поумнели. Их алгоритмы начали понимать базовые структуры языка и семантику на базе огромных неразмеченных данных, и потом использовать получившиеся представления для обучения нейросети определённой задаче. Это происходило быстрее, потому что базовые знания уже зашиты в модель и количество данных для выполнения задачи снизилось на несколько порядков. Для индустрии в целом это открыло новый формат работы: исследовательские центры обучали на огромных массивах фундаментальные модели, а компании использовали их как болванки для решения конкретных бизнес-задач. Третий этап в развитии NLP связан с появлением архитектуры трансформеров: она не страдала проблемами рекуррентных сетей, обладая гораздо более высоким уровнем параллелизма, что значило, что через такую модель можно было прогнать больше данных и сделать саму модель гораздо большей. В 2018 Google выкладывает BERT — трансформер, который стал де-факто стандартной болванкой для решения любых сложных NLP задач. С некоторыми модификациями эта архитектура стала доминирующей в NLP, и сейчас стандартный процесс решения задачи в NLP — это взять заранее натренированный BERT, и заточить его под свою задачу. Несмотря на обилие ML-решений от крупных компаний, таких, как Nvidia, Amazon и Google, многие компании не могут позволить себе ими воспользоваться. Почему? Даже если мы не будем рассматривать проблемы работы с данными, для поддержки таких решений нужны высококвалифицированные специалисты, и ценник на ML-команду достаточно высок. Это значит, что для многих бизнесов, не имеющих мощной технологической экспертизы не всегда понятно стоят ли усилия того, и сойдется ли в итоге экономика. Пилоты довольно дорогие и долгие, что отсекает большое количество компаний от применения технологии. Именно здесь на помощь приходит AutoML. Это попытка автоматизировать работу ML-инженеров. В идеальном мире позволяет людям без экспертизы в машинном обучении создавать и выводить в продакшн решения на базе ML. Пользователь заливает свои данные в систему и на выходе получает готовый к использованию масштабируемый сервис, разной степени интегрированности в инфраструктуру клиента. То есть клиенту остается только работа с данными и аналитикой, а вся инженерная работа уходит под капот. Звучит довольно заманчиво? Естественно, на рынке уже довольно много игроков, таких как Google Cloud, AWS или h2o.ai. Их решения позволяют решать ряд задач в области обработки естественного языка, но нужно понимать что порог входа достаточно высок и они целятся больше на software инженеров, а не на нетехнических специалистов. Скорее часть конструктора, нежели полноценное коробочное решение. Voximplant является облачной платформой для создания коммуникационных решений любой сложности, используя либо Serverless JavaScript, либо no-code подход для колл-центров. У нас есть телефония, чаты, множество вендоров качественного синтеза и распознавания речи, чаты, и единственное, чего нам не хватало для полноценной платформы для создания state-of-the-art голосовых роботов — это NLU движка. Изначально мы добавили поддержку решения стороннего вендора в нашу систему но практика использования показала, что у этого решения не всегда достаточно гибкости для сценариев из реального мира, и стороннее решение не могло полноценно пользоваться всей мощью нашей платформы. И тогда мы решили создать своё решение. Существует два класса разговорных ботов — это chit-chat и goal-oriented. Chit-chat, это робот-болталка, вроде Replica, который просто пытается ответить клиенту наиболее подходящей по смыслу фразой. Goal-oriented — это по сути голосовой интерфейс к некоторому набору действий. Цель робота — понять, какое действие хочет совершить пользователь и и выполнить его. Пример goal-oriented робота — это Siri или Alexa, умные IVR в колл-центрах и так далее. Так как мы хотим выпускать B2B-продукт, нам более интересны goal-oriented боты. Какие компоненты нужны для создания такого робота? Давайте рассмотрим на примере типичного флоу. Пользователь произносит голосом некоторую фразу, затем она отправляется в модуль распознавания голоса ASR, который преобразует голос в текст. В этом тексте мы в первую очередь хотим классифицировать намерение, или интент — по факту, нормализованный смысл фразы. Список этих интентов мы задаём в процессе создания бота и даём примеры фраз под каждый интент, чтобы бот смог сопоставить фразу пользователя с одним из этих примеров и понять, к какому интенту относится запрос. Также, нам пригодится извлечь какие-либо данные из речи пользователя, например, на какое время пользователь хочет забронировать столик. Для этих манипуляций надо привести данные из естественного языка в машиночитаемый — всё это является задачей по извлечению сущностей. Полученную информацию мы передаём в dialog state manager — компонент, который принимает решение, как отреагировать на уже нормализованный запрос. К этой задаче есть огромное количество подходов, как на базе ML, так и просто заскриптованные сценарии, но в целом, на этот компонент можно смотреть, как на некоторый скрипт, написанный под каждого конкретного бота. В итоге нам остаётся только озвучить ответ бота пользователю при помощи синтеза речи и продолжить цикл коммуникации. То есть, из компонентов нам понадобятся: Классификатор интентов, который учится на пользовательских данных Компонент для извлечения и нормализации пользовательских данных Dialog state manager для принятия решений API для управления всем этим Классификатор интентов должен иметь некоторый фронтенд, который принимает пользовательские данные и укладывает их на бэкенд, далее мы хотим запускать тренировку пользовательской модели, куда-то укладывать получившийся артефакт и доставлять его на инференс-сервер, чтобы тот в режиме реального времени обслуживал пользовательские запросы. Выглядит не очень сложно. Но на эту систему накладывается ряд ограничений: Сервис должен быть отзывчивым и не иметь задержек в телефонном разговоре Качество сервиса должно быть сопоставимо с SoTA Сервис должен быть устойчивым к наплыву пользователей Сервис должен иметь конкурентную цену Что ещё важнее, нам надо было запустить проект небольшой командой из 4-х человек на старте, и у нас было полгода, чтобы получить MVP. Также нам хотелось начать получать фидбэк с рынка как можно раньше, чтобы понять, насколько наш продукт реально решает проблемы клиентов. Поэтому мы решили запустить наш AutoML сервис за несколько итераций. На первой итерации мы не особо думали о проблемах масштабирования, нам нужно было сделать быстрый прототип и получить фидбэк с рынка. Мы решили сделать базовый остов, на который в дальнейшем можно было бы навешать функциональность, и начали с классификатора интентов. Имея только его, мы могли покрыть очень большое количество задач, вроде автоматической классификации комментариев к NPS опросам и умных IVR. Мы автоматизировали процесс тренировки для RoBerta Large для проектной команды, которая может собирать решения, ведь такой подход даёт наиболее высокое качество. Про цену инференса мы сознательно не хотели думать, чтобы не закопаться в деталях с одной стороны и не спугнуть плохим качеством клиентов с другой стороны. Мы взяли Bootstrap, чтобы сделать простую админку для управления данными, сделали бэкенд для неё на FastAPI, который кидает задачи на тренировку, а результаты тренировки стали укладывать в MLflow ModelRegistry. Туда же складывали метрики для отладки. Для инференс-сервера развернули несколько железных нод с GPU и установили Nvidia Triton. MLflow закрыл нам потребность в телеметрии и UI-инструменте для её анализа из коробки. Для каждой тренировки пользовательской модели мы проводили как тренировку с кроссвалидацией, так и тренировку финальной модели на полном наборе данных. Это позволяло нам отлавливать ситуации где наши эвристики по подбору гиперпараметров не работали хорошо и вручную тюнить формулу в случае проблем. Помимо этого MLflow помог нам решить проблему доставки моделей на инференс. Он предоставляет функционал ModelRegistry, который дает возможность не только трекать метрики, но и версионированно хранить артефакты моделей. Сервис инференса Nvidia Triton в свою очередь подтягивал модели в память, нам оставалось только написать небольшое решение на Python, которое опрашивало реестр моделей MLflow на предмет появление новых и подкладывало их в Triton. В итоге буквально за несколько месяцев у нас получился готовый продукт, которым можно было начать пользоваться и быстро реализовывать кейсы под конкретных клиентов без вмешательства ML-инженеров. Но хотя наше решение имело хорошее качество и работало без задержек, оно было совершенно немасштабируемым. Главная проблема была в том, что каждая клиентская модель всегда занимала минимум 2 гигабайта видеопамяти, даже если клиент написал hello world. Понятно, что если у нас появится большое количество клиентов, то наше решение не выдержит нагрузки. На второй итерации мы поставили цель сделать наше решение масштабируемым. Для этого нам нужен способ эффективно делить GPU ресурсы между клиентами. Какие есть решения? Можно делать автоскейлинг машин с GPU, однако это имеет свои минусы: дорого, долгая реакция на пиковые нагрузки, низкая утилизация железа. Можно добавить к автоскейлингу умную логику ротации моделей на GPU нодах и подгружать модели по требованию, и выгружать, если они не используется; но нужно учитывать, что Nvidia Triton грузит модели в память за секунды, а даже не сотни миллисекунд, что приведёт к большой задержке и неестественности при работе с голосом. К тому же это не решало проблему стоимости. Ещё одно возможное решений — перейти на модели предыдущих поколений и уйти с GPU на CPU, но то качество, которое мы получали, нас не устраивало. А что если копнуть ещё глубже? Главная загвоздка в том, что мы не хотим эксклюзивно выделять видеопамять для каждого клиента, но если какая-то часть модели будет одинаковая для всех клиентов — то проблема масштабирования упрощается на порядок, ведь можно будет масштабировать GPU ресурсы под реквесты а не под проекты пользователей. Естественно у нашей команды был большой опыт работы с трансформерами, и мы знали одну интересную особенность из практики — верхние слои трансформера в процессе файнтюнинга меняют веса гораздо сильнее, чем глубокие слои. В целом это разумно, нижние слои учат низкоуровневые особенности языка и базовую семантику, а верхние уже работают с высокоуровневыми представлениями. По факту в процессе файнтюнинга эмбеддинги и первые N слоев трансформера можно банально заморозить, не жертвуя особенно качеством. А слой эмбеддингов для сабвордов может занимать до половины всего размера модели, если добавить N первых слоев в заморозку - то без изменений можно оставить до 90% всей модели. Это фактически решает нашу проблему — 95% нашей модели будет заморожено и будет использовано только для добавления новых возможностей и пользовательских моделей, а последние несколько слоёв мы будем файнтюнить на пользовательские данные. Проблему масштабирования в этом случае мы решаем следующим образом: имеем пул GPU-машин, который мы масштабируем с ростом количества запросов к системе, что достаточно легко, в то же время пользовательскую часть весов мы инференсим на CPU с ротацией моделей в оперативной памяти. Модель на CPU мы можем быстро поднять менее чем за полсекунды. Финальная же картинка такая: когда приходит пользовательский запрос, он попадает на GPU машину с Backbone, который векторизирует его с помощью трансформера, и дальше этот массив эмбеддингов с 22 слоя RoBerta попадает на машину с CPU, где пользовательская модель либо уже закеширована в память, и запрос считается за примерно 100 миллисекунд, либо, если модель ещё не в памяти, то она поднимается с локального дискового кеша за еще 50-100 миллисекунд. Очень важный момент — в этой схеме, благодаря системе кеширования, мы получаем stateless систему - нам не нужно думать о том на каких подах лежат какие пользовательские модели — в принципе запрос можно слать на любой под и он сможет корректно отработать. Конечно же, лучше попасть на под, где модель уже лежит в кеше - но эту проблему можно уже решать на уровне инфраструктуры, не меняя логики приложений. Тем самым, мы решили нашу главную проблему — масштабирование, не пожертвовав качеством и теперь могли не бояться пиковых нагрузок на систему. Вдобавок, мы получили внятную модель тарификации, посчитав цену запроса. Последние штрихи, которые позволили бы нам спать спокойно — это вопрос подбора гиперпараметров и поддержка нескольких языков в нашей системе. Учитывая, что наша система в первую очередь рассчитана на людей, не сведущих в машинном обучении, мы решили оставить единственный гиперпараметр — количество шагов тренировки. Проблему можно было решить двумя подходами. Первый — сделать двухступенчатую тренировку (первый проход с кроссвалидацией и поиском оптимального количества шагов, второй — тренировка финальной модели на всех данных), но проблема в том, что с одной стороны это утяжеляет тренировку, которая и так не очень быстрая, а с другой стороны мы не управляем размером датасета. Как такая система поведет себя в режиме few-shot было сложно предсказать, скорее всего довольно плохо. Поэтому мы решили продолжить изучать зависимость между параметрами датасета и оптимальным количеством шагов. В этом нам помог датасет Massive от Meta Research. Это мультиязычный датасет на классификацию. На нем мы смогли нарезать тысячи датасетов разного размера и распределения данных между классами и начали изучать зависимость между оптимальным количеством шагов и параметрами датасета. Конкретно — количеством примеров в датасете, количеством классов, средним и дисперсией распределения примеров по классам. И в итоге с удивлением для себя обнаружили, что на разных слайсах этого многомерного распределения мы видели одну двумерную плоскость, на которую эта зависимость отображалась с достаточно хорошим качеством. Более того мы получили почти ту же самую обратную зависимость. То есть, дальше нам оставалось обычной регрессией из полученных данных получить общий вид формулы (естественно не забывая об минимальном и максимальном количестве эпох, чтобы избавиться от артефактов). И последний вопрос касался языков: в связи с географией работы Voximplant, нам требовалась поддержка русского, английского, испанского и португальского языков. Перед нами стоял простой вопрос, можем ли мы использовать мультиязычную модель, как backbone для всех языков, или под каждый язык нужна своя модель? К нашей удаче, ребята из Meta уже проделали огромную работу над этой проблемой и доказали отличную работу мультиязычной XLM-Roberta, так что в конечном итоге мы оставили отдельный русский backbone, потому что он всё-таки был заметно лучше XLM-Robert’ы, благодаря тому, что у нас было много неразмеченных данных, а для остальных языков мы взяли XLM-Roberta-large в качестве backbone. В конечном итоге примерно через 9 месяцев нашей работы мы получили полноценный инструмент для создания гибких сценариев коммуникации на базе SoTA NLU и serverless JavaScript, глубоко интегрированный с телефонией. Technical Writer