The User Simulator for Task-completion Dialogue 任务完整对话的用户模拟器-------文章解读+源码解析


Summary

问题:在对话系统中,由于构建特定领域且合适数据集成本的高昂和耗时。
解决方法:建立user simulator去模拟user,利用user simulation和agent的交互,选择下一个action并得到reward进行强化学习

本文实现两个主要任务:电影票预定和电影的查询

User Simulator的组成部分:user goal、user action、dialogue status、NLU、NLG、metrics-----如下图
在这里插入图片描述
任务完整对话系统的总体流程:
在这里插入图片描述

代码的大致流程如下,如有错误请批评指正~ 谢谢~
在这里插入图片描述

文章的各部分总结

摘要

拟解决的关键问题:

  1. RL要求与environment进行交互,传统corpus无法直接用于训练
  2. 每个任务都需要带有注释的 task-specific corpus
  3. 对于 task-oriented dialogue,去收集或注释数据集需要大量domain knowledge

总结:构建一个合适数据集的成本高昂且耗时
解决方法:建立 user simulator

user simulator 的优势:

  • 随着 agent 与 simulator 进行交互,用户能用在线方式去训练强化学习 dialogue agents
  • 在模拟器上被训练可以作为一个有效的起始点 一旦 agents 掌控
  • simulator,他们可能被用于真实环境中去和人交互,并继续被在线训练

本文中提出的user simulator执行两个任务:电影票售卖和电影搜索

介绍

实际的对话系统由自然语言理解(natural language understanding-NLU)、自然语言生成(natural language generation-NLG)、知识库(knowledge bases-KBs)、状态追踪器(state trackers-ST)、对话政策(dialogue policy-DP) 和动作(action)组成


每一轮人机交互过程中:

  • 对话系统利用 NLU 将用户语言映射为结构化且机器可理解的语义帧
  • 通过KBs、ST去维护和追踪系统内部对话的进化状态
  • 依据状态使用DP选择恰当的dialogue action
  • 通过NLG将dialogue action转换为自然语言回答

对话系统:通过与用户交互选择action并得到相应reward,进行强化学习


dialog policy可由rule所显式规则化,但基于rule存在缺点:
  • 很难设计一个基于推理的policy
  • 最佳policy会随用户action的改变而改变,基于rule无法处理其非稳定性
    所以,RL可以替代基于rule的方法,自动从experience中学习最优的policy

用户模拟的意义

通常,我们利用Supervised Learning and Reinforcement Learning方法找寻最优policy

  • In SL method:训练policy去模仿expert所观察到的action。
    需大量标注数据且其dialog state space难以被充分探索,阻止一个supervised learner找到一个最优policy
  • In RL method:在没有expert-generated的情况下,给出reward signal,RL训练agent去学习。
    RL需来自environment的例子,无法从实际用户开始学习
    所以利用user simulator去训练RL agent

User Simulator:为了生成自然且合理的对话,让RL agent去探索policy空间,基于模拟的方法让agent去探索可能不存在与之前观察的数据轨迹,克服基于模仿方法的中心限制,dialog agent训练simulators作为一个有效起始点,之后simulators通过RL被用于against human去进一步提升

相关工作

评估simulator没有一个公开和受大众所接受的指标
一个好的user simulator的连贯性行为需要贯穿整个dialog

user simulator的不同方法总结

  • 在粒度级别上,用户模拟器可以在dialog-act级别上操作,也可以在utterance级别上操作
  • 在方法级别上,用户模拟器可以使用基于规则的方法,或基于模型的方法

两种经典模型方法被引入去为用户建模:bi-gram model and 序列到序列且端到端可训练的 user simulator

  1. bi-gram:预测基于先前system action的下一个user action
    无法产生连续的user action
    缺点:仅仅看最后的system action,user改变goal,可能生成非逻辑的action
    解决方法:通过查找更长的对话历史选择下一个user action,通过显示包含user goal到user state中去建模
  2. user simulator:将user-turn 的对话到agent-turn的对话当成-----源到目标序列的生成问题
    agenda-based user simulator 提供一个方便的机制去显式编码用户的要求和限制。一个如堆的形式建模state转换和用户action生成,作为一个简单的入栈和出栈操作的序列,这确保了随着交流的user action的连续性

本文结合model-based and rule-based method 的优点,在dialog-act level上建立user simulator,利用一个agenda-based 的方法去完成任务完整对话的设置

任务完整的对话系统

在对话系统中通过user simulator和agent之间的语言交互,帮user预定电影票或去找寻user想要的电影

