聊天机器人(chatbot)终极指南:天然语言处理(NLP)和深度机器学习(Deep Machine Learning)

 

在过去的几个月中,我一直在收集天然语言处理(NLP)以及如何将NLP和深度学习(Deep Learning)应用到聊天机器人(Chatbots)方面的最好的资料。python

时不时地我会发现一个出色的资源,所以我很快就开始把这些资源编制成列表。 不久,我就发现本身开始与bot开发人员和bot社区的其余人共享这份清单以及一些很是有用的文章了。git

在这个过程当中,个人名单变成了一个指南,通过一些好友的敦促和鼓励,我决定和你们分享这个指南,或许是一个精简的版本 - 因为长度的缘由。github

这个指南主要基于Denny Britz所作的工做,他深刻地探索了机器人开发中深度学习技术的利用。 文章中包含代码片断和Github仓,好好利用!算法

闲话不扯了…让咱们开始吧!数据库

概述:聊天机器人开发中的深度学习技术

deep learning

聊天机器人是一个热门话题,许多公司都但愿可以开发出让人没法分辨真假的聊天机器人,许多人声称可使用天然语言处理(NLP)和深度学习(Deep Learning)技术来实现这一点。 可是人工智能(AI)如今吹得有点过了,让人有时候很难从科幻中分辨出事实。ubuntu

在本系列中,我想介绍一些用于构建对话式代理(conversational agents)的深度学习技术,首先我会解释下,如今咱们所处的位置,而后我会介绍下,哪些是可能作到的事情,哪些是至少在一段时间内几乎不可能实现的事情。数组

模型分类

hand

基于检索的模型 VS. 生成式模型

基于检索的模型(retrieval-based model)更容易实现,它使用预约义响应的数据库和某种启发式推理来根据输入(input)和上下文(context)选择适当的响应(response)。 启发式推理能够像基于规则(rule based)的表达式匹配同样简单,或者像机器学习中的分类器集合(classifier ensemble)同样复杂。 这些系统不会产生任何新的文本,他们只是从固定的集合中选择一个响应。网络

成式模型(generative model)要更难一些,它不依赖于预约义的响应,彻底从零开始生成新的响应。 生成式模型一般基于机器翻译技术,但不是从一种语言翻译到另外一种语言,而是从输入到输出(响应)的“翻译”:架构

seq2seq model

两种方法都有明显的优势和缺点。 因为使用手工打造的存储库,基于检索的方法不会产生语法错误。 可是,它们可能没法处理没有预约义响应的场景。 出于一样的缘由,这些模型不能引用上下文实体信息,如前面提到的名称。 生成式模型更“更聪明”一些。 它们能够引用输入中的实体,给人一种印象,即你正在与人交谈。 然而,这些模型很难训练,并且极可能会有语法错误(特别是在较长的句子上),而且一般须要大量的训练数据。app

深度学习技术既能够用于基于检索的模型,也能够用于生成式模型,可是chatbot领域的研究彷佛正在向生成式模型方向发展。 像seq2seq这样的深度学习体系结构很是适合l来生成文本,研究人员但愿在这个领域取得快速进展。 然而,咱们仍然处于创建合理、良好的生成式模型的初期阶段。如今上线的生产系统更多是采用了基于检索的模型。

对话的长短

chat cloud

对话越长,就越难实现自动化。 一种是短文本对话(更容易实现) ,其目标是为单个输入生成单个响应。 例如,你可能收到来自用户的特定问题,并回复相应的答案。 另外一种是很长的谈话(更难实现) ,谈话过程会经历多个转折,须要跟踪以前说过的话。 客户服务中的对话一般是涉及多个问题的长时间对话。

开放领域 VS. 封闭领域

domain over model

开放领域的chatbot更难实现,由于用户 不必定有明确的目标或意图。 像TwitterReddit这样的社交媒体网站上的对话一般是开放领域的 - 他们能够谈论任何方向的任何话题。 无数的话题和生成合理的反应所须要的知识规模,使得开放领域的聊天机器人实现至关困难。

