返回小栈
关于自然语言处理系列-聊天机器人之gensim
bq_wang2020-03-11 11:06:14

聊天机器人主要包括检索式聊天机器人和搭建神经网络机器人。

检索式聊天机器人包括:

1、检索式问答系统核心技术之倒排索引

技术点:关键词抽取,tf-idf,倒排索引,NLP

2、检索式问答系统核心技术之rank

技术点:ctr预估,learning to rank,排序模型指标评测,逻辑回归,gbdt

神经网络机器人包括:

1、核心技术之神经网络

技术点:神经网络,word2vec,DNN,seq2seq,lstm

2、前沿技术之度量学习

技术点:双塔模型,度量学习,triple-loss

现阶段讨论的是检索式聊天机器人,而搭建检索式聊天机器人目前主要通过Gensim来完成。

Gensim是一个免费的 Python库,旨在处理原始的非结构化数字文本。

在Gensim的算法,比如Word2Vec,FastText,潜在语义分析(LSI,LSA,see LsiModel),隐含狄利克雷分布(LDA,见LdaModel)等算法,这些算法是无监督学习的。

几个专有名词和概念

Document(文档): 一段文本或一篇文档。

Corpus(语料库): 文档的集合,也就是多文本多文档

Vector(向量): 文档的一种数学表达方式。

Model(模型): an algorithm for transforming vectors from one representation to another.(一种将向量从一种表示形式转换为另一种表示形式的算法。),听起来比较晦涩,实际上就是将向量转换为Gensim中内置的数据结构,以提升数据处理效率。

可以将整个语料库加载到内存中。但在实践中,语料库可能非常大,以至于无法直接加载到内存中。Gensim可以通过流式处理文档的方式进行语料库处理。


收集完完成语料库后,需要做一些数据预处理。

对于英文来讲,通过空格分隔符,可实现单词分解,然后再删除一些常用的英语单词和在语料库中只出现一次的单词。

对于中文而言,需要通过jieba、pkuseg进行分词,再进行停用词处理或剔除频次极地的词汇。

对于本文而言,由于聊天记录是非常口语化的,建议只做分词,不做进一步处理。

数据预处理后,需要将语料库中的每个单词与一个唯一的整数ID相关联,通过gensim.corpora.Dictionary类来进行,生成一个词典。


向量

为了推断我们语料库中的潜在结构,需要一种用数学方法处理文档的方法,这种方法就是将每个文档表示为特征向量。例如:

splonge这个词在文档中出现了多少次?0次

文本由几段语句组成?两个。

文档使用多少字体?五个

问题通常用整数id(如1、2和3)表示,文档的表示则为一系列键值对,如(1,0.0),(2,2.0),(3,5.0),这就是所谓的稠密向量,因为它包含对上述每个问题的明确答案。

如果事先知道所有问题,就可以将它们隐式地表示为(0,2,5),这个答案序列就是文档向量。出于实际目的,Gensim中只允许可以转换为单浮点数的答案的问题。

实际上,向量通常由许多零值组成。为了节省内存,Gensim忽略值为0.0的向量元素,上面的示例也就变成了(2,2.0),(3,5.0)。这就是所谓的稀疏向量或词袋向量。

假设相同的问题,我们可以比较两个不同文档的向量。例如,假设我们有两个向量(0.0,2.0,5.0)和(0.1,1.9,4.9)。因为这些向量彼此非常相似,所以对应于这些向量的文档也很相似。当然,这个结论的正确性取决于我们在第一时间选择问题的好坏。

另一种将文档表示为向量的方法是词袋模型。词袋模型中,每个文档由一个单词和单词词频的向量字典构成。例如,假设我们有一个包含单词[“咖啡”、“牛奶”、“糖”、“勺子”]的字典。一篇包含“咖啡 牛奶 咖啡”组成的文档由向量[2,1,0,0]表达,其中向量的条目文档中的全部单词,向量的长度是字典中的条目数。词袋模型完全忽略了标记的顺序。

模型

之前是将语料库向量化,现在开始使用模型对其进行转换。模型是将文档从一个表示转换到另外一种模式。在gensim中,文档被表示为向量,因此模型可以看作是两个向量空间之间的转换。当模型读取训练语料时,会在训练过程中进行转换。

