大集合中有效查找汉明距离低的二进制字符串


79

问题:

给定一个大的(约1亿个)无符号32位整数列表,一个无符号32位整数输入值以及最大汉明距离,请返回输入值指定汉明距离内的所有列表成员。

持有清单的实际数据结构是开放的,性能要求决定了内存中的解决方案,构建数据结构的成本是次要的,查询数据结构的低成本至关重要。

例:

For a maximum Hamming Distance of 1 (values typically will be quite small)

And input: 
00001000100000000000000001111101

The values:
01001000100000000000000001111101 
00001000100000000010000001111101 

should match because there is only 1 position in which the bits are different.

11001000100000000010000001111101

should not match because 3 bit positions are different.

到目前为止,我的想法是:

对于汉明距离为0的简并情况,只需使用排序列表并对特定输入值进行二进制搜索。

如果汉明距离只能为1,我可以翻转原始输入中的每一位并重复上述32次。

如何有效地(无需扫描整个列表)发现汉明距离> 1的列表成员。


如何通过预期的汉明距离来改变标准,循环函数可以做到这一点。下一步将是两个列表的并集吗?
XecP277 2011年

这是有关此问题的最新论文:大规模汉明距离查询处理
hammar 2011年

@Eric您说“最大汉明距离为1(通常值将很小)”。你能说出“很小”的意思吗?
Stefan Pochmann

@Eric另外,大约1亿个数字是否都是唯一的,还是重复?
Stefan Pochmann

@StefanPochmann:没有重复。最大的关注距离是4-5。
Eric J.

Answers:


110

问题:我们对汉明距离d(x,y)了解多少?

回答:

  1. 非负数:d(x,y)≥0
  2. 对于相同的输入,它仅为零:d(x,y)= 0⇔x = y
  3. 它是对称的:d(x,y)= d(y,x)
  4. 服从三角形不等式d(x,z)≤d(x,y)+ d(y,z)

题:我们为什么在乎?

答:因为这意味着汉明距离是一个度量度量空间。有一些用于度量空间索引的算法。

通常,您还可以查找“空间索引”的算法,并知道您的空间不是欧几里得,而是 一个度量空间。关于该主题的许多书籍都使用诸如汉明距离之类的度量标准来覆盖字符串索引。

脚注:如果您比较固定宽度字符串的汉明距离,则可以通过使用汇编或处理器内在函数来显着提高性能。例如,使用GCC(手册),您可以执行以下操作:

static inline int distance(unsigned x, unsigned y)
{
    return __builtin_popcount(x^y);
}

如果您随后告知GCC您正在使用SSE4a编译计算机,那么我认为应该减少到几个操作码。

编辑:根据许多消息来源,这有时/通常比通常的遮罩/移位/添加代码慢。基准测试表明,在我的系统上,C版本的性能优于GCC的__builtin_popcount160%。

附录:我自己对此问题很好奇,因此我介绍了三种实现:线性搜索,BK树和VP树。请注意,VP和BK树非常相似。BK树中节点的子级是树的“外壳”,其中包含的点距树的中心固定距离。VP树中的一个节点有两个子节点,一个子节点包含以该节点的中心为中心的球体中的所有点,另一个子节点包含外部的所有点。因此,您可以将VP节点视为具有两个非常厚的“壳”而不是许多更细的“壳”的BK节点。

结果是在我的3.2 GHz PC上捕获的,算法没有尝试利用多个内核(这应该很容易)。我选择的数据库大小为100M伪随机整数。结果是距离1..5的1000个查询的平均值,以及6..10和线性搜索的100个查询的平均值。

  • 数据库:100M伪随机整数
  • 测试次数:距离1..5为1000,距离6..10为100,线性
  • 结果:平均查询点击数(非常近似)
  • 速度:每秒查询数
  • 覆盖率:每个查询检查的数据库的平均百分比
                -BK树--VP树--线性-
距离结果速度速度速度速度速度速度
1 0.90 3800 0.048%4200 0.048%
2 11 300 0.68%330 0.65%
3130 56 3.8%63 3.4%
4970 18 12%22 10%
5 5700 8.5 26%10 22%
6 2.6e4 5.2 42%6.0 37%
7 1.1e5 3.7 60%4.1 54%
8 3.5e5 3.0 74%3.2 70%
9 1.0e6 2.6 85%2.7 82%
10 2.5e6 2.3 91%2.4 90%
任何2.2 100%

在您的评论中,您提到:

我认为可以通过生成一堆具有不同根节点的BK树并将其散布来改善BK树。

我认为这正是VP树比BK树(略)更好的原因。作为“更深”而不是“更浅”,它与更多点进行比较,而不是针对更少点进行更细粒度的比较。我怀疑在高维空间中差异更大。

