火眼金睛算法,教你海量短文本场景下去重

本文由QQ大数据发表算法

最朴素的作法数据库

在大多数状况下,大量的重复文本通常不会是什么好事情,好比互相抄袭的新闻,群发的垃圾短信,铺天盖地的广告文案等,这些都会形成网络内容的同质化并加剧数据库的存储负担,更糟糕的是下降了文本内容的质量。所以须要一种准确而高效率的文本去重算法。而最朴素的作法就是将全部文本进行两两比较,简单易理解,最符合人类的直觉,对于少许文原本说,实现起来也很方便,可是对于海量文原本说,这明显是行不通的,由于它的时间复杂度是,针对亿级别的文本去重时,时间消耗可能就要以年为单位,此路不通。网络

另外,咱们讲到去重,实际上暗含了两个方面的内容,第一是用什么方式去比较更为高效,第二是比较的时候去重标准是什么。这里的去重标准在文本领域来讲,就是如何度量两个文本的类似性,一般包含编辑距离,Jaccard距离,cosine距离,欧氏距离,语义距离等等,在不一样领域和场景下选用不一样的类似性度量方法,这里不是本文的重点,因此按下不表,下面着重解决如何进行高效率比较的问题。app

核心思想框架

下降时间复杂度的关键: > 尽力将潜在的类似文本聚合到一块,从而大大缩小须要比较的范围分布式

simHash算法工具

海量文本去重算法里面,最为知名的就是simHash算法,是谷歌提出来的一套算法,并被应用到实际的网页去重中。 simHash算法的最大特色是:将文本映射为一个01串,而且类似文本之间获得的01串也是类似的,只在少数几个位置上的0和1不同。为了表征原始文本的类似度,能够计算两个01串之间在多少个位置上不一样,这即是汉明距离,用来表征simHash算法下两个文本之间的类似度,一般来讲,越类似的文本,对应simHash映射获得的01串之间的汉明距离越小。大数据

为了让这个过程更为清晰,这里举个简单的例子。ui

t1 = "妈妈喊你来吃饭" t2 = "妈妈叫你来吃饭"blog

能够看到,上面这两个字符串虽然只有一个字不一样,可是经过简单的Hash算法获得的hash值可能就彻底不同了,于是没法利用获得的hash值来表征原始文本的类似性。然而经过simHash算法的映射后,获得的simHash值即是以下这样:

SH1 = "1000010010101101[1]1111110000010101101000[0]00111110000100101[1]001011" SH2 = "1000010010101101[0]1111110000010101101000[1]00111110000100101[0]001011"

仔细观察,上面的两个simHash值只有三个地方不同(不同的地方用"[]"标出),所以原始文本之间的汉明距离即是3。一般来讲,用于类似文本检测中的汉明距离判断标准就是3,也就是说,当两个文本对应的simHash之间的汉明距离小于或等于3,则认为这两个文本为类似,若是是要去重的话,就只能留下其中一个。

simHash算法的去重过程思路很简单,首先有一个关键点: > 假如类似文本判断标准为汉明距离3,在一个待去重语料集中存在两个类似文本,那也就是说这两个类似文本之间的汉明距离最大值为3(对应hash值最多有3个地方不一样),若是simHash为64位,能够将这个64位的hash值从高位到低位,划分红四个连续的16位,那么这3个不一样的位置最多只能填满4个中的任意3个区间(能够反过来想,若是这4个区间都填满了,那就变成汉明距离为4了)。也就是说两个类似文本一定在其中的一个连续16位上彻底一致。

想明白了这个关键点以后,就能够对整个待去重文本都进行一次simHash映射(本文中使用64位举例),接着将这些01串从高位到低位均分红四段,按照上面的讨论,两个类似的文本必定会有其中一段同样,仍用上面的例子,分红的四段以下所示:

t1 = "妈妈喊你来吃饭" SH1 = "1000010010101101[1]1111110000010101101000[0]00111110000100101[1]001011" SH1_1 = "1000010010101101" #第一段 SH1_2 = "[1]111111000001010" #第二段 SH1_3 = "1101000[0]00111110" #第三段 SH1_4 = "000100101[1]001011" #第四段 t2 = "妈妈叫你来吃饭" SH2 = "1000010010101101[0]1111110000010101101000[1]00111110000100101[0]001011" SH2_1 = "1000010010101101" #第一段 SH2_2 = "[0]111111000001010" #第二段 SH2_3 = "1101000[1]00111110" #第三段 SH2_4 = "000100101[0]001011" #第四段

这一步作完以后,接下来就是索引的创建。按照上面的讨论,每个simHash都从高位到低位均分红4段,每一段都是16位。在创建倒排索引的过程当中,这些截取出来的16位01串的片断,分别做为索引的key值,并将对应位置上具备这个片断的全部文本添加到这个索引的value域中。 直观上理解,首先有四个大桶,分别是1,2,3,4号(对应的是64位hash值中的第1、2、3、四段),在每个大桶中,又分别有个小桶,这些小桶的编号从0000000000000000到1111111111111111.在创建索引时,每个文本获得对应的simHash值后,分别去考察每一段(肯定是1,2,3和4中的哪一个大桶),再根据该段中的16位hash值,将文本放置到对应大桶中对应编号的小桶中。 索引创建好后,因为类似文本必定会存在于某一个16位hash值的桶中,所以针对这些分段的全部桶进行去重(能够并行作),即可以将文本集合中的全部类似文本去掉。

整个利用simHash进行去重的过程以下图所示:

img

总结一下,整个simHash去重的步骤主要是三个: 1. 针对每个待去重文本进行simHash映射; 2. 将simHash值分段创建倒排索引; 3. 在每个分段的hash值中并行化去重操做。

利用simHash进行去重有两个点很是关键: - simHash映射后仍然保持了原始文本的类似性; - 分而治之的思想大大下降了没必要要的比较次数。

所以,有了这两点作保证,对于长文本下的simHash算法以及使用汉明距离来度量文本之间的类似性,能够极大下降算法的时间复杂度,而且也能取得很好的去重效果。可是在短文本场景下,这种度量方法的效果将会变得不好,一般状况下,用来度量长文本类似的汉明距离阈值为3,可是短文本中,类似文本之间的汉明距离一般是大于3的,而且该算法中,基于汉明距离的类似性阈值选取的越高,该算法的时间复杂度也会越高,此时汉明距离没法继续做为短文本类似性的度量标准应用到短文本去重中。

基于文本局部信息的去重算法

基于文本局部信息的去重过程,其基本思想和simHash相似,只不过不是利用hash值,而是直接利用文本的一个子串做为key,而后凡是拥有这个子串的文本都会被放入到这个子串对应的桶中。 这里隐含了一个前提: > 任意两个可断定的类似文本,一定在一个或多个子串上是彻底一致的。

此外,子串的产生,能够经过相似于n-grams(若是是词和字层面的,对应shingles)的方法,直接从原始文本上滑动窗口截取,也能够去掉停用词后在剩下的有序词组合中截取,还能够对原始文本进行摘要生成后再截取,总之只要是基于原始文本或可接受范围内的有损文本,均可以利用相似的思想来产生这些做为索引的子串。

整个去重算法分为五个大的框架,分别包括:文本预处理,倒排索引的创建,并行化分治,去重算法的实现,文本归并等。

文本预处理

文本预处理根据所选用的具体子串截取方法的不一样,而有所不一样。若是子串是由词组合造成的,则须要对文本进行分词,若是须要去掉停用词,那么这也是文本预处理的工做。为了简化过程的分析,这里主要以原始文本直接截取子串为例,所以预处理的工做相对偏少一些。

倒排索引的创建

