Проекты
Slack бот, который пишет стендапы сам!

Все началось с того, что в начале этого года, по интернету, пролетела волна тренда, по которому самой востребованной профессией, на 2019, год будет — Data Scientist (Специалист по работе с данными). Курсы стали как грибы расти в интернете, но большая часть из этих курсов стоит дорого, а толку мало. И я решил начать с курса от Яндекс и МФТИ. Пройдя первые две специализации, решил дальше не продолжать, так как понял, что знаний не хватает. Нужно их подтягивать. Сразу же определился для себя, куда я хочу двигаться. Так как меня совсем не заинтересовала работа с изображениями, ввиду необходимости в мощном железе и сложности внедрения в повседневную жизнь, я принялся за изучение более приближенного к реальности и более интересного для меня направления Обработка Естественного Языка, далее как NLP.
Про результаты применения полученных знаний в NLP, на примере бота для Slack и пойдет речь в этой статье.
Описание задачи
Задача состояла в написании бота, который бы генерировал стендап состоящий из трех частей:
- Вчера
- Сегодня
- Проблемы
В качестве основы для текстового корпуса использовать базу данных comedian. Боту необходимо генерировать авто-стендап, в определенное время. Уметь пропускать дни, по которым не нужно генерировать стендапы.
Немного о стендапах, что это такое и для чего нужно
Стендап — это новомодное слово, которое часто можно услышать из афиш Comedy Club. Но как выяснилось, стендапить может не только комик или юморист, но так же и программист. Так что мы программисты, идем в ногу со временем. Стендап, в компании Mad Devs стал неотъемлемой частью процесса обмена информацией в команде, который с одной стороны экономит время (из-за того, что не тратится время на сбор команды, в одно условленное время, в одном условленном месте), а с другой стороны документирует каждый день программиста (всегда есть представление о проблемах и задачах, над которыми работал и будет работать программист). Всегда можно путешествовать назад и вспоминать, какие проблемы были раньше и какие задачи решались. Это своеобразная машина времени. Oleg Puzanov вполне доходчиво объясняет суть и роль стендапа в его статье “Как правильно участвовать в ремоут стендап митингах”.
Сферы применения NLP
Анализ текста при этом включает следующие направления:
- Извлечение информации
- Информационный поиск
- Анализ высказываний
- Анализ тональности текста
- Вопросно-ответные системы
Задачи анализа и синтеза в комплексе:
- Машинный перевод
- Автоматическое реферирование, аннотирование или упрощение текста
О цикле поддержки приложения использующее NLP в своей работе
- Сбор необходимой для анализа текстовой информации
- Очистка и подготовка текстового корпуса для преобразования в структуру, которая будет удобна для анализа. Здесь используются подход WORM — расшифровывается как (Write-Once Read-Many или Записал один раз, прочитал много раз). Он берет свое начало с CD дисков, на которые запись происходила один раз, а читать можно было много раз, пока диск не будет испорчен или CD привод не накроется. Так как данных бывает очень и очень много, такой принцип хранения данных очень подходит. Обычно документы делятся по тематикам или по определенным признакам.
- Предобработка данных:
- Токенизация — разбиение документа на предложения и отдельные слова.
- Нормализация текста — приведение слова в нормальную форму, для того чтобы исключить повторное использование одного и того же слова, но в разных временах и формах множественного или единственного числа. Сюда можно отнести Стемминг или Лемматизацию.
4. Выбор модели
5. Подготовка данных для выбранной модели
- Векторизация, Bag of Words (Мешок слов), Частотный словарь, Биграммы, Триграммы и так далее.
5. Обучение модели
6. Сериализация данных — это очень важный этап в машинном обучении, позволяющий экономить время. Почти на всех этапах есть необходимость сохранять промежуточные данные, для последующего их использования.
Немного отступления и поправок
Были разные эксперименты, по применению NLP, но было решено остановиться на Генерации текста. Почему было выбрано это направление? Для начала, я сделал автодополнялку текста, обученной на тексте стендапов и это было прикольно, набираешь одно слово и ниже текстового поля, появлялся список из нескольких, наиболее употребляемых с введенным словом, слов. Дал поглядеть Oleg Puzanov и он посмотрев на то, как прикольно получается накликивать вполне себе пригодный стендап, предложил написать бота, который бы генерировал стендап - сам. И началась эпопея Валеры! Так решили назвать бота — забавно и прикольно.
Для задач генерации текста, не нужен этап нормализации текста, так как при этом удаляются стоп слова, слова приводятся к нормальной форме, а для генерации текста нужны все части речи в предложений и стилистика написания, для того чтобы можно было подставлять следующие слова, в контексте предложения и последовательности слов. В этом случае можно генерировать очень даже неплохие и осмысленные предложения. Конечно, для того, чтобы генерировать более наполненные смыслом предложения, нужно увеличивать размерность n-грамм.
Вот так, выглядела первая версия помощника написания стендапов, на основе вариантов следующих слов.