agent收集顾客的期望信息并卖出电影票或确定感兴趣的电影,对话结束后,environment依据(1)是否一个电影被预定(2)是否电影满足用户的约束去评估结果是成功/失败

Database:
通过Amazon Mechanical Turk去收集的,注释有11个intents(如通知,要求,确认-问题,确认-答案等),和29个slots(如电影名字,开始时间,电影院,人数等)
总之,在电影领域标记了280个对话,每个对话大约平均11个

用户模拟器

依据agenda-based user simulator method,堆栈表示提供一个遍历机制去显式编码dialog history and user goal,state transition and user action generation 能作为入栈和出栈操作的序列。这里我们详细描述relu-based user simulator

用户目标

任务导向对话中,user simulator 的目标是生成一个user goal(agent在未知user goal的情况下帮助user去完成goal),完整的dialog conversation是隐式围绕goal去完成的。user goal的定义包括两部分:

  1. inform_slots:大量slot-value对作为用户约束
  2. request_slots:一系列slots,user没有关于value的信息,但想要在conversation中去从agent端获得value

为了goal更贴切实际,在user goal中添加约束

对于电影票预定
required slots:(moviename,theater,starttime,datatime,numberofpeople)and optional slots
request_slots:ticket为默认slot(任务的目标)

构造user goal数据集使用两种机制:

  1. user 的第一轮内容中包含一部分,甚至 all user 的要求。是提取用户第一轮的所有slot信息 (注:不包括greeting 轮)
  2. 提取首先出现在所有用户轮中的所有slot,然后将它们整合到一个user goal中

对于simulation,我们打包这些user goal为文件作为user goal的数据库,每跑一轮对话,随机采样一个来自数据库的user goal

用户行为

First user-act (第一个用户动作):电影订票中,user是说第一句话的那一方,根据user goal,随机生成user的第一轮对话内容

为了贴近现实,会在生成时加一些限制
例如:user的第一句话一般是个request 意图,
至少含有一个 inform_slot,如果user知道moviename,则把这个inform_slot添加到第一轮的对话内容中

在整个对话过程中,user simulator维护一个类似于栈的 user agenda。user state分为 agenda (A) 和 user goal (G) (约束©和请求®)

在每个时间步 t 中,user simulator依据current state 和最后一个system action,生成下一个user action,并更新current state。

在没有NLU情况下去训练和测试policy,引入error model 模仿NLU的噪声,实现user和agent之间的聒噪交流,error model中存在两种类型的噪音通道:【1】intent level 【2】slot level, 三种可能噪声

  • slot detection:simulate slot没有被NLU所识别的场景
  • incorrect slot value:simulate slot名字没有被正确识别的场景,如错误单词分割
  • incorrect slot:simulate slot和它的value都没被正确识别的场景

如果agent action 是 inform(task_complete),说明agent已经收集了所有信息,并准备订电影票了。user simulator 将会检测当前栈是否为空,并且检测是否满足约束条件来确保 agent 是否定了正确的电影票。

对话状态

对话状态有三种情况:

no_outcome_yet:是指agent没有 inform(task_comlete),并且轮数还没有达到最大
success:agent 在最大轮数内,必须回答了所有的user 的request,并且订了一张正确的电影票
failure:其他的情况都为失败

自然语言理解NLU

NLU是一个LSTM,单个NLU能同时做intent预测,slot填充。

NLU对intent和slots进行联合建模,预测的标签集合是一个拼接的IOB-format slot和intent标签的集合,并且一个附加的标记被添加到每一个utterance的最后

自然语言生成NLG

