Хабрахабр

Deep Learning vs common sense: разрабатываем чат-бота

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

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

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

Владислав Блинов — старший разработчик диалоговых систем в Тинькофф, часто бросается аббревиатурами: ML, NLP, DL и т.д. Кроме того, в аспирантуре исследует моделирование юмора через машинное обучение и нейросети.

Сейчас в команде диалоговых систем Тинькофф делает чат-ботов и преподает курс по Машинному обучению для студентов. Валерия Баранова пишет на Python крутые штуки в области NLP более 5 лет. Также занимается исследованиями в области computational humor, то есть учит AI понимать шутки и придумывать новые — об этом Валерия и Владислав расскажут на UseData Conf.

Для обеспечения круглосуточной поддержки такого количества пользователей необходим большой штат сотрудников, что приводит к высокой стоимости обслуживания. Услугами Тинькофф Банка пользуются миллионы людей. Кажется логичным, что на популярные вопросы пользователей можно отвечать автоматически с помощью чат-бота.

Интент или намерение пользователя

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

Если написать: «Переведи сотку Лере», чат-бот Олег поймет, что это интент денежного перевода, то есть намерение пользователя перевести деньги. Посмотрим на пример классификации интентов. А точнее, что нужно перевести Лере сумму в 100 рублей.

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

Классификация на словаре

Самое простое, что можно сделать в задаче классификации интентов, — использовать словарь. Например, если во фразе пользователя встречается слово «переведи», считать, что нужно сделать денежный перевод.

Давайте посмотрим на качество такого простого подхода.

Если классификатор просто по слову «переведи» будет определять намерение пользователя как «денежный перевод», то качество уже будет достаточно высоким. Точность (precision) — 88%, при этом полнота низкая, равна всего 23%. Это и понятно: слово «переведи» не описывает всех возможностей сказать «переведи кому-то деньги».

Тем не менее, у этого подхода есть плюсы:

  • Не нужна размеченная выборка (если не учишь модель, то и выборка не нужна).
  • Можно получить высокую точность, если хорошо составить словари (но это потребует времени и ресурсов).

Однако, полнота такого решения скорее всего будет низкой, так как все вариации какого-либо класса описать трудно.

Если к «переведи» кроме интента денежного перевода, может относиться еще и второй интент — «переведи на оператора». Рассмотрим контрпример. Когда мы добавляем новый интент перевода на оператора, то получаем уже другие результаты.

Точность падает на 18 пунктов, при этом, естественно, полнота не растет. Это показывает, что нужен более продвинутый подход.

Анализ текста

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

Слова, которые часто встречаются в разных текстах, в этом векторном представлении имеют меньший вес. Tf-idf-вектор учитывает встречаемость каждого слова во фразе пользователя и учитывает общую встречаемость слов в коллекции.

Посмотрим на качество линейной модели на tf-idf-представлениях (в нашем случае логистической регрессии).

В результате резко увеличилась полнота, а точность осталась соизмерима с использованием словаря, f1-мера (взвешенное гармоническое среднее между точностью и полнотой) тоже возросла. То есть, модель уже сама разбирается, какие слова важны для какого интента — не нужно ничего придумывать самим.

Визуализация данных

Визуализация данных помогает понять, как выглядят интенты, насколько хорошо они группируются в пространстве. Но мы не можем напрямую визуализировать tf-idf-представления из-за большой размерности, поэтому будем использовать метод сжатия размерности — t-SNE.

Основное отличие этого метода от PCA заключается в том, что при переводе в двумерное пространство относительное расстояние между объектами сохраняется.

t-SNE на tf-idf (топ-10 интентов), F1 score 0,92

Здесь есть зеленые точки, которые не относятся ни к одному интенту, и 10 кластеров, которые помечены разными цветами — это разные интенты. Выше представлены топ-10 интентов по встречаемости в нашей коллекции. Взвешенная f1-мера равна 0,92 — это достаточно много, с этим уже можно работать. Видно, что некоторые из них очень хорошо группируются.

Таким образом, с линейным классификатором над tf-idf:

  • гораздо выше полнота, чем при использовании словаря, при соизмеримой точности;
  • не нужно придумывать, какие слова какому интенту соответствуют. 

Но есть и минусы:

  • ограниченный словарь, можно получить вес только для тех слов, которые присутствуют в тренировочной выборке;
  • не учитывается перефразирование;
  • не учитывается порядок, в котором слова встречались в тексте.

Перефразирование

Рассмотрим подробнее проблему перефразирования.

Близость между векторами можно посчитать через косинус угла между ними. Tf-idf-векторы могут быть близкими только для текстов, которые пересекаются по словам. На картинке посчитана косинусная близость по векторному представлению tf-idf для конкретных примеров.

