ML: Чатбот RASA: Генератор историй


Граф диалога

Вместо историй мы пишем кейсы, каждый из которых имеет уникальное имя. Затем скрипт !core_stories_gen.py преобразует файл с кейсами в stories.yml. При помощи кейсов можно создавать любые ациклические графы диалога в достаточно компактном виде. Кейсы имеют схожий со stories в RASA синтаксис. Если в stories.yml заменить раздел "stories:" на "cases:", а каждое "- story:" на уникальное имя case_name: (без тире), то такой файл имеет верный синтаксис для !core_stories_gen.py (steps должен быть с отступом или отсутствовать).


Ветвление next

Новым, по сравнению со stories, является раздел next, который выполняет роль ветвления or, но содержит в качестве веток куски историй и другие кейсы. Если в кейсе нет дополнительных свойств типа name, story (см. ниже), то раздел steps можно опустить:

cases:                                      # раздел кейсов

 case_menu_main:                            # Главное меню
   name: Главное меню
   steps: 
   - intent: want_something                 # 🙎 что у вас есть?
   - action: utter_menu_main                # 💻 Вы можете выбрать напитки или пиццу
   - next:                                  # далее вероятны три ветки диалога:      
      - case_menu_drink:                    # 🙎 Я хочу напиток ...
      - case_menu_pizza:                    # 🙎 Я хочу пиццу   ...     
      - case_order:                         # 🙎 Хочу узнать что я заказал ...
   - action: utter_check_order              # 💻 некоторое продолжение

   
 case_pizza_menu:                           # Меню пиццы
   - intent: want_pizza                     # 🙎 Я хочу пиццу
   - action: utter_menu_pizza               # 💻 По названию или составу?
   - next:
      - case_menu_pizza_name:               # 🙎 по названию
      - case_menu_pizza_madeof:             # 🙎 по составу

Если в не прописанных кейсах нет больше ветвлений, то по этим двум кейсам будет получено четыре истории. Справа приведен граф подобного диалога. Точками обозначены прописанные события (intent - синий, action - зелёный),
а прямоугольники - это ещё не определённые кейсы. Если в них также есть ветвления, то тогда получится больше историй.

В силу того, что в кейсе case_menu_main после блока ветвления next идут события (utter_check_order), все ветки такого диалога сходятся в одну точку. Если бы после блока next ничего не было, то получился бы граф истории в виде дерева.


Безымянные кейсы

Кейс можно описать в разделе case, не вводя для него уникального имени:
      
 case_menu_drink:                           # Меню напитков   
   - intent: want_drink                     # 🙎 Я хочу напиток
   - action: utter_menu_drink               # 💻 У нас есть вода, кола, фанта и соки
   
   - next:                                  # далее две возможности
      - case:
         - intent: want_juice               # 🙎 сок  
         - action: utter_menu_juice         # 💻 У нас есть ананасовый, манго,...
      - case:                               # пустой (но ветка есть)
         
   - next:   
      - case_want_item:                     # 🙎 колу хочу (без количества)
      - case:                               
         - intent: want_item                # 🙎 две колы хочу (с количеством)
           entities: [NUMBER, ITEM]           
   - action: action_add_to_order            # 💻 добавил в заказ пять спрайтов

Вставки без ветвления

При помощи события insert можно в данном месте вставить некоторые кейс:
      
 case_name:
   - intent: intent_A
   ...
   - insert: case_some_name                  # <--- вставится кейс без параметров
   ...
   - insert:   
      - case_some_name: ПИЦЦА                # <--- вставится кейс с параметрами (см. ниже)
   ...
Даже если ветвления не используются, при помощи кейсов и события insert, можно упорядочить истории, выделев часто повторяющиеся куски в отдельные кейсы.

Аргументы кейса

Кейсам можно передавать список управляющих строк, подобно арагументам в функции. Например, пусть кейс case_want_item должен выдавать различные куски истории в зависимости от того, откуда его вызвали (из меню пицц, меню напитков и т.п.)
      
 case_drink_menu:                          # меню напитков
   - intent: want_drink                    # 🙎 Я хочу напитки
   - action: utter_drink_menu              # 💻 У нас есть вода, кола, фанта и соки.
   - next:                                 
         - case_want_item: НАПИТОК         # 🙎 Я хочу колу 
         - case_juice_menu:                # 🙎 Я хочу сок
Проверка управляющих параметров, передаваемых в кейс, производится в разделе if:
      
 case_want_item:                           # выбрал товар и возможно количество
   - intent: some
   ...
   - if:   НАПИТОК
     then: 
      - action: utter_drink_count          # 💻 Сколько бутылок {ITEM}? 
   - if:   ПИЦЦА                           
     then: 
      - action: utter_pizza_count          # 💻 Сколько штук пиццы {ITEM} Вы хотите?
   - if:                                   # вызвали кейс без аргументов
     then: 
      - action: utter_item_count           # 💻 Сколько {ITEM} Вы хотите заказать?      
