Предыстория
Всё началось, когда в самолёте я посмотрел типичную, на первый взгляд американскую комедию – «Почему, он?» (англ. Why him? 2016). Там, у одного из ключевых персонажей в доме был установлен голосовой помощник, который нескромно позиционировал себя «как Siri, только круче». К слову бот из фильма умел не только вызывающе разговаривать с гостями, иногда ругаясь матом, но также контролировать весь дом и прилегающую территорию – от центрального отопления до смыва унитаза. После просмотра фильма, мне пришла идея реализовать что-то подобное и я начал писать код.
Рисунок 1 – Кадр из того самого фильма. Голосовой помощник на потолке.
Начало разработки
Первый этап дался легко – было подключено Google Speech API для распознавания и синтеза речи. Текст, получаемый от Speech API обрабатывался через, вручную написанные, паттерны регулярных выражений, при совпадении с которыми определялось намерение (intent) человека, разговаривающего с чат ботом. На основании определённого regexp’ом намерения, рандомно выбиралась одна фраза из соответствующего списка ответов. Если сказанное человеком предложение не попадало ни под один паттерн, то бот говорил заранее заготовленные общие фразы, наподобие: «Мне нравится думать, что я не просто компьютер» и тд.
Очевидно, что вручную прописывать множество регулярных выражений для каждого intent’a – занятие трудоёмкое, поэтому, в результате поисков, я наткнулся на так называемый «наивный Байесовский классификатор». Наивным его называют потому, что при его использовании подразумевается, что слова в анализируемом тексте не связаны друг с другом. Несмотря на это, данный классификатор показывает неплохие результаты, о которых поговорим чуть ниже.
Пишем классификатор
Просто так засунуть строку в классификатор не получится. Входная строка обрабатывается по нижеприведённой схеме:
Рисунок 2 – Схема обработки входного текста
Объясню подробнее каждый этап. С токенизацией всё просто. Банально – это разбиение текста на слова. После чего, из полученных токенов (массив слов) удаляются так называемые стоп-слова. Заключительная стадия довольно непростая. Стемминг – это получение основы слова для заданного исходного слова. Причём, основа слова – это не всегда его корень. Я использовал Стеммер Портера для русского языка (ссылка ниже).
Перейдём к математической части. Формула, с которой всё начинается выглядит следующим образом:
– это вероятность присвоения какого либо intent’a данной входной строке иными словами фразе, которую сказал нам человек. – вероятность intent’a, которая определяется отношением количества документов, принадлежащих intent’у к общему количеству документов в обучающем наборе. Вероятность документа – , поэтому отбрасываем её. – вероятность отношения документа к intent’у. Она расписывается следующим образом:
где — соответствующий токен (слово) в документе
Распишем ещё поподробнее:
где:
– сколько раз токен был отнесён к данномй intent'у
– сглаживание, предотвращающее нулевые вероятности
– кол-во слов, отнесённых к intent'у в тренировочных данных
– количество уникальных слов в тренировочных данных
Для тренировки я создал несколько текстовых файлов с символичными названиями «hello», «howareyou», «whatareyoudoing», «weather» etc. Для примера приведу содержание файла hello:
Рисунок 3 – Пример содержимого текстового файла «hello.txt»
Процесс обучения в деталях я описывать не буду, ведь весь код на Java доступен на Github. Приведу лишь схему использования данного классификатора:
Рисунок 4 – Схема работы классификатора
После того, как мы обучили нашу модель, приступаем к классификации. Поскольку, в тренировочных данных мы определили несколько intent’ов, то и полученных вероятностей будет несколько.
Так какую же из них выбирать? Выбираем максимальную!
А теперь самое интересное, результаты классификации:
№ | Входная строка | Определённый 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)
savostin
19.11.2017 22:25Что-то мне подсказывает, что «не» не стоит заносить в стоп-слова…
perevalov_a Автор
19.11.2017 22:33Да, действительно. Приведенный выше список не окончательный и требует редактирования
alex4321
20.11.2017 00:37А мне — что стоит даже склеивать с прошлым и последующим токеном (впрочем, если данных достаточно — можно заюзать биграммы, а не делать это явно) — или, возможно, инвертировать значение соответсвующей им фичи.
Hardcoin
20.11.2017 01:20Это все (склеивать или инвертировать) — явное прописывание правил. До определенного момента работает, а потом упирается в предел. Причем упирается очень быстро, намного раньше, чем будет достигнут уровень Сири.
alex4321
20.11.2017 01:23Ну, ясное дело. Но думаю, тут мы раньше упрёмся в невозможность обучить таким правилам неявно на доступной выборке. Или ошибаюсь? (пока не вглядывался в датасет, да).
Hardcoin
20.11.2017 01:33Я так понял, датасет был сделан самостоятельно. На таком, конечно, можно писать правила самому, для неявного обучения он слишком маленький. Но можно взять чужой корпус текстов и на нем построить лингвистическую модель.
programania
20.11.2017 11:19При создании программы преобразования текста на ЕЯ при возникновении проблемы
есть возможность использовать эту же программу для ее решения, т.к. на ЕЯ можно описать что угодно.
При таком подходе проблемы будут все более узкими и поэтому когда-нибудь закончатся.
Например, автор начал с РВ, но возникла проблема их ручного ввода.
Так может вместо бесполезных разговоров сначала научить программу создавать эти РВ
из диалога на ЕЯ, хотя бы даже введя нужные для этого РВ вручную.
А «именованные сущности» в РВ уже есть: (?<name> ...)
Еще в РВ привлекает то, что они служат для обработки текста, сами являясь текстом.
Т.е. предполагают самоприменимость, однородность, бутстрапность.
Я сильно подозреваю, что при наличии достаточной ловкости ума, используя РВ,
ИИ можно написать вообще в сотню строк кода.
Ogoun
20.11.2017 15:57Лучше вместо Стеммера Портера использовать Snowball, а еще лучше лемматизатор. Точность намного выше. У себя для .NET использую вот этот, обернутый в WebAPI, развернутые в оперативной памяти словари занимают примерно 300Мб.
Когда требуется оффлайн вычисления, понижаю точность, используя эту реализацию Snowball.
tmnhy
Где можно оценить чат-бота?
perevalov_a Автор
В ближайшее время постараюсь сделать онлайн-версию для тестирования
samodum
У меня для проверки есть фраза, на которой ломаются 100% чат-ботов:
«Не говори мне на завтра погоду в Питере»
aleki
«Не говори мне на вчера погоду в Питере»