Ну а далее, поговорим про шаги, предшествующие запуску бота в нашем #general канале Slack.
Подготовка данных для Бота Валеры
Получение данных из БД
Данные находятся в БД comedian, а соответсвенно необходимо получить из нужных таблиц данные и сериализовать их в файловую структуру на диске.
Для начала нужно написать запрос в базу, после выполнения которого у меня будут только нужные данные по стендапам.
Дам некоторые пояснения:
- channel_name — можно считать названием проекта
- comment — текст стендапа
- user_name — ник пользователя
- user_id — идентификатор пользователя
Запрос можно составить и протестировать, хоть в консоле MySQL клиента или в MySQLWorkbench или Sequel Pro или DBeaver (Лучший визуальный инструмент для работы с БД).
Получение текстового корпуса из БД
Чтобы для конечного пользователя, этот процесс был простым, был создан Web интерфейс для обновления корпуса из БД, его компиляции, а так же создания моделей.

Все файлы имеют хешированные названия, по их контенту. Это очень удобно, так как позволяет не дублировать контент дважды, если он где-то встретится. Каждый файл соответствует отдельному стендапу. Все файлы сохраняются в папке corpus.
Сериализация текстового корпуса
Необходимо произвести разбиение по словам и предложениям. После этого нужно закрепить полученные результаты, чтобы можно было переиспользовать в будущем. В этом, нам поможет модуль pickle и его метод pickle.dump, который позволяет сериализовывать структуры данных в бинарном виде, в файл на диске. В интерфейсе, для создания бинарного корпуса, необходимо нажать одну кнопку “Скомпилировать корпус”. При этом будет произведен обход каждого файла, в каждой папки проекта и произведена токенизация, после чего данные будут сериализованы в формате pickle.

Файлы сохраняются в отдельную папку от текстового корпуса, с сохранением той же структуры каталогов. Названия файлов, так же хешированы, но эти хеши соответствуют аналогичному файлу в текстовом корпусе. Таким образом можно отслеживать какие файлы скомпилированы, а какие нет.
Создание модели на основе n-грамм

Из рисунка выше, следует логичный вывод, чем выше N, тем больше контекста, в котором может использоваться последнее слово, сохраняется. Соответственно выбранные мной библиотеки и алгоритмы вполне нормально работают при N=3, а значит будут использоваться триграммы.
В интерфейсе, присутствует кнопка для создания модели триграмм. В ходе ее создания производится:
- Загрузка скомпилированного корпуса в формате pickle.
- Получение слов и предложений корпуса.
- Создание частотного словаря на основе BOW (Bag of Words).
- Создание модели триграмм на основе частотного словаря и предложений по всему корпусу.
- Сериализация модели триграмм
- Создание модели Кнесера-Нея на основе модели триграмм
- Сериализация модели Кнесера-Нея
Эти все процессы хорошо видны в интерфейсе

