查看原文
其他

送书 | Chatbot这么火,但你真的知道它的玩法有哪几种吗?

2018-04-01 chatbot AI前线


作者 | 谢梁,鲁颖,劳虹岚
编辑 | Natalie
AI 前线导读:本文由浅入深地介绍了三种构造对话机器人的方法,分别是:基于 AIML 标识语言构造大量应答库,通过现有对文字结构的理解来构造的简单对话系统;使用深度学习方法来寻找对应于当前对话背景的最佳应答;使用深度学习技术实时生成应答。 

每一种方法都有各自的优缺点,比如第一种方法构造繁琐且费力,但不会出现语法错误;最后一种方法灵活度和智能度都极高,但是需要极大量的数据积累和比较复杂的模型才能得到较好的结果。那么,每一种方法该如何使用?如何根据不同的情况选择最合适的方法呢? 

本文节选自《Keras 快速上手:基于 Python 的深度学习实战》第 8 章。 

注意啦,文末有送书福利! 

更多干货内容请关注微信公众号“AI 前线”,(ID:ai-front)
文字生成和聊天机器人

文字信息是存在最广泛的信息形式之一,而深度学习的序列模型(SequentialModel) 在对文字生成建模(Generative Model)方面具备独特的优势。文字自动生成可应用于自然语言对话建模和自动文稿生成,极大地提高了零售客服、网络导购以及新闻业的生产效率。目前比较成熟的是单轮对话系统以及基于单轮对话系统的简单多轮对话系统, 这类系统应用范围广泛,技术相对成熟,在零售客服、网络导购等领域都有很高的边际收益。例如苹果手机中的 Siri,以及微软早期的小冰机器人,都属于这种系统的尝试。

从应用范围来看,自然对话系统分为所谓的闲聊(Chit-Chat)对话系统和专业(DomainSpecific)对话系统两种。

闲聊对话系统的典型代表有苹果的 Siri、微软的小冰机器人等,其构筑于数量极大, 超多样化的对话数据上,比如微信群的聊天数据、微博的对话数据等,其特点是应对话题广泛但是不深入。这种对话系统可以应对各种问题或者话题,比如“今天天气好吗?”、“你觉得奇瑞汽车如何?”、“回锅肉好好吃啊”等。这类系统根据建模数据的分布,会分别回应“天气不错”、“其实吉列的博瑞也不错”、“是啊”等。具体的回应是根据系统的建模数据分布以及反馈系统的熵值设定(即多样化设定)。

而专业对话系统主要应用于各种具体的业务领域,比如某类产品的导购或者客服 等,建模数据一般都是本领域清洗过的数据。这类系统可以应对的话题比较窄,但是非常深入,有较大可能性是一个多轮对话系统;而对于不涵盖的话题应对方式可以比较简单,因为客户对于比较少见的问题的回答期望较低,而且可以进一步接驳人工服务。

同样对于上面的“今天天气好吗”这样的问题,专业对话系统可以进行更为深入的回应,比如:“今天天气不错,多云间晴,最高气温为 27 摄氏度,最低气温为 21 摄氏度,湿度为 75%”等。在闲聊系统中也可以进行适应性改造,需要进行话题识别,在一定的范围内可以将其增强为专业系统,但是需要做大量的工作和相应的数据。

从技术上看,短对话聊天系统分为两种:基于检索的系统(RetrievalBasedSystem)和基于文字生成的系统(GenerativeSystem)。

基于检索的系统鲁棒性强,技术复杂度稍低,但是应对能力有限,比较适合非常窄的专业服务领域。基于检索的对话系统一般基于已有的语义分析理论,对于常见句式进行预先标注,将可能的文字输入分解为不同的部分,然后将相应的部分,比如主语、宾语等,设置为建模对象,实现对具有相似语法结构的不同话题的应对。比较有名的基于检索的对话系统有爱丽丝聊天机器人(Alicebot)及其各种变形。这种聊天机器人基于人工智能标志语言(AIML),通过将现有数据库中不具备的句式载入数据库,具备 初级的学习能力,但是它基于一个强假设,即与其聊天的人的回应是正确的,这种假设条件在实际应用中很难满足。

