基于LSI/LDA的文本检索的原理和操作步骤

  自从上次写了那个SVD的计算步骤之后,一直都想补充这一篇——用SVD进行文本检索,刚好这次需求驱动,着手试了一把,实验结果感觉还行!
  SVD算是线性代数里面的东西,PCA、LSI、LDA这些算是不同应用目的的不同叫法吧,其中LSI全名叫Latent Semantic Indexing,看他的名字就是立志要用在文本信息检索里面,但是SVD的应用可不仅仅如此,比如图像处理、推荐系统、语音处理等等。
  关于LSI的教材,本来Edel Garcia有一系列比较好的教程的,但是不知道为什么后来全部删掉了,附录的两个是可以下载的,内容说的也很清楚明白,推荐阅读。SVD由于原理简单,实现好的库也有很多。本文采用了gensim库已经封装好了,使用教程中有详细的操作步骤,个人将测试代码推送到chinese_nlp库。目前小范围的测试效果还可以,不知道语料多了之后效果怎样,同时需要知道SVD是比较耗费计算量的。

一、计算步骤

  参考文档中介绍的具体计算步骤,这里描述下来。
  (1) 文本预处理:中文分词,然后去除停用词、删除低频词词,进行word->id转换;
  (2) 可选的优化,比如用TF-IDF为词汇加上局部权重;
  (3) 将训练文本用dictionary转换成id表现的形式,这就得到了Term-Document矩阵$A$;
  (4) $A=USV^t$,进行SVD分解,得到$U、S、V$矩阵;
  (5) 降维,将奇异值S减少为k个(topic-mode参数),当然k是个经验数字,比如200-500,然后$U$选前k列,$V$ 选前k列;$S$选左上角k行k列对角方阵,其实$V’$的低n行就是A第n列(第n个文本)的向量表示,且满足$V=A^TUS^{-1}$,或者任意一个文本$d=d^TUS^{-1}$;
  (6) 对于一个新的查询文本q,其查询向量为$q=q^TUS^{-1}$,那么任意两个文本的相似度就可以计算为
$$sim(q,d)=sim(q^TUS^{-1}, d^TUS^{-1})$$
  两个向量相似度的计算常常使用consine余弦相似度。

二、gensim库的实现与使用

  gensim是基于python的,其专注在NLP方面,什么TF-IDF、LSI、LDA、word2vec等模型应有尽有。
  本文使用gensim的LSI进行文本检索,其实在使用上除去语料预处理,实现LSI文本检索也就两个重要模块的调用:LsiModel和similarities。

1
2
lsi = models.LsiModel(tfidf_corpus, id2word=dictionary, num_topics=int(k_value))
index = similarities.MatrixSimilarity(lsi[corpus]) # transform corpus to LSI space and index it

2.1 LSI模块

  主要用于将原始语料投影到LSI的空间上来,通过SVD分解降维之后,将原始稀疏word-id表示,变成各个文本的近似短向量表示。这里需要额外说明的是,gensim在完成SVD的基本计算之外,还参考了很多工程上大数据量的计算情况:其实现支持增量计算,模型参数和检索文档可以在线更新(Streaming流计算);训练数据分割成chunk分批次运算,不受物理内存的限制;支持分布式计算。
  真正训练操作是从添加文本调用add_documents(corpus)开始,其内部调用结构如下:

1
2
3
4
5
6
7
8
update = Projection(self.num_terms, self.num_topics, job, extra_dims=self.extra_samples, power_iters=self.power_iters)
u, s = stochastic_svd(
docs, k, chunksize=sys.maxsize,
num_terms=m, power_iters=self.power_iters,
extra_dims=self.extra_dims)
self.u = u[:, :k].copy()
self.s = s[:k].copy()
self.projection.merge(update, decay=decay)

  可以见到,针对每一个chunk语料(封装到job中),通过Projection来进行投影(SVD的原理其实也就是个最大方差投影操作),其内部通过stochastic_svd分解得到u,v矩阵,接着通过调用merge,对每个chunk的计算结果进行合并,然后更新u、s的参数;当然如果语料大小小于chunk大小(默认20000),就能一次计算,上面stochastic_svd计算的u、s就直接返回了。
  projection.merge的合并计算,更新u、s参数还是很复杂的,其参照的是Fast and Faster: A Comparison of Two Streamed Matrix Decomposition Algorithms描述的流计算增量更新方法。

2.2 similarities模块

  用于计算查询文本和每个Document的相似度(consine),排序后就可以作为检索结果了。首先通过下式建立index(其实就是每个Doc降维后向量化表示形式)

1
index = similarities.MatrixSimilarity(lsi[corpus])

  上面的式子lsi[corpus]中的[]运算符(get_item)才是关键,通过

1
2
topic_dist = (vec.T * self.projection.u[:, :self.num_topics]).T # (x^T * u).T = u^-1 * x
topic_dist = (1.0 / self.projection.s[:self.num_topics]) * topic_dist

  计算将每个文档用num_topics维度的向量记录下来,就是传说中的检索索引了。
  这时候,当需要进行一个新的query时候,执行下面的步骤:(1)中文分词,然后使用dictionary.doc2bow将词转换成ID,如含有未登录词舍弃;(2)采用上面的计算符,将查询语句的word-id转换成LSI空间的文本短向量;(3)用生成的向量与index中的每个文本向量计算余弦相似度,返回结果;(4)对相似度进行排序,推荐Top-N最佳检索结果。

1
result = numpy.dot(self.index, query.T).T

  gensim模块的实现中有着大量的density,sparse,CCS的变换,在此就不纠结这些细节了,否则自己很容易就被绕晕了,还有那个在线merge更新的,如果有时间还要好好看看论文,不然理解不了。

三、结果显示

  测试使用的16000条的问题语料,k大约1300的时候生成文档索引,然后测试文档检索。总体来看还是比较靠谱的,似乎隐约感受到那些LSI声称的语意搜索了。
LSI问题检索

四、总结

  既然名字叫Latent Semantic Indexing,实际隐藏的那个变量就是topic,通过topic将word和doc关联起来。
  另外,如果计算的文本语料数量比较少的时候,那个1频次去词的步骤就不要要了,否则很多的检索词汇没有,反而导致检索精度的下降。

本文完!

参考