Чисти, размечай: как мы учили чатбот различать клиентские вопросы


    Антон Чайников, разработчик Data Science, Redmadrobot
    Привет, Хабр! Сегодня я расскажу о терниях на пути к чатботу, облегчающему работу операторам чата страховой компании. А точнее, как мы учили бота отличать друг от друга запросы с помощью машинного обучения. С какими моделями экспериментировали и какие получили результаты. Как сделали четыре подхода к чистке и обогащению данных приличного качества и пять попыток чистки данных качества «неприличного».


    Задача


    В чат страховой компании приходит +100500 клиентских обращений в день. Большинство вопросов простейшие и повторяющиеся, но операторам от этого не легче, и клиентам всё равно приходится ждать по пять-десять минут. Как повысить качество сервиса и оптимизировать трудозатраты, чтобы у операторов было меньше рутинной работы, а у пользователей — больше приятных ощущений от быстрого решения их вопросов?


    А сделаем-ка мы чатбота. Пусть читает сообщения пользователей, для простых случаев выдаёт инструкции, а для сложных — задаёт стандартные вопросы, чтобы получить нужные оператору сведения. У живого оператора есть дерево сценариев — скрипт (или блок-схема), в котором написано, какие вопросы могут задавать пользователи и как на них реагировать. Мы бы взяли эту схему и положили в чатбот, да вот незадача — чатбот не понимает по-человечески и не умеет соотносить вопрос пользователя с веткой сценария.


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


    Как учить бота:


    • Рассмотрим варианты моделей: как сочетаются размер датасета, детали векторизации текстов, понижение размерности, классификатор и конечная точность.
    • Почистим приличные данные: найдём классы, которые можно спокойно выбросить; узнаем, почему последние полгода разметки лучше предыдущих трёх; определим, где врёт модель, а где разметка; выясним, чем могут быть полезны опечатки.
    • Почистим «неприличные» данные: разберёмся, в каких случаях кластеризация полезна и бесполезна, как разговаривают пользователи и операторы, когда пора перестать страдать и пойти собирать разметку.

    Фактура


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


    С технической точки зрения наша задача — классифицировать тексты. Это делается в два этапа: сначала тексты векторизуются (с помощью tf-idf, doc2vec и т.д.), затем на полученных векторах (и классах) обучается классифицирующая модель — случайный лес, SVM, нейронная сеть, и проч. и проч.


    Откуда берутся данные:


    • Sql-выгрузка истории сообщений в чате. Релевантные поля выгрузки: текст сообщений; автор (клиент или оператор); группировка сообщений в диалоги; таймстамп; категория обращения клиента (вопросы про ОСАГО, КАСКО, ДМС; вопросы о работе сайта; вопросы про программы лояльности; вопросы про изменение условий страхования и т. д.).
    • Дерево сценариев, или последовательности вопросов-ответов операторов клиентам с разными запросами.

    Без валидации, конечно, никуда. Все модели обучались на 70% данных и оценивались по результатам на оставшихся 30%.


    Метрики качества моделей, которые мы использовали:


    • При обучении: логлосс, за дифференцируемость;
    • При написании отчётов: точность классификации на тестовой выборке, за простоту и понятность (в т.ч. для заказчика);
    • При выборе направления дальнейших действий: интуиция дата-сайентиста, пристально глядящего в результаты.

    Эксперименты с моделями


    Редко, когда по задаче сразу понятно, какая модель даст лучшие результаты. Так и здесь: без экспериментов никуда.


    Будем пробовать варианты векторизации:


    • tf-idf на отдельных словах;
    • tf-idf на тройках символов (далее: 3-граммы);
    • tf-idf на 2-, 3-, 4-, 5-граммах по отдельности;
    • tf-idf на 2-, 3-, 4-, 5-граммах, взятых все вместе;
    • Всё вышеперечисленное + приведение слов в исходном тексте к словарной форме;
    • Всё вышеперечисленное + снижение размерности методом Truncated SVD;
    • С числом измерений: 10, 30, 100, 300;
    • doc2vec, тренированный на корпусе текстов из задачи.

    Варианты классификации на этом фоне выглядят довольно бедно: SVM, XGBoost, LSTM, случайные леса, наивный байес, случайный лес поверх предсказаний SVM и XGB.


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


    Результаты экспериментов:


    • В цепочке «предобработка-векторизация-понижение размерности-классификация» эффект от выбора на каждом шаге почти не зависит от остальных шагов. Что очень удобно, можно не перебирать десяток вариантов при каждой новой идее и на каждом шаге использовать лучший известный вариант.
    • tf-idf на словах проигрывает 3-граммам (точность 0.72 vs 0.78). 2-, 4-, 5-граммы проигрывают 3-граммам (0.75–0.76 vs 0.78). {2;5}-граммы все вместе совсем немного выигрывают у 3-грамм. С учётом резкого увеличения необходимой памяти, для тренировки выигрышем в 0.4% точности мы решили пренебречь.
    • По сравнению с tf-idf всех сортов doc2vec был беспомощен (точность 0.4 и ниже). Стоило бы попробовать тренировать его не на корпусе из задачи (~250000 текстов), а на сильно большем (2.5–25 миллионов текстов), но пока, увы, руки не дошли.
    • Truncated SVD не помогло. Точность монотонно растёт с ростом измерений, плавно выходя на точность без TSVD.
    • Среди классификаторов XGBoost побеждает с заметным отрывом (+5–10%). Ближайшие конкуренты — SVM и случайные леса. Наивный байес — не конкурент даже случайным лесам.
    • Успех LSTM сильно зависит от размера датасета: на выборке в 100000 объектов оно способно конкурировать с XGB. На выборке в 6000 — в отстающих вместе с байесом.
    • Случайный лес поверх SVM и XGB или всегда соглашается с XGB, или ошибается больше. Это весьма печально, мы надеялись, что SVM найдёт в данных хоть какие-то закономерности, недоступные XGB, но увы.
    • У XGBoost всё сложно со стабильностью. Например, его обновление с версии 0.72 до 0.80 необъяснимым образом снизило точность тренируемых моделей на 5–10%. И ещё: XGBoost поддерживает изменение параметров тренировки в процессе тренировки и совместимость со стандартным API scikit-learn, но строго по отдельности. Сделать и то, и другое вместе нельзя. Пришлось исправить.
    • Если привести слова к словарной форме, это немного улучшает качество, в сочетании с tf-idf на словах, но бесполезно во всех остальных случаях. В конце концов мы его отключили для экономии времени.

    Опыт 1. Чистка данных, или что делать с разметкой


    Операторы чата — всего лишь люди. При определении категории пользовательских запроса они часто ошибаются и по-разному понимают границы между категориями. Поэтому исходные данные приходится безжалостно и интенсивно чистить.


    Наши данные на обучения модели на первом проекте:


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

    Чем именно данные плохи, мы формулировали в качестве гипотез, далее проверяли и, где могли, исправляли. Вот что получилось:


    Подход первый. Из всего огромного списка классов можно безболезненно оставить 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% случаев разметка врёт, а модель очень разумно её поправляет».


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


    Для работы с такими выборками мы придумали процесс «итеративного обогащения»:


    1. Разбить датасет на 3–4 фрагмента.
    2. Натренировать модель на первом фрагменте.
    3. Предсказать тренированной моделью классы второго.
    4. Пристально посмотреть на предсказанные классы и степень уверенности модели, выбрать граничное значение уверенности.
    5. Убрать из второго фрагмента тексты (объекты), предсказанные с уверенностью ниже граничной, натренировать модель на этом.
    6. Повторять, пока не надоест или не закончатся фрагменты.

    С одной стороны, результаты получаются прекрасные: модель первой итерации имеет точность 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.


    Опыт 2. По колено в данных, или что делать без разметки


    Получив на первом проекте неплохие результаты, ко второму мы подошли со всей уверенностью. Но, к счастью, мы не разучились удивляться.


    С чем мы встретились:


    • Дерево сценариев с пятью ветками, согласованное с клиентом около года назад.
    • Размеченная выборка из 500 сообщений и 11 классов неизвестного происхождения.
    • Размеченная операторами чата выборка из 220000 сообщений, 21000 диалогов и 50 других классов.
    • SVM-модель, тренированная на первой выборке, с точностью 0.69, которая досталась в наследство от предыдущей команды дата-сайентистов. Почему именно SVM, история умалчивает.

    Первым делом смотрим на классы: в дереве сценариев, в выборке SVM-модели, в основной выборке. И вот что видим:


    • Классы SVM-модели примерно соответствуют веткам сценариев, но никак не соответствуют классам из большой выборки.
    • Дерево сценариев писалось по бизнес-процессам годичной давности, и устарело почти до бесполезности. SVM-модель устарела вместе с ним.
    • Два наибольших класса в большой выборке — это «Продажи» (50%) и «Прочее» (45%).
    • Из пяти следующих по размеру классов три настолько же общие, как и «Продажи».
    • Остальные 45 классов содержат менее 30 диалогов каждый. Т.е. дерева сценариев у нас нет, списка классов нет и разметки нет.

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


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


    Проверяем. Векторизируем реплики подсчётом 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 мы довольно быстро повторили успехи первого проекта:


    • Baseline: никак не фильтруем — точность 0.66.
    • Объединяем классы, одинаковые с точки зрения оператора. Получаем точность 0.73.
    • Убираем класс «Прочее» — точность растёт до 0.79.

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


    Выводы, или что показал опыт:


    • Экспериментировать с частями модели (предобработка, векторизация, классификация и пр.) можно по отдельности.
    • XGBoost по-прежнему правит бал, хотя если вам от него нужно что-то необычное, у вас проблемы.
    • Пользователь — это периферийное устройство хаотического ввода, так что чистить пользовательские данные нужно обязательно.
    • Итеративное обогащение — это круто, хотя и опасно.
    • Иногда стоит отдать данные обратно на разметку клиенту. Но не забывайте ему помочь получить качественный результат.

    To be concluded.

    Redmadrobot
    110,00
    №1 в разработке мобильных решений для бизнеса
    Поделиться публикацией

    Похожие публикации

    Комментарии 2

      0
      Какими библиотеками пользовались для лемматизации? Пробовали ли стемминг?

      Задачи с русскими текстами — нетривиальные. Было бы интересно узнать, что использовали и как.
        0
        Для лемматизации пользовались pymorphy. Стемминг не пробовали.

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

      Самое читаемое