基于文字生成的系统由数据驱动,应对能力强,反馈多样化,有自我学习能力,显得更为智能,但是其技术要求更高,而且系统的鲁棒性较差,对于很多边界条件需要提前考虑到。比如微软研究院于 2016 年推出的 Tay 聊天机器人就对边界条件考虑不周,在上线以后,接收的即时训练数据都是负面的或者不适宜的,但是这个系统在设计时没有考虑到这个问题,仍旧使用这些数据进行训练,造成对话系统的回应也变得很负面,大量用户反馈其爆粗口或者回应不适宜的话题。从技术上解决这个问题并不难,但 是要从一开始就考虑到各种各样的这类边界情况就很难了。

基于检索的对话系统

本节介绍如何利用人工智能标识语言(AIML,Artificial Intelligence Markup Language)搭建基于检索的对话系统。对于比较窄的应用领域,该系统搭建快速、鲁棒性强、具备一定的灵活性,可以和基于文字生成的对话系统组成混合系统,各自取长补短。AIML 系统具备以下几个优点。

  • AIML 系统是基于检索的对话系统中应用比较广泛的一个系统,其不同的变种已经商业化并应用在一些行业实践中。

  • 在基于深度学习、数据驱动的对话引擎大量出现之前,AIML 系统基本是基于检索的对话系统中最好的系统之一。

  • AIML 系统是开源软件,有不同的语言接口,比如 C++、Java、.NET、Python 等,因此虽然原系统是基于英文的,但是可以很容易地改造为一个中文系统。

  • 开源的 AIML 系统已经提供了大量的语法规则供开发者使用和参考,因此开发者可以根据自己的业务场景进行改造,以适应自己的需求。

  • AIML 系统是一个引擎,开发者可以很容易地将其嵌入自己的系统中。 -

AIML 是一种兼容 XML 语法的通用标识语言,非常容易学习。AIML 由一系列包括在尖括号中的元素(Tag)组成,其中一些重要的元素有:

  • <aiml> : 该元素标识 AIML 文档的起始。

  • <category>: 这个元素标识一个知识类,是 AIML 的基本对话构造部件。关于知识类,在后面会详细介绍。

  • <pattern>: 该元素标识一个语法模式,是对句式的概括和抽象。

  • <template>: 模板元素包含对上面语法模式的回应。

  • <that>: 指代对象标识元素。

  • <topic>: 上下文归类元素。

  • <random>: 随机选择元素,从模板包含的多个回应中随机选择一个,让聊天机器人看起来更智能。

  • <srai>: 递归标识元素。

  • <think>: 类似于条件控制语句,有针对性地控制回应。

  • <get>: 获取 AIML 变量中存储的值。

  • <set>: 将一个值存到指定的 AIML 变量中。

  • <star>: 通配指代元素。在<pattern>元素中可以使用通配符 * 替换任何语言要素,<star>可以在后面指代这个通配符对应的语言要素。

AIML 的基本知识检索单元被称为一个知识类,用<category>元素标注。这个单元包含 3 个子元素:输入模式、应对模板和其他可选项。

输入模式用<pattern>元素标识指代,在 AIML 本来的设计中一般是一个问题,但是并不局限于此,其实可以是任意句式。

应对模板使用<template>元素标识指代,一般是一个对应的回答或者跟输入的句式相对应的回应句式。比如输入不是一个问题而是一句问候:“你好呀!”回应就可以设定为:“我很好,你好。”或者“最近很忙,你呢?”等。

其他可选项分别是<that>对象指代元素和<topic>话题指代元素。

使用 AIML 构造基于检索的对话系统非常简单,下面是最简单的一个 AIML 语法库,其中定义了一个标准知识类<category>模板:

在这个模板中定义了一个最简单的知识类,输入句式是:“你好呀”,AIML 会进行精准匹配。回应模板中设计了一个固定的回应模式:“你好,最近很忙呢”。这个知识类就是一个固定的问答模式,只能用于展示 AIML 语法构成。

下面可以进行一些拓展。假设我们想让系统的回应具有一定的多样性,则可以通过设定随机选择元素<random>来拓展回应模板。下面的例子中建立了一个随机选择元素,包含 3 种回应方式,AIML 每次遇到能匹配的输入句式时会随机选择一个回应。

虽然随机性能让系统的回应更为人性化,但是仍然非常死板。首先,匹配模式不具有灵活性,问候的方式多种多样,我们可能需要一种比较灵活的指定句式的方法。其次,这个回应不能引申到上下文中。

下面使用通配符来改进特定句式的灵活性。对于问候,某些基本句式可以泛化。比如:“你的身体怎么样?”“你的汽车性能怎么样?”等。这时可以用通配符“*”代替“身体”和“车”等对象,然后在回应中可以使用<star>来指代通配符指定的对象,而且可以通过index=来指定对应第几个通配符:

