QA问答系统中的深度学习技术实现

应用场景

智能问答机器人火得不行,开始研究深度学习在NLP领域的应用已经有一段时间,最近在用深度学习模型直接进行QA系统的问答匹配。主流的还是CNN和LSTM,在网上没有找到特别合适的可用的代码,自己先写了一个CNN的(theano),效果还行,跟论文中的结论是吻合的。目前已经应用到了我们的产品上。

原理

参看《Applying Deep Learning To Answer Selection: A Study And An Open Task》,文中比较了好几种网络结构,选择了效果相对较好的其中一个来实现,网络描述如下:

qacnn_v2

Q&A共用一个网络,网络中包括HL,CNN,P+T和Cosine_Similarity,HL是一个g(W*X+b)的非线性变换,CNN就不说了,P是max_pooling,T是激活函数Tanh,最后的Cosine_Similarity表示将Q&A输出的语义表示向量进行相似度计算。

详细描述下从输入到输出的矩阵变换过程:

  1. Qp:[batch_size, sequence_len],Qp是Q之前的一个表示(在上图中没有画出)。所有句子需要截断或padding到一个固定长度(因为后面的CNN一般是处理固定长度的矩阵),例如句子包含3个字ABC,我们选择固定长度sequence_len为100,则需要将这个句子padding成ABC<a><a>…<a>(100个字),其中的<a>就是添加的专门用于padding的无意义的符号。训练时都是做mini-batch的,所以这里是一个batch_size行的矩阵,每行是一个句子。
  2. Q:[batch_size, sequence_len, embedding_size]。句子中的每个字都需要转换成对应的字向量,字向量的维度大小是embedding_size,这样Qp就从一个2维的矩阵变成了3维的Q
  3. HL层输出:[batch_size, embedding_size, hl_size]。HL层:[embedding_size, hl_size],Q中的每个句子会通过和HL层的点积进行变换,相当于将每个字的字向量从embedding_size大小变换到hl_size大小。
  4. CNN+P+T输出:[batch_size, num_filters_total]。CNN的filter大小是[filter_size, hl_size],列大小是hl_size,这个和字向量的大小是一样的,所以对每个句子而言,每个filter出来的结果是一个列向量(而不是矩阵),列向量再取max-pooling就变成了一个数字,每个filter输出一个数字,num_filters_total个filter出来的结果当然就是[num_filters_total]大小的向量,这样就得到了一个句子的语义表示向量。T就是在输出结果上加上Tanh激活函数。
  5. Cosine_Similarity:[batch_size]。最后的一层并不是通常的分类或者回归的方法,而是采用了计算两个向量(Q&A)夹角的方法,下面是网络损失函数。t2,m是需要设定的参数margin,VQ、VA+、VA-分别是问题、正向答案、负向答案对应的语义表示向量。损失函数的意义就是:让正向答案和问题之间的向量cosine值要大于负向答案和问题的向量cosine值,大多少,就是margin这个参数来定义的。cosine值越大,两个向量越相近,所以通俗的说这个Loss就是要让正向的答案和问题愈来愈相似,让负向的答案和问题越来越不相似。

实现

代码点击这里,使用的数据是一份英文的insuranceQA,下面介绍代码重点部分:

字向量。本文采用字向量的方法,没有使用词向量。使用字向量的目的主要是为了解决未登录词的问题,这样在测试的时候就很少会遇到Unknown的字向量的问题了。而且字向量的效果也不一定比词向量的效果差,还省去了分词的各种麻烦。先用word2vec生成一份字向量,相当于我们在做pre-training了(之后测试了随机初始化字向量的方法,效果差不多)

原理中的步骤2。这里没有做HL层的变换,实际测试中,增加HL层有非常非常小的提升,所以在这里就省去了改步骤。

t4

CNN可以设置多种大小的filter,最后各种filter的结果会拼接起来。

t5

原理中的步骤4。这里执行卷积,max-pooling和Tanh激活。

t6

生成的ouputs_1是一个python的list,使用concatenate将list的多个tensor拼接起来(list中的每个tensor表示一种大小的filter卷积的结果)t7

