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 лет (в конце)