Для векторного представления tf-idf это не очень близкие фразы, хотя для нас это один и тот же интент и один и тот же класс.

Например, можно вместо числа представлять слово целым вектором — это называется «эмбеддинг слова». Что можно с этим сделать?

Она называется word2vec и широко используется с тех пор. Одна из самых популярных моделей для решения этой задачи была предложена в 2013 году.

One-hot вектор — это вектор по размерности словаря, где только координата, соответствующая индексу слова в словаре, имеет значение 1, остальные 0. Один из способов обучения Word2vec работает примерно следующим образом: берем текст, берем какое-то слово из контекста и выбрасываем его, затем берем другое случайное слово из контекста и представляем оба слова в виде one-hot векторов.

На выходе получаем распределение вероятностей для всех слов из словаря быть следующим. Далее обучаем простую однослойную нейронную сеть без активации на внутреннем слое предсказывать следующее слово в контексте, то есть, по слову «рокетмен» предсказывать слово «вечерком». Так как мы знаем, какое слово было в действительности, можем посчитать ошибку, обновить веса и т.д.

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

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

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

Для того чтобы бороться с проблемой незнакомых слов (out-of-vocabulary), в 2014 году придумали модификацию word2vec — fasttext. Казалось бы, все хорошо, кроме того, что некоторых слов в вашей матрице может не быть, потому что модель их не видела при обучении.

Fasttext работает следующим образом: если слова нет в словаре, то оно разбивается на символьные n-граммы, для каждой n-граммы берется эмбеддинг из матрицы эмбеддингов n-грамм (которые обучаются подобно word2vec), усредняются эмбеддинги, получается вектор.

Теперь мы можем вычислять похожесть даже для незнакомых слов. Итого, получаем векторы для слов, которых нет в нашем словаре. И, что довольно важно, есть обученные модели для русского, английского и китайского языков, например, у Facebook и проекта DeepPavlov, поэтому это можно быстро включить в свой пайплайн.

Но недостатки остаются:

  • Модель не используется для вектора текста целиком. Чтобы получить общий вектор текста, нужно что-то придумывать: усреднять, или усреднять с перемножением на idf-веса, и в разных задачах это может работать по-разному.
  • Вектор для одного слова все еще один, независимо от контекста. Word2vec обучает один вектор слова для любых контекстов, в которых это слово встречалось. Для многозначных слов (таких как, например, язык) будет один и тот же вектор.

Действительно, косинусная близость в нашем примере по fasttext выше, чем косинусная близость по tf-idf, даже несмотря на то, что в этих фразах общее только «в».

t-SNE на fasttext (топ-10 интентов), F1 score: 0,86

F1-мера здесь 0,86 вместо 0,92. Однако при визуализации результатов fasttext по t-SNE разложению кластеры интентов выделяются гораздо хуже, чем для tf-idf.

Качество получилось абсолютно такое же, как при использовании только tf-idf. Мы провели эксперимент: объединили tf-idf и fasttext векторы. Нужно экспериментировать и пробовать. Это верно не для всех задач, бывают задачи, где объединенные tf-idf и fasttext работают лучше, чем просто tf-idf, или где fasttext работает лучше tf-idf.

Ниже кластеры для топ-30 интентов на tf-idf векторах. Давайте попробуем увеличить количество интентов (напомним, что у нас их 170).

t-SNE на tf-idf (топ-30 интентов), F1 score 0, 85 (на 10 было 0,92)

Качество падает на 7 пунктов, и мы теперь не видим выраженной кластерной структуры.

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

Очень похожие фразы, но это разные интенты. Например: «А если открыть вклад, какие проценты по нему?» и «А я хочу открыть вклад под 7 процентов». Чтобы разделять такие тексты на разные классы, нам понадобится что-то более сложное — deep learning. В первом случае человек хочет узнать условия по вкладам, а во втором случае — открыть вклад.

Языковая модель

Мы хотим получить вектор текста и, в частности, вектор слова, который будет зависеть от контекста употребления. Стандартным способом получить такой вектор является использование эмбеддингов из языковой модели.

А что это за задача? Языковая модель решает задачу языкового моделирования. Языковая модель выдает контекст на эмбеддинги. Пусть есть последовательность слов, например: «Буду говорить только в присутствии своего…», и мы пытаемся предсказать следующее слово в последовательности. Получив контекстные эмбеддинги и векторы для каждого слова, можно предсказывать вероятность следующего слова.

Мы снова знаем, какое слово было в действительности, считаем ошибку и обучаем модель. Есть вектор размерности словаря, и каждому слову назначается вероятность быть следующим.

и было предложено очень много разных архитектур. Языковых моделей довольно много, в прошлом году был бум? Одна из них — ELMo.