原理中的步骤5。计算问题、正向答案、负向答案的向量夹角

t8

生成Loss损失函数和Accuracy。t9

核心的网络构建代码就是这些,其他的代码都是训练数据、验证数据的读入,以及theano构建训练时的一些常规代码。

如果需要增加HL层,可参照如下的代码。Whl即是HL层的网络,将input和Whl点积即可。t10

dropout的实现。

t11

结果

使用上面的代码,Test 1的Top-1 Accuracy可以达到61%-62%,和论文中的结论基本一致了,至于论文中提到的GESD、AESD等方法没有再测试了,运行较慢,其他数据集也没有再测试了。

下面是国外友人用一个叫keras的工具(封装的theano和tensorflow)弄的类似代码,Test 1的Top-1准确率在50%左右,比他这个要高:)

http://benjaminbolte.com/blog/2016/keras-language-modeling.html

Test set Top-1 Accuracy Mean Reciprocal Rank
Test 1 0.4933 0.6189
Test 2 0.4606 0.5968
Dev 0.4700 0.6088

另外,原始的insuranceQA需要进行一些处理才能在这个代码上使用,具体参看github上的说明吧。

一些技巧

  1. 字向量和词向量的效果相当。所以优先使用字向量,省去了分词的麻烦,还能更好的避免未登录词的问题,何乐而不为。
  2. 字向量不是固定的,在训练中会更新
  3. Dropout的使用对最高的准确率没有很大的影响,但是使用了Dropout的结果更稳定,准确率的波动会更小,所以建议还是要使用Dropout的。不过Dropout也不易过度使用,比如Dropout的keep_prob概率如果设置到0.25,则模型收敛得更慢,训练时间长很多,效果也有可能会更差,设置会差很多。我这版代码使用的keep_prob为0.5,同时保证准确率和训练时间。另外,Dropout只应用到了max-pooling的结果上,其他地方没有再使用了,过多的使用反而不好。
  4. 如何生成训练集。每个训练case需要一个问题+一个正向答案+一个负向答案,很明显问题和正向答案都是有的,负向答案的生成方法就是随机采样,这样就不需要涉及任何人工标注工作了,可以很方便的应用到大数据集上。
  5. HL层的效果不明显,有很微量的提升。如果HL层的大小是200,字向量是100,则HL层相当于将字向量再放大一倍,这个感觉没有多少信息可利用的,还不如直接将字向量设置成200,还省去了HL这一层的变换。
  6. margin的值一般都设置得比较小。这里用的是0.05
  7. 如果将Cosine_similarity这一层换成分类或者回归,印象中效果是不如Cosine_similarity的(具体数据忘了)
  8. num_filters越大并不是效果越好,基本到了一定程度就很难提升了,反而会降低训练速度。
  9. 同时也写了tensorflow版本代码,对比theano的,效果差不多
  10. Adam和SGD两种训练方法比较,Adam训练速度貌似会更快一些,效果基本也持平吧,没有太细节的对比。不过同样的网络+SGD,theano好像训练要更快一些。
  11. Loss和Accuracy是比较重要的监控参数。如果写一个新的网络的话,类似的指标是很有必要的,可以在每个迭代中评估网络是否正在收敛。因为调试比较麻烦,所以通过这些参数能评估你的网络写对没,参数设置是否正确。
  12. 网络的参数还是比较重要的,如果一些参数设置不合理,很有可能结果千差万别,记得最初用tensorflow实现的时候,应该是dropout设置得太小,导致效果很差,很久才找到原因。所以调参和微调网络还是需要一定的技巧和经验的,做这版代码的时候就经历了一段比较痛苦的调参过程,最开始还怀疑是网络设计或是代码有问题,最后总结应该就是参数没设置好。

结语

如果关注这个东西的人多的话,后面还可以有tensorflow版本的QA CNN,以及LSTM的代码奉上:)

补充

tensorflow的CNN代码已添加到github上,点击这里

Contact: jiangwen127@gmail.com weibo:码坛奥沙利文