“开放领域 :能够提出一个关于任何主题的问题,并期待相关的回应,这很难实现。考虑一下,若是就抵押贷款再融资问题进行交谈的话,实际上你能够问任何事情“ —— 马克·克拉克

封闭领域的chatbot比较容易实现,可能的输入和输出的空间是有限的,由于系统试图实现一个很是特定的目标。 技术支持或购物助理是封闭领域问题的例子。 这些系统不须要谈论政治,只须要尽量有效地完成具体任务。 固然,用户仍然能够在任何他们想要的地方进行对话,但系统并不须要处理全部这些状况 - 用户也不指望这样作。

“封闭领域 :能够问一些关于特定主题的有限的问题,更容易实现。好比,迈阿密天气怎么样?“

“Square 1迈出了一个聊天机器人的可喜的第一步,它代表了可能不须要智能机器的复杂性,也能够提供商业和用户价值。

”Square 2使用了能够生成响应的智能机器技术。 生成的响应容许Chatbot处理常见问题和一些不可预见的状况,而这些状况没有预约义的响应。 智能机器能够处理更长的对话而且看起来更像人。 可是生成式响应增长了系统的复杂性,并且每每是增长了不少的复杂性。

咱们如今在客服中心解决这个问题的方法是,当有一个没法预知的状况时,在自助服务中将没有预约义的回应 ,这时咱们会把呼叫传递给一个真人“ Mark Clark

共同的挑战

在构建聊天机器人时,有一些挑战是显而易见的,还有一些则不那么明显,这些挑战中的大部分都是如今很活跃的研究领域。

使用上下文信息

dialog

为了产生明智的反应,系统可能须要结合语言上下文和实物上下文 。 在长时间的对话中,人们会跟踪说过的内容以及所交换的信息。上图是使用语言上下文的一个例子。最多见的实现方法是将对话嵌入到向量(vector)中,可是长时间的对话对这一技术带来了挑战。两个相关的论文:“使用生成式层级神经网络模型构建端到端的对话系统”以及“在神经网络对话模型中使用有目的的注意力”,都在朝着这个方向发展。此外,还可能须要在上下文中合并其余类型的数据,例如日期/时间,位置或关于用户的信息。

一致的个性

personality

理想状况下,当生成响应时代理应当对语义相同的输入产生一致的答案。 例如,对于这两个问题:“你几岁了?”和“你的年龄是?”,你会指望获得一样的回答。 这听起来很简单,可是如何将这种固定的知识或者说“个性”归入到模型里,仍是一个须要研究的问题。许多系统学能够生成语言上合理的响应,可是它们的训练目标并不包括产生语义一致的反应。 一般这是由于它们接受了来自多个不一样用户的大量数据的训练。 相似于论文“基于角色的神经对话模型”中的模型,正在向为个性建模的方向迈出第一步。 
persona

模型的评估

评估聊天代理的理想方法是衡量是否在给定的对话中完成其任务,例如解决客户支持问题。 可是这样的标签(label)的获取成本很高,由于它们须要人为的判断和评估。有时候没有良好定义的目标,就像在开放领域域的模型同样。通用的衡量指标,如BLEU, 最初是用于机器翻译的,它基于文本的匹配,所以并非特别适合于对话模型的衡量,由于一个明智的响应可能包含彻底不一样的单词或短语。 事实上,在论文“ 对话响应生成的无监督评估指标的实证研究”中,研究人员发现,没有任何经常使用指标与人类的判断具备真正相关性。

siri chatbot

意图和多样性

生成式系统的一个常见问题是,它们每每会生成一些相似于“很棒!”或“我不知道”之类的没有养分的响应,这些响应能够应对不少输入。 谷歌智能答复的早期版本倾向于用“我爱你”来回应几乎任何事情。这一现象的部分根源在于这些系统是如何训练的,不管是在数据方面仍是在实际的训练目标和算法方面。 一些研究人员试图经过各类目标函数(Object function)来人为地促进多样性 。 然而,人类一般会产生特定于输入的反应并带有意图。 由于生成式系统(特别是开放域系统)没有通过专门的意图训练,因此缺少这种多样性。

如今能实现到什么程度?

