SystemyDialogowe-ProjektMag.../lab/09-zarzadzanie-dialogiem-reguly(zmodyfikowany).ipynb
2022-05-27 18:01:18 +02:00

46 KiB
Raw Blame History

Logo 1

Systemy Dialogowe

9. Zarządzanie dialogiem z wykorzystaniem reguł [laboratoria]

Marek Kubis (2021)

Logo 2

Zarządzanie dialogiem z wykorzystaniem reguł

Agent dialogowy wykorzystuje do zarządzanie dialogiem dwa moduły:

  • monitor stanu dialogu (dialogue state tracker, DST) — moduł odpowiedzialny za śledzenie stanu dialogu.

  • taktykę prowadzenia dialogu (dialogue policy) — moduł, który na podstawie stanu dialogu podejmuje decyzję o tym jaką akcję (akt systemu) agent ma podjąć w kolejnej turze.

Oba moduły mogą być realizowane zarówno z wykorzystaniem reguł jak i uczenia maszynowego. Mogą one zostać również połączone w pojedynczy moduł zwany wówczas _menedżerem dialogu.

Przykład

Zaimplementujemy regułowe moduły monitora stanu dialogu oraz taktyki dialogowej a następnie osadzimy je w środowisku _ConvLab-2, które służy do ewaluacji systemów dialogowych.

Uwaga: Niektóre moduły środowiska _ConvLab-2 nie są zgodne z najnowszymi wersjami Pythona, dlatego przed uruchomieniem poniższych przykładów należy się upewnić, że mają Państwo interpreter Pythona w wersji 3.7. W przypadku nowszych wersji Ubuntu Pythona 3.7 można zainstalować z repozytorium deadsnakes, wykonując polecenia przedstawione poniżej.

sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.7 python3.7-dev python3.7-venv

W przypadku innych systemów można skorzystać np. z narzędzia pyenv lub środowiska conda.

Ze względu na to, że _ConvLab-2 ma wiele zależności zachęcam również do skorzystania ze środowiska wirtualnego venv, w którym moduły zależne mogą zostać zainstalowane. W tym celu należy wykonać następujące polecenia

python3.7 -m venv convenv                     # utworzenie nowego środowiska o nazwie convenv
source convenv/bin/activate                   # aktywacja środowiska w bieżącej powłoce
pip install --ignore-installed jupyter        # instalacja jupytera w środowisku convenv

Po skonfigurowaniu środowiska można przystąpić do instalacji _ConvLab-2, korzystając z następujących poleceń

mkdir -p l08
cd l08
git clone https://github.com/thu-coai/ConvLab-2.git
cd ConvLab-2
pip install -e .
python -m spacy download en_core_web_sm
cd ../..

Po zakończeniu instalacji należy ponownie uruchomić notatnik w powłoce, w której aktywne jest środowisko wirtualne _convenv.

jupyter notebook 08-zarzadzanie-dialogiem-reguly.ipynb

Działanie zaimplementowanych modułów zilustrujemy, korzystając ze zbioru danych MultiWOZ (Budzianowski i in., 2018), który zawiera wypowiedzi dotyczące m.in. rezerwacji pokoi hotelowych, zamawiania biletów kolejowych oraz rezerwacji stolików w restauracji.

Monitor Stanu Dialogu

Do reprezentowania stanu dialogu użyjemy struktury danych wykorzystywanej w _ConvLab-2.

# from convlab2.util.multiwoz.state import default_state
from utils.state import default_state
default_state()
{'user_action': [],
 'system_action': [],
 'belief_state': {'cinema': {'book': {'title': '',
    'date': '',
    'time': '',
    'quantity': '',
    'seats': '',
    'area': '',
    'interval': ''},
   'semi': {'goal': ''}}},
 'request_state': {},
 'terminated': False,
 'history': []}

Metoda update naszego monitora stanu dialogu będzie przyjmować akty użytkownika i odpowiednio modyfikować stan dialogu. W przypadku aktów typu inform wartości slotów zostaną zapamiętane w słownikach odpowiadających poszczególnym dziedzinom pod kluczem belief_state. W przypadku aktów typu request sloty, o które pyta użytkownik zostaną zapisane pod kluczem request_state.

import json
import os
from convlab2.dst.dst import DST
from convlab2.dst.rule.multiwoz.dst_util import normalize_value
from convlab2.util.multiwoz.multiwoz_slot_trans import REF_SYS_DA

class SimpleRuleDST(DST):
    def __init__(self):
        DST.__init__(self)
        self.state = default_state()
        self.value_dict = json.load(open('utils/value_dict.json'))

    def update(self, user_act=None):
        for intent, domain, slot, value in user_act:
            domain = domain.lower()
            intent = intent.lower()
            value = value.lower()

            # if domain in ['unk', 'cinema']:
            #     continue
            k = slot

            if intent == 'inform':
                # k = REF_SYS_DA[domain.capitalize()].get(slot, slot)
                # if k is None:
                #     continue

                domain_dic = self.state['belief_state'][domain]

                if k in domain_dic['semi']:
                    # nvalue = normalize_value(self.value_dict, domain, k, value)
                    self.state['belief_state'][domain]['semi'][k] = value
                elif k in domain_dic['book']:
                    self.state['belief_state'][domain]['book'][k] = value
                elif k.lower() in domain_dic['book']:
                    self.state['belief_state'][domain]['book'][k.lower()] = value
            elif intent == 'request':
                # k = REF_SYS_DA[domain.capitalize()].get(slot, slot)

                if domain not in self.state['request_state']:
                    self.state['request_state'][domain] = {}
                if k not in self.state['request_state'][domain]:
                    self.state['request_state'][domain][k] = 0

        return self.state

    def init_session(self):
        self.state = default_state()

'''
class SimpleRuleDST(DST):
    def __init__(self):
        DST.__init__(self)
        self.state = default_state()
        self.value_dict = json.load(open('l08/ConvLab-2/data/multiwoz/value_dict.json'))

    def update(self, user_act=None):
        for intent, domain, slot, value in user_act:
            domain = domain.lower()
            intent = intent.lower()

            if domain in ['unk', 'general', 'booking']:
                continue

            if intent == 'inform':
                k = REF_SYS_DA[domain.capitalize()].get(slot, slot)

                if k is None:
                    continue

                domain_dic = self.state['belief_state'][domain]

                if k in domain_dic['semi']:
                    nvalue = normalize_value(self.value_dict, domain, k, value)
                    self.state['belief_state'][domain]['semi'][k] = nvalue
                elif k in domain_dic['book']:
                    self.state['belief_state'][domain]['book'][k] = value
                elif k.lower() in domain_dic['book']:
                    self.state['belief_state'][domain]['book'][k.lower()] = value
            elif intent == 'request':
                k = REF_SYS_DA[domain.capitalize()].get(slot, slot)

                if domain not in self.state['request_state']:
                    self.state['request_state'][domain] = {}
                if k not in self.state['request_state'][domain]:
                    self.state['request_state'][domain][k] = 0

        return self.state

    def init_session(self):
        self.state = default_state()
'''
"\nclass SimpleRuleDST(DST):\n    def __init__(self):\n        DST.__init__(self)\n        self.state = default_state()\n        self.value_dict = json.load(open('l08/ConvLab-2/data/multiwoz/value_dict.json'))\n\n    def update(self, user_act=None):\n        for intent, domain, slot, value in user_act:\n            domain = domain.lower()\n            intent = intent.lower()\n\n            if domain in ['unk', 'general', 'booking']:\n                continue\n\n            if intent == 'inform':\n                k = REF_SYS_DA[domain.capitalize()].get(slot, slot)\n\n                if k is None:\n                    continue\n\n                domain_dic = self.state['belief_state'][domain]\n\n                if k in domain_dic['semi']:\n                    nvalue = normalize_value(self.value_dict, domain, k, value)\n                    self.state['belief_state'][domain]['semi'][k] = nvalue\n                elif k in domain_dic['book']:\n                    self.state['belief_state'][domain]['book'][k] = value\n                elif k.lower() in domain_dic['book']:\n                    self.state['belief_state'][domain]['book'][k.lower()] = value\n            elif intent == 'request':\n                k = REF_SYS_DA[domain.capitalize()].get(slot, slot)\n\n                if domain not in self.state['request_state']:\n                    self.state['request_state'][domain] = {}\n                if k not in self.state['request_state'][domain]:\n                    self.state['request_state'][domain][k] = 0\n\n        return self.state\n\n    def init_session(self):\n        self.state = default_state()\n"

W definicji metody update zakładamy, że akty dialogowe przekazywane do monitora stanu dialogu z modułu NLU są czteroelementowymi listami złożonymi z:

  • nazwy aktu użytkownika,
  • nazwy dziedziny, której dotyczy wypowiedź,
  • nazwy slotu,
  • wartości slotu.

Zobaczmy na kilku prostych przykładach jak stan dialogu zmienia się pod wpływem przekazanych aktów użytkownika.

dst = SimpleRuleDST()
dst.state
{'user_action': [],
 'system_action': [],
 'belief_state': {'cinema': {'book': {'title': '',
    'date': '',
    'time': '',
    'quantity': '',
    'seats': '',
    'area': '',
    'interval': ''},
   'semi': {'goal': ''}}},
 'request_state': {},
 'terminated': False,
 'history': []}
dst.update([['Inform', 'Cinema', 'time', '15:00'], ['Inform', 'Cinema', 'title', 'Batman']])
dst.state['belief_state']['cinema']
{'book': {'title': 'batman',
  'date': '',
  'time': '15:00',
  'quantity': '',
  'seats': '',
  'area': '',
  'interval': ''},
 'semi': {'goal': ''}}
dst.update([['Inform', 'Cinema', 'area', 'na górze na środku']])
dst.state['belief_state']['cinema']
{'book': {'title': 'batman',
  'date': '',
  'time': '15:00',
  'quantity': '',
  'seats': '',
  'area': 'na górze na środku',
  'interval': ''},
 'semi': {'goal': ''}}
dst.update([['Request', 'Cinema', 'date', '?']])
dst.state['request_state']
{'cinema': {'date': 0}}
dst.update([['Inform', 'Hotel', 'Day', 'tuesday'], ['Inform', 'Hotel', 'People', '2'], ['Inform', 'Hotel', 'Stay', '4']])
dst.state['belief_state']['hotel']
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
dst.state
{'user_action': [],
 'system_action': [],
 'belief_state': {'cinema': {'book': {'title': 'batman',
    'date': '',
    'time': '15:00',
    'quantity': '',
    'seats': '',
    'area': 'na górze na środku',
    'interval': ''},
   'semi': {'goal': ''}}},
 'request_state': {'cinema': {'date': 0}},
 'terminated': False,
 'history': []}

Taktyka Prowadzenia Dialogu

Prosta taktyka prowadzenia dialogu dla systemu rezerwacji pokoi hotelowych może składać się z następujących reguł:

  1. Jeżeli użytkownik przekazał w ostatniej turze akt typu Request, to udziel odpowiedzi na jego pytanie.

  2. Jeżeli użytkownik przekazał w ostatniej turze akt typu Inform, to zaproponuj mu hotel spełniający zdefiniowane przez niego kryteria.

  3. Jeżeli użytkownik przekazał w ostatniej turze akt typu Inform zawierający szczegóły rezerwacji, to zarezerwuj pokój.

Metoda predict taktyki SimpleRulePolicy realizuje reguły przedstawione powyżej.

from collections import defaultdict
import copy
import json
from copy import deepcopy

from convlab2.policy.policy import Policy
from convlab2.util.multiwoz.dbquery import Database
from convlab2.util.multiwoz.multiwoz_slot_trans import REF_SYS_DA, REF_USR_DA


class SimpleRulePolicy(Policy):
    def __init__(self):
        Policy.__init__(self)
        self.db = Database()

    def predict(self, state):
        self.results = []
        system_action = defaultdict(list)
        user_action = defaultdict(list)

        for intent, domain, slot, value in state['user_action']:
            user_action[(domain, intent)].append((slot, value))

        for user_act in user_action:
            self.update_system_action(user_act, user_action, state, system_action)

        # Reguła 3
        if any(True for slots in user_action.values() for (slot, _) in slots if slot in ['Stay', 'Day', 'People']):
            if self.results:
                system_action = {('Booking', 'Book'): [["Ref", self.results[0].get('Ref', 'N/A')]]}

        system_acts = [[intent, domain, slot, value] for (domain, intent), slots in system_action.items() for slot, value in slots]
        state['system_action'] = system_acts
        return system_acts

    def update_system_action(self, user_act, user_action, state, system_action):
        domain, intent = user_act
        constraints = [(slot, value) for slot, value in state['belief_state'][domain.lower()]['semi'].items() if value != '']
        self.results = deepcopy(self.db.query(domain.lower(), constraints))

        # Reguła 1
        if intent == 'Request':
            if len(self.results) == 0:
                system_action[(domain, 'NoOffer')] = []
            else:
                for slot in user_action[user_act]:
                    kb_slot_name = REF_SYS_DA[domain].get(slot[0], slot[0])

                    if kb_slot_name in self.results[0]:
                        system_action[(domain, 'Inform')].append([slot[0], self.results[0].get(kb_slot_name, 'unknown')])

        # Reguła 2
        elif intent == 'Inform':
            if len(self.results) == 0:
                system_action[(domain, 'NoOffer')] = []
            else:
                system_action[(domain, 'Inform')].append(['Choice', str(len(self.results))])
                choice = self.results[0]

                if domain in ["Hotel", "Attraction", "Police", "Restaurant"]:
                    system_action[(domain, 'Recommend')].append(['Name', choice['name']])
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'

Podobnie jak w przypadku aktów użytkownika akty systemowe przekazywane do modułu NLG są czteroelementowymi listami złożonymi z:

  • nazwy aktu systemowe,
  • nazwy dziedziny, której dotyczy wypowiedź,
  • nazwy slotu,
  • wartości slotu.

Sprawdźmy jakie akty systemowe zwraca taktyka SimpleRulePolicy w odpowiedzi na zmieniający się stan dialogu.

from convlab2.dialog_agent import PipelineAgent
dst.init_session()
policy = SimpleRulePolicy()
agent = PipelineAgent(nlu=None, dst=dst, policy=policy, nlg=None, name='sys')
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
agent.response([['Inform', 'Hotel', 'Price', 'cheap'], ['Inform', 'Hotel', 'Parking', 'yes']])
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
agent.response([['Inform', 'Hotel', 'Area', 'north']])
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
agent.response([['Request', 'Hotel', 'Area', '?']])
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
agent.response([['Inform', 'Hotel', 'Day', 'tuesday'], ['Inform', 'Hotel', 'People', '2'], ['Inform', 'Hotel', 'Stay', '4']])
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'

Testy End-to-End

Na koniec przeprowadźmy dialog łącząc w potok nasze moduły z modułami NLU i NLG dostępnymi dla MultiWOZ w środowisku ConvLab-2.

from convlab2.nlu.svm.multiwoz import SVMNLU
from convlab2.nlg.template.multiwoz import TemplateNLG

nlu = SVMNLU()
nlg = TemplateNLG(is_user=False)
agent = PipelineAgent(nlu=nlu, dst=dst, policy=policy, nlg=nlg, name='sys')
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
agent.response("I need a cheap hotel with free parking .")
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
agent.response("Where it is located ?")
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
agent.response("I would prefer the hotel be in the north part of town .")
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
agent.response("Yeah , could you book me a room for 2 people for 4 nights starting Tuesday ?")
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'
Running cells with 'Python 3.6.15 ('sysdial')' requires ipykernel package.

Run the following command to install 'ipykernel' into the Python environment. 

Command: 'conda install -n sysdial ipykernel --update-deps --force-reinstall'

Zauważmy, ze nasza prosta taktyka dialogowa zawiera wiele luk, do których należą m.in.:

  1. Niezdolność do udzielenia odpowiedzi na przywitanie, prośbę o pomoc lub restart.

  2. Brak reguł dopytujących użytkownika o szczegóły niezbędne do dokonania rezerwacji takie, jak długość pobytu czy liczba osób.

Bardziej zaawansowane moduły zarządzania dialogiem zbudowane z wykorzystaniem reguł można znaleźć w środowisku ConvLab-2. Należą do nich m.in. monitor RuleDST oraz taktyka RuleBasedMultiwozBot.

Zadania

  1. Zaimplementować w projekcie monitor stanu dialogu.

  2. Zaimplementować w projekcie taktykę prowadzenia dialogu.

Termin: 24.05.2021, godz. 23:59.

Literatura

  1. Pawel Budzianowski, Tsung-Hsien Wen, Bo-Hsiang Tseng, Iñigo Casanueva, Stefan Ultes, Osman Ramadan, Milica Gasic, MultiWOZ - A Large-Scale Multi-Domain Wizard-of-Oz Dataset for Task-Oriented Dialogue Modelling. EMNLP 2018, pp. 5016-5026
  2. Cathy Pearl, Basic principles for designing voice user interfaces, https://www.oreilly.com/content/basic-principles-for-designing-voice-user-interfaces/ data dostępu: 21 marca 2021
  3. Cathy Pearl, Designing Voice User Interfaces, Excerpts from Chapter 5: Advanced Voice User Interface Design, https://www.uxmatters.com/mt/archives/2018/01/designing-voice-user-interfaces.php data dostępu: 21 marca 2021