此条目发表在机器学习, 深度学习, 自然语言处理, 问答系统分类目录。将固定链接加入收藏夹。

QA问答系统中的深度学习技术实现》有 50 条评论

  1. stella说:

    太赞啦~好想求个tensorflow版本的代码!

    [回复]

    jiangwen 回复:

    已添加tensorflow代码,代码在这

    [回复]

  2. ZC说:

    谢谢分享!

    [回复]

  3. 苏剑林说:

    原理基本是看懂了,但是有个问题,怎么应用呢?

    我给你一个问题,可以通过Q部分的模型生成一个句向量。但问题是,这时候怎么从A部分生成一个句向量,然后比较余弦值?难道所有容许的答案是可以枚举的?

    [回复]

    jiangwen 回复:

    应用场景是从答案库里选择一个最合适的答案,答案都是现成的

    [回复]

    苏剑林 回复:

    这时候我基本搞清楚原理了。但是,就我的经验,有几点建议。

    1、我不知道你的具体领域是什么,导致你基于字向量也能做出不错的效果,但是,就我的认识,还是必须分词的,因为word embedding的意思是每个word具有比较确定的意思,所以才可以放到实数空间中,显然,单个汉字不具有这个特点;

    2、如果基于字向量,那么我觉得one-hot的方法还更好(one-hot的方法好处是没有信息损失),因为常用汉字其实就只有几千个,维数不大,用one-hot+多层lstm的方法应该可以得到不错的效果(如果分词后,那么lstm的层数可以减1,我的理解是,可以多加一层lstm使得模型自动将字整合为词)。而用cnn,虽然效果确实也不错,但事实上cnn用于自然语言处理,原理上比较难说通。

    3、不知道答案库规模多大?如果足够大,可以通过doc2vec之类的工具,预先训练好答案库的句向量。

    [回复]

    刘阳 回复:

    想看看lstm版本的代码

    jiangwen 回复:

    1. embedding在词和字之间的差别,暂时还没看到有文章分析过。
    2. cnn在自然语言处理中的原理问题,我也觉得有部分是不好解释的,不过卷积的窗口,可以看做是类似n-gram的处理,这也可能是为什么用字向量也有不错效果的原因。有其他人的论文也比较过词向量和字向量的效果,是差不多的(具体应用忘了,好像是文本分类)。

  4. ly说:

    有LSTM+CNN版本的吗

    [回复]

    jiangwen 回复:

    有theano版本的,还没放上去

    [回复]

  5. qanlp说:

    多谢分享,我有个疑问:跑了一下你分享的代码(没有参数改动),发现cost(loss)在1.0-3.0之间抖动,好像并没有在收敛,accu也是。不知道有没有合理的解释?

    [回复]

  6. Shijia说:

    请问您的代码实验环境是啥样的?2000000 epoch需要跑多久?

    [回复]

  7. Shijia说:

    请问您的代码实验环境是啥样的?2000000 epoch需要跑多久?另外,看您代码中的margin设置的是0.05,请问训练结束后,loss稳定在多少附近?

    [回复]

    jiangwen 回复:

    实验环境是Tesla的GPU,具体型号忘了
    loss一般都会很小,至少<1(loss跟batch_size是成正比的,默认batch_size=256)
    2百万的epoch,batch_size=256,估计得跑个几天吧。GPU,一般跑个半天到一天差不多了吧,越到后面会收敛越慢

    [回复]

  8. 李狗蛋说:

    请问在训练过程中字向量更新体现在哪?谢谢~

    [回复]

    Jw 回复:

    lookup_table

    [回复]

  9. Lawrence Luo说:

    那位用Keras的老兄似乎并没有用到CNN或者LSTM层,Embedding后只加了一层Maxpool就出来了

    如果我在他Maxpool的前面加一层LSTM的话不知道能不能更高的准确度

    [回复]

    Jw 回复:

    他的代码里有3种方法,映像中是Embedding, CNN还有LSTM 3种

    [回复]

    nlp狗狗 回复:

    那位外国朋友的keras代码github有完整的吗?看到他的博客里有一小段,不全。

    [回复]

    chennlp 回复:

    他博客里的keras代码不完整,有完整的代码吗?

    [回复]

    chendl 回复:

    外国朋友的keras代码有完整的吗?他的博客不全

    [回复]

  10. 拉夫说:

    楼主很赞

    [回复]

  11. 朱军说:

    身边没有GPU,能不能把你的代码放在CPU上跑?

    [回复]

  12. 朱军说:

    身边没有GPU,能不能把你的代码放在CPU上跑?

    [回复]

    bbXU 回复:

    可以跑,很慢,相当慢

    [回复]

  13. cc说:

    请问你说的“字向量”是针对中文来说的吧?

    [回复]

    Shijia 回复:

    根据代码来看,在原始的英文数据集上好像并不是character level的,等作者解答了。

    [回复]

    cc 回复:

    从代码来看,并不是“字向量”,vectors.nobin 是作者用到的词向量,我猜作者是把这个用到中文问答中,没有做分词,采用的字向量,应该是如此了。

    [回复]

    jw 回复:

    就是用的字向量,字向量的效果也是可以的,所以省了分词带来的各种困扰

  14. cc说:

    请问你说的”字向量”是针对中文来说的吗?还是说英文也是character级别的?

    另外,老外的文章里给出的模型里没有上CNN和LSTM吧?

    [回复]

  15. johnny说:

    您好,想請問是否可以貼出一段教學,從下載您的github上的程式碼到 訓練過程的影片!
    感謝您的教學

    [回复]

    jiangwen 回复:

    真是还没有这个

    [回复]

  16. 风若水说:

    请问一下,为什么我用同样的程序去跑,不带GPU的半天才能跑一个epoch,而用GPU跑了半个月才跑了一半?各项参数都没有改动,只是把路径更改了一下,不知道这是什么原因?

    [回复]

  17. 刘峰说:

    请问,为什么上面的程序跑的非常慢(有GPU的机器上跑,半个月才跑了一半,没有GPU的机器半天才能跑一个epoch)?以上参数都没有被修改。

    [回复]

    bbXU 回复:

    你跑一个epoch大概要花多少时间呢?

    [回复]

  18. bbXU说:

    可以提供一下模型的保存及predict的代码么^_^

    [回复]

    jiangwen 回复:

    没有完整的写保存模型,读入模型,预测的代码,只是在训练过程中进行predict

    [回复]

  19. shangqi说:

    无法使用nltk中的babelizer,这个有人会解决吗

    [回复]

  20. NLPbeginner说:

    你的问答产品是什么?在哪里?能够体验一下效果吗?谢谢!

    [回复]

    jiangwen 回复:

    真正线上应用的产品一般不会只使用这一种策略,是组合了很多策略的,不会这么单一

    [回复]

  21. 方子寒说:

    您好,我在编译QA的过程中遇到了点问题,希望得到您的解答,可以留个邮箱给我吗,我截了图,谢谢

    [回复]

  22. HXM说:

    请问训练好之后要执行哪个文件进行预测?

    [回复]

  23. HXM说:

    请问训练好之后,需要执行哪个文件进行预测呢?

    [回复]

    jiangwen 回复:

    训练过程中会定期对测试数据进行预测评估效果的,可以参看这个代码

    [回复]

    pxchen 回复:

    你好,请问有tensorflow版的cnn+lstm代码吗

    [回复]

  24. guodao说:

    您好,可以把您的实验数据放到百度网盘吗?

    然后这个方法可以运用到中文问答场景吗?

    [回复]

  25. 刘乐说:

    您好,请问可以分析一下lstm+cnn网络的结构图吗?

    [回复]

  26. pxchen说:

    你好,请问有tensorflow版的cnn+lstm吗

    [回复]

  27. yw说:

    如何评价微软的QnA Maker, 导入问答数据就可以自动生成问答系统

    [回复]

  28. mark说:

    您好,test1.sample 中的 0/1 qid:18 question answer中 前面的0/1是什么意思,qid是什么意思?
    一直没看懂

    [回复]

发表评论

电子邮件地址不会被公开。 必填项已用*标注