基于目前全部前沿的研究,咱们如今处于什么阶段,这些系统的实际工做状况到底怎么样? 再来看看咱们的模型分类。 基于检索的开放领域系统显然是不可能实现的,由于你永远不可能手工制做足够的响应来覆盖全部的状况。 生成式的开放域系统几乎是通用人工智能(AGI:Artificial General Intelligence),由于它须要处理全部可能的场景。 咱们离这个的实现还很远(可是在这个领域正在进行大量的研究)。

这就给咱们剩下了一些限定领域的问题,在这些领域中,生成式和基于检索的方法都是合适的,对话越长,情境越重要,问题就越困难。

(前)百度首席科学家Andrew Ng 最近接受采访时说:

现阶段深度学习的大部分价值能够体如今一个能够得到大量的数据的狭窄领域。 下面是一个它作不到的例子:进行一个真正有意义的对话。 常常会有一些演示,利用一些精挑细选过的对话,让它看起来像是在进行有意义的对话,但若是你真的本身去尝试和它对话,它就会很快地偏离正常的轨道。

许多公司开始将他们的聊天外包给人力工做者,并承诺一旦他们收集了足够的数据就能够“自动化”。 这只有在一个很是狭窄的领域运行时才会发生 - 好比说一个叫Uber的聊天界面。 任何开放的领域(好比销售电子邮件)都是咱们目前没法作到的。 可是,咱们也能够经过提出和纠正答案来利用这些系统来协助工做人员。 这更可行。

生产系统中的语法错误是很是昂贵的,由于它们可能会把用户赶跑。 这就是为何大多数系统可能最好采用基于检索的方法,这样就没有语法错误和攻击性的反应。 若是公司可以以某种方式掌握大量的数据,那么生成式模型就变得可行 - 可是它们必须辅以其余技术,以防止它们像微软的Tay那样脱轨。

用TENSORFLOW实现一个基于检索的模型

本教程的代码和数据在Github上。

基于检索的博客

当今绝大多数的生产系统都是基于检索的,或者是基于检索的和生成式相结合。 Google的Smart Reply就是一个很好的例子。 生成式模型是一个活跃的研究领域,但咱们还不能很好的实现。 若是你如今想构建一个聊天代理,最好的选择就是基于检索的模型。

UBUNTU DIALOG CORPUS

在这篇文章中,咱们将使用Ubuntu对话语料库( 论文 , github )。 Ubuntu 对话语料库(UDC)是可用的最大的公共对话数据集之一。 它基于公共IRC网络上的Ubuntu频道的聊天记录。 论文详细说明了这个语料库是如何建立的,因此在这里我再也不重复。 可是,了解咱们正在处理的是什么样的数据很是重要,因此咱们先作一些数据方面的探索。

训练数据包括100万个样例,50%的正样例(标签1)和50%的负样例(标签0)。 每一个样例都包含一个上下文 ,即直到这一点的谈话记录,以及一个话语 (utterance),即对上下文的回应。 一个正标签意味着话语是对当前语境上下文的实际响应,一个负标签意味着这个话语不是真实的响应 - 它是从语料库的某个地方随机挑选出来的。 这是一些示例数据:

udc dialogs

请注意,数据集生成脚本已经为咱们作了一堆预处理 - 它使用NLTK工具对输出进行了分词(tokenize), 词干处理(stem)和词形 规范化(lemmatize) 。 该脚本还用特殊的标记替换了名称,位置,组织,URL和系统路径等实体(entity)。 这个预处理并非绝对必要的,但它可能会提升几个百分点的性能。 上下文的平均长度是86字,平均话语长17字。 使用Jupyter notebook来查看数据分析 。

数据集拆分为测试集和验证集。 这些格式与训练数据的格式不一样。 测试/验证集合中的每一个记录都包含一个上下文,一个基准的真实话语(真实的响应)和9个不正确的话语,称为干扰项(distractors) 。 这个模型的目标是给真正的话语分配最高的分数,并调低错误话语的分数。

udc data