多轮对话比单轮对话增加的难度在于对上下文的感知。AIML 提供了两个帮助联系上下文的元素——<that><topic><that>元素对于前面提到的回应进行进一步的应 对。比如有一段对话是:

人:“你喜欢什么?”

程序:“你想聊聊跑车吗?”

这个时候人的可能回答是:喜欢或者不喜欢。根据人的回答,程序应该有不同的回应,这时候就轮到<that>元素派上用场了。

针对“想聊聊跑车吗?”这个第一轮回应,可以做出不同的第二轮回应,在每一个回应中,在<pattern>元素后面使用<that>元素来引用第一轮的回应。

<topic>元素用来定义一段对话的知识类以供以后检索对应回答。类似于<that>元素,该元素也通常使用于肯定与否定的回答中,不同的是,该元素保存的是整个知识类,而不是某一个具体回应。

使用<topic>元素可以构造比较灵活的上下文敏感的简单多轮对话系统。

但是在应用<that>元素的例子里,针对 3 个正面回应,第二轮的回应都是一样的,但是需要写 3 个知识类,非常烦琐,有没有什么办法简化呢?这时候就 需要用到递归元素<srai>。该元素让 AIML 能够对同一个模板定义不同的目标,从而不仅能简化同类型回应,还有其他非常强大的作用,使机器人的回应显得更加拟人化。

递归元素能用于解决以下几类问题:

  • 句式归一(SymbolicReduction)

  • 分治(DivideandConquer)

  • 同义词解析(SynonymsResolution)

  • 关键词检测(KeywordDetection)

句式归一是用来简化句式的一种方法,其能将复杂的句式分解为较为简单的句式, 反过来说,就是用以前定义的较为简单的句式来重新定义复杂的句式。举例来讲,一个问题可以有多种提法,比如,我们可以问:“谁是黄晓明?”也可以这么问:“你认识黄晓明吗?”而且“黄晓明”这个名字也可以替换成任何人。因为这是一个类型语义的多种不同表达方式,都有一定的句式:要么以“谁是”开头,要么以“你认识”开头,因此可以使用<srai>进行句式归一。

首先将询问的对象用通配符“*”泛化,然后再通过<srai>把类似的问句归纳到第一个知识类的句式,这样系统就能自动匹配并应对多样化的问句。

分治功能主要是通过重用一个知识类句式的一部分来减少重复定义的句式。比如说到再见,可以说“再见了”;或者“再见,哥们儿”;或者“再见,某某某”等。只要 以“再见”开头的句式一般都是道别,因此对于类似的道别句式,都可以归纳到一个回应模板中。

同义词解析这个功能很直白,即对于具有同样含义的对象,应该有同样的理解。这一点与上面的句式归一功能很类似,用法也几乎一样,只是其不是定义在句式上,而是定义在句子里的一个对象上。

关键字检测是指当在输入的句子中包含某一个特定对象时,AIML 会有一个标准的回应。比如,如果句子中提到“帕拉梅拉”,则 AIML 就会回应:“帕拉梅拉是保时捷产 四座豪华轿跑,操控极佳,我喜欢”。下面的代码就实现这个功能:

这里使用了前缀通配符“_”和一般通配符“*”来匹配任意句式。

前面提到过,AIML 是一个检索式对话引擎,可以嵌入任何系统中提供服务。利用平时收集的业务数据,读者可以根据自己的业务需求快速搭建一些有针对性的简易应答机器人,对于常见问题,它们是能够较好地回答的。如果要搭建更为智能的对话机器人,则需要继续学习下面两节的内容。

基于深度学习的检索式对话系统

上面的基于 AIML 的聊天对话系统虽然已经成功应用于早期的商业客服领域,但是有几个大的问题阻碍了其更大范围的应用。

首先,建立对话库需要大量的人力和时间的积累。虽然 AIML 系统开源项目已经公开了数十万句英语对话,但是要将其转换为中文,仍然需要做大量的工作,而且这些对话仍然基于一般的聊天场景,并不针对某一个具体业务,而这类系统最实用的应用是范围比较窄的具体业务,如果要建立这套应用系统,则工作量仍然是巨大的。

其次,虽然 AIML 系统提供了一些功能赋予了聊天对话系统一定的灵活性,比如对对象的记忆、通配、递归等,但是其功能仍然受到很大限制。