假定潜在的两个类似文本(要求去重后其中一个被去掉)分别是t1和t2,两者之间彻底一致的最大连续子文本串有k个,它们组成一个集合,将其定义为S = {s1,s2,...,sk},这些子文本串的长度也对应一个集合L = {l1,l2,...,lk},针对该特定的两个文本进行去重时,所选择的截取子文本串长度不能超过某一个阈值,由于若是截取长度超过了该阈值,这两个文本便再也不会拥有同一个子文本串的索引,于是算法自始至终都不会去比较这两个文本,从而没法达到去重的目的。这个阈值便被定义为这两个文本上的最大可去重长度,有:

img

在全部的全局文本上去重的话,相应的也有一个全局去重长度m,它表征了若是要将这部分全局文本中的类似文本进行去重的话,针对每个文本须要选取一个合适的截取长度。通常来讲,全局去重长度的选择跟去重率和算法的时间复杂度相关,实际选择的时候,都是去重率和时间复杂度的折中考虑。全局去重长度选择的越小,文本的去重效果越好(去重率会增大),但相应的时间复杂度也越高。全局去重长度选择越大,类似文本去重的效果变差(部分类似文本不会获得比较),但时间复杂度会下降。这里的缘由是:若是全局去重长度选择的太高,就会大于不少类似文本的最大可去重长度,于是这些类似文本便再也不会断定为类似文本,去重率于是会降低,但也正是由于比较次数减小,时间复杂度会下降。相反,随着全局去重长度的减少,更多的类似文本会划分到同一个索引下,通过类似度计算以后,相应的类似文本也会被去除掉,于是全局的去重率会上升,可是因为比较次数增多,时间复杂度会增大。

假定有一个从真实文本中抽样出来的类似文本集C,能够根据这个样例集来决定全局去重长度m,实际状况代表,一般来讲当m>=4(通常对应两个中文词的长度),算法并行计算的时候,时间复杂度已经下降到能够接受的范围,所以能够获得:

img

假定某个待去重的文本t,其长度为n。定义S为截取的m-gram子串的集合,根据m和n的大小关系,有下列两种状况: (1)当n>=m时,能够按照m的大小截取出一些m-gram子串的集合,该集合的大小为n-m+1,用符号表示为S = {s1,s2,...,sn-m+1}; (2)当n<m时,没法截取长度为m的子串,所以将整个文本做为一个总体加入到子串集合当中,所以有S={t}. 每个待去重文本的m-gram子串集合生成以后,针对每一个文本t,遍历对应集合中的元素,将该集合中的每个子串做为key,原始文本t做为对应value组合成一个key-value对。全部文本的m-gram子串集合遍历结束后,即可以获得每个文本与其n-m+1个m-gram子串的倒排索引。 接下来,按照索引key值的不一样,能够将同一个索引key值下的全部文本进行聚合,以便进行去重逻辑的实现。

img

算法的并行框架

这里的并行框架主要依托于Spark来实现,原始的文本集合以HDFS的形式存储在集群的各个节点上,将这些文本按照上面所讲的方法将每个文本划分到对应的索引下以后,以每个索引做为key进行hash,并根据hash值将全部待去重文本分配到相应的机器节点(下图中的Server),分布式集群中的每个工做节点只需负责本机器下的去重工做。基于Spark的分布式框架以下,每个Server即是一个工做节点,Driver负责分发和调配,将以HDFS存储形式的文本集合分发到这些节点上,至关于将潜在的可能重复文本进行一次粗粒度的各自聚合,不重复的文本已经被彻底分割开,于是每一个Server只须要负责该节点上的去重工做便可,最终每一个Server中留下的即是初次去重以后的文本。

img

去重的实现

并行化框架创建后,能够针对划分到每个索引下的文本进行两两比较(如上一个图所示,每个Server有可能处理多个索引对应的文本),从而作到文本去重。根据1中的分析,任意两个可断定的类似文本t1和t2,一定在一个或多个子文本串上是彻底一致的。根据3.1.1中的设定,这些彻底一致的最大连续子串组成了一个集合S = {s1,s2,...,sk},针对t1和t2划分m-gram子串的过程当中,假定能够分别获得m-gram子串的集合为S1和S2,不妨假设S中有一个子串为si,它的长度|si|大于全局去重长度m,那么必定能够将该子串si划分为|si|-m+1个m-gram子串,而且这些子串必定会既存在于S1中,也会存在于S2中。更进一步,t1和t2都会同时出如今以这|si|-m+1个m-gram子串为key的倒排索引中。