有多种方式能够用来评估咱们的模型作得如何。 经常使用的衡量指标是k召回(recall@k ),它表示咱们让模型从10个可能的回答中选出k个最好的回答(1个真实和9个干扰)。 若是正在选中的回答中包含正确的,咱们就将该测试示例标记为正确的。 因此,更大的k意味着任务变得更容易。 若是咱们设定k = 10,咱们获得100%的召回,由于咱们只有10个回答。 若是咱们设置k = 1,模型只有一个机会选择正确的响应。

此时你可能想知道如何选择9个干扰项。 在这个数据集中,9个干扰项是随机挑选的。 然而,在现实世界中,你可能有数以百万计的可能的反应,你不知道哪个是正确的。 你不可能评估一百万个潜在的答案,选择一个分数最高的答案 - 这个成本过高了。 Google的“ 智能答复”使用集群技术来提出一系列可能的答案,以便从中选择。 或者,若是你只有几百个潜在的回应,你能够对全部可能的回应进行评估。

基准

在开始研究神经网络模型以前,咱们先创建一些简单的基准模型,以帮助咱们理解能够期待什么样的性能。 咱们将使用如下函数来评估咱们的recall@ k指标:




1 def evaluate_recall(y, y_test, k=1):
2     num_examples = float(len(y))    
3     num_correct = 0 
4     for predictions, label in zip(y, y_test):   
5         if label in predictions[:k]:    
6             num_correct += 1    
7     return num_correct/num_examples

这里,y是咱们按照降序排序的预测列表,y_test是实际的标签。 例如,[0,3,1,2,5,6,4,7,8,9]中的ay表示话语0得分最高,话语9得分最低。 请记住,对于每一个测试样例,咱们有10个话语,第一个(索引0)始终是正确的,由于咱们数据中的话语列位于干扰项以前。

直觉是,一个彻底随机的预测器也应该能够在recall@ 1指标上拿10分,在recall@2指标上得20分,依此类推。 让咱们来看看是不是这种状况:

 1 # Random Predictor
 2 def predict_random(context, utterances):
 3     return np.random.choice(len(utterances), 10, replace=False)
 4 
 5 # Evaluate Random predictor
 6 y_random = [predict_random(test_df.Context[x], test_df.iloc[x,1:].values) for x in range(len(test_df))]
 7 
 8 y_test = np.zeros(len(y_random))
 9 for n in [1, 2, 5, 10]:
10     print(“Recall @ ({}, 10): {:g}”.format(n, evaluate_recall(y_random, y_test, n)))
 

测试结果:



1 Recall @ (1, 10): 0.0937632
2 Recall @ (2, 10): 0.194503
3 Recall @ (5, 10): 0.49297
4 Recall @ (10, 10): 1

 

 

