Антон Чайников, разработчик Data Science, Redmadrobot
Привет, Хабр! Сегодня я расскажу о терниях на пути к чатботу, облегчающему работу операторам чата страховой компании. А точнее, как мы учили бота отличать друг от друга запросы с помощью машинного обучения. С какими моделями экспериментировали и какие получили результаты. Как сделали четыре подхода к чистке и обогащению данных приличного качества и пять попыток чистки данных качества «неприличного».
В чат страховой компании приходит +100500 клиентских обращений в день. Большинство вопросов простейшие и повторяющиеся, но операторам от этого не легче, и клиентам всё равно приходится ждать по пять-десять минут. Как повысить качество сервиса и оптимизировать трудозатраты, чтобы у операторов было меньше рутинной работы, а у пользователей — больше приятных ощущений от быстрого решения их вопросов?
А сделаем-ка мы чатбота. Пусть читает сообщения пользователей, для простых случаев выдаёт инструкции, а для сложных — задаёт стандартные вопросы, чтобы получить нужные оператору сведения. У живого оператора есть дерево сценариев — скрипт (или блок-схема), в котором написано, какие вопросы могут задавать пользователи и как на них реагировать. Мы бы взяли эту схему и положили в чатбот, да вот незадача — чатбот не понимает по-человечески и не умеет соотносить вопрос пользователя с веткой сценария.
Значит, будем его учить с помощью старого доброго машинного обучения. Но нельзя просто взять кусок данных, сгенерированный пользователями, и научить на нём модель приличного качества. Для этого с архитектурой модели нужно экспериментировать, данные — чистить, а иногда и собирать заново.
Как учить бота:
Клиентов — страховых компаний с онлайн-чатами — и проектов по обучению чатботов у нас было два (называть их не будем, это не принципиально), с резко разным качеством данных. Хорошо, если половину проблем второго проекта удалось решить манипуляциями из первого. Подробности — ниже.
С технической точки зрения наша задача — классифицировать тексты. Это делается в два этапа: сначала тексты векторизуются (с помощью tf-idf, doc2vec и т.д.), затем на полученных векторах (и классах) обучается классифицирующая модель — случайный лес, SVM, нейронная сеть, и проч. и проч.
Откуда берутся данные:
Без валидации, конечно, никуда. Все модели обучались на 70% данных и оценивались по результатам на оставшихся 30%.
Метрики качества моделей, которые мы использовали:
Редко, когда по задаче сразу понятно, какая модель даст лучшие результаты. Так и здесь: без экспериментов никуда.
Будем пробовать варианты векторизации:
Варианты классификации на этом фоне выглядят довольно бедно: SVM, XGBoost, LSTM, случайные леса, наивный байес, случайный лес поверх предсказаний SVM и XGB.
И хотя мы и проверяли воспроизводимость результатов на трёх независимо собранных датасетах и их фрагментах, поручиться за широкую применимость возьмёмся лишь отчасти.
Результаты экспериментов:
Операторы чата — всего лишь люди. При определении категории пользовательских запроса они часто ошибаются и по-разному понимают границы между категориями. Поэтому исходные данные приходится безжалостно и интенсивно чистить.
Наши данные на обучения модели на первом проекте:
Чем именно данные плохи, мы формулировали в качестве гипотез, далее проверяли и, где могли, исправляли. Вот что получилось:
Подход первый. Из всего огромного списка классов можно безболезненно оставить 5–10.
Отбрасываем маленькие классы (<1% выборки): мало данных + маленький импакт. Объединяем сложноотличимые классы, на которые операторы всё равно реагируют одинаково. Например:
'дмс' + 'как записаться к врачу' + 'вопрос по наполнению программы'
'аннулирование' + 'статус аннулирования' + 'аннулирование оплаченного полиса'
'вопрос по продлению' + 'как продлить полис?'
Далее выбрасываем классы типа «другое», «прочее» и тому подобное: для чатбота они бесполезны (всё равно перенаправлять на оператора), и при этом сильно портят точность, поскольку 20% (30, 50, 90) запросов операторы классифицируют не куда положено, а сюда. Теперь выбрасываем класс, с которыми чатбот работать (пока) не может.
Результат: в одном случае — рост с точности 0.40 до 0.69, в другом — с 0.66 до 0.77.
Подход второй. В начале работы чата операторы сами плохо понимают, как выбирать класс для обращения пользователя, поэтому в данных много «шума» и ошибок.
Эксперимент: берём только последние два (три, шесть, …) месяцев диалогов и обучаем модель на
них.
Результат: в одном примечательном случае точность возросла с 0.40 до 0.60, в другом — с 0.69 до 0.78.
Подход третий. Иногда точность 0.70 означает не «в 30% случаев модель ошибается», а «в 30% случаев разметка врёт, а модель очень разумно её поправляет».
Метриками типа точности или логлосса эту гипотезу не проверишь. Для целей эксперимента мы ограничились пристальным взглядом дата-сайентиста, но в идеальном случае здесь нужно качественно переразметить датасет, не забывая о кроссвалидации.
Для работы с такими выборками мы придумали процесс «итеративного обогащения»:
С одной стороны, результаты получаются прекрасные: модель первой итерации имеет точность 70%, второй — 95%, третей — 99+%. Пристальный взгляд на результаты предсказаний такую точность вполне подтверждают.
С другой стороны, как в этом процессе систематически убедиться, что последующие модели не выучиваются заблуждениям предыдущих? Есть идея проверить процесс на вручную «зашумлённом» датасете с качественной исходной разметкой, типа MNIST. Но времени на это, увы, не хватало. А без верификации мы не рискнули запускать итеративное обогащение и полученные модели в продакшен.
Подход четвёртый. Датасет можно расширить — и тем самым повысить точность и уменьшить переобучение, добавив к имеющимся текстам множество вариантов опечаток.
Варианты опечаток — удвоение буквы, пропуск буквы, перестановка соседних букв местами, замена буквы на соседнюю на клавиатуре.
Эксперимент: Доля p букв, в которых произойдёт опечатка: 2%, 4%, 6%, 8%, 10%, 12%. Увеличение датасета: обычно до размера 60000 реплик. В зависимости от исходного размера (после фильтров) это означало увеличение в 3–30 раз.
Результат: зависит от датасета. На маленьком датасете (~300 реплик) 4–6% опечаток дают стабильный и существенный рост точности (0.40 > 0.60). На больших датасетах всё хуже. При доле опечаток 8% или больше тексты превращаются в ахинею и точность падает. При доле ошибок 2–8% точность колеблется в диапазоне нескольких процентов, очень редко превосходит точность без опечаток и, по ощущениям, не стоит увеличения времени тренировки в несколько раз.
В итоге получаем модель, различающую 5 классов обращений с точностью 0.86. Согласовываем с клиентом тексты вопросов и ответов по каждой из пяти развилок, прикручиваем тексты к чатботу, отправляем в QA.
Получив на первом проекте неплохие результаты, ко второму мы подошли со всей уверенностью. Но, к счастью, мы не разучились удивляться.
С чем мы встретились:
Первым делом смотрим на классы: в дереве сценариев, в выборке SVM-модели, в основной выборке. И вот что видим:
Что делать в таких случаях? Мы засучили рукава и пошли самостоятельно вытаскивать классы и разметку из данных.
Попытка первая. Попробуем-ка кластеризовать вопросы пользователей, т.е. первые сообщения в диалоге, за исключением приветствий.
Проверяем. Векторизируем реплики подсчётом 3-грамм. Понижаем размерность до первых десяти измерений TSVD. Кластеризируем аггломеративной кластеризацией с евклидовым расстоянием и целевой функцией Варда. Ещё раз понижаем размерность с помощью t-SNE (до двух измерений, чтобы на результаты можно было смотреть глазами). Рисуем точки-реплики на плоскости, раскрасив в цвета кластеров.
Результат: страх и ужас. Вменяемых кластеров, можно считать, что нет:
Почти нет — есть один, оранжевый слева, это потому, что все сообщения в нём содержат 3-грамму «@». Эта 3-грамма — артефакт предобработки. Где-то в процессе фильтрации знаков препинания «@» не только не отфильтровалась, но ещё и обросла пробелами. Зато артефакт полезный. В этот кластер попали пользователи, которые первым сообщением пишут свою электронную почту. К сожалению, только по наличию почты совсем непонятно, какой у пользователя запрос. Двигаемся дальше.
Попытка вторая. А вдруг операторы часто отвечают более-менее стандартными ссылками?
Проверяем. Вытаскиваем из сообщений операторов ссылко-подобные подстроки, немного правим ссылки, разные по написанию, но одинаковые по смыслу (http/https, /search?city=%city%), считаем частотности ссылок.
Результат: малоперспективно. Во-первых, операторы отвечают ссылками лишь на малую долю запросов (<10%). Во-вторых, даже после ручной чистки и отсеивания ссылок, встретившихся единожды, их остаётся больше тридцати. В-третьих, в поведении пользователей, которые заканчивают диалог ссылкой, нет особенного сходства.
Попытка третья. Поищем стандартные ответы операторов — вдруг они будут индикаторами какой-никакой классификации сообщений?
Проверяем. В каждом диалоге берём последнюю реплику оператора (не считая прощаний: «могу ещё чем-нибудь помочь» и т.п.) и считаем частотность уникальных реплик.
Результат: перспективно, но неудобно. 50% ответов операторов уникальны, ещё 10–20% встречаются дважды, оставшиеся 30–40% покрываются сравнительно небольшим количеством популярных шаблонов. Сравнительно небольшим — примерно тремястами. При пристальном взгляде на эти шаблоны видно, что многие из них являются вариантами одного и того же по смыслу ответа — отличаются где на одну букву, где на одно слово, где на один абзац. Хочется сгруппировать эти близкие по смыслу ответы.
Попытка четвёртая. Кластеризуем последние реплики операторов. Эти кластеризуются гораздо лучше:
С этим уже можно работать.
Кластеризуем и рисуем реплики на плоскости, как в первой попытке, вручную определяем наиболее чётко отделившиеся кластеры, убираем их из датасета и кластеризуем заново. После отделения примерно половины датасета чёткие кластеры заканчиваются, и мы начинаем думать, какие бы им назначить классы. Разбрасываем кластеры по оригинальным пяти классам — выборка «перекошена», и три из пяти оригинальных классов не получают ни одного кластера. Плохо. Разбрасываем кластеры по пяти классам, которые намечаем произвольно, на: «звоните», «приезжайте», «ждите ответа сутки», «проблемы с капчей», «другое». Перекос поменьше, но точность всего 0.4–0.5. Опять плохо. Назначаем каждому из 30+ кластеров свой класс. Выборка снова «перекошена», и точность снова 0.5, хотя около пяти избранных классов имеют приличные точность и полноту (0.8 и выше). Но результат по-прежнему не впечатляет.
Попытка пятая. Нам нужна вся подноготная кластеризации. Извлекаем полную дендрограмму кластеризации вместо верхних тридцати кластеров. Сохраняем её в формате, доступном для аналитиков клиента, и помогаем им сделать разметку — делаем набросок списка классов.
Для каждого сообщения мы вычисляем цепочку кластеров, в которые входит каждое сообщение, начиная с корневого. Строим таблицу со столбцами: текст, id первого кластера в цепочке, id второго кластера в цепочке, ..., id кластера, соответствующего тексту. Сохраняем таблицу в csv/xls. Дальше с ней можно работать офисными инструментами.
Данные и набросок списка классов отдаём для разметки клиенту. Аналитики клиента разметили заново ~10000 первых сообщений пользователей. Мы, уже наученные опытом, попросили размечивать каждое сообщение минимум дважды. И не зря — 4000 из этих 10000 приходится выбросить, потому что два аналитика разметили по-разному. На оставшихся 6000 мы довольно быстро повторили успехи первого проекта:
Модель готова, теперь нужно нарисовать дерево сценариев. По причинам, которые не возьмёмся объяснить, у нас не было доступа к скриптам ответов операторов. Мы не растерялись, притворились пользователями и за пару часов в поле собрали шаблоны ответов и уточняющие вопросы операторов на все случаи жизни. Оформили их в дерево, запаковали в бота и отправились тестировать. Клиент одобрил.
To be concluded.
К сожалению, не доступен сервер mySQL