Начну свою первую статью с небольшой предыстории. К моменту когда все началось, я уже на протяжении 7 лет участвовал в научном проекте, целью которого была разработка семантической технологии проектирования интеллектуальных систем. А началось все с прочтения одной замечетельной статьи (спасибо vovochkin) во второй половине 2015 года. Именно тогда я понял, что разрабатываемая нами технология хорошо подходит под решение задач в области интернета вещей. Это был первый фактор который привел меня к текущему проекту. Вторым фактором было то, что мне сильно нравился фильм «Железный человек» и я сильно хотел иметь своего «Джарвиса» у себя дома.
Несколько месяцев планирования и чтения привели меня к следующим задачам, которые предстояло решить:
- Доработать ядро для хранения и обработки базы знаний
- Реализовать среду для визуальной отладки модели дома
- Реализовать голосовой ввод и вывод
Немного теории
Приведу немного теории, которая позволит лучше понимать дальнейшее описание.
Вся система строится вокруг базы знаний (далее буду использовать сокращение БЗ). Знания в ней представлены в виде семантической сети (более подробно про язык представления знаний можно почитать в статье). Я же приведу пару простых примеров. Которые просто позволят понимать дальнейший текст и изображения. В своих примерах я буду использовать два языка: графический — SCg и текстовый — SCs
Для начала надо усвоить как задается отношение «is a». Все достаточно просто. На изображении слева указан факт, что яблоко и ананас — это фрукты. Для этого мы просто создали узел обозначающий множество всех яблок — apple и добавили входящую дугу принадлежности из узла обозначающего множество всех фруктов — fruit. Тоже самое мы сделали и с ананасом.
В принципе на основе отношения «is a» строится все хранение в БЗ, включая и любые другие отношения. Для примера рассмотрим как в БЗ указывается изображение объекта или класса объектов. На данной картинке видно, что мы ввели множество обозначающее все экземпляры отношения «быть изображением чего-то» и в него добавляем все дуги обозначающие бинарные связки.
Теперь немного поговорим про то как происходит обработка хранимых в знаний. Для этого используется агентно-ориентированный подход. Важный принцип, что агенты могут взаимодействовать между собой только через БЗ (меняя ее состояние). Сразу приведу ссылку на документацию по разработке агентов на языке C++. Взаимодействие между агентами реализовано с помощью специализированного языка команд-запросов (документация по нему в процессе разработки). Здесь я приведу один пример запроса (читайте данное изображение по примеру двух прошлых, используя «is a» отношение):
Это самый простой пример запроса, погоды в Минске. На данном изображении можно увидеть узел, который обозначет экземпляр запроса и входит во множество command и command_find_weather, где последний определяет тип запроса. По сути запрос — это множество элементами которого являются его аргументы.
Каждый агент в БЗ имеет свою спецификацию, где указан ожидаемый результат, условие инициирования и другие свойства агента. Одним из таких свойств является — событие при котором он инициируется. В данный момент доступны следующие виды таких событий: удаление(создание) входящей(выходящей) дуги, удаление элемента, изменение содержимого ссылки. Другими словами мы подписываем агента на событие в базе знаний и когда оно происходит агент инициируется. Для команд таким событием является добавление экземпляра запроса во множество command_initiated. Когда это происходит все агенты подписанные на это событие инициируются. После этого лишь агенты, которые умеют обрабатывать данный тип запроса, продолжают работать.
На выходе каждый агент генерирует результат, который связан с экземпляром запроса, с помощью отношения nrel_result.
Таким образом получается нечто типа форума, где разные агенты могут общаться между собой используя специализированный язык. При этом никто из них при формировании запроса, даже не знает будет на него ответ или нет и кто его даст. Это позволяет использовать различные реализации агентов и добавлять их в систему на ходу не прекращая ее работу.
Описание получилось достаточно большим хотя я старался его сделать минимальным и понятным. На любые вопросы по нему я готов ответить подробно в комментариях или же при необходимости сделать отдельную статью с детальным описанием и примерами. Но мы двигаемся дальше.
Доработки ядра
К моменту когда я начал работу над «Джарвисом» наше ядро находилось в достаточно стабильном и рабочем состоянии, но разработка агентов была возможна лишь с использованием языка Си, в репозитории еще остались примеры таких агентов.
Первое что необходимо было сделать — это упростить их разработку. Поэтому было решено написать С++ библиотеку, которая брала на себя множество рутинных задач. На тот момент я работал разработчиком игр на Unreal Engine 4 и поэтому решил, что удобно было бы использовать генератор кода и реализовал такую возможность.
Теперь описание агента начало сводиться к:
class AMyAgent : public ScAgentAction
{
SC_CLASS(Agent, CmdClass("command_my"))
SC_GENERATED_BODY()
};
Реализация этого агента заключалась в реализации одной функции:
SC_AGENT_ACTION_IMPLEMENTATION(AMyAgent)
{
// implement agent logic there
return SC_RESULT_OK;
}
Более детально о реализации агентов можно почитать в документации. Примеры реализации агентов с новой библиотекой можно посмотреть тут (они временно переехали в мой закрытый репозиторий, но скоро они снова будут открыты).
Второе что необходимо было сделать — это многопоточный запуск агентов. Ранее для обработки событий в БЗ использовался один поток, который последовательно запускал агенты из очереди. Сейчас используются все доступные ядра в системе. Сразу скажу, что было очень сложно обеспечить асинхронный доступ к БЗ от множества агентов в разных потоках. Описание этого механизма наверное — это отдельная статья, которую я могу написать, если возникнет такая потребность. Тут я остановлюсь лишь на паре нюансов:
- работа с элементами, которые хранятся в памяти реализована с помощью lock-free подхода. Я не силен в классификации таких подходов, но думаю что у меня реализация «Без блокировок», когда хотя бы один агент (чаще их больше одного или почти все) продвигается вперед;
- на текущий момент, агенты не могут гарантировать, что данные с которыми они собираются работать не будут удалены другим агентом. В будущем планируется ввести блокировки, которые будут заставлять хранить объект до тех пор пока он необходим агенту и в нужном контексте (пока идет обсуждение необходимых видов блокировок). Для текущей реализации агентов это не страшно, так как если элемент был удален, то агент вернет ошибку при попытке сделать что-то в памяти с этим элементом.
Третье на что я натолкнулся, что большинство сервисов Amazon, Google и т.д. имеют готовые API на разных языках, но не для С++. Поэтому было решено сделать возможность запускать Python код внутри С++. Реализовано это все с помощью Boost-Python (используя Python 3, вот тут я уже могу много кому помочь с этим).
Помимо описанных вещей была написана документация (ссылки давались уже). Вся библиотека с++ покрыта unit-тестами. На все это ушло около года и все это делалось параллельно с остальными вещами и делается до сих пор.
Визуальная модель
Визуальная модель разрабатывалась для отработки концепции и отладки базовых вещей. Видео небольшого Proof of concept в этой модели:
Эта модель помогла отладить систему очень здорово, так как ее можно было собрать и дать поиграться другим людям. Вдаваться в подробности ее реализации не буду, если кому будет интересно — отвечу в комментариях. На текущий момент данная штука не развивается, все перешло к реальным устройствам.
Голосовой интерфейс
В раннем прототипе я реализовал голосовой ввод с помощью Android API, а получаемый текст разбирал с помощью api.ai, который возвращал мне класс запроса для инициирования и его параметры. В таком или немного измененном виде он существует и сейчас. Но разговор мы поведем в другом русле — как этот механизм реализован с помощью агентов.
Если вспомнить про сравнение общения между агентами с форумом, то можно описать этот механизм следующим логом (где A<имя_агента> — это агенты, решающие разные задачи; user — это пользователь):
user: яблоко это фрукт
ADialogueProcessMessage: сделайте анализ входного текста "яблоко это фрукт"
AApiAiParseUserTextAgent: что за объект "яблоко"?
AResolveAddr: адрес элемента "яблоко" - 3452
AApiAiParseUserTextAgent: что за объект "фрукт"?
AResolveAddr: адрес элемента "фрукт" - 3443
AApiAiParseUserTextAgent: нужно добавить элемент 3452 во множество 3443
AAddIntoSet: сделано
ADialogueProcessMessageAgent: сгенерируйте текст на языке пользователя для указанного запроса
AGenCmdTextResult: сгенерируйте текст на русском языке по шаблону "..."
AGenText: готово - "Теперь буду знать"
Вот что происходит под капотом, при решении такой простой задачи. Но это еще не все, далее нам необходимо ввести еще два понятия. Кроме агентов, которые я уже описал (реагируют на изменение БЗ и меняют ее состояние), есть еще два вида агентов:
- эффекторные — это агенты, которые на изменение в БЗ делают изменение во внешней среде. Другими словами отвечают за вывод информации: экран, манипулятор и т. д.
- рецепторные — это агенты, которые на изменения во внешней среде, делают изменения в БЗ. Отвечают за ввод информации: устройства ввода, датчики и т. д.
На видео можно увидеть, как лампа (куб) включается, когда узел, обозначающий её, добавляют во множество включенных устройств. И наоборот, когда её оттуда удаляют, то она выключается. Тоже самое происходит и с краном.
Когда агенты разобрали запрос пользователя и сформировали на него ответ на том же языке, то этот ответ добавляется в множество обозначающее диалог с ним (пользователем). В этот момент один из «эффекторных» агентов выводит этот ответ пользователю в виде строки. Вот пример работы на видео:
Отдельно, пользовательский интерфейс делает запрос на генерацию речи по тексту. Агент формирует ее в виде звукового файла (OGG) используя сервис ivona.
Что дальше?
Прошлый год был проведен за отладкой и новым функционалом ядра. Получен работающий прототип, который уже может решать интересные задачи. За прошлый год на разработку было потрачено около 750 часов свободного от основной работы времени.
У данного подхода есть потенциал:
- добавление нового функционала не затрагивает уже работающий. К примеру, когда мне понадобилось сделать планировщик задач (для запуска задач в определенное время или периодически), то для этого понадобился лишь 1 агент, который всего лишь добавляет уже сформированный запрос во множество инициированных в указанное время. Получается что любой запрос, который доступен на языке запросов может быть запланирован. Например: "«напомни позвонить жене»"; «выключи свет в 10»; «включи телевизор в 11.05»; ...;
- в данной статье я не описывал, но существует поиск по шаблонам, на базе которого можно делать логический вывод (и уже делали тестовые решатели задач по геометрии и физике);
- в качестве анализатора входного текста планируется использовать Cloud Natural Language API, что позволит увеличить качестве языкового интерфейса;
На этом наверное я закончу свою первую статью, в которой постарался описать основные принципы и что было сделано по моей реализации «умного дома», «умного дворецкого» или «Джарвиса» (кому как удобно). Не питаю иллюзий, что материал получился хорошим, поэтому надеюсь на обратную связь. Спасибо всем прочитавшим.
Автор: DenisKoronchik