И как вы можете заметить, в нижней таблице, отображается список обученых и сериализованных моделей.
knm — модель Кнесера-Нея основана на сглаживание Кнесера — Нея (Kneser — Ney) использует частоту униграммы не саму по себе, а в отношении к n-граммам, которые она завершает. Одни слова появляются в самых разных контекстах, но другие появляются часто и только в определенных контекстах их нужно интерпретировать по-разному.
trigram_counts — модель на основе триграмм.
Генерация стендапа на основе модели триграмм и сглаживания Кнесера-Нея
Довольно легко написать приложение, решающее простые задачи вероятностной генерации языка, но, чтобы создать что-то более сложное (например, приложение, генерирующее целые предложения), в модели необходимо закодировать больше информации о языке. Этого можно добиться с применением n-грамм высокого порядка и больших предметно-ориентированных корпусов.
К сожалению, подходы на основе грамматик, даже самые эффективные, не всегда дают хороший результат. Во-первых, они в значительной степени зависят от успешной маркировки слов тегами частей речи, то есть мы должны быть уверены, что ваш механизм маркировки, правильно определяет существительные, глаголы, прилагательные и другие части речи.
По сути модель триграмм строится на последовательности из трех слов в предложении. Тем самым на основе предыдущих двух слов, можно судить о контексте применения крайнего слова. Таким образом строятся авто-подсказки на ваших телефонах, при наборе одного или парочки слов, вам предлагается на выбор, несколько следующих слов, которые когда либо, кто-либо употреблял в своих предложениях, в совокупности с несколькими набранными словами.
Генерация стендапа осуществляется в отдельном приложении бота, которое использует сериализованные файлы моделей, загружая их в память при помощи все той же библиотеки pickle и метода pickle.load.
Проблемы при генерации текста
Если основываться на механизме выбора следующего, лучшего слова-кандидата, то фактически, мы будет целиком воспроизводить исходный текст, с некоторыми допущениями, что не годится. Для того, чтобы разнообразить текст и сделать разнообразным, а порой и немного странным содержимое текста стендапа, пришлось добавить рандома. Но рандом не основан на простом выборе любого из возможных слов из списка предполагаемых, а основан на кол-ве частей речи, которые следуют за текущим словом. Побеждает слово, чья часть речи, представлена в большем количестве.
Так же нужно было убирать ссылки на тикеты, так как в любом случае не для всех они доступны и в целом не будут полезны в тексте стендапа.
Так же нужно было учитывать, что стендап состоит из трех частей, Вчера, Сегодня и Проблемы. Но текст стендапа, не всегда начинался на эти заветные слова, бывало, что они заменялись на названия дней недели или другие словосочетания означающие тоже самое.
Генерация трех составляющих стендапа
По сути, это следующего содержания функция, которая по очереди генерирует части стендапа, так как используются начальные слова в markdown формате.
- *Вчера*
- *Сегодня*
- *Проблемы*

Часть текста стендапа генерируется при помощи частотной модели триграмм, а часть на основе сглаживания Кнесера-Нея.
Пока есть проблема со смесью мужского и женского рода в одном предложении, что тоже немного забавно, но в целом эту проблему можно решить, используя контекстное приведение глаголов и прилагательных и других частей речи в нормальную форму с трансформацией в мужской род. Так как Валера все же мужик!.
Как работает бот?

Сам бот размещается на Heroku, вместе с бинарной частотной моделью и моделью на основе сглаживания Кнесера-Нея.
Бот соединяется со Slack, компании, при помощи RTM соединения, основанного на веб-сокетах. Что позволяет отсылать в реалтайме сообщения и получать все события в момент их возникновения, без особых затрат на запросы и ответы как с веб-хуками. И отмечу сразу, такой способ очень хорош в планет того, что не нужно регистрировать точку доступа, смотрящую в интернет. Все без проблем работает и с локального хоста. Так как инициатором соединения является моя сторона, то сервер Слака, после установления соединения, без проблем можете передавать данные посредством установленного веб-сокетного соединения.
Firebase тут нужен лишь для хранения даты последнего стендапа, чтобы при перезапусках серверов на Heroku, не терять временную метку отправленного стендапа, что уберегает от повторной отправки стендапа в слак. Так как на Heroku эфемерная файловая система, в которой любые изменения, сделанные пользователем, теряются после перезагрузке контейнера. Поведение очень похоже на папку /tmp.
Бот в 9:00 утра по UTC+6 отправляет стендап в #general канал Slack.
Если же хочется, чтобы бот, сгенерировал стендап, то на этот случай есть фразы, для триггера генерации стендапа:

- Валера стендап
- 🔫 стендап
- 🔫стендап (без пробела между иконкой пистолета и словом)
Так же наряду с фразами настраивается и время авто-стендапа и дни когда не нужно слать авто-стендап и необходимые для работы приложения параметры.

Ну и сама демонстрация генерации стендапа по запросу


Нюансы деплоя на хероку приложений использующих NLTK библиотеку
На Heroku, бот Валера, запущен как воркер (worker). Для пометки тегами части речи каждого слова в предложении и для токенизации, используется библиотека NLTK. Для этой библиотеки необходимо скачивать словари и нужные данные для ее работы.
Чтобы установить необходимые данные для NLTK библиотеки, необходимо проделать следующие шаги:
- Установить переменную окружения NLTK_DATA со значением пути /app/.heroku/python/nltk_data. Этот каталог подходит для хранения данных.
- Создать папку bin, в корне проекта.
- В папке bin файл post_compile с командой по установке необходимых библиотек.
Еще один упрощенный способ, если нет необходимости устанавливать корпусы данных из нестандартных источников, то достаточно создать файл nltk.txt и в нем перечислить названия нужных словарей и данных для работы библиотеки, к примеру:
- punkt
- averaged_perceptron_tagger
- averaged_perceptron_tagger_ru
- wordnet
- stopwords
Но у меня есть необходимость устанавливать данные из github. А вообще я столкнулся с реальной проблемой поддержки русского языка. Очень мало библиотек и данных позволяющих полноценно работать с русским текстом. По этой причине для скачивания библиотек мне потребовалось написать специальный downloader.
Потребовался токенизатор для русских предложений, для чего и скачивается и распаковывается в нужную директорию.

Все копируется в папку nltk_data/tokenizers/punkt и необходимые подпапки.
Таким образом при помощи содержимого файла post_compile
происходит запуска скрипта на python для скачивания всех необходимых данных, а переменная окружения рулит пунктом назначения для установки данных.
Этот файл также пригодится и локально, так как автоматизирует процесс скачивания данных локально.
Заключение
Сейчас уже не так сложно запустить генерацию текста на основе уже существующего, уже есть все необходимое, но для русского языка, ощущается нехватка инструментов. Но как говорится, все в наших руках и мы можем нашими общими усилиями, внести вклад в NLP для русского сообщества. Вообще создание такого бота может выявлять стилистику написания стендапов разными людьми. И наблюдать грамматические ошибки, интересные ветвления в мысленном процессе при их составлении.
Код бота и системы генерации моделей не выкладываю, так как он не очень хорошо оформлен и в принципе, при желании, примеры реализации легко гуглятся в интернете, решений для генерации текста — множество. Можете попробовать использовать LSTM сети для генерации текста, но в этом случае получается точное воспроизведение текста, что не хорошо, ведь проще выводить куски текста как есть. Ну а для разнообразия генерации можно придумать разные способы.
Посмотрев фильм “Большой хак” (Great Hack) от Netflix, еще раз осознал, что полученные знания и умения, по машинному обучению и Искусственному Интеллекту, а также безграничный доступ к данным пользователей, может быть использован в не очень хороших целях. Так что используйте свои знания в ИИ (AI) и МО (ML) с умом, руководствуясь этическими нормами. Хотя порой деньги и выгода перевешивают этику и нормы морали.
Спасибо за прочтение!