再次,AIML 系统在本质上仍然是一个检索性质的系统,而且属于硬性匹配,对于没有见过的询问模式,会无法检索到答案,影响用户体验。

基于深度学习的检索式对话系统在后两点上有较大的改进。通过机器学习,可以实现软匹配,不需要询问语句模式一定要在库里出现,如果通过对词汇和其组织顺序的建模发现有较高概率匹配的数据点,就能实现比较好的回应。其次,深度学习方法提供了更高的灵活性,能够实现记忆和识别等功能。下面通过一个简单的例子来学习如何在 Keras 里训练基于深度学习的检索式对话系统。

对话数据的构造

在讨论建模之前,一个很重要的工作是对数据进行处理。公开的对话数据,特别是带标定的可以用于索引式对话模型建模的中文对话数据不是很好找。这里使用的训练数据是英文的 Ubuntu 论坛讨论数据,该数据是目前较大的带标定的对话数据,由 McGill 大学的 Ryan Lowe、Nissan Pow、Iulian V. Serban 和 Joelle Pineau 创建。他们根据这个数据构造了一个索引式深度学习多轮对话系统,发表于 SIGDial2015 会议上。

读者可以去以下网站下载原始数据:

http://dataset.cs.mcgill.ca/ubuntu-corpus-1.0/。

下面要讨论一下这个数据的构造,这样读者在将其应用到自己的项目时就知道如何从原始对话数据开始对其进行组织和标定,从而能够纳入下面的建模框架进行建模。Ubuntu 对话数据的特点非常适合特定领域自动客服和导购对话系统的构建,因为这类对话具有以下特定的性质。

  • 对话内容涵盖领域非常具体,涉及内容比较窄。

  • 双人应答式多轮对话。

  • 数据量大,一般要求数百万条对话以供训练深度学习模型。

图 8.2 展示了从一个原始对话扩展为可建模用双人多轮对话的过程。

图 8.2 Ubuntu 多人多轮对话处理展示

从图 8.2 可以看出,一个长对话根据标识被划分为两人之间的对话,如果回答没有引用是针对谁,比如图 8.2 中的用户 RC 的回答,那么这两次没有应用客户的回答都被归入上一次最近引用的用户的回应中。

这里要强调的是,很多读者可能认为在自己的内部系统中,客服数据已经是一个很干净的单一话题双人多轮对话数据,为何这里还要介绍如何处理这样一个不是很像内部系统数据的数据处理过程?这是因为在实际业务中,可能某一个单次对话是典型的双人多轮对话,但是很多时候一次客户服务的过程并不能解决用户的疑问,用户有时会间隔一两天或者更长时间再跟客服联系,要求解决一样的问题,这在一些比较复杂的业务场景中是经常出现的。比如作者以前所在的财险公司,当保险客户需要增加保险标的,或者修改保险条款时,有超过 25% 的情况需要联系客服一次以上,因为这些业务问题往往涉及多个方面,比如修改保险标的会涉及多标的折扣、条款修改,新文档寄送,以及增值服务销售等,有时候客服或者用户不能一次想到所有的相关问题。如果仍然只用一次联系的数据作为训练,那么训练结果不能反映这种深刻的联系,但是如果能解决这种相关话题的涵盖问题,不仅能提高客服对话系统的智能度,还能用于训练新的人工客服。

因为这里是要建立一个索引式的深度学习对话系统,所以在本质上我们需要建 一个基于深度学习算法并且还有记忆功能的分类模型,那么我们还需要对这组已经分成双人多轮对话的数据进行进一步处理,具体包括以下三个部分。

(1) 首先,需要将整个对话分成两个部分,即背景 + 回应。背景是一段对话在截止回应之前的所有相关文字,如果有多轮对话,则各段文字用一个特定符号分开。回应就是截止该轮对话之后的立即回应。比如针对前面那个叫 dell 的用户和 cucho 的用户的对话,在第二轮对话之后,其背景是两人对话的前两句:

dell: well, can I move the drives?

cucho: ah not like that.

而回应则是 dell 对 cucho 说的第 2 句话:

I guess I could just get an enclosure and copy via USB.

注意,这里背景的文字是将所有本轮对话之前的对话集合起来,用特殊字符串分开。比如原作者会将上面的背景写成:well, can I move the drives EOS ah not like that EOS I guess I could just get an enclosure and copy via USB.