最后一个提示:对于线性扫描,树中的叶节点应该只是整数的平面数组。对于小集合(可能为1000点或更小),这将更快且内存效率更高。


9
万岁!我的10k代表在这里;-)
Dietrich Epp

我考虑了度量空间,但是当我意识到所有事物之间有多么紧密时,我将其取消。显然,BK-tree只是蛮力,因此它不是一种优化。M-tree和VP-tree也不是一种优化,因为所有内容之间的紧密程度。(汉明距离4对应于距离2,而汉明距离2对应于根2的距离。)
Neil G

1
如果您将整数视为位字符串,则固定大小整数的汉明距离与L1范数相同。否则,两个字符串之间的“标准” L1范数是元素之间的正距离之和。
2014年

2
@DietrichEpp这是我在SO上找到的最神奇的答案之一。我正要问建立索引要花多长时间,但是后来我看到您发布了代码。答案:在3.5Ghz i7-3770K上,以0.034s构建1M项BK树,并以13s构建100M项BK树。VP树的构建时间大约要长4倍,并使我的粉丝开始大声旋转。
Mark E. Haase 2015年

2
@StefanPochmann:您似乎已经将“添加其他答案”按钮与“添加评论”按钮混淆了。查看页面底部,您将在此处找到“添加其他答案”按钮。
Dietrich Epp

12

我写了一个解决方案,其中我用2个32位的位集表示输入数字,因此我可以检查O(1)输入中是否有某个数字。然后,对于查询的数字和最大距离,我递归地生成该距离内的所有数字,并对照位集检查它们。

例如,对于最大距离5,这是242825个数字(总和d = 0到5 {32选择d})。为了进行比较,例如Dietrich Epp的VP-tree解决方案通过了1亿个数字的22%,即达到了2200万个数字。

我使用Dietrich的代码/解决方案作为添加解决方案并将其与他的比较的基础。以下是每秒最多查询的速度,最大距离为10:

Dist     BK Tree     VP Tree         Bitset   Linear

   1   10,133.83   15,773.69   1,905,202.76   4.73
   2      677.78    1,006.95     218,624.08   4.70
   3      113.14      173.15      27,022.32   4.76
   4       34.06       54.13       4,239.28   4.75
   5       15.21       23.81         932.18   4.79
   6        8.96       13.23         236.09   4.78
   7        6.52        8.37          69.18   4.77
   8        5.11        6.15          23.76   4.68
   9        4.39        4.83           9.01   4.47
  10        3.69        3.94           2.82   4.13

Prepare     4.1s       21.0s          1.52s  0.13s
times (for building the data structure before the queries)

对于小距离,比特集解决方案是迄今为止四个中最快的。问题作者埃里克(Eric)在下面评论说,最大的关注距离可能是4-5。自然,对于更大的距离,我的位集解决方案变得更慢,甚至比线性搜索还慢(对于距离32,它将经历2 32个数字)。但是对于距离9,它仍然很容易领先。

我还修改了Dietrich的测试。上面的每个结果都是让算法在大约15秒内解决至少三个查询和尽可能多的查询(我对1,2,4,4,8,16等查询进行回合,直到至少10秒总共通过了)。那是相当稳定的,我什至在短短1秒钟内就得到了类似的数字。

我的CPU是i7-6700。我的代码(基于Dietrich的代码)在这里(至少暂时不考虑那里的文档,不确定该怎么做,但其中tree.c包含所有代码和test.bat展示了我如何编译和运行(我使用了Dietrich的标志Makefile)) 。我的解决方案的快捷方式

一个警告:我的查询结果仅包含一个数字,因此,如果输入列表包含重复的数字,则可能会或可能不会。在作者埃里克(Eric)的情况下,没有重复的内容(请参阅下面的评论)。无论如何,这种解决方案对于输入中没有重复项或不想或不需要查询结果中重复项的人们可能是一个好选择(我认为纯查询结果很可能只是达到目的的一种手段,然后其他一些代码会将数字转换为其他数字,例如,将数字映射到哈希为该数字的文件列表的映射)。


感兴趣的最大距离可能是4-5,因此此解决方案非常有趣。在实际域中没有重复的内容引起了这个问题。
Eric J.

3

一种常见的方法(至少对我来说是通用的)是将位字符串分成几个块,并在这些块上进行查询以进行精确匹配,作为预过滤步骤。如果使用文件,则创建的文件数应与块数(例如此处为4)一样多,每个块都在前面排列,然后对文件进行排序。您可以使用二进制搜索,甚至可以在匹配块的上方和下方扩展搜索以获取奖金。

然后,您可以对返回的结果执行按位汉明距离计算,该结果应该只是整个数据集的较小子集。可以使用数据文件或SQL表来完成。

