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

Предыстория


Всё началось, когда в самолёте я посмотрел типичную, на первый взгляд американскую комедию – «Почему, он?» (англ. 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!

Литература


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

Комментарии (13)


  1. tmnhy
    19.11.2017 21:45

    Где можно оценить чат-бота?


    1. perevalov_a Автор
      19.11.2017 21:56

      В ближайшее время постараюсь сделать онлайн-версию для тестирования


      1. samodum
        20.11.2017 01:47

        У меня для проверки есть фраза, на которой ломаются 100% чат-ботов:
        «Не говори мне на завтра погоду в Питере»


        1. aleki
          20.11.2017 11:19

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


  1. savostin
    19.11.2017 22:25

    Что-то мне подсказывает, что «не» не стоит заносить в стоп-слова…


    1. perevalov_a Автор
      19.11.2017 22:33

      Да, действительно. Приведенный выше список не окончательный и требует редактирования


    1. alex4321
      20.11.2017 00:37

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


      1. Hardcoin
        20.11.2017 01:20

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


        1. alex4321
          20.11.2017 01:23

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


          1. Hardcoin
            20.11.2017 01:33

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


  1. erwins22
    20.11.2017 08:12

    А прогонять через нейронку.
    текст будет более связный.


  1. programania
    20.11.2017 11:19

    При создании программы преобразования текста на ЕЯ при возникновении проблемы
    есть возможность использовать эту же программу для ее решения, т.к. на ЕЯ можно описать что угодно.
    При таком подходе проблемы будут все более узкими и поэтому когда-нибудь закончатся.
    Например, автор начал с РВ, но возникла проблема их ручного ввода.
    Так может вместо бесполезных разговоров сначала научить программу создавать эти РВ
    из диалога на ЕЯ, хотя бы даже введя нужные для этого РВ вручную.
    А «именованные сущности» в РВ уже есть: (?<name> ...)
    Еще в РВ привлекает то, что они служат для обработки текста, сами являясь текстом.
    Т.е. предполагают самоприменимость, однородность, бутстрапность.
    Я сильно подозреваю, что при наличии достаточной ловкости ума, используя РВ,
    ИИ можно написать вообще в сотню строк кода.


  1. Ogoun
    20.11.2017 15:57

    Лучше вместо Стеммера Портера использовать Snowball, а еще лучше лемматизатор. Точность намного выше. У себя для .NET использую вот этот, обернутый в WebAPI, развернутые в оперативной памяти словари занимают примерно 300Мб.
    Когда требуется оффлайн вычисления, понижаю точность, используя эту реализацию Snowball.