这里不使用“;”或者“,”“。”等标点符号是因为原有语句的标点可能包含了这些常用或者不常用的标点符号,因此非常特殊的专门构造的分割字符串是比较理想的选择。在这里用户 ID 不需要出现。

(2) 其次,因为这是一个分类问题,因此需要生成正确的回应和一个或者多个不正确的回应,这样深度学习算法才能训练模型。读者可能要问,在标准的对话系统里,回应肯定都是针对问题的,都是正确的,怎么找出不正确的回应呢?这里是通过在与此次对话不相关的其他对话的回应中进行随机取样获得。根据具体情况,可以随机取样一个不相关的回应,或者多个不相关的回应。

(3) 最后,对正确的回应及其背景,生成一个标识为 1,属于正样本;对于同样的背景和随机抽取的不正确回应配对,生成一个标识为 0 或者−1,属于负样本。

如此处理以后的数据就是一个三维对话数,如表 8.1 所示。

表 8.1 最终数据集的构造

构造深度学习索引模型

完成数据处理以后,要运用上面的三维对话数据构造索引式对话系统,则可以使用一个基于深度学习的分类模型来进行。一般来说,一个分类模型建模包含以下几部分:

  • 选择模型。

  • 数据预处理使其适合所用模型。

  • 对模型进行拟合。

  • 检验模型性能,结合上一步调参。

这里我们选用原作者的双编码长短记忆(DualEncoderLSTM)模型。该模型在原文中有最好的性能,因为 LSTM 能够记忆较长的内容,比较适合多轮对话的场景。这个模型的结构如图 8.3 所示。

图 8.3 展示了对一段对话的背景和回应分别编码建立长短记忆模型,再合并计算余弦相似度的结构。图 8.3 中的上半部分循环时间模型对应的是背景部分的数据,其中 ct 对应 t 时刻的背景信息,ht 则是状态变量,下半部分对应的是应答模型,其中 rt 是 t 时刻的回应。函数σ则是合并函数。

对于含有文本的数据,正如在第 1 章提到的,一般先要进行必要的预处理使其变为索引数字。对于英文文本,这些预处理包括 tokenization、stemming、lemmantization 等。对于中文文本,这些预处理包含分词等操作。这些操作结束以后,经过处理后的每一个单词或者单字都用来建立一个索引,并且被分配一个索引下标号,这样对文本的建模就变为对索引标号这种整数的建模。

图 8.3 双编码长短记忆模型

得到了索引标号以后,每一段对话的背景和回应都是一组整数。最长的一段对话的背景有 2002 个索引号,最短的有 3 个索引号,平均有 162.5 个索引号,中位数则是 120 个索引号,因此这个数据的维度还是很高的。我们可以进行几个进一的操作来帮助建模。

首先,最自然的方法是做向量嵌入(Embedding)操作,将高维的索引标号数据投影到一个固定低维度的实数向量中。其次,因为每段对话长短不一,需要将其长度标准化,即补齐工作。对于补齐工作,并不是将所有向量补齐为最长的 2002 个索引号,而是选择一个合理的长度,通过移动窗口将上下文纳入不同时间步构造一个不是太宽的叠加起来的向量。这里可以选择中位数 120 作为补齐长度。

对于一组索引下标向量,用 Keras 做向量嵌入最简单的方法就是将每个长短不一的向量补齐以后用 Embedding 方法进行映射,以上操作可以通过下面的代码实现。

这里首先使用了 data generator 函数,针对一组批量的输入索引下标向量列表进行补齐,否则一次性对所有样本数据进行操作内存消耗太大。在这个 datagenerator 里,先从原始数据里抽取一组指定批量数大小的样本,这组样本是下标标识向量列表,而这些下标标识对应的就是不定长的句子里面每个字的索引号。对于这组列表,按照指定长度进行向量嵌入有两种方法:一种是指定长度大于或等于最长句子的长度,这样每段对话都可以用一个向量表达,能够对所有列表中的向量一次进行补齐;第二种是指定长度短于最长句子的长度,比如用中位值作为指定长度,这时候对于不够指定长度的较短向量可以直接补齐,而对于超过指定长度的向量,则按照指定的长度建立移动 窗口,建立不同时间步对应的对话上下文向量列表,最后对补齐的输出进行映射。不论 是哪种方法,都需要生成三维的张量,中间维度是时间步,对于第一种情况,时间步长度永远为 1,对于第二种情况,时间步长度则是可变的。

另外,这里的status=np.sum([isinstance(e,list)foreinXtemp])语句用来判断返回的样本是否是列表的列表,因为 pad_sequences 只能对列表的列表进行操作,否则会出错。

