Чатбот, который «как Siri, только круче» на наивном Байесовском классификаторе

в 17:35, , рубрики: Google API, java, искуственный интеллект, классификация, математика, машинное обучение, чатбот

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

Предыстория

Всё началось, когда в самолёте я посмотрел типичную, на первый взгляд американскую комедию – «Почему, он?» (англ. Why him? 2016). Там, у одного из ключевых персонажей в доме был установлен голосовой помощник, который нескромно позиционировал себя «как Siri, только круче». К слову бот из фильма умел не только вызывающе разговаривать с гостями, иногда ругаясь матом, но также контролировать весь дом и прилегающую территорию – от центрального отопления до смыва унитаза. После просмотра фильма, мне пришла идея реализовать что-то подобное и я начал писать код.

image

Рисунок 1 – Кадр из того самого фильма. Голосовой помощник на потолке.

Начало разработки

Первый этап дался легко – было подключено Google Speech API для распознавания и синтеза речи. Текст, получаемый от Speech API обрабатывался через, вручную написанные, паттерны регулярных выражений, при совпадении с которыми определялось намерение (intent) человека, разговаривающего с чат ботом. На основании определённого regexp’ом намерения, рандомно выбиралась одна фраза из соответствующего списка ответов. Если сказанное человеком предложение не попадало ни под один паттерн, то бот говорил заранее заготовленные общие фразы, наподобие: «Мне нравится думать, что я не просто компьютер» и тд.

Очевидно, что вручную прописывать множество регулярных выражений для каждого intent’a – занятие трудоёмкое, поэтому, в результате поисков, я наткнулся на так называемый «наивный Байесовский классификатор». Наивным его называют потому, что при его использовании подразумевается, что слова в анализируемом тексте не связаны друг с другом. Несмотря на это, данный классификатор показывает неплохие результаты, о которых поговорим чуть ниже.

Пишем классификатор

Просто так засунуть строку в классификатор не получится. Входная строка обрабатывается по нижеприведённой схеме:

image

Рисунок 2 – Схема обработки входного текста

Объясню подробнее каждый этап. С токенизацией всё просто. Банально – это разбиение текста на слова. После чего, из полученных токенов (массив слов) удаляются так называемые стоп-слова. Заключительная стадия довольно непростая. Стемминг – это получение основы слова для заданного исходного слова. Причём, основа слова – это не всегда его корень. Я использовал Стеммер Портера для русского языка (ссылка ниже).

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

$$display$$P(I | D)= P (D| I)* P(I) / P(D) , где I – Intent (намерение), D – документ$$display$$

$inline$P ( I | D )$inline$ – это вероятность присвоения какого либо intent’a данной входной строке иными словами фразе, которую сказал нам человек. $inline$P ( I )$inline$ – вероятность intent’a, которая определяется отношением количества документов, принадлежащих intent’у к общему количеству документов в обучающем наборе. Вероятность документа – $inline$P(D) = 1$inline$, поэтому отбрасываем её. $inline$P (D | I)$inline$ – вероятность отношения документа к intent’у. Она расписывается следующим образом:

$$display$$P(D | I)=P(w_1,w_2…w_n ) | I)= ∑ _ i^n P(w_i| I), $$display$$

где $inline$w_i$inline$ — соответствующий токен (слово) в документе

Распишем ещё поподробнее:

$$display$$P(w_i | I)= (count(w_i,I)+ α)/(count (I)+ α*uniqueWords) $$display$$

где:
$inline$count(w_i,I)$inline$ – сколько раз токен был отнесён к данномй intent'у
$inline$α$inline$ – сглаживание, предотвращающее нулевые вероятности
$inline$count(I)$inline$ – кол-во слов, отнесённых к intent'у в тренировочных данных
$inline$uniqueWords$inline$ – количество уникальных слов в тренировочных данных

Для тренировки я создал несколько текстовых файлов с символичными названиями «hello», «howareyou», «whatareyoudoing», «weather» etc. Для примера приведу содержание файла hello:

image

Рисунок 3 – Пример содержимого текстового файла «hello.txt»

Процесс обучения в деталях я описывать не буду, ведь весь код на Java доступен на Github. Приведу лишь схему использования данного классификатора:

image

Рисунок 4 – Схема работы классификатора

После того, как мы обучили нашу модель, приступаем к классификации. Поскольку, в тренировочных данных мы определили несколько intent’ов, то и полученных вероятностей $inline$ P(I| D)$inline$ будет несколько.

Так какую же из них выбирать? Выбираем максимальную!

$$display$$classify(I_1,I_2,I_3….I_n | D)= argmax P ( I_i | D)$$display$$

А теперь самое интересное, результаты классификации:

Входная строка Определённый intent Верно ли?
1 Здравствуйте, как дела? Howareyou Да
2 Рад вас приветствовать, друг Whatdoyoulike Нет
3 Как прошел вчерашний день Howareyou Да
4 Какая погода за окном? Weather Да
5 Какую погоду обещают на завтра? Whatdoyoulike Нет
6 Прошу прощения, мне нужно отлучиться Whatdoyoulike Нет
7 Удачного дня Bye Да
8 Давай познакомимся? Name Да
9 Привет Hello Да
10 Рад вас приветствовать Hello Да

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

  • Фразы №3 и №10 отличаются одним словом, но дают разные результаты.
  • Все неправильно определенные intent’ы определяются как whatdoyoulike.

Решилась данная проблема уменьшением параметра сглаживания ($inline$α$inline$) с 0.5 до 0.1, после чего получились следующие результаты:

Входная строка Определённый intent Верно ли?
1 Здравствуйте, как дела? Howareyou Да
2 Рад вас приветствовать, друг Hello Да
3 Как прошел вчерашний день Howareyou Да
4 Какая погода за окном? Weather Да
5 Какую погоду обещают на завтра? Weather Да
6 Прошу прощения, мне нужно отлучиться Bye Да
7 Удачного дня Bye Да
8 Давай познакомимся? Name Да
9 Привет Hello Да
10 Рад вас приветствовать Hello Да

Полученные результаты я считаю удачными, и учитывая мой предыдущий опыт с regular expressions могу сказать, что наивный Байесовский классификатор намного более удобное и универсальное решение, особенно, когда дело касается масштабирования тренировочных данных.
Следующим этапом в данном проекте будет разработка модуля определения именованных сущностей в тексте (Named Entity Recognition), а также совершенствование текущих возможностей.
Спасибо за внимание, to be continued!

Литература

Википедия
Стоп-слова
Стеммер Портера

Автор: Александр Перевалов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js