【问题标题】:Efficiently find binary strings with low Hamming distance in large set在大集合中有效地找到具有低汉明距离的二进制字符串
【发布时间】:2011-09-17 09:43:17
【问题描述】:

问题:

给定一个大型(约 1 亿)无符号 32 位整数列表、一个无符号 32 位整数输入值和最大值 Hamming Distance,返回输入值的指定汉明距离内的所有列表成员.

保存列表的实际数据结构是开放的,性能要求决定了内存解决方案,构建数据结构的成本是次要的,查询数据结构的低成本是关键。

示例:

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 的列表成员。

【问题讨论】:

  • 如何通过预期的汉明距离改变标准,循环函数可以做到这一点。下一步将是获得两个列表的并集?。
  • 最近有一篇关于这个问题的论文:Large scale Hamming distance query processing.
  • @Eric 你说 “对于 1 的最大汉明距离(值通常会非常小)”。你能说出“相当小”是什么意思吗?
  • @Eric 另外,这大约 1 亿个数字是唯一的,还是有重复的?
  • @StefanPochmann:没有重复。感兴趣的最大距离为 4-5。

标签: algorithm bit-manipulation bitwise-operators hamming-distance


【解决方案1】:

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

【讨论】:

  • 对于 1 的汉明距离,这是合理的,因为原始输入有 32 种排列(将原始输入中的每个位翻转一次)。对于 2 的汉明距离,需要搜索 许多 更多排列的输入值。
  • 1024+32+1 搜索并不是非常多的二分搜索。即使是 32^3 次搜索也不多。
  • @EricJ - 但是,有 1 亿条数据。对于合理的汉明距离,这仍然是合理的 - 鉴于海报声明“构建数据结构的成本是次要的”。
  • 参见bit-string-nearest-neighbour-searching,它使用各种排序,然后是二分查找。
【解决方案2】:

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

如果是,存储与布隆过滤器中每个位置关联的所有原始值的列表,并一次通过它们。优化布隆过滤器的大小以在速度/内存之间进行权衡。

不确定这一切是否完全正常,但如果您有运行时 RAM 需要刻录并且愿意在预计算上花费很长时间,这似乎是一个好方法。

【讨论】:

  • 没有的可能性不大吗?存在 2% 的条目。
【解决方案3】:

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

答案:

  1. 非负数:d(x,y) ≥ 0
  2. 对于相同的输入,它仅为零:d(x,y) = 0 ⇔ x = y
  3. 它是对称的:d(x,y) = d(y,x)
  4. 它遵循triangle inequality,d(x,z) ≤ d(x,y) + d(y,z)

问题:我们为什么要关心?

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

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

脚注:如果您要比较固定宽度字符串的汉明距离,则可以通过使用程序集或处理器内部函数来获得显着的性能改进。例如,使用 GCC (manual) 你可以这样做:

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

如果您随后通知 GCC 您正在为具有 SSE4a 的计算机进行编译,那么我认为应该减少到只有几个操作码。

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

附录:我自己也很好奇这个问题,所以我分析了三个实现:线性搜索、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 树 -- -- 线性 -- 分布结果 Speed Cov Speed Cov Speed Cov 1 0.90 3800 0.048% 4200 0.048% 2 11 300 0.68% 330 0.65% 3 130 56 3.8% 63 3.4% 4 970 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-trees 可以通过生成一堆具有不同根节点的 BK-trees 并将它们展开来改进。

我认为这正是 VP 树的性能(略)优于 BK 树的原因。作为“更深”而不是“更浅”,它与更多点进行比较,而不是对更少点使用更细粒度的比较。我怀疑在高维空间中差异会更加极端。

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