创建好模型,就可以用它做各种各样的酷的事情。例如,要通过TfIdf转换整个语料库并对其进行索引,以准备相似性查询:


这里有一个简单的例子。让我们初始化tf-idf模型,在我们的语料库上对其进行训练,并转换字符串“系统子项”:

corpora.Dictionary 生成词典,{'南京': 0, '哪里': 1, '在': 2, '他': 3, '以为': 4, '会': 5, '我': 6, '是': 7, '从不'}

dictionary.doc2bow是把文档 doc变成一个稀疏向量,[(0, 1), (1, 1)],表明id为0,1的词汇出现了1次,至于其他词汇,没有出现

文档和向量的区别在于前者是文本,后者是文本在数学上的方便表示。

models.TfidfModel是通过tf-idf模型将词包表示中的向量转换成一个向量空间,在向量空间中,根据每个词在语料库中的相对稀疏性对频率计数进行加权。


TfidfModel,基于文档词频和逆文本频率指数,把结果向量规格化为(欧几里德)单位长度。

LsiModel,LSI/LSA潜在语义索引,将文档从单词或TfIdf权重转换为低维稀疏矩阵。一般情况下推进200-500维度,LSI可以增量训练

RpModel,随机投影(RP)旨在降低向量空间维数

LdaModel,LDA是另一个从词袋计数到低维主题空间的转换,是LSA的扩展,LDA的主题可以解释为单词上的概率分布

HdpModel,HDP是一种非参数贝叶斯方法


所有索引类都可以用similarities.Similarity,similarities.MatrixSimilarity和similarities.SparseMatrixSimilarity

similarities.MatrixSimilarity类仅仅适合能将所有的向量都在内存中的情况

similarities.Similarity类适合在向量存到硬盘上。

similarities.SparseMatrixSimilarity类是稀疏矩阵方式,不过有毒

索引也可以通过标准的save()和load()函数来存储到硬盘

