simhash的背景、原理、计算、使用、存储

 

0x01 背景介绍

原文链接:Simhash的生成及存储 - chinawangfei的专栏 - CSDN博客

根据 Detecting Near-Duplicates for Web Crawling 论文中的介绍,在互联网中有很多网页的内容是一样的,但是它们的网页元素却不是完全相同的。每个域名下的网页总会有一些自己的东西,比如广告、导航栏、网站版权之类的东西,但是对于搜索引擎来讲,只有内容部分才是有意义的,虽然网页元素不同,但是对搜索结果没有任何影响,所以在判定内容是否重复的时候,应该忽视后面的部分。当新爬取的内容和数据库中的某个网页的内容一样的时候,就称其为Near-Duplicates(重复文章)。对于重复文章,不应再执行入库操作,这种操作的优点是(A)节省带宽、(B)节省磁盘、(C)减轻服务器负荷以及(D)去除相似文章噪点干扰,提升索引的质量。

在现实中,一模一样的网页的概率是很小的,大部分的相似网页都会存在一些细节的变化,而如何进行这种判定就是一个本文要解决的一个问题。除了近似文章判定算法的难题,还有以下待解决的难点(按照80亿篇文章来考虑):

数据规模巨大,对于海量数据如何存储

查找速度,如何做到在毫秒级别返回检索结果

0x02 simhash算法概述

一段文字所包含的信息,就是它的信息熵。如果对这段信息进行无损压缩编码,理论上编码后的最短长度就是它的信息熵大小。如果仅仅是用来做区分,则远不需要那么长的编码,任何一段信息(文字、语音、视频、图片等),都可以被映射(Hash编码)为一个不太长的随机数,作为区别这段信息和其他信息的指纹,只要Hash算法设计得好,任何两段信息的指纹都很难重复。

simhash是locality sensitive hash(局部敏感哈希)的一种,最早由Moses Charikar在《similarity estimation techniques from rounding algorithms》一文中提出。Google就是基于此算法实现网页文件查重的。

simhash属于局部敏感型(locality sensitive hash)的一种,主要思想是降维,将高维的特征向量映射成一个f-bit的指纹(fingerprint),通过比较两篇文章的f-bit指纹的Hamming Distance来确定文章是否重复或者高度近似,海明距离越小,相似度越低(根据 Detecting Near-Duplicates for Web Crawling 论文中所说),一般海明距离为3就代表两篇文章相同。。为了陈述方便,假设输入的是一个文档的特征集合,每个特征有一定的权重。比如特征可以是文档中的词,其权重可以是这个词出现的次数。

simhash算法如下:

1.将一个f维的向量V初始化为0;f位的二进制数S初始化为0;

2.对每一个特征:用传统的hash算法对该特征产生一个f位的fingerprint值b。对i=1到f: 如果b的第i位为1,则V的第i个元素加上该特征的权重;否则,V的第i个元素减去该特征的权重。

3.如果V的第i个元素大于0,则S的第i位为1,否则为0;

4.输出S作为fingerprint。

优点:

加密算法(MD5、SHA-1等)也是将不定长的信息变成定长的128位或160位二进制随机数,但是加密算法的一个明显特征是,两段信息哪怕只有微小的不同,最终结果也会截然不同。 SimHash的最大特点就是它的局部敏感性

  1. 因为SimHash算法最终是将权重值相加,而加法满足交换律,所以对词的顺序不敏感

  2. 如果两篇文章只有少数权重小的词不相同,几乎可以肯定它的SimHash值也会相同。 SimHash去重非常高效:一是指纹的计算可以离线进行,而是海明距离的计算也非常高效(直接异或,还可以使用Hash分桶原理,假如我们认为海明距离在3以内的具有很高的相似性,我们可以将simhash值分成4段,那么至少有一段完全相等的情况下才能满足海明距离在3以内)。

  3. 通过大量测试,simhash用于比较大文本,比如500字以上效果都还蛮好,距离小于3的基本都是相似,误判率也比较低。但是如果我们处理的是微博信息,最多也就140个字,使用simhash的效果并不那么理想,我们测试短文本很多看起来相似的距离确实为10,如果使用距离为3,短文本大量重复信息不会被过滤,如果使用距离为10,长文本的错误率也非常高。