Кейс принимает одно слово или список слов. Все эти слова должны находиться в списке управляющих слов, поступающих на вход кейса. Если if пустой, то он сработает только, если входной список также пустой (выше последний if). Желательно списки слов не использовать, так как это может привести к потере контроля за логикой (будут срабатывать несколько if-ов).

Свойства кейса

По умолчанию, кейсы (после вставок в них других кейсов), становятся историями для обучения диалогам в RASA.
При помощи свойства count: 0 можно запретить кейсу быть самостоятельной историей:
      
 case_want_item:                            # товар без количества
   name:  Хочу товар                        # имя кейса для имени истории
   count: 0                                 # кейс не будет самостоятельной историей
   prob:  0.8                               # может участвовать в случайных вставках random
   steps:
   - intent: want_item                      # 🙎 спрайт | апельсиновый сок  
     entities:  
      - NUMBER: null              
      - ITEM   

Важно помнить, что кейс не оканчивающийся действием, обычно, не может быть самостоятельной историей. В общем случае count задаёт число повторов истории в обучающей выборке (по умолчанию count: 1). Если при большом числе историй, RASA плохо учится каким-то важным историям, можно увеличить их количество.

По умолчанию, имена сгенеренных историй формируются из имён участвующих в них кейсов. Если необходимо, что-бы подобные имена были более читаемыми, нужным текстом заполняется поле name.

Ещё одно свойство prob: 1.0 задаёт вероятность (от 0 до 1) с которой этот кейс может быть вставлен в событии random (по умолчанию prob: 0.0). Параметры prob (если они есть) всех кейсов складываются и делятся на их количество (нормируются так, чтобы их сумма была равна единице). Если все случайные кейсы равновероятны, можно указать для них prob: 1.0


Остановка генерации

В списки событий можно также вставлять оператор остановки - stop:. Он блокирует дальнейшее построение истории.

   
  case_some_name:
    - intent: x
    - action: a
    - next:
        - case:
            - intent: y
            - action: b
            - stop:
         - case:
            - intent: z
    - action: c            


Случайные вставки

Свойство кейса random позволяет с заданной вероятностью вставить в данное место истории случайный кейс из списка кейсов, имеющих свойство prob:
      
 case_some_name:   
   - intent: intent_A
   - action: action_B
   - random: 0.8                            # в этом месте случайная вставка
   - random: 0.9                            # и ещё одна с вероятностью 0.9
   - intent: intent_C

Пример: генерация случайных вставок

Пусть есть три последовательности: (i1,a1,i4,a5); (i2,a2,i4,a6); (i3,a3,i4,a7), в которых, в зависимости от первых двух шагов, на намерение i4 бот должен отвечать различным образом. Чтобы усложнить сети жизнь, перед i4 будем вставлять одну или две случайные вставки из двух шагов, имитируя стековую структуру диалога. Кейсы базовых историй имеют вид:

      
cases:
  case_i1:                  # создаст 30 таких историй
    count: 30
    steps:
    - intent: i1
    - action: utter_1
    - random: 0.9
    - random: 0.9
    - intent: i4            # i1,a1,i4 -> a5
    - action: utter_5
    
  case_i2:
    count: 30
    steps:
    - intent: i2
    - action: utter_2
    - random: 0.9
    - random: 0.9
    - intent: i4            # i1,a1,i4 -> a5
    - action: utter_6
    
  case_i3:
    count: 30
    steps:
    - intent: i3
    - action: utter_3
    - random: 0.9
    - random: 0.9
    - intent: i4            # i1,a1,i4 -> a5
    - action: utter_7
Дополнительно необходимо создать достаточное число историй которые будут служить вставками:
      
  case_rnd1:
    count: 0
    prob:  1.0
    steps:
    - intent: i5
    - action: utter_8
    
  case_rnd2:
    count: 0
    prob:  1.0
    steps:
    - intent: i6
    - action: utter_5
...
В результате запуска !core_stories_gen.py получится 90 историй различной длины. RASA вполне справляется выявлением требуемой закономерности (даже если увеличить число событий random). Проверить это можно запуситив генератор снова и полученный файл пропустив через !agent_test.py, обращая внимание на last error (на незнакомых случайных вставках RASA, конечно, может ошибаться).

Структура диалога

Пример диалогового стека (dialogue stack):

💻 С вас $15. Могу ли я списать средства с вашей карты?               -| 
🙎 Остался ли на моем счету кредит от полученного возмещения?   -|
💻 Да, на вашем счету 10 долларов на счете.                      
🙎 Отлично.                                                     -|
💻 Могу я разместить заказ?                                        
🙎 Да.                                                             
💻 Готово. Завтра у тебя должны быть свои вещи.                       -|
Transformers