ELMo

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

Когда речь идет только о символах, размерность такой матрицы небольшая. Рассмотрим, как получается посимвольный эмбеддинг: разбиваем слово на символы, для каждого символа применяем эмбеддинг-слой и получаем матрицу эмбеддингов. К этому вектору применяется двухслойная, так называемая, highway-сеть, которая вычисляет общий вектор слова. Потом к матрице эмбеддингов применятся одномерная свертка, как обычно делается в NLP, с max pooling в конце, получается один вектор.

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

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

Но в ELMo есть две особенности: После применения двухслойной BiLSTM-сети обычно просто берутся hidden states последнего слоя, и считается, что это и есть контекстные эмбеддинги.

  • Residual connection между входом первого слоя LSTM и ее выходом. Вход LSTM прибавляется к выходу, чтобы избежать проблемы исчезающих градиентов.
  • Авторы ELMo предлагают объединять посимвольный эмбеддинг для каждого слова, выход первого слоя LSTM и выход второго слоя LSTM с некоторыми весами, которые подбираются для каждой задачи. Это нужно, чтобы учитывать как низкоуровневые признаки, так и более высокоуровневые признаки, которые дают первый и второй слои LSTM. 

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

Языковая модели дает следующие преимущества:

  • Вектор слова зависит от того, в каком контексте слово употребляется. То есть, например, для слова «язык» в значении части тела и лингвистического термина получим разные векторы. 
  • Как и в случае word2vec и fasttext, есть много обученных моделей, например, от проекта DeepPavlov. Можно взять готовую модель и попробовать применить в своей задаче. 
  • Больше не нужно задумываться о том, как усреднять векторы слов. Модель ELMo выдаёт сразу вектор всего текста.
  • Можно дообучить языковую модель для своей задачи, есть различные способы для этого, например, ULMFiT. 

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

В нашем примере с рестораном значения косинусной близости по модели ELMo действительно стали выше.

t-SNE на ELMo (топ-10 интентов), F1 score 0,93 (0,92 по tf-idf)

На рисунке выше четко видны все 10 кластеров, при этом точность чуть-чуть выросла. Кластеры при топ-10 интентах тоже более выражены.

t-SNE на ELMo (топ-30 интентов) F1 score 0,86 (0,85 по tf-idf)

Для топ-30 интентов все еще сохраняется кластерная структура, а также происходит прирост по качеству на один пункт.

С ELMo мы просто учим языковую модель, и, если семантически тексты похожи, то они будут близки. Но в такой модели нет гарантии, что предложения «А если открыть вклад, какие проценты по ним?» и «А я хочу открыть вклад под 7 процентов» будут далеко друг от друга, хоть они и лежат в разных классах. ELMo не знает ничего о наших классах, но можно сблизить векторы текстов одного интента в пространстве, используя метки классов.

Сиамская сеть

Берём вашу любимую архитектуру нейросети для векторизации текстов и два примера интентов. Для каждого из примеров получаем эмбеддинги, а затем считаем косинусное расстояние между ними.

 Косинусное расстояние равно единица минус косинусная близость, с которой мы ранее знакомились.

Такой подход называется сиамской сетью.

То есть, косинусное расстояние между их векторами должно быть как можно меньше, в идеале — ноль. Мы хотим, чтобы тексты из одного класса, например, «сделай перевод» и «закинь денег», лежали близко в пространстве. А тексты, относящиеся к разным намерениям, должны лежать как можно дальше друг от друга.

Гораздо лучше работает функция потерь, называемая «triplet loss». Но на практике такой способ обучения работает не так хорошо, поскольку объекты разных классов не получаются достаточно удалёнными друг от друга. В ней используются тройки объектов, называемые триплетами.

Отрицательный объект и якорь лежат в разных классах, а положительный и якорь — в одном. На иллюстрации изображен триплет: объект-якорь в синем кружочке, положительный объект в зеленом и отрицательный объект в красном кружочке.

Для этого считаем косинусное расстояние между парами объектов и вводим гиперпараметр — «margin» — расстояние, которое мы ожидаем, что будет между положительным и отрицательным объектами. Мы хотим добиться того, чтобы после обучения положительный объект был ближе к якорю, чем отрицательный.

Функция потерь выглядит так:

$" data-tex="display"/> <img src="https://habrastorage.org/getpro/habr/formulas/258/05e/cad/25805ecad025b4bffc539dfb0129772b.svg" alt="$triplet\_loss = \max[0, margin + dist(A, P)- dist(A, N)].

Другими словами, в ходе обучения мы добиваемся, чтобы положительный объект был к якорю ближе, чем отрицательный, хотя бы на margin. Если функция потерь равна нулю, то это получилось, и мы заканчиваем обучение, иначе продолжаем минимизировать целевую функцию.

