文本相似搜索算法以及改进
以前做的一个相关博文推荐的项目,整理了一下
目的:针对于博客,推荐内容相关博客 。
方法:将博客分词、去除停用词、tf-idf、标题加权等做成vsm向量,将一篇文章和其它的所有文章求相似度(文本一般采用cos相似度),然后取相似度最大的N篇文章 。
总共300万篇文章左右,遇到的困难:每篇文章都要和其它的300W篇做计算,然后去相似度的TopN,总共要计算的是300W*300W=90000亿次的计算,在初始计算的时候,计算量会特别大 ,当时计算的是大约要一个月左右的时间重跑一遍,当时也没有机器,只能借了2台服务服务器多线程跑,最后大致缩短到一个星期,但后续改进后整个算法的时间缩短到了3个小时左右。
文本的向量化
将一篇文章向量化其实有很多步骤要做,这里简单列下,列出最终的文本向量的数据结构
1、文本切词、去除停用词、计算idf、去除无用词、只取名词 。
2、单个文本切词后,统计并对此加权求权重。
3、统计文本中的所有词,每个词赋予整形的id,按照id排序,然后以稀疏矩阵的方式存储文本向量vsm 。
4、所有文本向量归一化,为了计算方便,将长度归一化到127(byte表示的最大数),向量的维度值都取整 。
几点:最初我们是将向量归一化到1,每个维度的取值是浮点小数 ,当时以为浮点计算会比较慢,所以将所有的替换成整数,但是效果提升的并不明显,所以在这样密集型的计算中,浮点数和byte整数的效率差别不是很大 。
结构如下:
文章id
{1,[{5,13},{2018,75},{5001,10},{6040,73},{6701,25}]}
{2,[{1,20},{12,71},{300,40},{5555,73}]}
.........................................................
{N,[{5,15},{300,71},{2018,37},{5001,73},{5555,23},{6701,15},{6900,20}]}
id是文章的id ,后续存储的是{词id,该词的维度值}
计算两篇文章的相似度算法:
类似于归并排序的方法,对于两个vsm从最初的id开始扫描,start_w_id_1 ,start_w_id_2 ,每次较小的向后移动一下,若是相等的话,则维度值相乘,不相等则跳过,继续完后扫描,总的时间复杂度是O(m+n)
一般的算法
针对每篇文章和其它的文章求相似度,然后选中相似度最大的前N篇文章 。
改进
因为向量很稀疏,所以有很多向量之间根本就没有共同的词,其cos值是0,所以可以直接过掉 ,这样需要做一个倒排向量的,以词的id和词所在的文章来做倒排表,这样每次要计算某一篇文章的时候,先根据这篇文章包含的词id到倒排表重寻找这些词的倒排链,然后合并倒排表(合并的算法和归并算法一样),然后只和筛选后的文章做相似度计算并选出TopN,这样筛选后的数量平均在20W向量左右(这个过滤还是挺大的)。
最终改进后的算法
修改倒排表,倒排链的每个文章id中附加上这个词在这篇文章中的权重,具体如下
首先将300万篇一次读入内存,建立如上图结构的基于单词的倒排索引,每篇文章经过归一化后向量长度均是127(byte型表示的最大数),并统计所有文章的最大id 。
2、在针对某篇文章计算前,在内存中初始化一个数据,数组的size是所有文章的最大id+1 。
shot[] cosValue=new shot[所有文章的最大id+1] ,如下
存储每篇文章的余弦值的数组cosValue(PS:数组的第0个元素舍去,不用做存储实际值),数组中只存储余弦公式中分子的值(因为向量经过归一化处理后,长度固定127,是以分母大小固定为127*127,所以比较cosValue的大小可以直接比较分母,经过计算后,cosValue数组的第k个值,即cosValue[k]即所求原文章与 id为k的文章 之间余弦值(的分子) 。
3、 计算
Ø 对于每一个要求的文章向量,扫描其文本向量中的每个词。
Ø 对于该向量的每个词(假设向量中该词的维度值为weight),找到该词在倒排序表中的索引数据 。
Ø 该行索引中的每个值分别与weight(该词在这篇文章中的维度值)相乘,累加到其id对应在 cosValue中的值,伪代码如下:
For(Type e :该行索引数据 )
cosValue[e.id]= cosValue[e.id]+e.value * weight ;
Ø 计算完毕后,在数组中查找出值最大的前N(现取得是5,需要一个大小是5的小顶堆,整个扫描一遍数组即可,其时间复杂度是O(m*logN))个,作为最相似的文章,其数组下标即为相似文章的id 。
相对于原先计算提升的地方:
内存改进的地方:原先以对象形式来存储向量数据,以集合类来存储这些数据 ,现在改为用基本类型来存储,以数组的形式存储 ,在计算和存储效率上面确实有一些改进。
效率改进的地方:前后最大的计算改进,原先的算法(在算两个向量的cosValue)的时候会大量使用if-else的操作(可以的话,自己实现下这块代码看看),这样在会严重阻断cpu的流水线操作,过量的判断分支操作使得cpu的流水线基本没有发挥作用,但是切换待第二种以后,所有的只是乘法、加法两种操作,速度提升了很多,所以在高密集型的计算操作中,尽量避免判断分支等阻断流水线操作,这样会使得性能下降很多,远不止流水线的5倍 。
分布式的问题:
由于把所有的数据均存入内存中,是以在内存消耗这一块着较大(300万左右的博文,平均取单词数目在40左右,功耗内存400M+);若条件允许的话,可以将倒排索引才用分布式在几台机器上存储,并行计算,最后结果累加。
github代码地址:https://github.com/ddc496601562/nlp-datamining-tools/tree/master/nlp-datamining-tools/vsm-similar-search