user simulator可以被设计为dialog act level(输出到agent结构化的语义解析信息),也能被设计为utterance level (输出到agent自然语言形式)。utterance level这里就需要NLG了。我们在框架中提供一个NLG。由于有限标记的数据集,我们从经验测试发现一个纯净基于模型的NLG无法具备很好的泛化能力,对于policy,我们将引入大量噪声。因此策略是先模板后模型:
1、template-based NLG:为dialog acts输出一些预定义好的基于规则的模板
2、model-based NLG:输入为 dialog-act,输出为带slot标签的句子,(如:我希望电影从{start-time}开始),之后再进行替换。decoder采用beam search n取值为3 。(DM训练可以将beam search n=3 的句子作为输入噪声。

Usage

利用user simulator训练agents去完成电影票预定的完整任务对话设置和KB-InfoBot
【1】agent goal是帮助user成功预订一部电影
【2】agent和带有两个intents的user进行converation。使用两个intents和6个slots完成user goal对话

衡量agent的质量,有三个指标:
success rate (任务完成率)
average reward (平均奖励)
average turns(平均轮数)

一般来说,一个好的政策应该有较高的任务完成率、较高的平均奖励和较低的平均轮数。可以选择成功率作为主要的评估指标来评价agent的效果

讨论

relu-based simulator:为完成完整对话任务利用强化学习训练agent 。缺陷明显:需手工编写规则(耗时)

提升user simulator的完整对话设置方向:
【1】去囊括user goal的改变,使conversation更加复杂且真实
【2】利用model-based user simulator去完成完整任务对话(缺点:只要有数据驱动能很容易应用于其他领域
,优点:数据驱动导致不确定性,对于user simulator去训练RL agents 来说 比较危险,使得RL agents学习错误或错误对话“success”)

补充知识

Deep Q Network

  • experience replay 经验池
  • 神经网络计算Q值
  • 暂时冻结q_target参数(切断相关性)—off policy

DQN更新步骤:

  • 记忆库里存一些东西,之后才开始学习,每几步学习一次
  • 基于观察的选择action,RL采取action并获得下一个观察和reward
  • 判断学习是否结束,未结束进入下一回合继续学习到结束

User Simulator 的组成

RL agents方法的代码执行流程:

  1. 初始化参数,加载字典、用户目标集合,将目标集合拆分为训练集、校验集和测试集
  2. 加载电影数据集,行为集合,槽集合,电影字典
  3. 实例化AgentDQN->实例化DQN
    input hidden:Wxh,bxh
    output hidden:Wd,bd
    update:Wxh,bxh,Wd,bd
    regularize:Wxh,Wd
  4. user simulatior 的参数
  5. 实例化user simulator—RuleSimulator
  6. 加载训练好的NLG model
    包括
    hidden_size:100
    output_size:1047
    bd:【1,1047】
    Wd:【100,1047】
    WLSTM:【1148,400】
    bah:【1,400】
    Wah:【1116,400】
    model: listm_tanh
    进一步加载模型
  7. 加载训练好的NLU model
  8. 实例化Dialog Manager
  9. 开始迭代交流模拟

User Simulator 源码解析

Run Commands:
python run.py --agt 5 --usr 1 --max_turn 40 --movie_kb_path .\deep_dialog\data\movie_kb.1k.p --goal_file_path .\deep_dialog\data\user_goals_first_turn_template.part.movie.v1.p --intent_err_prob 0.00 --slot_err_prob 0.00 --episodes 500 --act_level 1 --run_mode 1

训练基于Rule的user simulator

代码具体流程:

初始化相关参数后,进入run_episodes函数
其中num_episodes:500
status:{‘successes’: 0, ‘count’: 0, ‘cumulative_reward’: 0}

run_episodes(num_episodes, status)
def run_episodes(count, status):
    successes = 0
    cumulative_reward = 0
    cumulative_turns = 0
    
    if agt == 9 and params['trained_model_path'] == None and warm_start == 1:  # 不满足该条件跳过
        print('warm_start starting ...')
        warm_start_simulation()
        print('warm_start finished, start RL training ...')
    
    for episode in range(count):  # 进入训练迭代
        print("Episode: %s" % (episode))  
        # 为了新对话更新states
        # 初始化新的episodes(dialog),更新当前state和追踪的slots
        dialog_manager.initialize_episode() 
        """	可以看到输出如下:
        	Episode: 0
			New episode, user goal:
			{
			  "request_slots": {
			    "ticket": "UNK"
			  },
			  "diaact": "request",
			  "inform_slots": {
			    "city": "seattle",
			    "numberofpeople": "2",
			    "theater": "regal meridian 16",
			    "starttime": "9:25 pm",
			    "date": "tomorrow",
			    "moviename": "zoolander 2"
			  }
			}
			Turn 0 usr: request, inform_slots: {'moviename': 			'zoolander 2', 'theater': 'regal meridian 16'}, 			request_slots: {'ticket': 'UNK'}
		"""
        episode_over = False
        
        # 当迭代没有结束
        while(not episode_over):
        # 进行下一轮迭代(agent和user之间交流),返回迭代是否结束以及奖励
            episode_over, reward = dialog_manager.next_turn()
            # 奖励累加
            cumulative_reward += reward
            # 如果迭代结束
            if episode_over:
                if reward > 0:
                    print ("Successful Dialog!")
                    successes += 1
                else: print ("Failed Dialog!")
                
                # 状态追踪器跟着更新下一轮并累加到下一轮
                cumulative_turns += dialog_manager.state_tracker.turn_count
        
        # simulation agt为9,无训练好的模型
        if agt == 9 and params['trained_model_path'] == None:
            agent.predict_mode = True
            # simulation 的 下一步迭代更新  
            # simulation_res:user_action, kb_results_dict, turn, agent_action
            simulation_res = simulation_epoch(simulation_epoch_size)
            
            performance_records['success_rate'][episode] = simulation_res['success_rate']
            performance_records['ave_turns'][episode] = simulation_res['ave_turns']
            performance_records['ave_reward'][episode] = simulation_res['ave_reward']
            
            if simulation_res['success_rate'] >= best_res['success_rate']:
                if simulation_res['success_rate'] >= success_rate_threshold: # threshold = 0.30
                    agent.experience_replay_pool = []  # 过去的累积经验 
                    simulation_epoch(simulation_epoch_size)   # 模拟器更新
                
            if simulation_res['success_rate'] > best_res['success_rate']:
                best_model['model'] = copy.deepcopy(agent)
                best_res['success_rate'] = simulation_res['success_rate']
                best_res['ave_reward'] = simulation_res['ave_reward']
                best_res['ave_turns'] = simulation_res['ave_turns']
                best_res['epoch'] = episode
            
            # 初始化dqn网络模型    
            agent.clone_dqn = copy.deepcopy(agent.dqn)
            # batch训练agent
            agent.train(batch_size, 1)
            agent.predict_mode = False
            
            print ("Simulation success rate %s, Ave reward %s, Ave turns %s, Best success rate %s" % (performance_records['success_rate'][episode], performance_records['ave_reward'][episode], performance_records['ave_turns'][episode], best_res['success_rate']))
            if episode % save_check_point == 0 and params['trained_model_path'] == None: # save the model every 10 episodes
                save_model(params['write_model_dir'], agt, best_res['success_rate'], best_model['model'], best_res['epoch'], episode)
                save_performance_records(params['write_model_dir'], agt, performance_records)
        
        print("Progress: %s / %s, Success rate: %s / %s Avg reward: %.2f Avg turns: %.2f" % (episode+1, count, successes, episode+1, float(cumulative_reward)/(episode+1), float(cumulative_turns)/(episode+1)))
    print("Success rate: %s / %s Avg reward: %.2f Avg turns: %.2f" % (successes, count, float(cumulative_reward)/count, float(cumulative_turns)/count))
    status['successes'] += successes
    status['count'] += count
    
    if agt == 9 and params['trained_model_path'] == None:
        save_model(params['write_model_dir'], agt, float(successes)/count, best_model['model'], best_res['epoch'], count)
        save_performance_records(params['write_model_dir'], agt, performance_records)
    

实例化DialogManager类

通过调用初始化episodes/next_turn/reward函数进行user和agent间的conversation:

class DialogManager:
    """ A dialog manager to mediate the interaction between an agent and a customer """
    
    def __init__(self, agent, user, act_set, slot_set, movie_dictionary):
        self.agent = agent
        self.user = user
        self.act_set = act_set
        self.slot_set = slot_set
        self.state_tracker = StateTracker(act_set, slot_set, movie_dictionary)
        self.user_action = None
        self.reward = 0
        self.episode_over = False

    def initialize_episode(self):
        """ Refresh state for new dialog """
        
        self.reward = 0
        self.episode_over = False
        # 状态追踪器的初始化
        self.state_tracker.initialize_episode()
        # 随机选择第一个动作
        self.user_action = self.user.initialize_episode()
        # 依据用户动作更新状态追踪器
        self.state_tracker.update(user_action=self.user_action)
        
        if dialog_config.run_mode < 3:
            print("New episode, user goal:")
            print(json.dumps(self.user.goal, indent=2))
        self.print_function(user_action=self.user_action)
        # agent初始化
        self.agent.initialize_episode()

    def next_turn(self, record_training_data=True):
        """ This function initiates each subsequent exchange between agent and user (agent first) """
        
        ########################################################################
        #   agent进入下一论
        #   为 agent 获得状态并追踪
        #   agent state 转到 action
        ########################################################################
        self.state = self.state_tracker.get_state_for_agent()
        self.agent_action = self.agent.state_to_action(self.state)
        
        ########################################################################
        #   利用状态追踪器去做出agent行为 
        #   更新基于上一个action的state
        #   依据系统action做出状态更新和动作选择
        ########################################################################
        self.state_tracker.update(agent_action=self.agent_action)
        #   添加噪声到action中       腐蚀state
        self.agent.add_nl_to_action(self.agent_action)  # add NL to Agent Dia_Act
        self.print_function(agent_action=self.agent_action['act_slot_response'])
        
        ########################################################################
        #   user进入下一轮
        #   sys_action 初始化
        #   依据系统action去更新 user_action, episode_over, dialog_statues
        #   依据对话状态去计算reward
        ########################################################################
        self.sys_action = self.state_tracker.dialog_history_dictionaries()[-1]
        self.user_action, self.episode_over, dialog_status = self.user.next(self.sys_action)
        self.reward = self.reward_function(dialog_status)
        
        ########################################################################
        #  利用最后的用户行为更新状态追踪器
        ########################################################################
        if self.episode_over != True:
            self.state_tracker.update(user_action=self.user_action)
            self.print_function(user_action=self.user_action)

        ########################################################################
        #  Inform agent of the outcome for this time step (s_t, a_t, r, s_{t+1}, episode_over)
        ########################################################################
        if record_training_data:
            # 利用经验池 保存先前的值
            self.agent.register_experience_replay_tuple(self.state, self.agent_action, self.reward, self.state_tracker.get_state_for_agent(), self.episode_over)
        
        return (self.episode_over, self.reward)

    
    def reward_function(self, dialog_status):
        """
        对话失败,reward为-10   对话成功 reward为20
        其他情况reward -1
        """
        if dialog_status == dialog_config.FAILED_DIALOG:  
            reward = - self.user.max_turn  # 10
        elif dialog_status == dialog_config.SUCCESS_DIALOG:
            reward = 2 * self.user.max_turn  # 20
        else:
            reward = -1
        return reward
    
    def reward_function_without_penalty(self, dialog_status):
	    """
        不带惩罚的reward function
        对话失败 reward为0    对话成功 reward为 20
        其他情况为reward为0
        """
        if dialog_status == dialog_config.FAILED_DIALOG:
            reward = 0
        elif dialog_status == dialog_config.SUCCESS_DIALOG:
            reward = 2*self.user.max_turn
        else:
            reward = 0
        return reward
    
    
    def print_function(self, agent_action=None, user_action=None):
        """ Print Function """
            
        if agent_action:
            if dialog_config.run_mode == 0:
                if self.agent.__class__.__name__ != 'AgentCmd':
                    print("Turn %d sys: %s" % (agent_action['turn'], agent_action['nl']))
            elif dialog_config.run_mode == 1:
                if self.agent.__class__.__name__ != 'AgentCmd':
                    print("Turn %d sys: %s, inform_slots: %s, request slots: %s" % (agent_action['turn'], agent_action['diaact'], agent_action['inform_slots'], agent_action['request_slots']))
            elif dialog_config.run_mode == 2: # debug mode
                print("Turn %d sys: %s, inform_slots: %s, request slots: %s" % (agent_action['turn'], agent_action['diaact'], agent_action['inform_slots'], agent_action['request_slots']))
                print ("Turn %d sys: %s" % (agent_action['turn'], agent_action['nl']))
            
            if dialog_config.auto_suggest == 1:
                print('(Suggested Values: %s)' % (self.state_tracker.get_suggest_slots_values(agent_action['request_slots'])))
        elif user_action:
            if dialog_config.run_mode == 0:
                print("Turn %d usr: %s" % (user_action['turn'], user_action['nl']))
            elif dialog_config.run_mode == 1: 
                print("Turn %s usr: %s, inform_slots: %s, request_slots: %s" % (user_action['turn'], user_action['diaact'], user_action['inform_slots'], user_action['request_slots']))
            elif dialog_config.run_mode == 2: # debug mode, show both
                print("Turn %d usr: %s, inform_slots: %s, request_slots: %s" % (user_action['turn'], user_action['diaact'], user_action['inform_slots'], user_action['request_slots']))
                print("Turn %d usr: %s" % (user_action['turn'], user_action['nl']))
            
            if self.agent.__class__.__name__ == 'AgentCmd': # command line agent
                user_request_slots = user_action['request_slots']
                if 'ticket'in user_request_slots.keys(): del user_request_slots['ticket']
                if len(user_request_slots) > 0:
                    possible_values = self.state_tracker.get_suggest_slots_values(user_action['request_slots'])
                    for slot in possible_values.keys():
                        if len(possible_values[slot]) > 0:
                            print('(Suggested Values: %s: %s)' % (slot, possible_values[slot]))
                        elif len(possible_values[slot]) == 0:
                            print('(Suggested Values: there is no available %s)' % (slot))
                else:
                    kb_results = self.state_tracker.get_current_kb_results()
                    print('(Number of movies in KB satisfying current constraints: %s)' % len(kb_results))

代码运行结果展示:
模拟器训练到中间132 epoch时的结果:
成功率、平均奖励和平均轮数 经验池尺寸
在这里插入图片描述
500 epoch训练完毕的simulation的success rate可视化曲线图:
在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值