缺点:

主要有两点, 对于短文本,k值很敏感,在处理小于500字的短文本时,simhash的表现并不是很好; 另一个是由于算法是以空间换时间,系统内存吃不消。

0x03 simhash算法几何意义和原理

首先,simhash将每一个特征映射到f维空间中的一个向量,具体的映射规则并不是十分重要,这里主要关注的是:对于不同的特征而言,其所对应的向量是均匀随机分布的;对于相同的特征而言,其对应的向量唯一。

其次,将一个文档中所包含的各个特征对应的向量加权求和,加权的系数为该特征对应的权重值,这个和的值即可以表征这个文档,因此可以使用向量之间的夹角衡量对应文档之间的相似度。

最后,为了最终得到一个f位的签名,需要进一步将其压缩,如果和向量的某一维大于0,则最终签名的对应位为1,否则为0。这样的压缩相当于只留下了和向量所在的象限这个信息,而64位的签名可以表示多达264个象限,因此只保存所在象限的信息也足够表征一个文档了。

0x04 simhash与hash对比

传统Hash算法只负责将原始内容尽量均匀随机地映射为一个签名值,原理上仅相当于伪随机数产生算法。传统hash算法产生的两个签名,如果不相等,除了说明原始内容不相等外,不再提供任何信息,因为即使原始内容只相差一个字节,所产生的签名也很可能差别很大,所以传统Hash是无法在签名的维度上来衡量原内容的相似度。而SimHash本身属于一种局部敏感哈希算法,它产生的hash签名在一定程度上可以表征原内容的相似度。

  我们主要解决的是文本相似度计算,要比较的是两个文章是否相似,当然我们降维生成了hash签名也是用于这个目的。看到这里,估计大家就明白了,即使把文章中的字符串变成 01 串,我们使用的simhash算法也还是可以用于计算相似度,而传统的hash却不行。我们可以来做个测试,两个相差只有一个字符的文本串,“你妈妈喊你回家吃饭哦,回家罗回家罗” 和 “你妈妈叫你回家吃饭啦,回家罗回家罗”。

通过simhash计算结果为:  

1000010010101101111111100000101011010001001111100001001011001011  1000010010101101011111100000101011010001001111100001101010001011

通过传统hash计算为:

0001000001100110100111011011110

1010010001111111110010110011101

大家可以看得出来,相似的文本只有部分 01 串变化了,而普通的hash却不能做到,这个就是局部敏感哈希的魅力。

0x05 simhash算法流程

原文链接:simhash文本相似度计算 - a1368783069的专栏 - CSDN博客

五个步骤:分词->hash->加权->合并->降维

(1)分词---jieba

把需要判断文本分词形成这个文章的特征单词。最后形成去掉噪音词的单词序列并为每个词加上权重,得到若干个(词组,权重),假设权重分为5个级别(1~5)。

比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==> 分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”,括号里是代表单词在整个句子里重要程度,数字越大越重要。

在上图中,feature表示分词后的词语,英文中以单个单词表示,并去掉词语中的stop words,w1……wn表示对应的feature(词语)的权重,这个权重可以是 tf-idf 或词频值。

权重一般用 tf-idf算法,TF表示词组在本文章内的频次比例,出现越多则对这篇文章来说越重要,文章分词后TF可以立马计算出来。

IDF是词组在所有文章中的出现比例,出现越多说明词组对文章的区分度越低越不重要,但是IDF因为需要基于所有文章统计,所以一般是离线去批量计算出一个IDF字典。

结巴分词支持加载IDF词典并且提供了一个默认的词典,它包含了大量的词组以及基于海量文本统计出来的IDF词频,基本可以拿来即用,除非你想自己去挖掘这样一个字典。如果文章产生的分词在IDF字典里不存在,那么会采用IDF字典的中位数作为默认IDF,所谓中庸之道。

综上,可以使用结巴分词做一个分词服务,产生文章的(分词,权重),作为simhash计算的输入。

文章分词产生的词组太多,一般我们取 tf-idf最大的N个,这个N根据具体情况而定。

