Select Git revision
unified_datasets_util.py
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
SimulatedUsersManager.py 15.29 KiB
###############################################################################
# PyDial: Multi-domain Statistical Spoken Dialogue System Software
###############################################################################
#
# Copyright 2015 - 2019
# Cambridge University Engineering Department Dialogue Systems Group
#
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
###############################################################################
'''
SimulatedUsersManager.py - combines simulated components into functional simulator
==================================================================================
Copyright CUED Dialogue Systems Group 2015 - 2017
.. seealso:: CUED Imports/Dependencies:
import :mod:`usersimulator.UserModel` |.|
import :mod:`usersimulator.ErrorSimulator` |.|
import :mod:`ontology.Ontology` |.|
import :mod:`ontology.OntologyUtils` |.|
import :mod:`utils.Settings` |.|
import :mod:`utils.DiaAct` |.|
import :mod:`utils.ContextLogger`
************************
'''
__author__ = "cued_dialogue_systems_group"
from usersimulator import UserModel
from usersimulator import ErrorModel
from utils import DiaAct, Settings, ContextLogger
from ontology import Ontology, OntologyUtils
logger = ContextLogger.getLogger('')
class DomainsSimulatedUser(object):
'''User Simulator for a single domain. Comprised of a behaviour component: :class:`UserModel` to produce a semantic act
and an error simulator to produce from the semantic act a list of semantic hypotheses.
:param str: domain string
'''
def __init__(self, domainString, error_rate):
'''
'''
self.um = UserModel.UM(domainString)
self.error_simulator = ErrorModel.DomainsErrorSimulator(domainString, error_rate)
self.randomLearning = False
if Settings.config.has_option("mogp_"+domainString, "randomweightlearning"):
self.randomLearning = Settings.config.getboolean("mogp_"+domainString, "randomweightlearning")
if self.randomLearning:
import policy.morl.WeightGenerator as wg
self.weightGen = wg.WeightGenerator(domainString)
def restart(self, otherDomainsConstraints):
'''Resets all components (**User Model**) that are statefull.
:param otherDomainsConstraints: of domain goal tuples (slot=val)
:type otherDomainsConstraints: list
:returns: None
'''
self.um.init(otherDomainsConstraints)
if self.randomLearning:
self.weightGen.updateWeights()
def act_on(self, sys_act_string):
'''Thru the UserModel member, receives the system action and then responds.
:param sys_act_string: system action
:type sys_act_string: unicode str
:returns: (str) user action
'''
sys_act = DiaAct.DiaAct(sys_act_string)
self.um.receive(sys_act)
user_act = self.um.respond()
return user_act
class SimulatedUsersManager(object):
"""
The multidomain simulated user, which is made up of a dictionary of simulated users indexed by domain.
:param (list): of domain strings
"""
def __init__(self, error_rate):
self.possible_domains = Ontology.global_ontology.possible_domains
if Settings.config.has_option("GENERAL", "testdomains"):
self.possible_domains = Settings.config.get("GENERAL", "testdomains").split(',')
if Settings.config.has_option("GENERAL", "traindomains"):
self.possible_domains = Settings.config.get("GENERAL", "traindomains").split(',')
self.error_rate = error_rate
logger.info('Simulating with error rate: ' + str(error_rate))
self.simUserManagers = dict.fromkeys(OntologyUtils.available_domains, None)
# DEFAULTS:
self.MIN_DOMAINS_PER_DIALOG = 1
self.MAX_DOMAINS_PER_DIALOG = 3
self.INCLUDE_DOMAIN_PROB = 0.6
self.CONDITIONAL_BEHAVIOUR = False
self.forceNullPositive = False
self.traceDialog = 2
self.temp_domains = []
# CONFIG OPTIONS:
if Settings.config.has_option("simulate", "forcenullpositive"):
self.forceNullPositive = Settings.config.getboolean("simulate", "forcenullpositive")
if Settings.config.has_option("simulate","includedomainprob"):
self.INCLUDE_DOMAIN_PROB = Settings.config.getfloat("simulate","includedomainprob")
assert(self.INCLUDE_DOMAIN_PROB <= 1.0 and self.INCLUDE_DOMAIN_PROB > 0)
if Settings.config.has_option("simulate","maxdomainsperdialog"):
self.MAX_DOMAINS_PER_DIALOG = Settings.config.getint("simulate","maxdomainsperdialog")
if Settings.config.has_option("simulate","mindomainsperdialog"):
self.MIN_DOMAINS_PER_DIALOG = Settings.config.getint("simulate","mindomainsperdialog")
assert(self.MIN_DOMAINS_PER_DIALOG <= self.MAX_DOMAINS_PER_DIALOG)
assert(self.MIN_DOMAINS_PER_DIALOG <= len(Ontology.global_ontology.possible_domains))
if Settings.config.has_option("conditional","conditionalsimuser"):
self.CONDITIONAL_BEHAVIOUR = Settings.config.getboolean("conditional","conditionalsimuser")
if Settings.config.has_option("GENERAL", "tracedialog"):
self.traceDialog = Settings.config.getint("GENERAL", "tracedialog")
if Settings.config.has_option("simulate", "domainsampling"):
self.domainSampling = Settings.config.get("simulate", "domainsampling")
else:
self.domainSampling = "random"
self.sim_level = 'dial_act'
if Settings.config.has_option("usermodel", "simlevel"):
self.sim_level = Settings.config.get("usermodel", "simlevel")
def set_allowed_codomains(self, ROOTDOMAIN):
"""
Sets member (list) *allowed_codomains* given a root domain name (ie the domain of the first constraint)
Uses the hardcoded rules in Settings.py to do so. Also, based on determined allowed_codomains, sets
the probability of each being included, independently.
:param ROOTDOMAIN: domain tag
:type ROOTDOMAIN: str
:returns: None
"""
if self.CONDITIONAL_BEHAVIOUR and ROOTDOMAIN in OntologyUtils.ALLOWED_CODOMAIN_RULES:
#NB: These next few lines depend on hard coding of OntologyUtils.ALLOWED_CODOMAIN_RULES
self.allowed_codomains = OntologyUtils.ALLOWED_CODOMAIN_RULES[ROOTDOMAIN]
if self.allowed_codomains is not None:
if len(self.allowed_codomains) > 1:
if isinstance(self.allowed_codomains[1],list):
randindex = Settings.random.randint(0,len(OntologyUtils.ALLOWED_CODOMAIN_RULES[ROOTDOMAIN]))
self.allowed_codomains = OntologyUtils.ALLOWED_CODOMAIN_RULES[ROOTDOMAIN][randindex]
else:
self.allowed_codomains = []
# based on the allowed_codomains, set the prob of each one independently being in the dialog:
#self.INCLUDE_DOMAIN_PROB = min(0.3,1.0/len(self.allowed_codomains))
else:
self.allowed_codomains = list(self.possible_domains)
self.allowed_codomains.remove(ROOTDOMAIN)
return
def sample_domains(self):
"""Randomly select a set of domains from those available.
The selected domains will be used by the simulated user over a single dialog.
:param None:
:returns: None
"""
if self.domainSampling == "random":
# sample from possible_domains
self.using_domains = []
self.using_domains.append(Settings.random.choice(self.possible_domains)) # must have at least 1 element
root_domain = self.using_domains[0] # the first chosen domain - will affect which codomains can be partnered with
self.set_allowed_codomains(ROOTDOMAIN=root_domain)
shuffled_possible_domains = list(self.possible_domains)
Settings.random.shuffle(shuffled_possible_domains)
for dstring in shuffled_possible_domains:
if len(self.using_domains) == self.MAX_DOMAINS_PER_DIALOG:
break
if dstring not in self.allowed_codomains:
continue
if dstring in self.using_domains:
continue
if len(self.using_domains) < self.MIN_DOMAINS_PER_DIALOG:
self.using_domains.append(dstring)
elif Settings.random.rand() < self.INCLUDE_DOMAIN_PROB:
self.using_domains.append(dstring)
# Note - we may not have met the Min domains limit at this point - for example the allowed codomains for the root_domain
# may have been too low.
if len(self.using_domains) < self.MIN_DOMAINS_PER_DIALOG:
logger.warning("Found {} domains only, which is less than the config set minimum domains per dialog of {}".format(\
len(self.using_domains), self.MIN_DOMAINS_PER_DIALOG))
Settings.random.shuffle(self.using_domains) # list order is persistent. Simulated user will act in this order now.
logger.info('Order sim user will execute goals:'+str(self.using_domains))
elif self.domainSampling == "roundrobin":
if self.MIN_DOMAINS_PER_DIALOG > 1:
logger.warning('With "roundrobin" domains ampling "mindomainsperdialog" cannot be larger than 1, setting it to 1')
self.MIN_DOMAINS_PER_DIALOG = 1
if self.MAX_DOMAINS_PER_DIALOG > 1:
logger.warning('With "roundrobin" domain sampling "maxdomainsperdialog" cannot be larger than 1, setting it to 1')
self.MAX_DOMAINS_PER_DIALOG = 1
if self.temp_domains == []:
self.temp_domains = list(self.possible_domains)
Settings.random.shuffle(self.temp_domains)
self.using_domains=[self.temp_domains.pop()]
self.number_domains_this_dialog = len(self.using_domains)
self.uncompleted_domains = list(self.using_domains)
return
def restart(self):
"""Restarts/boots up the selected domains simulated user components. Shuts down those running and not needed for
the next dialog.
:param None:
:returns: None
"""
# sample domain for this dialog and randomise order:
self.sample_domains()
# reset domain simulators:
otherDomainsConstraints = [] # used to conditionally generate domain goals
for dstring in self.using_domains: # doing this way to generate goals/behaviour in an order.
# fire up or check if it is running
if self.sim_level == 'sys2text':
if self.simUserManagers[dstring] is None:
from usersimulator.US_sys2text.Sys2TextSimulatedUser import Sys2TextSimulatedUser
self.simUserManagers[dstring] = Sys2TextSimulatedUser(dstring)
self.simUserManagers[dstring].restart()
else:
if self.simUserManagers[dstring] is None:
self.simUserManagers[dstring] = DomainsSimulatedUser(dstring, self.error_rate)
self.simUserManagers[dstring].restart(otherDomainsConstraints)
# DEBUG prints to inspect goals we have generated:
if self.sim_level != 'sys2text':
logger.debug(str(self.simUserManagers[dstring].um.goal))
logger.debug(str(self.simUserManagers[dstring].um.goal.copied_constraints))
logger.debug(str(self.simUserManagers[dstring].um.hdcSim.agenda.agenda_items))
logger.debug("DOMAIN-----"+dstring)
#raw_input('goal and agenda for domain '+dstring)
if self.CONDITIONAL_BEHAVIOUR and self.sim_level != 'sys2text':
otherDomainsConstraints += self.simUserManagers[dstring].um.goal.constraints
for dstring in self.possible_domains: #STATELESS, no memory here. Fine to actually `kill' domains not using
if dstring not in self.using_domains:
self.simUserManagers[dstring] = None
return
def act_on(self, sys_act):
""" First produce a single semantic act from the simulated user. Then create from it a list of semantic hypotheses which
include simulated errors.
"""
user_act, user_actsDomain = self._user_act(sys_act)
if self.sim_level != 'sys2text':
hyps = self._confuse_user_act_and_enforce_null(user_act, user_actsDomain)
else:
hyps = None
return user_act, user_actsDomain, hyps
def _user_act(self, sys_act):
'''Produces the next user semantic act from the simulated user. Also returns the domain that the act came from
--> which avoids doing topictracking during simulate
'''
# TODO - this is just a start. lots needs thinking about here.
# -- needs to return the current simulation domain explictly for now
# return dstring too - use this information for error simulation.
logger.debug('simulated users uncompleted domains:'+str(self.uncompleted_domains))
for dstring in self.using_domains:
if dstring in self.uncompleted_domains:
user_act = self.simUserManagers[dstring].act_on(sys_act)
if self.sim_level != 'sys2text':
if 'bye(' in user_act.to_string():
sys_act = 'hello()'
self.uncompleted_domains.remove(dstring)
if len(self.uncompleted_domains):
continue
else:
break
else:
break
return user_act, dstring
def _confuse_user_act_and_enforce_null(self, user_act, user_actsDomain):
'''Simulate errors in the semantic parses. Returns a set of confused hypotheses.
Also enforces a null() act if config set to do so.
'''
# Confused user act.
hyps = self.simUserManagers[user_actsDomain].error_simulator.confuse_act(user_act)
null_prob = 0.0
for h in hyps:
act = h.to_string()
prob = h.P_Au_O
if act == 'null()':
null_prob += prob
if self.traceDialog>1:
print(' Semi >', act, '[%.6f]' % prob)
logger.info('| Semi > %s [%.6f]' % (act,prob))
if self.forceNullPositive and null_prob < 0.001:
nullAct = DiaAct.DiaActWithProb('null()')
nullAct.P_Au_O = 0.001
hyps.append(nullAct)
if self.traceDialog>1:
print('| Semi > null() [0.001]')
logger.info(' Semi > null() [0.001]')
return hyps
# END OF FILE