Tensorflow聊天机器人
聊天机器人也叫做对话系统,是一个热门领域。微软、facebook、苹果、google、微信、slack都在上面做了大的投入,这是一波新的试图改变人和服务交流的创业浪潮。例如operator x.ai,chatfuel,以及一些库例如botkit,微软的bot开发库。
许多公司都希望机器人可以自然对话,和人类没有区别。并且许多对外声明说用了NLP和深度学习技术来实现这个目标。但围绕AI这些天花乱坠的宣传有时候也很难区别现实和虚化的差别。
我要在这个系列文章里将一些构建对话系统用到的深度学习技术。开始介绍一下我们的现状、哪些是可能做到的,哪些是短期内还几乎不可能实现的。本文是引言,后续还有介绍实现方式的
模型分类
检索式 VS 生成式
检索式模型(简单些)用一个预定义好的回复库并且在输入和上下文基础上启发式挑选近似回答。i挑选的办法可以简单用基于规则的正则匹配,也可以复杂一点用一些机器学习的分类器。系统不产生任何新的文本,都是在固定的集合里挑选答案。
生成式模型(难) 不依赖预定义好的回复库,生成新的语料答复。典型的依赖于机器翻译技术,但是并不是真正的从一门语言翻译到另外一门语言。我们是从一个输入翻译到一个输出(回复)
两种模型都有各自明显的优缺点。检索式因为依赖了预定义的语料,不会犯语法错误,然而可能没法处理语料库里没有遇到过的问题。同样的原因,也不能引用回上下文相关的实体信息例如对话前面提到名字、地点等。生成式模型更“聪明”,理论上可以识别上下文的实体信息(其实没见过业界有实现到的),但是模型非常难训练,且很容易犯语法错误(特别是长句子)并需要非常非常海量的训练数据。
两种模型都可以应用深度学习技术,但是学界研究都主要转移到生成式模型。像sentence to sentence 深度学习架构就非常适合生成文本,学者非常希望在这个领域有很快的进展。但是我们仍然在让这个模型建设且能好好工作的早期阶段。正式的产品系统现在还是更多采用检索式架构。
长 vs 短 对话
对话越长机器人越难自动跟上,另外一种情况就是目标是针对每个问题做一个答复的短文本对话。例如一个问题很容易得到满意的答复,但是多个问题的长对话就很难跟上,需要跟踪多轮对话并记住已经说过的话。客服机器人就是典型的需要在多轮对话上保持长对话的情景
开域 vs 闭域
开域的机器人,设定用户就是随时随地的开聊,不需要一个定义很好的目标和意图。例如在社交平台twitter、reddit上就是典型的开域。无限的话题、海量的知识库需求才能满足用户的聊天答复,所以这就非常难
闭域设定用户的输入以及机器人的输出都是在有限的空间里的,因为机器人有设定好的服务目的。技术支持、商店助手就是典型的闭域问题。这些系统不需要陪客人聊政治,只需要满足指定任务就好了。当然用户可以随时输入其他领域的问题,但是用户也没有期望机器人有好的互答
通用的挑战
当建设机器人的时候,还是有很多明显或者没那么明显的挑战存在,大多数已经研究的比较热门了。
综合上下文
去产生一个合理的回答,需要考虑语言学、语境、自然环境综合起来的上下文结果。在长对话里,人记住说过的话、交流过的信息。那是一个语言学的上下文,最常用的方式是把对话内容嵌入到一个向量,但是在长对话领域做这个事情非常难。Building End-To-End Dialogue Systems Using Generative Hierarchical Neural Network Models和Attention with Intention for a Neural Network Conversation Model两篇论文都走进这个领域做了些尝试。还需要结合其他环境信息例如:日期时间、地点、用户信息。
COHERENT PERSONALITY
人格一致性
理想情况下机器人应该对同一个语义输入保持一致的答复,例如问:“你多大?” “你的年龄是多少?”应该有同样的答复。这个听起来简单,但是没有固定的知识库或者模型里的“人格化"就是一个非常棘手的问题。许多系统学着产生一些花言巧语搪塞性质的话术来答复,但并没有训练成产生语义一致的回答。因为通常训练数据都是来自很多不同用户的海量数据,而不是同一个用户。A Persona-Based Neural Conversation Model 这个论文在尝试在建设一个人格化的模型方向迈出了第一步。
EVALUATION OF MODELS
模型评估
评估一个对话机器人的理想方式是量化它是否完成任务例如在特点对话里解决了一个客服问题。但是需要人工来判断这个目标,所以这个评估办法成本非常高,高到很难实现。有时候连目标都不是很明确,例如开放式领域的聊天机器人,什么都聊本身也没什么目标。常规的量化指标例如BLEU用于机器翻译,并且基于文本匹配的办法也不怎么合适,因为一个合理的答复可能包含很多不同的词语短语。实际有这篇论文:How NOT To Evaluate Your Dialogue System: An Empirical Study of Unsupervised Evaluation Metrics for Dialogue Response Generation研究组发现没有任何一种通用的评估办法和真正的和人类判断是相关的。
INTENTION AND DIVERSITY
意图和多样化
生成式模型有个常见的问题就是趋向于生成一些普适性的答案例如:“不错” 或者“不知道”。google的smart relpy早期版本就十分喜欢对几乎所有输入都回答:“我爱你” 可以参看:googleblog 。部分原因是系统学习训练的方式,包括数据和用的算法模型相关。一些研究者尝试提出多样化的目标函数来达到多样化。但是人类读输入做出的回复是带有明显的意图的,而生成式模型(特别是开域型系统)训练的时候是不带意图的,所以缺少多样性的。
那到底该怎样开展实际工作呢?
已经给到了所有的前沿研究现在,那我们到底处在什么阶段并且这些聊天机器人系统实际怎么搭建的呢?让我们再次考虑架构的分类上来,一个开域的机器人明显不可能利用检索式架构完成,因为你没法手工创造足够覆盖所有领域的回复库。一个生成式架构的开源机器人几乎达到人造的综合智能了(AGI),因为他需要处理所有可能的场景。我们离这个阶段还非常非常远,尽管很多人在这个领域做研究
剩下给我们的就是在闭域机器人上搭建检索式和生成式架构还算比较合适,但是随着对话越长、上下文更重要后,问题也变得更加难。
在最近的一个采访里:andrew Ng ,百度的首席科学家,说的不错:
“现在大量的深度学习的价值是在你能得到大量数据的垂直领域里。这里就有一些我们没法做的领域例如:产生一个有意义的对话。现在有一些demo,但如果你在一些看起来有意义的对话上稍微挑剔的试一下,很快就会偏离轨道”
许多公司开始开放他们的对话机器人产品给用户,并承诺他们能随着收集足够多的数据后会变得越来越好。其实这更容易在一些非常垂直的领域上容易取得成功,例如聊天呼叫uber打车。在开放一点的领域里就超越我们现在的能力范围了。然而我们可以用这些系统的提议和修正回复去辅助人工更可行一些。
在外网产品系统上犯语法错误是非常有代价的并且会赶走我们的用户。这也是为啥大部分外网产品优先选择检索式架构,它不会发语法错误或者冒犯用户的回答。如果公司可以放手在海量数据上做,生成式模型也可行,不但是也必须有其他技术去辅助防止偏离轨道,就像微软的tay
本文将要实现一个检索式的机器人。检索式架构有预定好的语料答复库。检索式模型的输入是上下文潜在的答复。模型输出对这些答复的打分,可以选择最高分的答案作为回复。
既然生成式的模型更弹性,也不需要预定义的语料,为何不选择它呢?
生成式模型的问题就是实际使用起来并不能好好工作,至少现在是。因为答复比较自由,容易犯语法错误和不相关、不合逻辑的答案,并且需要大量的数据且很难做优化。大量的生产系统上还是采用检索模型或者检索模型和生成模型结合的方式。例如google的smart reply。生成模型是研究的热门领域,但是我们还没到应用它的程度。如果你想要做一个聊天机器人,最好还是选用检索式模型
ubuntu语料库
本文采用了ubuntu语料库(UDC),它是目前公开的最大的数据集。paper介绍了这个语料库建设细节。我们就不重复说这个方面了。但是了解你所用到的语料是非常重要的。
训练数据包含100w的例子,一半是正面(label=1)一半是负面(lable=0),每个例子包含一个上下文,一个正面的标注意味着答复是符合上下文的,负面的标注意味着答复不符合(答复是从其他语料对里去随机挑选搭配过来的)
下面是一些例子:
注意下,上面的数据集生成脚本已经使用自然语言开发包 NLTK做了一系列的语料处理包括(分词,提取词干,词意恢复),脚本也做了把名字、地点、组织、URL。系统路径等实体信息用特殊的token来替代。这些预处理不是严格必要的,但是能改善一些系统的表现。语料的上下文平均有86个词语,答复平均有17个词语长。有人做了语料的统计分析:data analysis
数据集有测试和验证集。他们的格式和训练集不一样。每个测试验证集记录包含上下文、正确答复和9个不正确的干扰项。模型的目标是给争取答复打上高分,给干扰项目打上低分
有多个办法评估我们的模型,一种常用的办法是 K召回,它的意思是让模型在10个备选答案(1个正确答案和9个干扰项)里找出最好的K个答案,如果正确的那个答案在K个答案里,就标记为成功案例。所以K越大,模型的任务就越轻松,如果我们让K=10,这就得到一个100%的召回率,因为我们最多就10个备选。如果K=1,模型只有一次机会选中正确答案。
这里你或许好奇9个干扰项目怎么选出来的,这个数据集里是随机的方法选择的。但是现实世界里你可能数百万的可能答复,并且你并不知道答复是否合理正确。你没能力从数百万的可能的答复里去挑选一个得分最高的正确答复。成本太高了! google的smart reply用分布式集群技术计算一系列的可能答复去挑选paper,或者你只有百来个备选答案,可以去评估每一个
BASELINES
在开始梦幻的神经网络模型之前,我们先建立一些简单的基础模型去帮助我们理解我们模型有些什么表现可以期待。将要用下面的函数去评估K召回量化办法:
这里y是根据模型预测的分数降序排列的,y_test是数据集里的标注数据。例如y=[0,3,1,2,5,6,4,7,8,9]意味这0是分数最高的预测答案,9是最低分数。也y_test是10个备选答案,且下标为0的答案用于存正确的答案,后面都是干扰项。
直觉的来看,模型完全随机的预测正确答案,在K=1的时候,召回率是10%,K=2的时候,召回率是20%。
好的,看起来recall运行结果符合预期。当然我们不需要一个随机模型。另外一个基础模型还采用过tf-idf预测tf-idf代表:“term frequency – inverse document” frequency,(词频/文档),它可以量化一个词语在一个文档里的重要程度。这也是和整个语料库相关的指标。不继续深入tf idf,你可以网上找资料看看。含有相似内容的文档有相似的tf idf向量,直觉的看一个上下文问题和答复有相似的词语,更像是合格的问答对。至少比随机的办法好。需要多库都有内置的tf idf库函数实现例如scikit-learn.所以很容易使用,我们可以构造一个tf idf模型来看看表现如何
评估:
Recall @ (1, 10): 0.495032
Recall @ (2, 10): 0.596882
Recall @ (5, 10): 0.766121
Recall @ (10, 10): 1
可以看到,tf idf模型表现的比随机模型好非常多。但是这个效果里完美还很远,首先因为我们基于tf-idf是假设问答是相似的,其实正确的答案并不需要和问题相似,其次tfidf忽略了词语的顺序,而这个是一个重要的信息。用神经网络模型可以做的更好一点。
DUAL ENCODER LSTM
本文要构造的深度学习模型:Dual Encoder LSTM ,(双编码LSTM)
这是我们可以应用的神经网络中的一种,并不需要是最好的一个,你也可以继续看看所有的没有使用过的神经网络架构,这是个热门研究领域,例如句子到句子Sequence-to-Sequence Models的模型经常用到机器翻译方面,或许也能做的不错。而我们这次采用双编码的LSTM模型,因为被论文paper证明在这个语料库上表现不错,有相当不错的改进。这就意味着我们知道对这个模型的该有什么期待以及确保我们的实现是正确的。在这个问题上应用其他模型也是很有趣的项目。
双编码LSTM看起来是这样的:
它大致工作原理如下:
1、上下文问题和答案都分成每个词语,每个词语嵌入embedding成向量,词向量首先用standford的Glove工具,并在训练中进行调优(这个是可选项,并没有在上图里表达出来。我也发现用Glove来初始化词向量并没有显著改善模型的表现)
2、向量化后的问题和答案都输入一个词一个词的输入到同一个循环神经网络RNN里,RNN就会产生一个向量,大致可以当作是问题和答案(图里的c和r)的“语义”,我们可以选择向量的维度大小,这里我们用了256维
3、用一个矩阵M乘以c得到一个答案的预测结果r' ,c是一个256d的向量,M就是一个256x256的矩阵,所以结果是另外一个256d的向量,当作是生成的回复。M需要在训练中学习
4、我们计算r'和r的相似度。方法是对向量就点积,点积越大说明向量越相似,这个答案就应该得到更高的分数,然后我们用sigmoid逻辑函数把这个分数转化为一个概率。注意上图里把第三步和第四步放到一起了。
我们需要一个损失(代价)函数来训练网络。将要使用分类问题常用到的二分cross-entropy.对QA对(上下文问题和答复)的真实标注为y,y可以是1(实际答复)或者0(不正确答案),上面第四步计算出来的概率记作 y‘ 。所以这个corss entropy计算公式是:L= −y * ln(y') − (1 − y) * ln(1−y') 公式背后的意义很简单:如果y=1,L = -ln(y'),这就惩罚了那些预测值远离1的情况,如果y=0,L= −ln(1−y'),惩罚了那些预测值远离0的情况。
实现代码上我们使用了numpy pandas tensorflow 以及TF-Learn (有一些对高层次tensorflow函数的组合使用)
DATA PREPROCESSING 数据预处理
数据集本来是CSV格式,虽然我们可以直接使用,但是最好是把数据转化成tensorflow例子用到的格式,(也有tf.SequenceExample,但是看起来tf.learn不支持)用这个格式的好处是允许tensorflow直接load输入数据,并接着完成清洗、批量操作、排队等环节。我们也需要建设一个词库,和意味着需要映射每一个词语为一个整数,例如 cat 这个词语或许可以用 2631表示,TFRecord文件会生成并存储这些数字,而不是存储词语。我们也要存储这个映射表方便用整数找到词语。
每个例子包含下面的字段:
1、上下文(Q):代表上下文文本的一系列词id序列例如[231,2190,737,0,912]
2、上下文长度
3、回复 :同1一样的词id序列
4、回复长度
5、标注:训练数据里才有 0 or 1
6、干扰项[N] 测试/验证集里有,N 属于0-8.都是词id的序列
7、干扰项[N]长度
预处理代码prepare_data.py,产生三个文件train.tfrecords, validation.tfrecords test.tfrecords ,你可以下载UDC数据集自己运行下。
CREATING AN INPUT FUNCTION
创建一个输入函数
为了利用tesorflow内置的支持训练和评估的功能,我们需要创建一个输入函数,输入函数返回输入数据的批量结果。实际上因为训练数据和测试数据格式不同,我们需要分别些对应的输入函数。输入函数应该返回一系列特征和标注(如果允许)例如下面的代码行:
因为需要在训练和评估中用不同的输入函数,且我们讨厌代码拷贝,于是我们创建一个叫做create_input_fn的封装,里面为适当的模式(mode)创建合适的输入函数。当然需要一些其他参数帮忙,下面是我们正在用的定义:
完整的代码可以看udc_inputs.py,概括起来,这个函数实现下列功能:
1、定义了描述example 文件的字段的特征
2、用tf.TFRecordReader从输入文件中读取数据
3、根据字段特征定义解析记录
4、提取训练标注
5、批量处理examples和训练标注
6、返回批量处理好的examples和训练标注
DEFINING EVALUATION METRICS
定义评估标准
我们已经提到要用recall@K来评价模型,幸运的是tensorflow已经提供许多标准的评估办法,包括recall@K, 要使用这些功能,需要创建一个映射评估方法名到一个以预测结果和标注数据作为参数的处理函数的字典。
上面我们用到了functools.partial去把一个需要3个参数的函数转化只需要2个参数。别让streaming_sparse_recall_at_k 这个名字把你搞晕了,Streaming(流)只是代表这个量化办法是在多个批量结果上的累加求和,并且sparse(稀疏)和我们的标注格式有关。
这带来一个重要的点:评估期间,到底我们的预测结果是一个什么形式\格式呢?在训练阶段,我们预测样本例子里答复是否正确的概率。但是在评估阶段,我们的目的是给测试样本里的答复和9个干扰项目打分并且挑选最好的1个答案---我们没有简单预测正确或者错误。这就代表我们给每个样本评估的时候要返回一个10个分数的向量,例如
[0.34, 0.11, 0.22, 0.45, 0.01, 0.02, 0.03, 0.08, 0.33, 0.11]
这些分数和10个备选答案各自独立相关,所以概率之和不需要为1。因为在测试样本里下标为0的答案是真的回答。测试集里每个样本的标注数据全是0(译注:这是在介绍数据集格式,有些干扰文章)。 上面这个得分向量结果来看,因为第一个答复是第二大的(第一大的0.45),所以这个样本的测试结果在recall@1里,第一个答复只能算错误,那么测试为模型失败。而在recall@2里,则测试通过。
BOILERPLATE TRAINING CODE
训练代码的范式化
在些实际的神经网络之前,我想要写一个针对训练和评估模型的代码范式。因为一旦你依赖了正确的接口方式,就非常方便更换你用过任何类型神经网络模型。我们假设有一个模型函数model_fn用批量特征、标注数据、模式(训练 或者 评估)为参数,并返回预测结果。那么我们可以写出训练模型的通用代码:
这里我们为模型创建了评估器modle_fn,训练和评估数据的两个输入函数,和我们的评估办法(一堆映射了recall k的函数集合),也定义了一个监视函数,在训练过程中每FLAGS.eval_every评价一下模型。训练程序是无休止的运行,但是tensorflow会自动保存节点数据到MODEL_DIR目录下,所以你可以任何时候停止训练。还有一个非常不错的技术就是early stopping要用上,意味着当验证集结果已经没有什么改善的时候(例如模型开始过拟合了)就要自动停止训练。可以看完整的代码udc_train.py
上面提到FLAGS,关于它我简要提两点:
1、FLAGS是一种拿到命令行方法的途径,类似python的参数解析。
2、hparams是在hparams.py代码里用户自定义的对象,保存了假设参数、网络节点数等我们可以调整模型的数据。这些hparams对象实例化的时候就给到模型。
创建模型
现在我们已经设置好了数据输入、解析、评估、训练方面的代码规范接口,该是我们些DUAL LSTM神经网络模型代码的时候了。我们已经写了一个create_model_fn的封装来透明训练数据和验证数据格式的不同,把数据都转为模型需要的格式。需要一个model_impl作为参数,这是实际做预测的函数,在我们这里就是要用的这个Dual Encoder LSTM,但是我们可以简单更换模型为其他神经网络。我们看看到底是怎样的代码:
完整的代码在dual_encoder.py,到这里我们可以给训练过程代码udc_train.py实例化模型了。
好了,我们可以运行:
python udc_train.py
它就可以开始训练网络,并时而验证出召回率数据(你可以设置这个验证频率,参数是eval_every),可以运行python udc_train.py --help. 得到我们定义tf.flags和hparams的完整命令行标志列表。
评估模型:
训练完数据后,你可以运行
python udc_test.py --model_dir=$MODEL_DIR_FROM_TRAINING,
例如:.python udc_test.py --model_dir=~/github/chatbot-retrieval/runs/1467389151.
来在测试集上测试模型。这将在测试集而不是验证集上计算recall@K结果。注意训练和测试用到的参数要一致,例如用128维的向量来embbeding,那么测试脚本也要用同样的大小。
训练大概20000轮(快一点的GPU上计算,需要大约一个小时)得到的模型在测试集上的测试结果如下:
recall_at_1 = 0.507581018519
recall_at_2 = 0.689699074074
recall_at_5 = 0.913020833333
recall@1的结果和TFIDF模型接近,recall@2,recall@5有明显的改善,暗示我们的神经网络在正确答案上分配了更高的分数。本文参考的论文汇报的对应recall@1、recall@2、recall@5结果是0.55\0.72\0.92,但是我没法重现这么高的分数。或许数据预处理或者神经网络参数优化可以把分数再提高一点。
MAKING PREDICTIONS
预测
你可以修改和运行udc_predict.py去得到新输入的问题答案的预测得分,例如:
python udc_predict.py --model_dir=./runs/1467576365/
输出
Context: Example context
Response 1: 0.44806
Response 2: 0.481638
你可以给同一个上下文问题输入100个潜在的答复数据去测试分数,然后选择最高分数的那个答案了。
结论:
本文我们已经实现了一个检索式架构的神经网络模型可以给定一个上下文问题下给潜在答复去打分。这里有很多改进空间,可以用其他神经网络替代dual LSTM encoder,也可以在参数优化方面下手,或者在数据预处理上做。本文的代码和数据可以在github上得到 chatbot:code&data