(2)hash

通过hash算法把每个词变成hash值,比如“美国”通过hash算法计算为 100101,“51区”通过hash算法计算为 101011。这样,字符串就变成了一串串数字,因为转化为数字计算可以降维,能提高相似度计算性能。

对于每个词组,套用一个哈希算法(比如times33,murmurhash…)把词组转换成一个64位的整型,对应的二进制就是01010101000…这样的。

 

(3)加权

通过(2)步骤的hash生成结果,需要按照单词的权重形成加权数字串,比如“美国”的hash值为“100101”,通过加权计算为“4 -4 -4 4 -4 4;“51区”的hash值为“101011”,通过加权计算为 “ 5 -5 5 -5 5 5”。

即遍历词组哈希值的64比特,若对应位置是0则记为-权重,是1就是+权重,可以得到一个宽度64的向量:[-权重,+权重,-权重,+权重…]。

 

(4)合并

把上面各个单词算出来的序列值累加,变成只有一个序列串。比如 “美国”的 “4 -4 -4 4 -4 4”,“51区”的 “ 5 -5 5 -5 5 5”, 把每一位进行累加, “4+5 -4+-5 -4+5 4+-5 -4+5 4+5” ==》 “9 -9 1 -1 1 9”。这里作为示例只算了两个单词的,真实计算需要把所有单词的序列串累加。

hash矩阵中,每列(如图中的1,2,3列)的数值分别进行加和,最终得到和值 12,108, -22, -5, -32,55

将TOP N词组的向量做加法,得到一个最终的宽64的向量。

接下来,遍历这个宽度64的向量,若对应位置<0则记录为0,>0则记录为1,从而又变成了一个64比特的整型,这个整型就是文章的simhash。

(5)降维

把(4)步算出来的 “9 -9 1 -1 1 9” 变成 0 1 串,形成我们最终的simhash签名。 如果每一位大于0 记为 1,小于0 记为 0。最后算出结果为:“1 0 1 0 1 1”。

(6)计算海明距离(Hamming distance)

(在信息编码中,两个合法代码对应位上编码不同的位数称为码距,又称海明距离。)计算海明距就可以计算出两个simhash到底相似不相似。例如: 10101 和 00110 从第一位开始依次有第一位、第四、第五位不同,则海明距离为3。计算汉明距离的一种方法,就是对两个位串进行异或(xor)运算,并计算出异或运算结果中1的个数。

(7) documents相似度

当我们算出所有doc的simhash值之后,需要计算doc A和doc B之间是否相似的条件是:A和B的海明距离是否小于等于n,这个n值根据经验一般取值为3。

 

0x06 simhash的使用

Python 中已实现该算法,我们安装相应的包即可。

pip install simhash

(1)计算simhash值

>>> from simhash import Simhash
>>> print '%x' % Simhash(u'I am very happy'.split()).value
f8fd7efdb1ded7f

Simhash()接收一个token序列,或者叫特征序列。

(2)计算两个simhash值距离

>>> hash1 = Simhash(u'I am very happy'.split())
>>> hash2 = Simhash(u'I am very sad'.split())
>>> print(hash1.distance(hash2))
5

(3)建立索引

simhash被用来去重。如果两两分别计算simhash值,数据量较大的情况下肯定hold不住。有专门的数据结构,参考:http://www.cnblogs.com/maybe2030/p/5203186.html#_label4

from simhash import Simhash, SimhashIndex
# 建立索引
data = {
    u'1': u'How are you I Am fine . blar blar blar blar blar Thanks .'.lower().split(),
    u'2': u'How are you i am fine .'.lower().split(),
    u'3': u'This is simhash test .'.lower().split(),
}
objs = [(id, Simhash(sent)) for id, sent in data.items()]
index = SimhashIndex(objs, k=10)  # k是容忍度;k越大,检索出的相似文本就越多
# 检索
s1 = Simhash(u'How are you . blar blar blar blar blar Thanks'.lower().split())
print index.get_near_dups(s1)
# 增加新索引
index.add(u'4', s1)

0x07 simhash分表存储策略

原文链接:Simhash的生成及存储 - chinawangfei的专栏 - CSDN博客

