Чат-бот на RASA: опыт Parallels
В настоящее время бурно развивается индустрия чат-ботов. Сначала они были достаточно глупыми и могли вести диалог с пользователем, являясь ведущими и предлагая возможные ответы. Потом боты слегка поумнели и начали требовать от пользователя текстового ввода, чтобы из ответов вытаскивать ключевые слова. Развитие машинного обучения привело к появлению возможности общаться с ботом еще и голосом. Однако, большая часть решений не сильно далеко ушла от все того же построения графа диалогов и перехода между его узлами по ключевым словам.
Недавно в Parallels мы решили оптимизировать ряд внутренних процессов и сделать в качестве эксперимента бота для собственных нужд. После недолгих поисков мы решили попытать счастья на проекте с открытым исходным кодом RASA. По утверждениям самих разработчиков они сделали чат бот третьего поколения. То есть этот бот не просто ходит по графу состояний, а умеет сохранять и использовать контекст предыдущего диалога. На сегодняшний день лучшая иллюстрация для современных чат-ботов выглядит примерно так:
То есть чат боты — это просто набор выверенных правил перехода из одной точки графа в другую. Если посмотреть на существующие решения от гигантов рынка, по сути ничего сильно отличающегося от набора правил там нет. Грубо говоря, выглядит этот набор примерно так:
Диалог в точке ХХХ.
Если пользователь ввел предложение со словами [«купить», «билет»], перейти в точку «СПРОСИТЬ КУДА»
Если пользователь ввел предложение со словами [«купить», «котлет»], перейти в точку «СПРОСИТЬ ИЗ ЧЕГО»
Сразу видно, что тут получается фигня, если пользователь ввел: «Я хотел бы купить билет в Порто», — его все равно спросят, — «А куда вы хотите поехать?». Чтобы сделать диалог более человечным, придется добавлять новые правила о том, что делать, если есть указание места.
Потом добавлять правила о том, что делать, если есть указание места и времени, и так далее.
Этот набор правил будет разрастаться достаточно быстро, но и это не самое страшное, все «правильные» пути можно описать, улучшить и вылизать.
Самое неприятное в том, что человек — существо непредсказуемое, в отличие от бота, и может в произвольный момент начать спрашивать совершенно другое. То есть в момент, когда бот уже готов забронировать билет, человек может спросить «кстати, а что там с погодой?» или «хотя нет, я хотел бы поехать на своей машине, сколько займет дорога?».
Впрочем, он может это спросить и в момент после выбора города, но до выбора времени вылета или вообще до выбора места, куда он хочет отправиться. Бота, основанного на конечных автоматах, заклинит и его механические ложноножки будут грустно подергиваться, а пользователь фрустрировать.
Тут можно (и нужно) использовать машинное обучение. Но тогда возникают новые проблемы: к примеру, если использовать обучение с подкреплением, чтобы предсказывать переходы в точки графа, то возникают вопросы:, а где брать данные для этого обучения, и кто будет выставлять оценки за качество ответов?
Пользователи вряд ли согласятся учить вашего бота, да и, как показывает практика, сообщество пользователей может научить бота совсем не тому, что вам хочется, и что общество считает приличным. К тому же бот на начальном этапе будет отвечать совсем невпопад, что заставит пользователей понервничать и не связываться с такой поддержкой в принципе.
Проанализировав и подумав над всеми недостатками существующих ботов, разработчики RASA постарались решить проблемы следующим образом:
- Любой ввод от пользователя проходит через «определение намерения», то есть вводимый текст при помощи машинного обучения сопоставляется одному (или нескольким) намерениям. Также из текста при необходимости вычленяются сущности и складываются в память бота.
- Этот процесс аналогичен другим ботам, за исключением используемой модели определения намерения.
- Следующее действие бота предсказывается при помощи машинного обучения на основе контекста, то есть предыдущих действий, намерений и состояния памяти бота.
- При этом данных для первоначального обучения надо не очень много, и бот может вполне себе предсказывать какое действие совершать даже без конкретных примеров и правил.
Рассмотрим механизмы работы более подробно.
RASA NLU
Начнем с первого кита, на котором покоится бот. Это Natural Language Understanding, состоящий из двух основных частей: определение намерения и распознавание сущностей.
Intent detection
Определение намерения строится на модифицированном алгоритме под названием StarSpace от Facebook, реализованном на Tensorflow. При этом не используются предобученные модели векторных представлений слов, что позволяет обойти ограничения данных представлений.
Например, определение намерения в алгоритмах RASA будет хорошо работать для любого языка, а также с любыми специфическими словами, которые вы укажете в обучающих примерах. При реализации же через предобученные векторные представления вроде GloVe или word2vec локализация бота и его применение в узкоспециализированных областях принесет достаточно головной боли.
Алгоритм работает на основе векторизации предложений через bag of words и сравнения их «похожести». Примеры намерений и сами намерения преобразуются в вектора при помощи bag of words и подаются на вход соответствующим нейросетям. На выходе нейросети получается вектор для именно этого набора слов (тот самый embedding).
Обучение происходит таким образом, чтобы минимизировать функцию потерь в виде суммы попарных расстояний (либо косинусных, либо векторных произведений) между двумя похожими векторами и k-непохожими. Таким образом, после обучения каждому намерению будет поставлен в соответствие некий вектор.
При получении пользовательского ввода, предложение аналогичным образом векторизируется и прогоняется через обученную модель. После чего рассчитывается расстояние от полученного вектора до всех векторов намерений. Результат ранжируется, выделяя наиболее вероятные намерения и отсекая отрицательные значения, то есть совсем непохожие.
Дополнительно к вышеперечисленным плюшкам этот подход позволяет автоматически выделять более одного намерения из предложения. К примеру: «да, я это понял. А как же мне теперь доехать до дома?» распознается как «intent_confirm+intent_how_to_drive», что позволяет строить более человечные диалоги с ботом.
К слову, перед обучением можно из примеров создать искусственные предложения, путем перемешивания существующих, для увеличения числа примеров для обучения.
RASA Entity recognition
Вторая часть NLU — это выделение сущностей из текста. Например, пользователь пишет «Я хочу сходить в китайский ресторан с двумя друзьями», бот должен выделить не только намерение, но и данные ему соответствующие. То есть заполнить в своей памяти, что блюда в ресторане должны быть китайскими, и что число посетителей равно трем.
Для этого используется подход, основанный на Conditional Random Fields, который был уже описан где-то на хабре, так что не буду повторяться. Желающие могут почитать про данный алгоритм на сайте Стэнфорда.
Дополнительно отмечу, что можно получать сущности из текста на основе шаблонов, текстов (например названия городов), а также подключить отдельным сервисом Facebook Duckling, про который тоже неплохо бы написать когда-нибудь.
RASA Stories
Второй синий кит, на котором основана RASA Core — это истории. Общая суть историй — примеры реальных диалогов с ботом, отформатированные в виде намерение-реакция. На основе этих историй обучается рекуррентная нейросеть (LSTM), которая сопоставляет предыдущую историю сообщений в требуемое действие. Это позволяет не задавать графы диалогов жестко, а также не определять все возможные состояния и переходы между ними.
При достаточном количестве примеров сеть будет адекватно предсказывать следующее состояние для перехода вне зависимости от наличия конкретного примера. К сожалению, точное число историй для этого неизвестно и все, чем можно руководствоваться, — это фраза разработчиков: «чем больше, тем лучше».
Чтобы обучать систему, не просто записывая какие-то там придуманные диалоги, можно использовать интерактивное обучение.
Тут варианта два:
1. Заставить некоторое количество инженеров заниматься разговорами с ботом, корректируя неправильные предсказания, неправильное определение сущностей и затыки с предсказанием действия по историям.
2. Сохранять разговоры в базу данных и дальше специально обученными инженерами просматривать те диалоги, где пользователь не смог решить свою проблему, то есть переключился на человека, или же бот признался в своей беспомощности и не смог ответить.
Для того, чтобы понять механизм историй, проще всего разобрать какой-то простой пример. Допустим бронь столика в ресторане, пример, предоставленный разработчиками в разделе примеров исходного кода. Для начала определим намерения, а дальше сделаем пару историй.
Намерения и их примеры:
Intent_hello
- Hi
- Hello
- Aloha
- Good morning
…
Intent_thanks
- Thanks
- Nice
- Thank you
…
Intent_request
- Пока пропустим
Intent_inform
- Пока пропустим
Далее надо сделать память бота, то есть определить слоты в которые будет записываться то, что требуется пользователю. Определим слоты:
cuisine:
type: unfeaturized
auto_fill: false
num_people:
type: unfeaturized
auto_fill: false
А теперь покажем примеры (небольшую часть) для пропущенных выше намерений. Скобки в примерах — это данные для обучения Ner_CRF, в формате [сущность](имя переменной для хранения: что храним).
intent_request_restaurant
- im looking for a restaurant
- can i get [swedish](cuisine) food for [six people](num_people:6)
- a restaurant that serves [caribbean](cuisine) food
- id like a restaurant
- im looking for a restaurant that serves [mediterranean](cuisine) food
intent_inform
- [2](num_people) people
- for [three](num_people:3) people
- just [one](num_people:1) person
- how bout [asian oriental](cuisine)
- what about [indian](cuisine) food
- uh how about [turkish](cuisine) type of food
- um [english](cuisine)
Теперь определяем историю основного пути:
* greet
— utter_greet
* Intent_request
— restaurant_form
— form{«name»: «restaurant_form»}
— form{«name»: null}
— action_book_restaurant
* thankyou
— utter_noworries
Вот и весь идеальный бот для идеального мира. Если пользователь сразу же в первом предложении указал все нужные данные, то столик забронируют. Например он пишет «i want to book table in spanish restaurant for five people». В этом случае num_people будет 5, а cuisine — spanish, что хватит боту для дальнейших действий по бронированию.
Однако, если посмотреть на примеры, видно, что данные не всегда присутствуют в необходимом количестве, а иногда их вообще нет. Так появляются неосновные диалоги.
Допустим в запросе нет данных о кухне, то есть примерно такой диалог:
Hello
Hi
I want to book restaurant for five people
…
Чтобы он корректно завершился, надо определить историю следующего вида:
* greet
— utter_greet
* Intent_request
— restaurant_form
— form{«name»: «restaurant_form»}
— slot{«requested_slot»: «num_people»}
— utter_ask_cuisine
* form: inform{«cuisine»: «mexican»}
— slot{«cuisine»: «mexican»}
— form: restaurant_form
…
И самое приятное, что если создать истории для нескольких кухонь, то, встретив незнакомую, бот предскажет следующее действие самостоятельно, хотя и будет не очень уверен. При этом если создать аналогичную историю, но где заполнен слот «cuisine», а не «num_people», то боту станет абсолютно все равно в каком порядке будет предоставлена информация о параметрах бронирования столика.
Всякие попытки увести бота с правильного пути можно пресекать двумя способами: определить возможные истории для разговоров «ни о чем», либо на все попытки начать подобный разговор — отвечать, что стоит вернуться к делу.
Так как наша компания находится в начале удивительного путешествия в мир чат-ботов, то есть шанс, что будут и новые статьи о том, какие грабли мы собирали, и что вообще делали. Stay tuned!