ML: Чатбот RASA: Слоты и формы
Введение
Любой приличный бот должен обладать памятью. В RASA функцию памяти, по-мимо историй, выполняют слоты (slots). Они могут заполняться автоматически (при извлечении из намерения сущностей), в формах или действиях. Слоты имеют различные типы, а их совокупность называется формой (form). Например, данные о человеке (имя, возраст, пол) можно объединить в форму. Форма - это активный объект, который после запуска, стремится сам заполнить все свои слоты, настойчиво задавая человеку соответствующие вопросы.
Типы слотов
Возможные следующие типы слотов:
- text - произвольный текст (его содержание для историй роли не играет);
- bool - сохраняет одно из двух значений: true, false, которые могут влиять на истории;
- float - вещественное число; для него можно определить диапазон min_value, max_value;
- categorical - одно из предопределённых значений (также могут влиять на истории);
- list - список величин (массив);
- any - величина любого типа, который можно передать в JSON (словарь, список и т.п.)
slots: # domain.yml TEMPERATURE: type: float min_value: -100.0 # минимальное значение max_value: 100.0 # максимальное значение initial_value: 0 # начальное значение RISK: type: categorical values: # может принимать следующие значения: - low - medium - high SHOPPING_ITEMS: type: any influence_conversation: false # не влияет на логику диалогов
Если у слота установлено свойство "influence_conversation: true" (по умолчанию), то такие слоты могут влиять на поток диалога в историях (при помощи разделов slot_was_set). Если же значение этого свойства false, то слоты только хранят информацию и не влияют на диалоги. При желании можно также создавать кастомизированные типы слотов.
Существует мнение, что для слотов, которые влияют на истории (influence_conversation: true), не стоит устанавливать начальное значение (initial_value). RASA при обучении видит установленное всюду значение слота, и думает, что это "нормальный" случай, который должен быть всегда, что искажает обучение.
Важно не забывать, что slot_was_set в историях и правилах считает одинаковыми текстовые слоты с различными значениями. Если нужны подобные конкретизации, следует использовать категориальные слоты.
Заполнение слотов
По умолчанию, если имя слота (slot) и имя сущности (entity) совпадают, то слот заполняется значением при извлечении из текста одноименной сущности. Так, пусть в domain.yml определено:
entities: # domain.yml - PERSON slots: PERSON: type: textВ data/nlu.yml приведём примеры для намерения пользователя сообщить своё имя:
- intent: my_name # data/nlu.yml examples: | - меня зовут [Настя](PERSON) - меня зовут [Анастасия](PERSON) - меня зовут [Тимур](PERSON) - [Маша](PERSON)
При распознании этого намерения, будет извлечена сущность PERSON и помещена в одноименный слот PERSON, который можно использовать, например, в откликах:
responses: # domain.yml utter_glad_to_meet_you: - text: "Рад с тобой познакомиться {PERSON}."Пример такого бота находится в проекте Bot02_PERSON.zip.
Для сложных ботов, одна и та-же сущность (entity)
может встречаться в различных намерениях (intent).
Поэтому автоматическое отождествление имени
config: # domain.yml
store_entities_as_slots: false # ни одна сущность не свяжется со слотом
Если же отождествление необходимо запретить только для одного слота, это делается в списке слотов
(свойство auto_fill: false):
slots: # domain.yml
ORDER: # имя слота
type: text # тип слота
auto_fill: false # автоматически не заполняется
Формы
Сущности и слоты с различными названиями (или свойством auto_fill: false) можно связать при помощи форм. Их размещают в файле domain.yml. Имя формы одновременно является именем действия, которое можно использовать в историях (story) или правилах (rule). Когда форма с несколькими слотами активирована, RASA начинает повторять вопросы, пока все слоты формы не будут заполнены. Как и для правил, чтобы формы в циклах работали, в "config.yml" надо указать:
policies: # config.yml - name: RulePolicy
Пусть есть форма с именем form_person_info и двумя слотами (именем и возрастом):
forms: form_person_info: # имя формы required_slots: # для полного заполнения требуются такие слоты: NAME: # имя слота - type: from_entity # заполняется из сущности, entity: PERSON # которая имеет имя PERSON AGE: # имя слота - type: from_entity # заполняется из сущности, entity: NUMBER # которая имеет имя NUMBER not_intent: # где игнорировать заполнение слота - number_of_itemsДля формирования соответствующих вопросов необходимо в domain.yml определить отклики бота. Они начинаются с префикса utter_ask_... Далее идёт _<имя формы>_<имя слота> (имя формы можно опустить). В нашем случае это могут быть отклики: utter_ask_form_person_info_NAME и utter_ask_form_person_info_AGE. В раздел entities, как обычно, добавляем PERSON, NUMBER, а в data/nlu.yml прописываем достаточное число примеров для этих сущностей.
Активация формы
Чтобы форма запустилась, в историях напишем пример диалога в котором, при активации формы, бот "проваливается" в заполнение её слотов:stories: # data/stories.yml - story: 1. привет, выяснение имени и возраста с удачным прохождением steps: - intent: greet # 🙎 привет - action: utter_greet # 💻 Рад тебе - action: form_person_info # активируем форму - active_loop: form_person_info # идёт событие Loop(form_person_info) - active_loop: null # форма заполнена, нет активных форм - slot_was_set: # были определены все слоты - NAME - AGE - action: utter_you_NAME_AGE # 💻 Тебя зовут Настя и тебе 16 лет (в конце)
Разберём теперь ход истории.
- - action: form_person_info является действием, аналогичным любому utter-у. Но при этом выводится не один отклик, а запускается петля, в которой бот будет пытаться выяснить значение слотов, задавая вопросы и ожидая получить из ответов сущности, заполняющие слоты.
- - active_loop: form_person_info - ни чего не делает, а лишь сообщает, что в этой точке форма всё ещё активна (аналогично работает slot_was_set в историях).
- - active_loop: null информирует, что активных форм, находящихся в цикле, больше нет.
Полезно проанализировать события, возникающие при заполнении этой формы (команда "=e" в !agent.py):
- intent: greet [1.00] привет - action: utter_greet < [1.00] policy_0_MemoizationPolicy Как я рад тебя видеть! - action: form_person_info < [1.00] policy_0_MemoizationPolicy - action: Loop(form_person_info) - action: SlotSet(key: requested_slot, value: NAME) Как тебя зовут? - intent: my_name [1.00] Маша - PERSON [1.00] pos:[ 0, 4] value: Маша - action: form_person_info < [1.00] policy_1_RulePolicy - action: SlotSet(key: NAME, value: Маша) - action: SlotSet(key: requested_slot, value: AGE) Сколько тебе лет? - intent: my_age [1.00] 16 - NUMBER [1.00] pos:[ 0, 2] value: 16 - action: form_person_info < [1.00] policy_1_RulePolicy - action: SlotSet(key: AGE, value: 16) - action: SlotSet(key: requested_slot, value: None) - action: Loop(None) - action: utter_you_NAME_AGE < [0.90] policy_2_TEDPolicy Тебя зовут Маша и тебе 16 лет.В принципе, форма запустится и корректно отработает, если в истории убрать событие active_loop: form_person_info. Он в любом случае будет активировано RASA. Однако, наличие его в обучающей последовательности полезно. Обратим внимание на политику policy_1_RulePolicy которая применяется при активации формы. Аналогично другим правилам (data/rules.yml), запуск формы не "портит" истории, что-бы не происходило внутри заполнения формы (это как бы отдельная процедура).
Несчастливые пути
Иногда пользователь при заполнении формы начинает нести бред или выявляет желание прервать заполнение слотов, например, таким намерением:
nlu: # data/nlu.yml - intent: irritation examples: | - что ты достал со своими вопросами - отвали - отстаньДобавим истории, которые обрабатывают эту ситуацию. Первая история при появления намерения irritation переспрашивает, и получив намерение продолжить, продолжает заполнение слотов:
- story: 2. привет, выяснение имени и возраста с прерыванием steps: - intent: greet # 🙎 привет - action: utter_greet # 💻 Рад тебе - action: form_person_info # активируем форму и запускаем цикл - active_loop: form_person_info # форма сейчас активна (заполняется) - intent: irritation # 🙎 отвали - action: utter_not_understand # 💻 Не понял тебя. Продолжим? - intent: affirm # 🙎 да - action: form_person_info # активируем форму - active_loop: form_person_info # форма сейчас активна (заполняется) - active_loop: null # форма заполнена, нет активных форм - slot_was_set: # были определены все слоты - NAME - AGE - action: utter_you_NAME_AGE # 💻 Тебя зовут Настя и тебе 16 лет (в конце)Вторая история прекращает петлю заполнения формы вызовом действия action_deactivate_loop:
- story: 3. привет, выяснение имени и возраста с прерыванием steps: - intent: greet # 🙎 привет - action: utter_greet # 💻 Рад тебе - action: form_person_info # активируем форму и запускаем цикл - active_loop: form_person_info # форма сейчас активна (заполняется) - intent: irritation # 🙎 отвали - action: utter_not_understand # 💻 Не понял тебя. Продолжим? - intent: deny # 🙎 нет - action: action_deactivate_loop # насильно прекращаем форму - active_loop: null - action: utter_goodbye # До скорой встречи
Можно также обрабатывать прерывания заполнения формы в правилах:
rules: - rule: Example of an unhappy path condition: - active_loop: restaurant_form # Условие - форма активан steps: - intent: chitchat # Несчастливое прерывание болтовней `chitchat`. - action: utter_chitchat - action: restaurant_form # Возвращаемся к заполнению формы - active_loop: restaurant_form
Тестирования формы без прерывания
Для тестирования заполнения формы в ситуации, когда человек "послушно" отвечает, в tests/test_stories.yml создадим следующий пример истории:stories: # tests/test_stories.yml - story: 1. привет, выяснение имени и возраста с удачным прохождением steps: - user: | # 🙎 привет привет intent: greet - action: utter_greet # 💻 Рад тебе - action: form_person_info # должна активироваться форма - active_loop: form_person_info # и войти в цикл - user: | # 🙎 Настя [Настя](PERSON) intent: my_name - action: form_person_info - active_loop: form_person_info # продолжить цикл - user: | # 🙎 16 [16](NUMBER) intent: my_age - action: form_person_info - active_loop: null # должна деактивироваться - action: utter_you_NAME_AGE # 💻 Тебя зовут Настя и тебе 16 лет (в конце)
Тестирования формы с прерыванием
Пусть человек начинает вести себя непослушно в самом начале диалога. Это соответствует следующему тесту:- story: 2. привет, выяснение имени и возраста с прерыванием в начале steps: - user: | # 🙎 привет привет intent: greet - action: utter_greet # 💻 Рад тебе - action: form_person_info # должна активироваться форма - active_loop: form_person_info # форма сейчас активна (заполняется) - user: | # 🙎 отвали отвали intent: irritation - action: utter_not_understand # 💻 Не понял тебя. Продолжим? - user: | # 🙎 да ну давай intent: affirm # 🙎 ну давай - action: form_person_info # должна активироваться форма - active_loop: form_person_info # и снова войти в цикл - user: | # 🙎 Настя [Настя](PERSON) intent: my_name - action: form_person_info - active_loop: form_person_info # продолжить цикл - user: | # 🙎 16 [16](NUMBER) intent: my_age - action: form_person_info - active_loop: null # должна деактивироваться - action: utter_you_NAME_AGE # 💻 Тебя зовут Настя и тебе 16 лет (в конце)Аналогично, человек мог попытаться прервать заполнение формы в её середине. Для этого напишем ещё один тест:
- story: 3. привет, выяснение имени и возраста с прерыванием в середине steps: - user: | # 🙎 привет привет intent: greet - action: utter_greet # 💻 Рад тебе - action: form_person_info # должна активироваться форма - active_loop: form_person_info # форма сейчас активна (заполняется) - user: | # 🙎 Настя [Настя](PERSON) intent: my_name - action: form_person_info - active_loop: form_person_info # продолжить цикл - user: | # 🙎 отвали отвали intent: irritation - action: utter_not_understand # 💻 Не понял тебя. Продолжим? - user: | # 🙎 да ну давай intent: affirm # 🙎 ну давай - action: form_person_info # должна активироваться форма - active_loop: form_person_info # и снова войти в цикл - user: | # 🙎 16 [16](NUMBER) intent: my_age - action: form_person_info - active_loop: null # должна деактивироваться - action: utter_you_NAME_AGE # 💻 Тебя зовут Настя и тебе 16 лет (в конце)