最后两个命令分别为背景对话和应答定义了通用模型,并添加一个向量嵌入层作为首层网络。这个嵌入层将 2002 维的索引向量映射到一个只有 256 维的较低维度的致密实数向量中。这里选择 256 维是考虑到我们的硬件限制。如果硬件条件允许,则也可以将这个维度稍微提高一点。比如先映射到 512 维,再通过几个全连接层逐次降维到 256 维再输入长短记忆层进行处理。

得到映射好的实数向量以后,可以直接添加一个长短记忆层(LSTM),这个层接受一个对话的相关信息,并输出 128 位的神经元向量。其次,我们需要对背景对话和相应的应答分别建立循环神经网络。在第 1 章提到了,Keras 的循环神经网络本身是一个抽象类,因此我们只能用其 3 个实现的子类来构造具体的循环神经网络,比如 SimpleRNN、LSTM 和 GRU。这里采用了 LSTM,分别对背景对话和应答进行建模。用 LSTM 进行建模在 Keras 里非常容易。首先建立一个序列模型,然后在序列模型中添加 相应的 LSTM 网络层即可。

最后,将两个不同的 LSTM 网络通过运用张量点乘的方法合并,然后在其上训练一个全连接网络。

最后对于这类对话系统的评估需要一个标准。一般来说,这种模型的评价采用 recall@N 指标。

这个指标指的是,对于一个有 M 个正负例子(一般是 1 个正例子,M−1 个负例子,M⩾2)的分类模型,模型根据打分结果选出前 N 个可能的最佳选择(N ⩽M),如果正例子在这 N 个选择里面,那么这个打分选择就标注为正确。如果 N=M,因为一共只有 M 个选择,那么 recall@M 一定是 100%。因此最难的指标是 recall@1,因为只有一次选择,最简单的是 recall@M,因为结果总是 100%。

recall@N 的计算通过下面的程序可以方便地获得:

基于文字生成的对话系统

在上面的基于索引信息取回式的对话系统中,如果在一个问句或者对话背景中找不到一个比较好的匹配,那么机器所选择的回应可能会显得风马牛不相及。这时有以下两种选择。

一是当匹配应答的分数低于一个阈值的时候,选择给定一个默认应答,一般表示该系统无法应对,比如“我不理解您的问题,能不能换一种说法?”之类来提示用户。二是使用基于文字生成的对话机器,在更多的情况下尽可能地提供更为智能的应答。

本节会介绍如何使用循环神经网络自动生成智能应答。

这里使用作家老舍的小说《四世同堂》作为训练数据进行演示。读者可以根据自己的应用和业务环境,选择合适的数据。

在很多英文环境的生成式对话系统中,建模的单位有单字和单字符两种。前者给每一个进行过预处理的单词词根建立索引,然后使用单词在该索引上的映射作为原始数据来建模。而后者对于每个英文字符,包括大小写字母、阿拉伯数字以及其他字符等建立索引,然后根据每个单字符在该索引上的映射作为原始数据来建模。在中文里也有相应的两种情况。一种是对进行中文分词以后的词组建立索引并建模,另外一种就是对每个中文单字以及符号等建立索引并建模。

根据以往的经验,在英文环境下,很多基于单字符进行建模都取得了不错的效果,而在中文环境下,中文分词有时候是一个问题,很多具体业务需要建立自己的分词库来准确得到对应的词组,如果采用已有的通用分词库,则很有可能不能得到较好的分词效果,对于一些新出现的词组不能有效进行分割,但是这些新的词组往往是业务发展的体现,因此,即使是较少的错误分词仍然可能造成较大的问题。比如在没有及时更新词库的情况下,“……和美国总统川普通话”这句话会被切分为 [和,美国,总统,川,普通话]。因此为了简化建模流程,在没有专门制定分词库的情况下,我们选择对单个中文字及相关符号进行建模,这样就跳过了建立自己的分词库或者使用通用分词库但是分词效果不一定好的情况。

对于训练文本可以一次性读入:

alltext = open("e:\\data\\Text\\四世同堂.txt", encoding='utf-8').read()

得到的结果是一个巨大的字符串列表。因为我们将以每个单字作为建模对象,因此这样读入数据是最方便以后操作的。如果要以词组和单句进行建模,则分段读入最佳。《四世同堂》这本书一共有 3545 个不重复的单字和符号。