После того, как обучили модель, мы еще не получаем классификатор, это просто метод для получения таких эмбеддингов, что объекты, которые лежат в одном интенте, скорее всего, будут иметь близкие векторы.

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

Среди соседей считаем самый частый класс и заключаем, что новый объект принадлежит этому классу. Напомним, как работает kNN для текстов: берем элемент текста, получаем для него эмбеддинг, переводим его в векторное пространство, и затем смотрим, кто его сосед.

Стандартные методы поиска ближайших соседей нам не подходят по производительности. Размерность эмбеддингов, которые мы используем, равна 300, а в тренировочной выборке около 500 000 объектов. Мы использовали метод HNSW — Hierarchical Navigable Small World.

В нашем случае, длина ребра будет определяться косинусным расстоянием, т.е. Navigable Small World — это связный граф, в котором мало ребер между вершинами, которые находятся на большом расстоянии, и много ребер между близкими вершинами. для тренировочной выборки мы вычисляем расстояние между всеми примерами интентов, потом выбрасываем случайным образом очень большие расстояния так, чтобы граф все еще оставался связанным.

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

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

Это приближенный метод, то есть нет гарантии, что мы найдем самых близких соседей в каждом случае, но у него очень хорошая полнота — около 0,95-0,99, в зависимости от настроек. Затем перемещаемся на следующий уровень, повторяем процесс до тех пор, пока не наберем нужное количество таких соседей, что, скорее всего, они будут самыми близкими к начальной вершине.

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

Ниже на графике видно, что все интенты образуют отдельные кластеры. Давайте посмотрим, какой прирост дает использование сиамской сети. В таком случае даже линейная модель позволит неплохо разделить пространство.

t-SNE на siamese (топ-10 интентов), F1 score 0,95 (0,93 по ELMo)

t-SNE на siamese (топ-30 интентов), F1 score 0,87 (0,86 по ELMo)

Для 10 интентов мы получили прирост на два пункта по сравнению с языковой моделью ELMo, для 30 — на один, при этом кластерная структура все еще сохраняется.

Итоги

Метод использования шаблонов и словарей вполне допустим, если у вас немного интентов, например, 2-5, когда они хорошо разделимы и действительно описываются словарями или шаблонами. Он может хорошо сработать, если в классе мало примеров, в нашем случае это могут быть намерения пользователей, для которых не более 20-30 вариантов формулировок. Тогда нет смысла строить сложные модели, можно обойтись словарями.

Такие модели быстро обучаются, и с ходу дают хорошее качество, которое еще можно повысить, если поработать с выборкой и настроить параметры. Если интентов больше, но при этом они хорошо разделяются, у них мало пересекающихся слов, можно обходиться стандартными линейными моделями поверх tf-idf.

В вашей задаче классификации текстов их стоит попробовать, тем более это так же быстро, как и использовании более простых векторных представлений, потому что есть предобученные модели. Если требуется учитывать перефразирование, обратите внимание на word2vec и fasttext. Это рабочие модели, несмотря на то, что в нашей конкретной задаче они не дали прироста.

Если вам понадобится дообучиться на своих данных, то, конечно, нужно будет разобраться с техническими деталями, но все это тоже не слишком долго, зато можно получить значительный прирост качества. Чуть больше времени, скорее всего, займет обучение ELMo. Особенно хорошо сработает ELMo, когда в задаче требуется учесть семантические особенности текстов, то есть когда к одному классу относятся близкие по смыслу предложения.

В нашем случае хорошо сработала сиамская сеть. Если же тексты из разных классов в задаче классификации сильно пересекаются по смыслу, то придется использовать что-то более продвинутое. То есть, в нашем случае, каждое намерение нужно было представить большим количеством положительных и отрицательных примеров. Применение этого метода займёт больше времени, потому что как минимум для обучения такой сети нужно много примеров из каждого класса. Но эта работа окупится, и вы получите хороший прирост качества.
Кроме того, нужно придумать подходящую нейронную архитектуру, протестировать несколько вариантов и т.д.

Полезные ссылки:

Следуя идее, что в каждом продакшен-решении необходим здравый смысл, мы собрали программу из 18 докладов о применении машинного обучения в практических задачах, в которых нет усложнения ради усложнения, а есть реально работающие подходы и результаты адекватные требованиям бизнеса. Заголовок этой статьи — «Deep Learning vs common sense» — хорошо отражает наш подход к конференции UseData Conf.

Если вам надоело раз за разом самому придумывать, как применить алгоритмы из статей к реальным данным, или вы хотите знать, как современные методы машинного обучения могут помочь вашему проекту, ждем вас 16 сентября на UseData Conf.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть