Skip to content
Snippets Groups Projects
Select Git revision
  • fb16cd9ca310af86142edb4ef8d9fba471611f79
  • master default protected
2 results

SimulatedUsersManager.py

Blame
  • 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