在线上查询算法中,首先建立多个指纹表:T1,T2,√…,Tt。每个指纹表 Ti 关联两个未知数:一个整型pi和一个在f bit-positions上的排列πi,Ti就是对已经存在的所有指纹进行排列πi得到的有序集合。对于一个指纹f和一个整数k,算法分两个步骤:

1 找到Ti中所有的前pi个bit-positions和πi(F)的前pi个bit-positions相同的指纹,假设为指纹集合F。

2 在F中的每一个指纹,比较其是否和πi(F)有的差异不超过k个。

(1)分表存储原理

借鉴“hashmap算法找出可以hash的key值”。因为我们使用的simhash是局部敏感哈希,这个算法的特点是:只要相似的字符串,只有个别的位数是有差别变化的。这样,我们可以推断两个相似的文本,至少有16位的simhash是一样的。

(2)分表存储设计

假设f = 64 ,k=3,并且我们有80亿 = 2^34个数的网页指纹,d=34,可以有下面四种设计方法 (f:指纹位数,k:海明距离,d:将文章数量转化成2的幂次方,d就是幂值)

1.20个表

将64bit分为11,11,11,11,10,10六个bit块。根据排列组合,如果想从这6个块中找3个作为leading bits的话(这样才能满足|pi-d|是个小整数),一共有C(6,3)=20种找法,所以需要20个表,每个表的前三块来自不同的三个块,那么pi就有11+11+11、11+ 11+10和11+10+10三种可能了。一次嗅探平均需要检索2^(34-31)=8个指纹。

2.16个表

先将64bit均分成4份,然后针对每份,将剩余的48bit再均分成四份,也就是16,12,12,12,12,12五部分,很明显这种组合的可能是4*4,而pi = 28。一次嗅探平均需要检索2^(34-28)=64个指纹。

3.10个表

将64bit分成 13,13,13,13,12 五个bit快。根据排列组合,需要从5块中找到2个作为leading bits,共有C(5,2)=10种找法,需要10张表,而pi=25或26。一次嗅探平均需要检索2^(34-25)=512个指纹。

4.4个表

同理 64 等分为4份,每份16bit,从四份中找出1个leading bits,共有C(4,1)=4种找法,pi=16,一次嗅探平均需要检索2^(34-16)=256K个指纹。

(3)分表存储实现

存储:

1、将一个64位的simhash签名拆分成4个16位的二进制码。(图上红色的16位)   

2、分别拿这4个16位二进制码查找当前对应位置上是否有元素。(放大后的16位)   

3、对应位置没有元素,直接追加到链表上;对应位置有则直接追加到链表尾端。(图上的 S1 — SN)

查找:

1、将需要比较的simhash签名拆分成4个16位的二进制码。   

2、分别拿着4个16位二进制码每一个去查找simhash集合对应位置上是否有元素。   

3、如果有元素,则把链表拿出来顺序查找比较,直到simhash小于一定大小的值,整个过程完成。

原理:借鉴“hashmap算法找出可以hash的key值”。因为我们使用的simhash是局部敏感哈希,这个算法的特点是:只要相似的字符串,只有个别的位数是有差别变化的。那这样我们可以推断两个相似的文本,至少有16位的simhash是一样的。具体选择16位、8位、4位,大家根据自己的数据测试选择,虽然比较的位数越小越精准,但是空间会变大。分为4个16位段的存储空间是单独simhash存储空间的4倍。之前算出5000w数据是 382 Mb,扩大4倍1.5G左右,还可以接受

(4)最佳分表策略

根据 4.2节分表存储设计,给定 f,k 我们可以有很多种分表的方式,增加表的个数会减少检索时间,但是会增加内存的消耗,相反的,减少表的个数,会减少内存的压力,但是会增加检索时间。 根据google大量的实验,存在一个分表策略满足时间和空间的平衡点

τ=d-pi (pi**计算看4.2章节,取最小pi**)

(5)simhash存储实现(Go)

国外有一大神用go实现了d=3和6的实现,在他的基础上作者实现了d到8的扩展,源码请看https://github.com/kricen/shstorage

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值