去重的时候,针对每个索引下的全部文本,能够计算两两之间的类似性。具体的作法是,动态维护一个结果集,初始状态下随机从该索引下的文本中选取一条做为种子文本,随后遍历该索引下的待去重文本,尝试将遍历到的每一条文本加入结果集中,在添加的过程当中,计算该遍历到的文本与结果集中的每一条文本是否能够断定为类似(利用类似性度量的阈值),若是与结果集中某条文本达到了类似的条件,则退出结果集的遍历,若是结果集中彻底遍历仍未触发类似条件,则代表这次待去重的文本和已知结果集中没有任何重复,所以将该文本添加到结果集中,并开始待去重文本的下一次遍历。 去重的时候,两个文本之间的类似性度量很是关键,直接影响到去重的效果。可使用的方法包括编辑距离、Jaccard类似度等等。在实际使用时,Jaccard类似度的计算通常要求将待比较的文本进行分词,假定两个待比较的文本分词后的集合分别为A和B,那么按照Jaccard类似度的定义能够获得这两个文本的类似度 显然,两个彻底不一致的文本其Jaccard类似度为0,相反两个彻底同样的文本其Jaccard类似度为1,所以Jaccard类似度是一个介于0和1之间的数,去重的时候,能够根据实际须要决定一个合适的阈值,大于该阈值的都将被断定为类似文本从而被去掉。

整个的去重实现伪代码以下:

初始状态: 文本集合T = {t_1,t_2,...,t_n} 去重结果R = {} 类似度阈值sim_th 输出结果: 去重结果R 算法过程: for i in T: flag = true for j in R: if( similarity(i,j) < sim_th ) flag = false break -> next i else continue -> next j if( flag ) R.append(i) #表示i文本和当前结果集中的任意文本都不重复,则将i添加到结果集中

文本归并去重

这一个步骤的主要目的是将分处在各个不一样机器节点上的文本按照预先编排好的id,从新进行一次普通的hash去重,由于根据上一步的过程当中,可能在不一样子串对应的桶中会留下同一个文本,这一步通过hash去重后,便将这些重复的id去除掉。 最终获得的结果即是,在整个文本集上,全部的重复文本都只保留了一条,完成了去重的目的。整个的去重流程以下图所示:

img

和simHash进行比较

这里提出来的去重算法与simHash比较,分别从时间复杂度和去重准确度上来讲,

首先,时间复杂度大大下降 - 分桶的个数根据文本量的大小动态变化,大约为文本数的2倍,平均单个桶内不到一条文本,桶内的计算复杂度大大下降;而simHash算法中,桶的个数是固定的4*216=26万个 - 通常来讲,只有类似文本才有类似的词组合,因此某个特定的词组合下类似文本占大多数,单个桶内的去重时间复杂度倾向于O(N);相应的,simHash单个桶内依然有不少不类似文本,去重时间复杂度倾向于O(N^2)

其次,类似性的度量更为精准: - 可使用更为精准的类似性度量工具,可是simHash的汉明距离在短文本里面行不通,召回过低,不少类似文本并不知足汉明距离小于3的条件

总结

这里提出的基于文本局部信息的去重算法,是在短文本场景下simHash等去重算法没法知足去重目的而提出的,实际上,一样也能够应用于长文本下的去重要求,理论上,时间复杂度能够比simHash低不少,效果可以和simHash差很少,惟一的缺点是存储空间会大一些,由于算法要求存储不少个文本的副本,但在存储这些文本的副本时候,可使用全局惟一的id来代替,因此存储压力并不会提高不少,相比时间复杂度的大大下降,这点空间存储压力是彻底能够承担的。

此文已由做者受权腾讯云+社区发布,更多原文请点击

搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!

相关文章
相关标签/搜索