回顾一下:假设您在数据库或文件中有一堆32位的字符串,并且您想要查找3个汉明距离以内或小于“查询”位字符串的字符串中的每个哈希:

  1. 创建一个包含四列的表:每列将包含32位哈希(从1到4)的8位(作为字符串或整数)切片。或者,如果您使用文件,则创建四个文件,每个文件都是具有以下内容的切片的排列每个“行”的前面有一个“ islice”

  2. 在qslice 1到4中以相同的方式对查询位字符串进行切片。

  3. 查询此表,使任何一个qslice1=islice1 or qslice2=islice2 or qslice3=islice3 or qslice4=islice4。这将为您提供8 - 1查询字符串中7位()之内的每个字符串。如果使用文件,请在四个排列的文件中的每个文件中进行二进制搜索以得到相同的结果。

  4. 对于每个返回的位串,通过查询位串成对计算精确的汉明距离(从数据库或置换文件的四个切片中重建索引侧位串)

第4步中的操作数应比整个表的完整成对汉明计算少得多,并且在实践中非常有效。此外,很容易使用并行机制将文件分片为更小的文件。

当然,根据您的情况,现在您正在寻找一种排序的自联接,即所有相距一定距离的值。恕我直言,相同的方法仍然有效,尽管您将必须从一个起始点向上和向下扩展以共享共享起始块的排列(使用文件或列表),并计算所得簇的汉明距离。

如果在内存而不是文件中运行,则100M 32位字符串数据集的范围为4 GB。因此,四个排列的列表可能需要大约16GB以上的RAM。尽管我使用内存映射文件获得了出色的结果,但是对于类似大小的数据集,它必须使用更少的RAM。

有可用的开源实现。空间中最好的是恕我直言,这是由Moz(C ++)为Simhash完成的,但设计用于64位字符串而不是32位。

这种有界的距离距离方法是Moses Charikar在其“ simhash”开创性论文和相应的Google专利中首次描述了AFAIK :

  1. 汉明空间中的近似最近邻搜索

[...]

给定每个由d个比特组成的比特向量,我们选择N = O(n 1 /(1+))个比特的随机排列。对于每个随机排列σ,我们按σ排列的位的字典顺序保持位向量的排序顺序Oσ。给定查询位向量q,我们通过执行以下操作找到近似的最近邻居:

对于每个排列σ,我们在Oσ上执行二进制搜索以找到最接近q的两个位向量(按由σ排列的位获得的字典顺序)。现在,我们按照与q匹配的最长前缀长度的顺序,在二进制搜索返回的位置之上和之下的每个排序顺序Oσ中搜索元素。

Monika Henziger在她的论文“查找几乎重复的网页:算法的大规模评估”中对此进行了扩展:

3.3算法C的结果

我们将每个页面的位串划分为12个非重叠的4字节片段,创建20B片段,并计算出至少有一个共同点的所有页面的C相似度。该方法可以确保找到差异最大为11的所有页面对,即C相似度373,但可能会因较大差异而错过一些页面。

Gurmeet Singh Manku,Arvind Jain和Anish Das Sarma在论文“检测Web爬网的几乎重复项”中也对此进行了解释:

  1. 汉明距离问题

定义:给定f位指纹和查询指纹F的集合,请确定现有指纹是否最多与k位在F上有所不同。(在上述问题的批处理模式版本中,我们具有一组查询指纹,而不是单个查询指纹)

[...]

直觉:考虑一个2 df位真正随机指纹的排序表。仅关注表中的最高有效d位。这些d位数字的列表在某种意义上相当于“几乎是一个计数器”,其中(a)存在相当多的2 d位组合,并且(b)很少有d位组合被复制。另一方面,最低有效的f-d位是“几乎随机的”。

现在选择d,使| d − d | 是一个小整数。由于对表进行了排序,因此单个探针足以识别在d个最高有效位中与F匹配的所有指纹。由于| d − d | 很小,此类匹配的数量也预计会很小。对于每个匹配的指纹,我们可以很容易地找出它在最多k个比特位置上是否与F不同(这些差异自然会限制在f-d个最低有效比特位置上)。

上面描述的过程可以帮助我们找到在k位位置上不同于F的现有指纹,所有指纹都被限制在F的最低有效f-d位之中。这可以处理很多情况。为了涵盖所有情况,只需建立少量其他排序表即可,这将在下一节中正式概述。

注意:我对一个仅DB相关问题发布了类似的答案


2

您可以在指定的汉明距离内预先计算原始列表的所有可能变体,并将其存储在布隆过滤器中。这给您一个快速的“否”,但不一定是关于“是”的明确答案。