【讨论】:

  • 万岁!我的 10k 代表在这里 ;-)
  • 我考虑过公制空间,但当我意识到一切都靠得如此紧密时,我放弃了它。显然,BK-tree 只是蛮力,所以它不会是优化。 M-tree 和 VP-tree 也不会是优化,因为一切都靠得很近。 (汉明距离 4 对应于距离 2,而汉明距离 2 对应于根 2 的距离。)
  • 固定大小 整数 的汉明距离与 L1 范数相同,如果您将整数视为位串。否则,两个字符串之间的“标准”L1 范数是元素之间正距离的总和。
  • @DietrichEpp 这是我在 SO 上找到的最令人惊奇的答案之一。我正要问建立索引需要多长时间,但后来我看到你发布了代码。答:在3.5Ghz i7-3770K上,0.034s构建1M item BK Tree,13s构建100M item BK Tree。构建 VP 树需要大约 4 倍的时间,并且让我的粉丝开始大声旋转。
  • @StefanPochmann:您似乎将“添加另一个答案”按钮与“添加评论”按钮混淆了。查看页面底部,您会在此处找到“添加另一个答案”按钮。
【解决方案4】:

解决此问题的一种可能方法是使用Disjoint-set data structure。这个想法是在同一集合中合并具有汉明距离

  • 对于每个列表成员,使用汉明距离 值。对于 k=1,有 32 个值(对于 32 位值)。对于 k=2, 32 + 32*31/2 值。

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

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

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

您从 N 个不相交的集合开始算法(其中 N 是输入中的元素数)。每次执行联合操作时,不相交集的数量就会减少 1。当算法终止时,不相交集数据结构会将汉明距离 almost linear time中计算出来。

【讨论】:

  • 我不明白。如果您的输入集是 {11000000, 0110000, 00110000, 00011000, 00001100, 00000110, 00000011} 并且 k=2,我认为您的算法会将每个元素与其下一个邻居统一(它们的汉明距离为 2),从而统一所有元素.但是11000000和00000011没有汉明距离2;他们的汉明距离是 4。使用不相交集森林(联合查找)的基本问题是接近度不是等价关系。
  • 好点!但是您必须考虑到每个元素都是按顺序处理的,一旦找到匹配项,匹配的元素就会从列表中删除。因此,在您的示例中,在 11000000 和 01100000 之间进行联合运算后,后者将无法与 00110000 联合。您最终将得到 5 个集合,您只需将输入与每个集合的一个代表元素进行比较。跨度>
  • 我不明白你的建议。也许你可以把它编码(对于一些小的 n 值)?这是要测试的事情:如果您有四个列表成员 x、y、z、w,每个成员的汉明距离为 3,并且您的查询汉明距离为 5,那么 x 和 y 是否属于同一个等价类(即联合查找树)? y和z会吗?将z和w?你如何使用等价类来决定输出什么?据我所知,如果您将 union-find 用于任何您使用它来重复输出的内容,我认为哈希集可以做得很好。但我不确定我是否理解?
【解决方案5】:

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