按照对文字序列建模的顺序,我们依次进行下面的操作。

(1) 首先对所有待建模的单字和字符进行索引。

(2) 其次构造句子序列。

(3) 然后建立神经网络模型,对索引标号序列进行向量嵌入后的向量构造长短记忆神经网络。

(4) 最后我们来检验建模效果。

对单字和字符建立索引非常简单,用下面三句命令即可:

第一句的对用 set 函数抽取的每个单字的集合按照编码从小到大排序。第二句对每一个单字进行编号索引,第三句进行反向操作,对每个索引建立单字的词典,主要是为了方便预测出来的索引标号向量转换为人能够阅读的文字。

构造句子序列也非常简单:

构造句子序列的原因是原始数据是单字列表,因此需要人为构造句子的序列来模 仿句子序列。在上面的代码中,maxlen=40 标识人工构造的句子长度为 40 个单字,step=3 表示在构造句子时每次跳过 3 个单字,比如用这一串单字列表“这首小令是李清照的奠定才女地位之作,轰动朝野。传闻就是这首词,使得赵明诚日夜作相思之梦,充分说明了这首小令在当时引起的轰动。又说此词是化用韩偓《懒起》诗意。”来构造句子的时候,假设句子长度为 10,那么第一句是“这首小令是李清照的奠”,而第二句则是移 动 3 个单字以后的“令是李清照的奠定才女”。跳字的目的是为了增加句子与句子之间的变化,否则每两个相邻句子之间只有一个单字的差异,但是这两个相邻句子是用来构造前后对话序列的,缺乏变化使得建模效果不好。当然,如果跳字太多,那么会大大降低数据量。比如《四世同堂》一共有 711501 个单字和符号,每隔三个字或者符号进行跳字操作构造的句子只有 237154 个,是原数据量的 1/3。如何选择跳字的个数是读者在建模的时候要根据情况调整的一个参数。

需要注意的是,因为句子是人工构造的,都有固定的长度,因此这里不需要进行句子补齐操作。同时,这些句子的向量其实都是一个稀疏矩阵,因为它们只将包含数据的 索引编号计入。

人工构造句子完毕后就可以对其矩阵化,即对于每一句话,将其中的索引标号映射到所有出现的单字和符号,每一句话所对应的 40 个字符的向量被投影到一个 3545 个元素的向量中,在这个向量中,如果某个元素出现在这句话中,则其值为 1,否则为 0。下面的代码执行这个操作:

当然这个新生成的数据会非常大,比如 X 会是一个 237154×40×3545 的实数矩阵,实际计算的时候占用的内存会超过 20GB。因此这里需要使用前面提到的数据生成器(datagenerator)方法,对一个具有较小批量数的样本进行投影操作。可以通过下面 这个很简单的函数实现:

这个函数与前面的 batch_generator 函数非常相似,主要区别是这个函数同时处理 X 和 Y 矩阵的小批量生成,另外要求输入和输出数据都是 NumPy 多维矩阵而不是列表的列表。另外 Python 里的数值数据是 float64 类型的,因此专门使用 astype('float32') 将矩阵的数据类型强制定为 32 位浮点数以符合 CNTK 对数据类型的要求,这样不需要在后台再进行数据类型转换,从而提高效率。现在可以构造我们的长短记忆神经网络模型了。这时候再次体现了 Keras 的高效建模能力。下面短短几个命令就可以让我们构 造一个深度学习模型:

其中第一句命令指定要生成一个序列模型,第二到第四句命令要求依次添加三层 网络,分别是一个长短记忆网络和一个全连接网络,最后使用一个 softmax 的激活层输出预测。在长短记忆网络里,规定输入数据的维度为(时间步数,所有出现的不重复字符的个数),即输入的数据是对应每一句话处理以后的形式,并且对输入神经元权重和隐藏状态权重分别设定了 10% 的放弃率。全连接层的输出维度为所有字符的个数,方便最后的激活函数计算。最后两条命令指定网络优化算法的参数,比如里面指定损失 函数为典型的 categorical_crossentropy,优化算法是指定的学习速率为 0.01 的 RMSprop 算法。对于循环神经网络,这个优化算法通常表现较好。

最后我们开始训练模型:

这里使用 fit_generator 方法,而不是我们平时所用的 fit 方法,数据输入也是通过 data_generator() 函数,fit_generator 将每个批量的数据读入,从稀疏矩阵变为密集矩阵,然后计算。这样对内存的压力大大降低了。下面展示了拟合过程前 5 个迭代的时间和损失函数的值:

如果不强制转换数据类型,则运行时间会增加大约 110 秒。最后来看一看效果。先随机抽取一组 40 个连续的字符,然后生成对应的投影到所有字符空间的自变量 x:

接下来依次对每一句的下 20 个字符进行预测,并根据预测得到的索引标号找出对应的文字供人阅读:

读者可能会留意到这里有一个 sample 函数,用于从预测结果得到新生成的文字。这是因为这个模型返回的是对应于每个字符在下一句里的出现概率,而这个函数就是负责根据这个得到的概率对所有索引标号进行依概率的随机取样。但是这个函数还有第二个参数用来控制概率差异的扩大或者缩小。这个参数通常被称为“温度(temperature)” 参数。其作用于语句 preds=np.log(preds)/temperature,当温度参数为 1 时,对预测概率没有影响;当温度参数小于 1 时,预测概率的差异被扩大,有利于增加生成语句的多样性;当温度参数大于 1 时,预测概率的差异被缩小,会缩小生成语句的多样性,即在很多时候生成的语句都非常类似,会有很多单字不断地重复出现。通常来说,温度参数应该设置比较小,我们的实验通常设置在小于 0.1 的水平。这个 sample 函数的代码 如下:

那么结果如何呢?我们随机选择的字符串是下面这样的:

一句,小顺儿的妈点一次头,或说一声“是”。老人的话,她已经听过起码有五十次,但是

而生成的字符串是这样的:

不 知 道 , 他 的 心 中 就 是 一 个 人 , 而 只 觉 得 自 己

乍一看,其实还读的通,但是仔细读就发现整个句子跟上下文并无任何关系。原因可能是模型不够复杂,无法抓取很多潜在信息;也很可能是数据量太小,一般来说,训练这样一种生成式模型需要数百万条语句才能得到较好的结果,而《四世同堂》这一部小说是达不到这个水平的。不过在实际应用中,一般公司都会积累大量的客服对话数据,因此这个数据量不会成为模型瓶颈。

总结

本章由浅入深地介绍了三种构造对话机器人的方法,其中有两种属于索引式模型, 一种属于最新的生成式模型。第一种是基于深度学习流行之前的技术,使用 AIML 这种标识语言构造大量的应答库,通过现有的对文字结构的理解来构造的简单对话系统。这种系统的构造费时费力,灵活性差,扩展性差,智能度低,很难构造多轮对话系统,但是因为应答都是真人生成的,不会有语法错误,语言标准,所以适用于简单集中的业务环境。第二种是使用深度学习方法来寻找对应于当前对话背景的最佳应答,相对于第一种方法降低了很多人工构造应答库的工作,灵活性高,扩展性强,有一定智能度,可以用来构造多轮对话系统。第三种是目前最新研究的领域,使用深度学习技术实时生成应答,灵活度和智能度都极高,属于自动扩展,但是需要极大量的数据积累和比较复杂的模型才能得到较好的结果。通常第三种系统需要与第二种系统相结合,在第二系统已有的应答库中无法找到足够满意的选项时,可以启用第三种系统来实时生成应答。

福利时间到!

距离上一次送书已经过去快两个月啦,小前又给大家争取到了一波新福利!这次 AI 前线联合博文视点给粉丝们送出《Keras 快速上手:基于 Python 的深度学习实战》纸质书籍共 10 本!本书由腾讯 AI Lab 负责人亲笔作序力荐,更有谷歌、微软、Twitter 等数位资深数据科学家倾情推荐,不要错过啦!

方式一:在本文下方留言给出你想要这本书的理由,我们会由编辑精选出 5  位赠送本书。开奖时间:4 月 3 日(周二)17:00,获奖者每人获得一本。


方式二:长按识别下图小程序,参与我们的抽奖活动,我们会由小程序随机抽出 5 位赠送本书。开奖时间:4 月 3 日(周二)17:00,获奖者每人获得一本。另附京东购买地址,戳「阅读原文」!

注意:如果两种方式均获奖的读者,仅送出一本。本活动最终解释权归AI前线所有。



今日荐文

点击下方图片即可阅读

 年薪超 70 万,为什么数据科学家们还都忙着找新工作?

AI前线紧跟前沿的AI技术社群


一些粉丝朋友还没有养成阅读后点赞的习惯,希望大家在阅读后顺便点个赞,以示鼓励。原创是一种信仰,专注是一种态度。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存