如果为“是”,则将与每个位置相关联的所有原始值的列表存储在bloom过滤器中,并一次遍历一个原始值。优化布隆过滤器的大小,以权衡速度/内存。

不确定是否所有功能都可以正常工作,但是如果您有运行时RAM要刻录并且愿意花很长时间进行预计算,那么这似乎是一个好方法。


难道不是很不可能吗?存在2%的条目。
尼尔·G

1

如何对列表进行排序,然后在汉明距离中的不同可能值上对已排序的列表进行二进制搜索?


2
对于1的汉明距离,这是合理的,因为原始输入有32个排列(将原始输入中的每个位翻转一次)。对于2的汉明距离,还有很多,将有要搜索的更多的置换输入值。
埃里克·J.

2
1024 + 32 + 1搜索并不是非常大量的二进制搜索。甚至32 ^ 3搜索也不是很多。
τεκ

@EricJ-但是,有1亿条数据。考虑到发布者指出“构建数据结构的成本是次要的”,对于合理的汉明距离,这仍然是合理的。
2011年

请参阅bit-string-nearest-neighbour-searching,它使用各种类型,然后进行二进制搜索。
丹尼斯

1

解决此问题的一种可能方法是使用不交集数据结构。这个想法是合并汉明距离<= k的列表成员。这是算法的概述:

  • 对于每个列表成员,计算汉明距离<= k的每个可能。对于k = 1,有32个值(对于32位值)。对于k = 2,值为32 + 32 * 31/2。

    • 对于每个计算,测试它是否在原始输入中。您可以使用大小为2 ^ 32的数组或哈希映射来执行此检查。

    • 如果该在原始输入中,请对列表成员执行“联合”操作。

    • 将执行的联合操作数保持在变量中。

您从N个不交集开始算法(其中N是输入中的元素数)。每次执行联合操作时,不相交集的数量减少1。当算法终止时,不相交集数据结构会将所有汉明距离<= k的值分组为不相交集。这种不相交的数据结构几乎可以在线性时间内计算出来。


我不明白 如果您的输入集是{11000000,0110000,00110000,00011000,00001100,00000110,00000011}并且k = 2,我认为您的算法会将每个元素与其下一个邻居(它们的汉明距离为2)统一,从而将所有元素统一。但是11000000和00000011没有汉明距离2;它们的汉明距离为4。使用不交集的森林(联合查找)的基本问题是,邻近度不是等价关系。
JonasKölker

好点子!但是,您必须考虑到每个元素都是按顺序处理的,一旦找到匹配项,则将匹配的元素从列表中删除。因此,在您的示例中,在11000000和01100000之间进行联合运算后,后者将无法与00110000进行联合。您将获得5个集合,并且只能将输入与每个集合的一个代表性元素进行比较。
Marcio Fonseca

1

这是一个简单的想法:对100m个输入整数按字节进行基数排序,最高有效字节在前,在某些外部结构中跟踪存储桶边界的前三个级别。

要进行查询,请从的距离预算d和输入的单词开始w。对于具有字节值的顶层中的每个存储桶b,计算d_0之间的汉明距离b与的高字节w。递归搜索预算为的存储桶d - d_0:即,对于每个字节值b',令d_1为到之间的汉明距离b'与的第二个字节w。以的预算递归搜索到第三层d - d_0 - d_1,依此类推。

请注意,存储桶形成一棵树。每当您的预算为负数时,就停止搜索该子树。如果递归地进入叶子而不浪费距离预算,则该叶子值应该是输出的一部分。

这是表示外部存储桶边界结构的一种方法:具有长度为16_777_216(= (2**8)**3 = 2**24)的数组,其中元素位于索引处i是存储区的起始索引,其中包含[256 * i,256 * i + 255]范围内的值。要在该存储桶的末尾找到索引1,请查找索引i + 1(或将数组的末尾用于i + 1 = 2 ** 24)。

内存预算为100m *每个字4个字节= 400 MB用于输入,而2 ** 24 * 4个字节每个地址= 64 MiB用于索引结构,或总计略少于半个千兆字节。索引结构在原始数据上的开销为6.25%。当然,一旦构建了索引结构,您只需要存储每个输入字的最低字节,因为其他三个在索引结构中的索引中都是隐式的,总共约为(64 + 50)MB。

如果您的输入不是均匀分布的,则可以使用(单个,通用共享)置换来置换输入单词的位,该置换将所有熵置于树的顶部。这样,第一级修剪将消除较大的搜索空间块。

我尝试了一些实验,它的效果与线性搜索一样好,有时甚至更糟。这个奇特的想法非常重要。哦,至少它的内存效率高。


感谢您分享这种选择。在我的环境中,“内存便宜”,但是一种内存有效的解决方案可能会使其他人受益。
Eric J.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.