例如对于最大距离 5,这是 242825 个数字 (sumd = 0 to 5 {32 choose 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)

对于小距离,bitset 解决方案是迄今为止四种解决方案中最快的。问题作者 Eric 在下面评论说,最大的兴趣距离可能是 4-5。自然地,我的 bitset 解决方案对于更大的距离变得更慢,甚至比线性搜索更慢(对于距离 32,它会经过 232 个数字)。但是对于距离 9 来说,它仍然很容易领先。

我还修改了 Dietrich 的测试。上述每个结果都是为了让算法在大约 15 秒内解决至少三个查询和尽可能多的查询(我用 1、2、4、8、16 等查询进行轮次,直到至少 10 秒有共通过)。这相当稳定,我什至只用了 1 秒就得到了相似的数字。

我的 CPU 是 i7-6700。 My code (based on Dietrich's) is here(至少暂时忽略那里的文档,不知道该怎么做,但tree.c 包含所有代码,我的test.bat 显示了我是如何编译和运行的(我使用了来自Dietrich 的@987654327 的标志) @))。 Shortcut to my solution.

一个警告:我的查询结果只包含一次数字,因此如果输入列表包含重复的数字,则可能需要也可能不需要。在有问题的作者埃里克的案例中,没有重复(见下面的评论)。无论如何,此解决方案可能适用于输入中没有重复项或不希望或不需要查询结果中的重复项的人(我认为纯查询结果很可能只是达到目的的一种手段,然后其他一些代码将数字转换为其他内容,例如将数字映射到哈希为该数字的文件列表的映射)。

【讨论】:

  • 感兴趣的最大距离可能是 4-5,所以这个解决方案非常有趣。激发问题的实际域中没有重复项。
【解决方案6】:

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

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

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

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

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

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

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

第 4 步中的操作数应该比整个表的完整成对汉明计算少得多,并且在实践中非常有效。 此外,根据需要使用并行性提高速度,可以轻松地将文件分片为较小的文件。

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

如果在内存而不是文件中运行,您的 100M 32 位字符串数据集将在 4 GB 范围内。因此,四个置换列表可能需要大约 16GB+ 的 RAM。虽然我使用内存映射文件获得了出色的结果,并且对于类似大小的数据集必须更少的 RAM。

有可用的开源实现。该领域中最好的是恕我直言,它是为Simhash by Moz、C++ 而设计的,但设计用于 64 位字符串而不是 32 位。

Moses Charikar 在其“simhash”开创性paper 和相应的谷歌patent 中首次描述了这种有界重叠距离方法:

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

[...]

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

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

Monika Henziger 在她的论文"Finding near-duplicate web pages: a large-scale evaluation of algorithms" 中对此进行了扩展:

3.3 算法C的结果

我们将每一页的位串分成 12 个非 重叠 4 字节片段,创建 20B 片段,并计算所有页面的 C 相似度,其中至少有一个 一块共同点。这种方法保证能找到所有 差异高达 11 的页面对,即 C 相似度 373, 但可能会因为较大的差异而遗漏一些。

Gurmeet Singh Manku、Arvind Jain 和 Anish Das Sarma 的论文 Detecting Near-Duplicates for Web Crawling 也解释了这一点:

  1. 汉明距离问题

定义:给定一组 f 位指纹和一个 查询指纹F,识别是否已有指纹 与 F 的差异最多为 k 位。 (在批处理模式版本中 对于上述问题,我们有一组查询指纹 而不是单个查询指纹)

[...]

直觉:考虑一个 2 d f 位真正随机指纹的排序表。只关注最重要的 d 位 在表中。这些 d 位数字的列表相当于 “几乎是一个计数器”,因为 (a) 相当多的 2 d 位- 存在组合,并且 (b) 很少有 d 位组合 重复。另一方面,最不显着的 f - d 位“几乎是随机的”。

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

上述过程可以帮助我们找到现有的 在 k 位位置上与 F 不同的指纹,所有这些 被限制在最低有效 f - d 位之间 F. 这处理了相当多的案例。覆盖所有 在这种情况下,构建少量额外的就足够了 排序表,如下一节中正式概述的那样。

注意:我在related DB-only question 上发布了类似的答案

【讨论】:

    【解决方案7】:

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

    要查询,从d 的距离预算和您的输入词w 开始。对于顶层中每个字节值为b的桶,计算bw的高字节之间的汉明距离d_0。以d - d_0 的预算递归搜索该桶:即对于每个字节值b',令d_1b'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 用于索引结构,或者总共不到半个 gig。索引结构是原始数据的 6.25% 开销。当然,一旦你构建了索引结构,你只需要存储每个输入字的最低字节,因为其他三个隐含在索引结构的索引中,总共 ~(64 + 50) MB。

    如果您的输入不是均匀分布的,您可以使用(单个、普遍共享的)排列来排列输入单词的位,这会将所有熵放在树的顶部。这样,第一级修剪将消除更大的搜索空间块。

    我尝试了一些实验,结果与线性搜索差不多,有时甚至更差。这么多这个奇特的想法。哦,好吧,至少它的内存效率很高。

    【讨论】:

    • 感谢分享这个替代方案。在我的环境中“内存很便宜”,但内存效率高的解决方案可能会让其他人受益。
    猜你喜欢
    • 2011-06-14
    • 1970-01-01
    • 2011-07-29
    • 1970-01-01
    • 2017-04-03
    • 2014-08-28
    • 1970-01-01
    • 2014-01-28
    • 1970-01-01
    相关资源
    最近更新 更多