很好,看起来符合预期。 固然,咱们不仅是想要一个随机预测器。 原始论文中讨论的另外一个基准模型是一个tf-idf预测器。 tf-idf表明“term frequency - inverse document frequency”,它衡量文档中的单词与整个语料库的相对重要性。 这里不阐述具体的的细节了(你能够在网上找到许多关于tf-idf的教程),那些具备类似内容的文档将具备相似的tf-idf向量。 直觉上讲,若是上下文和响应具备类似的词语,则它们更多是正确的配对。 至少比随机更可能。 许多库(如scikit-learn 都带有内置的tf-idf函数,因此它很是易于使用。 如今,让咱们来构建一个tf-idf预测器,看看它的表现如何。

 1 class TFIDFPredictor:
 2     def __init__(self): 
 3         self.vectorizer = TfidfVectorizer() 
 4 
 5     def train(self, data):
 6         self.vectorizer.fit(np.append(data.Context.values,
 7                                 data.Utterance.values))
 8     def predict(self, context, utterances):
 9         # Convert context and utterances into tfidf vector
10         vector_context = self.vectorizer.transform([context])
11         vector_doc = self.vectorizer.transform(utterances)
12 
13         # The dot product measures the similarity of the resulting vectors
14         result = np.dot(vector_doc, vector_context.T).todense()
15         result = np.asarray(result).flatten()
16 
17         # Sort by top results and return the indices in descending order
18         return np.argsort(result, axis=0)[::-1]
19 
20 
21 # Evaluate TFIDF predictor
22 pred = TFIDFPredictor()
23 pred.train(train_df)
24 
25 y = [pred.predict(test_df.Context[x], test_df.iloc[x,1:].values) for x in range(len(test_df))]
26 
27 for n in [1, 2, 5, 10]:
28     print(“Recall @ ({}, 10): {:g}”.format(n, evaluate_recall(y, y_test, n)))

 

 

运行结果:

Recall @ (1, 10): 0.495032 Recall @ (2, 10): 0.596882 Recall @ (5, 10): 0.766121 Recall @ (10, 10): 1

咱们能够看到tf-idf模型比随机模型表现得更好。 尽管如此,这还不够完美。 咱们所作的假设不是很好。 首先,响应不必定须要与上下文类似才是正确的。 其次,tf-idf忽略了词序,这多是一个重要的改进信号。 使用一个神经网络模型,咱们应该能够作得更好一点。

双编码器LSTM

咱们将在本文中构建的深度学习模型称为双编码器LSTM网络(Dual Encoder LSTM Network)。 这种类型的网络只是能够应用于这个问题的众多网络之一,并不必定是最好的。 你能够尝试各类深度学习架构 - 这是一个活跃的研究领域。 例如,常常在机器翻译中使用的seq2seq模型在这个任务上可能会作得很好。 咱们打算使用双编码器的缘由是由于据报道它在这个数据集上性能不错。 这意味着咱们知道该期待什么,而且能够确定咱们的丝线代码是正确的。 将其余模型应用于这个问题将是一个有趣的项目。

咱们将创建的双编码器LSTM看起来像这样( 论文 ): 
dual encoder lstm

它的大体工做原理以下:

  1. 上下文和响应文本都是按照单词分割的,每一个单词都嵌入到一个向量中。 词嵌入是用斯坦福大学的GloVe矢量进行初始化的,而且在训练过程当中进行了微调(注:这是可选的,而且没有在图片中显示,我发现用GloVe进行初始化对模型性能没有太大的影响)。
  2. 嵌入的上下文和响应都逐字地输入到相同的递归神经网络(Recurrent Neural Network)中。 RNN生成一个矢量表示,不严格地说,这个表示捕捉了上下文和响应(图片中的c和r)中的“含义”。 咱们能够自由选择矢量的大小,不过先选择256个维度吧。
  3. 咱们用矩阵M乘以c来“预测”一个响应r'。 若是c是256维向量,则M是256×256维矩阵,结果是另外一个256维向量,咱们能够将其解释为产生的响应。 矩阵M是在训练中学习到的。
  4. 咱们经过取这两个向量的点积来度量预测响应r'和实际响应r的类似度。 大的点积意味着两个向量更类似,所以应该获得高分。 而后,咱们应用sigmoid函数将该分数转换为几率。 请注意,步骤3和4在图中组合在一块儿。

为了训练网络,咱们还须要一个损失(成本)函数。 咱们将使用分类问题中常见的二项交叉熵损失(binary cross-entropy loss)。 让咱们将上下文响应的真实标签称为y。 这能够是1(实际响应)或0(不正确的响应)。 让咱们把上面第4条中提到的预测几率称为y'。 而后,交叉熵损的计算公式为L = -y * ln(y') - (1-y)* ln(1-y')。 这个公式背后的直觉很简单。 若是y = 1,则剩下L = -ln(y'),这意味着对远离1的预测加以惩罚;若是y = 0,则剩下L = -ln(1-y'),这惩罚了远离0的预测。

咱们的实现将使用numpy ,pandas , TensorflowTF Learn ( Tensorflow的高层API)的组合。

数据预处理

原始的数据集是CSV格式。 咱们能够直接使用CSV,但最好将咱们的数据转换成Tensorflow专有的example格式。 (顺便说一下:还有一个tf.SequenceExample,但tf.learn彷佛不支持这一格式)。 example格式的主要好处是它容许咱们直接从输入文件加载张量(tensor),并让Tensorflow来对输入进行随机排序(shuffle),批次处理(batch)和队列处理(queue)。 做为预处理的一部分,咱们还建立了一个词表。 这意味着咱们将每一个单词映射到一个整数,例如“cat”可能变成2631.咱们将生成的TFRecord文件,存储的就是这些整数而不是字串。 咱们会保留词表,以便后续能够从整数映射回单词。

每一个样例包含如下字段:

  • context:表示上下文文本的词序列,例如[231,2190,737,0,912]
  • context_len:上下文的长度,例如上面例子中的5
  • utterance:表示话语(响应)的一系列单词id
  • utterance_len:话语的长度
  • label:标签,在训练数据中才有。 0或1。
  • distractor_ [N]:仅在测试/验证数据中。 N的范围从0到8.表明干扰项的词序列id。
  • distractor_ [N] _len:仅在测试/验证数据中。 N的范围是从0到8.发音的长度。

预处理由Python脚本prepare_data.py 完成,该脚本生成3个文件:train.tfrecordsvalidation.tfrecordstest.tfrecords。 你能够本身运行脚本或者在这里下载数据文件 。

建立一个输入函数

为了使用Tensorflow内置的训练和评估支持,咱们须要建立一个输入函数 - 一个返回批量输入数据的函数。 事实上,因为咱们的训练和测试数据有不一样的格式,咱们须要不一样的输入功能。 输入函数应返回一批特征和标签(若是可用)。 模板以下:

def input_fn(): # TODO Load and preprocess data here return batched_features, labels

 

 

由于在训练和评估过程当中咱们须要不一样的输入函数,而且由于咱们讨厌复制代码,因此咱们建立了一个名为create_input_fn的包装器,以便为相应的模式(mode)建立一个输入函数。 它也须要一些其余参数。 这是咱们使用的定义:

 

完整的代码能够在udc_inputs.py中找到。 这个函数主要执行如下操做:

  1. 建立描述样例文件中字段的特征定义(feature definition
  2. 使用tf.TFRecordReader从输入文件中读取记录
  3. 根据特征定义解析记录
  4. 提取训练标签
  5. 将多个样例和培训标签构形成一个批次
  6. 返回批次

定义评估指标

咱们已经提到,咱们要使recall@ k指标来评估咱们的模型。 幸运的是,Tensorflow预置了不少咱们可使用的标准的评估指标,包括recall@ k。 要使用这些指标,咱们须要建立一个从指标名称映射到函数(以预测和标签为参数)的字典:

 

上面代码中,咱们使用functools.partial将一个带有3个参数的函数转换为只带有2个参数的函数。 不要让名称streaming_sparse_recall_at_k把你搞糊涂。 streaming只是意味着指标是在多个批次上累积的,而sparse则是指咱们标签的格式。

这带来了一个重要的问题:评估过程当中咱们的预测究竟是什么格式? 在训练期间,咱们预测样例正确的几率。 可是在评估过程当中,咱们的目标是对话语和9个干扰项进行评分,并挑选分最高的一个 - 咱们不能简单地预测正确仍是不正确。 这意味着在评估过程当中,每一个样例都应该获得一个有10个分值的向量,例如[0.34,0.1,0.22,0.45,0.01,0.02,0.03,0.08,0.33,0.11],每个分数分别对应于真实的响应和9个干扰项。 每一个话语都是独立评分的,因此几率不须要加起来为1.由于真正的响应在数组中老是为0,因此每一个例子的标签都是0。上面的例子将被recall@ 1指标视为分类错误,由于第三个干扰项的几率是0.45,而真实的回答只有0.34。 然而,它会被recall@ 2指标视为正确的。

brain

训练代码样板

在编写实际的神经网络代码以前,我喜欢编写用于训练和评估模型的样板代码。 这是由于,只要你坚持正确的接口,很容易换出你使用的是什么样的网络。 假设咱们有一个模型函数model_fn,它以批次特征,标签和模式(训练或评估)做为输入,并返回预测结果。 那么咱们能够编写以下的通用代码来训练咱们的模型:

estimator = tf.contrib.learn.Estimator(
    model_fn=model_fn,
    model_dir=MODEL_DIR,
    config=tf.contrib.learn.RunConfig())

input_fn_train = udc_inputs.create_input_fn(
    mode=tf.contrib.learn.ModeKeys.TRAIN,
    input_files=[TRAIN_FILE],
    batch_size=hparams.batch_size)

input_fn_eval = udc_inputs.create_input_fn(
    mode=tf.contrib.learn.ModeKeys.EVAL,
    input_files=[VALIDATION_FILE],
    batch_size=hparams.eval_batch_size,
    num_epochs=1)

eval_metrics = udc_metrics.create_evaluation_metrics()

# We need to subclass theis manually for now. The next TF version will
# have support ValidationMonitors with metrics built-in.
# It’s already on the master branch.
class EvaluationMonitor(tf.contrib.learn.monitors.EveryN):
    def every_n_step_end(self, step, outputs):
        self._estimator.evaluate(
            input_fn=input_fn_eval,
            metrics=eval_metrics,
            steps=None)

eval_monitor = EvaluationMonitor(every_n_steps=FLAGS.eval_every)
estimator.fit(input_fn=input_fn_train, steps=None, monitors=[eval_monitor])

 

 

 

在这里,咱们为model_fn,训练和评估数据的两个输入函数以及评估指标字典建立了一个估计器。 咱们还定义了一个监视器,在训练期间每隔FLAGS.eval_every_every指定的步数对模型进行评估。 最后,咱们训练模型。 训练过程能够无限期地运行,但Tensorflow能够自动地将检查点文件保存在MODEL_DIR指定的目录中,所以能够随时中止训练。 一个更炫的技巧是使用早期中止,这意味着当验证集指标中止改进时(即开始过拟合),将自动中止训练。 你能够在udc_train.py中看到完整的代码。

我想简要说起的两件事是FLAGS的使用。 这是给程序提供命令行参数的一种方法(相似于Pythonargparse)。 hparams是咱们在hparams.py中建立的一个自定义对象,它包含用来调整模型的参数、超参数。 咱们在实例化模型时将这个hparams对象赋予给模型。

建立模型

如今咱们已经创建了关于输入,解析,评估和训练的样板代码,能够为咱们的Dual LSTM神经网络编写代码了。 由于咱们有不一样格式的训练和评估数据,因此我写了一个create_model_fn包装器,它负责为咱们提供正确的格式。 它接受一个model_impl参数,应当指向一个实际进行预测的函数。 在咱们的例子中就是上面介绍的双编码器LSTM,可是咱们能够很容易地把它换成其余的神经网络。 让咱们看看是什么样的:

 1 def dual_encoder_model(
 2     hparams,
 3     mode,
 4     context,
 5     context_len,
 6     utterance,
 7     utterance_len,
 8     targets):
 9 
10     # Initialize embedidngs randomly or with pre-trained vectors if available
11     embeddings_W = get_embeddings(hparams)
12 
13     # Embed the context and the utterance
14     context_embedded = tf.nn.embedding_lookup(
15         embeddings_W, context, name=”embed_context”)
16 
17     utterance_embedded = tf.nn.embedding_lookup(
18         embeddings_W, utterance, name=”embed_utterance”)
19 
20     # Build the RNN
21     with tf.variable_scope(“rnn”) as vs:
22         # We use an LSTM Cell
23         cell = tf.nn.rnn_cell.LSTMCell(
24             hparams.rnn_dim,
25             forget_bias=2.0,
26             use_peepholes=True,
27             state_is_tuple=True)
28 
29     # Run the utterance and context through the RNN
30     rnn_outputs, rnn_states = tf.nn.dynamic_rnn(
31         cell,
32         tf.concat(0, [context_embedded, utterance_embedded]),
33         sequence_length=tf.concat(0, [context_len, utterance_len]),
34         dtype=tf.float32)
35 
36     encoding_context, encoding_utterance = tf.split(0, 2, rnn_states.h)
37 
38     with tf.variable_scope(“prediction”) as vs:
39         M = tf.get_variable(“M”,
40         shape=[hparams.rnn_dim, hparams.rnn_dim],
41         initializer=tf.truncated_normal_initializer())
42 
43     # “Predict” a response: c * M
44     generated_response = tf.matmul(encoding_context, M)
45     generated_response = tf.expand_dims(generated_response, 2)
46     encoding_utterance = tf.expand_dims(encoding_utterance, 2)
47 
48     # Dot product between generated response and actual response
49     # (c * M) * r
50     logits = tf.batch_matmul(generated_response, encoding_utterance, True)
51     logits = tf.squeeze(logits, [2])
52 
53     # Apply sigmoid to convert logits to probabilities
54     probs = tf.sigmoid(logits)
55 
56     # Calculate the binary cross-entropy loss
57     losses = tf.nn.sigmoid_cross_entropy_with_logits(logits, tf.to_float(targets))
58 
59     # Mean loss across the batch of examples
60     mean_loss = tf.reduce_mean(losses, name=”mean_loss”)
61 
62     return probs, mean_loss

 

完整的代码在dual_encoder.py中 。 鉴于此,咱们如今能够在咱们以前定义的udc_train.py的主例程中实例化咱们的模型函数。

model_fn = udc_model.create_model_fn( hparams=hparams, model_impl=dual_encoder_model)

 

好了! 咱们如今能够运行python udc_train.py,它将开始训练咱们的网络,间或评估验证数据的召回状况(你能够选择使用-eval_every开关来选择评估的频率)。 要得到咱们使用tf.flagshparams定义的全部可用的命令行标志的完整列表,你能够运行python udc_train.py --help

1 INFO:tensorflow:training step 20200, loss = 0.36895 (0.330 sec/batch).
2 INFO:tensorflow:Step 20201: mean_loss:0 = 0.385877
3 INFO:tensorflow:training step 20300, loss = 0.25251 (0.338 sec/batch).
4 INFO:tensorflow:Step 20301: mean_loss:0 = 0.405653
5 6 INFO:tensorflow:Results after 270 steps (0.248 sec/batch): recall_at_1 = 0.507581018519, recall_at_2 = 0.689699074074, recall_at_5 = 0.913020833333, recall_at_10 = 1.0, loss = 0.5383
7

 

评估模型

在你训练完模型以后,你能够在测试集上使用python udc_test.py - model_dir = $ MODEL_DIR_FROM_TRAINING来评估它,例如python udc_test.py - model_dir =〜/ github / chatbot-retrieval / runs / 1467389151。 这将在测试集而不是验证集上运行recall@ k评估指标。 请注意,你必须使用在训练期间使用的相同参数调用udc_test.py。 因此,若是你用 - embedding_size = 128进行训练,就须要用相同的方法调用测试脚本。

通过约20,000步的训练(在快速GPU上一个小时左右),咱们的模型在测试集上获得如下结果:

1 recall_at_1 = 0.507581018519
2 recall_at_2 = 0.689699074074
3 recall_at_5 = 0.913020833333

 

 

虽然recall@ 1接近咱们的TFIDF模型,recall@ 2recall@ 5显着更好,这代表咱们的神经网络为正确的答案分配了更高的分数。 原始论文中recall@1recall@2recall@5的值分别是0.55,0.72和0.92,可是我还没能重现。 也许额外的数据预处理或超参数优化可能会使分数上升一点。

预测

你能够修改并运行udc_predict.py,以获取不可见数据的几率得分。 例如python udc_predict.py — model_dir=./runs/1467576365/,将获得输出:

1 Context: Example context
2 Response 1: 0.44806
3 Response 2: 0.481638

 


你能够想象为,在一个上下文中输入100个潜在的响应,而后选择一个最高分的。

结论

在这篇文章中,咱们已经实现了一个基于检索的神经网络模型,能够根据对话上下文对潜在的响应打分。 然而,还有不少改进的余地。 能够想象,与双LSTM编码器相比,其余神经网络在这个任务上作得更好。 超参数优化还有不少空间,或者预处理步骤的改进。 本教程的代码和数据在Github上,请查看

原文:Ultimate Guide to Leveraging NLP & Machine Learning for your Chatbot

相关文章
相关标签/搜索