近来由于工作需要,需要将字符串的相似度的计算速度进行提升。之前曾采用最长公共子序列、编辑距离等算法实现过,但总满足不了实时比较的性能及速度需求。前些天由同事推荐局部敏感哈希算法,便尝试了一把,结果发现速度还不错,本着记录与分享的精神,简单总结下实现的过程及思路。
【Shingle】
将待查询的字符串集进行映射,映射到一个集合里,如字符串“abcdeeeefg", 映射到集合”(a,b,c,d,e,f,g)", 注意集合中元素是无重复的。这一步骤其实叫Shingling, 意即构建文档中的短字符串集合,即shingle集合。
这是最简单的映射,直接以一个字符进行切分了,也可以映射到更复杂的集合,如(ab, bc, de, ef ,fg),(abc, bcd, def, efg)等
字符串集映射的集合,可以进行一步哈希,如a hash到1, b hash 到2,c hash到1 等等, 映射到桶有个很大的好处,是可以减少数据量, 映射到桶之后,我们便可以将字符串用桶编号来进行表示了
【特征矩阵】
假设有k个桶,根据字符串进行shingling之后,是否映射到桶,我们可以得到一个矩阵,可以称之为特征矩阵,这个矩阵以桶对应的hash值为行,以文档字符串集为列,矩阵中的元素为0或1,表示是否可以映射到相应桶中,举例来说:
字符串1 | 字符串2 | 字符串3 | 字符串4 | |
桶1 | 0 | 1 | 1 | 1 |
桶2 | 1 | 0 | 0 | 1 |
桶3 | 0 | 0 | 0 | 0 |
桶4 | 1 | 0 | 1 | 0 |
桶5 | 0 | 1 | 0 | 1 |
矩阵中为1指对应列字符串的shingle集合有元素可以映射到对应桶, 为0代表没有无素映射到对应桶
由《大数据互联网大规模数据挖掘与分布式处理》一书可以得知字符串之间本身的相似度可以用它们映射到的桶相间的相似度来度量,如字符串1和字符串2的相似度可以用(0 1 0 1 0)及(1 0 0 0 1)的相似度来进行表示
【排列转换】
但shingle集合一般都非常大,即使将每个shingle都哈希到4个字节(即上表中的桶),很可能也不能把字符串的shingle集合全部放入内存中,那我们有什么 办法可以避免大数据量呢?试想我们将该特征矩阵映射到更小维度、规模更小的签名,用签名来代替特征矩阵,岂不是更好?
为了对特征矩阵每列所表示的集合进行最小哈希计算,首先选择行的一个排列转换。任意一列的最小哈希值是在排列转换后的行排列次序下第一个列值为1的行的行号,仍然举例来说,我们把以上表中的桶顺序打乱
字符串1 | 字符串2 | 字符串3 | 字符串4 | |
桶2 | 1 | 0 | 0 | 1 |
桶3 | 0 | 0 | 0 | 0 |
桶1 | 0 | 1 | 1 | 1 |
桶4 | 1 | 0 | 1 | 0 |
桶 5 |
0 | 1 | 0 | 1 |
对于字符串1,其在第一行就已经可以遇到到桶2,那就把字符串1的哈希签名h设为桶2对应的hash值,即h(字符串1)=桶2 hash; 同理,h(字符串2)=桶1 hash; h(字符串3)=桶1 hash; h(字符串4)=桶2 hash;
【最小哈希签名矩阵的计算】
假如我们选择n个排列转换用于以上特征矩阵的处理,对于特征矩阵的列,分别调用这些排列转换所决定的最小哈希函数h1, h2...., hn, 就可以构建特征矩阵的最小哈希签名矩阵
假设SIG为最终得到的最小哈希签名矩阵,SIG(i, c)为签名矩阵中第i个哈希函数在第c列上的元素,刚开始时,矩阵元素设为无穷大。然后对于每一行r, 进行如下处理
(1). 计算h1(r), h2(r), .... hn(r)
(2). 对每一列c进行如下处理
(a) 如果特征矩阵中第c列第r行为0, 什么也不做
(b) 如果特征矩阵中第c列第r行为1,将SIG(i, c)设为原来的SIG(i, c)和hi(r)的最小值
为了简便计算,我们可以这样做,对于特征矩阵中每一列,我们集中该列中所有为1的行,对这些行应用哈希函数hi,取最小的哈希值,即可得到哈希函数hi对应的该列的签名,应用所有哈希函数,即可得到该列的签名向量。
最后,得到n行的签名矩阵,这比特征矩阵,远小了很多。
到这时,两个字符串的相似度的计算,又可转换成为对应的签名向量的相似度计算,如何度量向量的相似度呢,我们采用jaccard相似度
【jaccard相似度】
jaccard相似度用于计算两个集合之间的相似情况,也就是两个集合的交集与并集大小之间的比率
利用jaccard相似度,我们可以计算出字符串对应的签名向量的相似度,但如果字符串的数量太多,两两比较签名向量,也是很耗时的工作,实际中我们往往只需要得到那些最相似或者相似度大于某一阀值的字符串对,如果我们能首先将这些候选的字符串对找出来,再运用jaccard相似度,岂不是可以极大地减少计算量?
【行条化策略】
很显然,如果两个字符串相似,那么它们对应的签名向量应该也相似,在局部某个范围内极有可能相同, 相同,如果采用哈希的话,就可以映射到同一个桶。
设想我们对签名矩阵划分若干个行条,每一个行条里有数列,如果两个字符串相似,那猜测肯定在某个行条里,这两个字符串对应的签名向量应该相等,它们会映射到同一个桶里。
对每一个行条,我们设置一个大桶,对行条中的每一列计算其hash值,相同hash值的列会映射到同一个hash桶。在hash桶里的列,就组成了候选对。
对最后得到的候选对再计算jaccard相似度
【需要注意的问题】
1. 最小哈希函数族是性能的关键,哈希函数之间一定要独立,不要出现一个哈希函数在同样的参数条件下,都比另一个哈希函数大或者小的情况,最好是乱七八糟,毫不相干
2. 最小哈希函数映射到的value范围不宜过小,太小了容易产生冲突
3.行条化采用的哈希函数映射到的Value范围也不宜过小,太小了同样容易冲突,后期要查询的候选对太多会影响速度
4. 注意大素数的应用,构造hash函数,大素数相当有用
注:原理及部分表述取自《大数据互联网大规模数据挖掘与分布式处理》第三章