代码示例

  1. from collections import defaultdict

  2. from gensim import corpora

  3. from gensim import similarities

  4. from gensim import models

  5. import pprint

  6. # Document -- Gensim的文档,一般是字符串

  7. # 不过英文词汇是靠空格天然分割的,通过split()分开即可

  8. # 中文文档需要通过jiebapkuseg进行分词预处理后,才等同于Gensim的文档

  9. document_en = "Human machine interface for lab abc computer applications"

  10. document_ch_org = "实验室abc计算机应用的人机界面"

  11. document_ch = "实验室 abc 计算机 应用 的 人机 界面"

  12. # Corpus -- Gensim的语料库,是文档的集合对象,有以下两种作用:

  13. # 1、作为模型训练的输入。训练过程中,模型通过训练语料库发现共同的主题,并初始化内部模型参数。

  14. # Gensim侧重无监督训练模型,不需要人工干预,如注释或手工标记。

  15. # 2、文档重组。经过训练后,主题模型便可用于从新文档中提取主题。

  16. # 语料库可以被索引,应用于相似度查询、语义相似度、聚类等场景。

  17. text_corpus_en = [

  18. "Human machine interface for lab abc computer applications",

  19. "A survey of user opinion of computer system response time",

  20. "The EPS user interface management system",

  21. "System and human system engineering testing of EPS",

  22. "Relation of user perceived response time to error measurement",

  23. "The generation of random binary unordered trees",

  24. "The intersection graph of paths in trees",

  25. "Graph minors IV Widths of trees and well quasi ordering",

  26. "Graph minors A survey",

  27. ]

  28. text_corpus_ch_org = [

  29. '南京在哪里',

  30. '我以为会是他',

  31. '我从不说反话',

  32. '我没有,哈哈 ,你这个大熊猫'

  33. ]

  34. text_corpus_ch = [

  35. '南京 在 哪里 ',

  36. '我 以为 会 是 他 ',

  37. '我 从不 说 反话 ',

  38. '我 没有 , 哈哈 , 你 这个 大熊猫 '

  39. ]


  40. # ---------------------------英文处理-------------------------------

  41. # 英文常用停用词

  42. stoplist_en = set('for a of the and to in'.split(' '))

  43. # 大小写转换后,再进行停用词过滤

  44. texts_en = [[word for word in document.lower().split() if word not in stoplist_en]

  45. for document in text_corpus_en]

  46. #texts_en= [

  47. # ['human', 'machine', 'interface', 'lab', 'abc', 'computer', 'applications'],

  48. # ['survey', 'user', 'opinion', 'computer', 'system', 'response', 'time']

  49. # 统计词频

  50. frequency = defaultdict(int)

  51. for text in texts_en:

  52. for token in text:

  53. frequency[token] += 1

  54. # frequency= defaultdict(<class 'int'>, {'human': 2, 'machine': 1, 'interface': 2, 'lab': 1, 'abc': 1, 'computer': 2,'applications': 1, 'survey': 2, 'user': 3,


  55. # 只保留出现一次以上的单词

  56. processed_corpus_en = [[token for token in text if frequency[token] > 1] for text in texts_en]

  57. # [['human', 'interface', 'computer'],

  58. # ['survey', 'user', 'computer', 'system', 'response', 'time'],


  59. # ---------------------------中文处理-------------------------------

  60. # 中文常用停用词,因为都是口语短语,所以只处理标点符号

  61. stoplist_ch = set(', . : ; ? ! , 。;:?!'.split(' '))


  62. # 进行停用词过滤

  63. texts_ch = [[word for word in document.lower().split() if word not in stoplist_ch]

  64. for document in text_corpus_ch]

  65. # texts_ch= [

  66. # ['南京', '在', '哪里'],

  67. # ['我', '以为', '会', '是', '他'],

  68. # ['我', '从不', '说', '反话'],

  69. # ['我', '没有', '哈哈', '你', '这个', '大熊猫']]


  70. # 统计词频

  71. frequency = defaultdict(int)

  72. for text in texts_ch:

  73. for token in text:

  74. frequency[token] += 1

  75. # frequency= defaultdict(<class 'int'>, {'南京': 1, '在': 1, '哪里': 1, '我': 3, '以为': 1, '会': 1, '是': 1, '他': 1, '从不': 1, '说': 1, '反话': 1, '没有': 1, '哈哈': 1, '你': 1, '这个': 1, '大熊猫': 1})


  76. # 文本较多时需要过滤低频单词,这里不做过滤,processed_corpus_ch = texts_ch

  77. # 所以下面代码可以忽略,理论上processed_corpus_ch等同于texts_ch

  78. processed_corpus_ch = [[token for token in text] for text in texts_ch]



  79. # ---------------------------语料库字典-------------------------------

  80. # 将预处理后的语料生成corpora词典

  81. dictionary_en = corpora.Dictionary(processed_corpus_en)

  82. # Dictionary(12 unique tokens: ['computer', 'human', 'interface', 'response', 'survey']...)

  83. dictionary_ch = corpora.Dictionary(processed_corpus_ch)

  84. # 存储字典

  85. dictionary_ch.save('dictionary_ch.dict')

  86. # Dictionary(21 unique tokens: ['南京', '哪里', '在', '他', '以为']...)

  87. # corpora.Dictionary包括了很多属性和方法,暂时只列举常用的一些属性

  88. pprint.pprint(dictionary_ch.token2id)

  89. # 通过pprint.pprint打印dictionary_ch.token2id,字典缺省按字符串排序

  90. # {'么': 18,'从不': 8,'他': 3, '以为': 4, '会': 5, '你': 11,'南京': ,'反话': 9,'哈哈': 12,'哪里': 1,'在': 2,'大熊猫': 13,'学长': 16,'必须': 17,'我': 6,'是': 7,'没有': 14,'笑话': 19,'说': 10,'还有': 20,'这个': 15}

  91. # dictionary_ch.token2id,返回单词->tokenid的对应表

  92. # {'南京': , '哪里': 1, '在': 2, '他': 3, '以为': 4, '会': 5, '我': 6, '是': 7, '从不': 8, '反话': 9, '说': 10, '你': 11, '哈哈': 12, '大熊猫': 13, '没有': 14, '这个': 15, '学长': 16, '必须': 17, '么': 18, '笑话': 19, '还有': 20}

  93. # dictionary_ch.dfs,返回tokenid->多少文档包含这个token

  94. # {: 1, 2: 1, 1: 1, 6: 3, 4: 1, 5: 1, 7: 2, 3: 1, 8: 1, 10: 1, 9: 1, 14: 1, 12: 1, 11: 1, 15: 1, 13: 1, 17: 1, 16: 1, 20: 1, 19: 1, 18: 1}

  95. # dictionary_ch.num_docs,返回处理文档的数量

  96. # 6

  97. # dictionary_ch.num_nnz,返回整个语料库中每个文档的唯一单词数之和)

  98. # 24


  99. # ---------------------------向量化-------------------------------

  100. # 例如,想把短语“南京 北京 南京”矢量化,可以使用字典的doc2bow方法为创建词袋,该方法返回单词计数的稀疏表示:

  101. new_doc = "南京 北京 我 南京"

  102. new_vec = dictionary_ch.doc2bow(new_doc.split())

  103. # [(, 2), (6, 1)] ,可以看到南京为2,我为1,北京不存在

  104. bow_corpus_ch = [dictionary_ch.doc2bow(text) for text in processed_corpus_ch]

  105. # [[(, 1), (1, 1), (2, 1)],

  106. # [(3, 1), (4, 1), (5, 1), (6, 1), (7, 1)],

  107. # [(6, 1), (8, 1), (9, 1), (10, 1)],

  108. # [(6, 1), (11, 1), (12, 1), (13, 1), (14, 1), (15, 1)]]

  109. corpora.MmCorpus.serialize('dictionary_ch.mm',bow_corpus_ch)

  110. # ---------------------------模型化-------------------------------

  111. # 训练模型,使用TfidfModel模型,将词包表示中的向量转换成一个向量空间

  112. tfidf_ch = models.TfidfModel(bow_corpus_ch)

  113. # tfidf_ch= TfidfModel(num_docs=4, num_nnz=18) 4篇文档,18个词


  114. # 转换字符串

  115. words = "南京 您好".lower().split()

  116. tfidf_ch[dictionary_ch.doc2bow(words)]

  117. # [(, 1.0)]

  118. words = "南京 我 从来 不 反话 学长 ".lower().split()

  119. tfidf_ch[dictionary_ch.doc2bow(words)]

  120. # [(, 0.699614836733826), (6, 0.1451831961481918), (9, 0.699614836733826)]


  121. # 通过similarities.MatrixSimilarity,完成相似度查询匹配

  122. index = similarities.MatrixSimilarity(tfidf_ch[bow_corpus_ch])

  123. # <gensim.similarities.docsim.SparseMatrixSimilarity object at 0x0000017161873C88>


  124. # 根据语料库中的每个文档查询新文档的文档相似性:

  125. query_document_ch = '我 从来 不 反话 学长'.split()

  126. #

  127. query_bow_ch = dictionary_ch.doc2bow(query_document_ch)

  128. # [(6, 1), (9, 1)]

  129. #

  130. tfidf_ch[query_bow_ch]

  131. #[(6, 0.20318977863036336), (9, 0.9791393740730397)]

  132. # 完成文本相似度匹配

  133. sims_ch = index[tfidf_ch[query_bow_ch]]

  134. # [0. 0.02097026 0.5854637 0.01877638]

  135. # 以列表方式打印

  136. print(list(enumerate(sims_ch)))

  137. # [(, 0.4039228), (1, 0.014983676), (2, 0.41832557), (3, 0.013416105)]

  138. # 排序后再打印

  139. for document_number, score in sorted(enumerate(sims_ch), key=lambda x: x[1], reverse=True):

  140. print(document_number, score,text_corpus_ch_org[document_number])

  141. # 2 0.5854637 我从不说反话

  142. # 1 0.020970263 我以为会是他

  143. # 3 0.018776383 我没有,哈哈 ,你这个大熊猫

  144. # 0.0 南京在哪里



4
0