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

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

Предыстория


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

image

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

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


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

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

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


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

image

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

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

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

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



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

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


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

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

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


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


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

image

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

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

image

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

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

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

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



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

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

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

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

Решилась данная проблема уменьшением параметра сглаживания ($α$) с 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!

Литература


Википедия
Стоп-слова
Стеммер Портера
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 13
  • 0
    Где можно оценить чат-бота?
    • 0
      В ближайшее время постараюсь сделать онлайн-версию для тестирования
      • +10
        У меня для проверки есть фраза, на которой ломаются 100% чат-ботов:
        «Не говори мне на завтра погоду в Питере»
        • 0

          «Не говори мне на вчера погоду в Питере»

    • +8
      Что-то мне подсказывает, что «не» не стоит заносить в стоп-слова…
      • 0
        Да, действительно. Приведенный выше список не окончательный и требует редактирования
        • 0
          А мне — что стоит даже склеивать с прошлым и последующим токеном (впрочем, если данных достаточно — можно заюзать биграммы, а не делать это явно) — или, возможно, инвертировать значение соответсвующей им фичи.
          • +1

            Это все (склеивать или инвертировать) — явное прописывание правил. До определенного момента работает, а потом упирается в предел. Причем упирается очень быстро, намного раньше, чем будет достигнут уровень Сири.

            • +1
              Ну, ясное дело. Но думаю, тут мы раньше упрёмся в невозможность обучить таким правилам неявно на доступной выборке. Или ошибаюсь? (пока не вглядывался в датасет, да).
              • 0

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

        • –4
          А прогонять через нейронку.
          текст будет более связный.
          • 0
            При создании программы преобразования текста на ЕЯ при возникновении проблемы
            есть возможность использовать эту же программу для ее решения, т.к. на ЕЯ можно описать что угодно.
            При таком подходе проблемы будут все более узкими и поэтому когда-нибудь закончатся.
            Например, автор начал с РВ, но возникла проблема их ручного ввода.
            Так может вместо бесполезных разговоров сначала научить программу создавать эти РВ
            из диалога на ЕЯ, хотя бы даже введя нужные для этого РВ вручную.
            А «именованные сущности» в РВ уже есть: (?<name> ...)
            Еще в РВ привлекает то, что они служат для обработки текста, сами являясь текстом.
            Т.е. предполагают самоприменимость, однородность, бутстрапность.
            Я сильно подозреваю, что при наличии достаточной ловкости ума, используя РВ,
            ИИ можно написать вообще в сотню строк кода.
            • 0
              Лучше вместо Стеммера Портера использовать Snowball, а еще лучше лемматизатор. Точность намного выше. У себя для .NET использую вот этот, обернутый в WebAPI, развернутые в оперативной памяти словари занимают примерно 300Мб.
              Когда требуется оффлайн вычисления, понижаю точность, используя эту реализацию Snowball.

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