web-dev-qa-db-ja.com

大規模なセットでハミング距離が低いバイナリ文字列を効率的に見つける

問題:

符号なし32ビット整数の大きなリスト(最大1億)、符号なし32ビット整数の入力値、および最大 Hamming Distance を指定すると、指定された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のリストメンバーを効率的に(リスト全体をスキャンせずに)発見するにはどうすればよいですか。

68
Eric J.

質問:ハミング距離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)に従います

質問:なぜ気にするのですか?

Answer:これは、ハミング距離がmetric spaceに対してmetricであることを意味するためです。メトリック空間にインデックスを付けるためのアルゴリズムがあります。

また、一般に「空間インデックス」のアルゴリズムを調べることもできます。これは、空間がユークリッドではなく、isメトリック空間であるという知識を備えています。この主題に関する多くの本は、ハミング距離などのメトリックを使用した文字列のインデックス付けを扱っています。

脚注:固定幅の文字列のハミング距離を比較する場合、アセンブリまたはプロセッサの組み込み関数を使用することにより、パフォーマンスを大幅に改善できる場合があります。たとえば、GCC( manual )を使用すると、次のようになります。

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

その後、SSE4aを使用してコンピューター用にコンパイルしていることをGCCに通知すると、数個のオペコードに削減されるはずです。

編集:いくつかの情報源によると、これは通常のマスク/シフト/追加コードよりも遅い場合があります。ベンチマークでは、私のシステムでは、CバージョンがGCCの__builtin_popcount約160%。

補遺:私はこの問題に興味があったので、線形検索、BKツリー、VPツリーの3つの実装をプロファイルしました。 VPツリーとBKツリーは非常に似ていることに注意してください。 BKツリーのノードの子は、それぞれがツリーの中心から一定の距離にあるポイントを含むツリーの「シェル」です。 VPツリーのノードには2つの子があります。1つはノードの中心を中心とする球内のすべてのポイントを含み、もう1つは外側のすべてのポイントを含みます。したがって、VPノードは、多くのより細かい「シェル」ではなく、2つの非常に厚い「シェル」を持つBKノードと考えることができます。

結果は3.2 GHz PCでキャプチャされ、アルゴリズムは複数のコアを利用しようとしません(簡単なはずです)。 100Mの擬似ランダム整数のデータベースサイズを選択しました。結果は、距離1..5での1,000クエリの平均、および6..10および線形検索での100クエリです。

  • データベース:1億個の擬似乱数整数
  • テスト数:距離1..5では1000、距離6..10では100、線形
  • 結果:クエリヒットの平均数(非常に近似)
  • 速度:1秒あたりのクエリ数
  • カバレッジ:クエリごとに調べられたデータベースの平均割合
-BK Tree--VP Tree--Linear-
 Dist Results 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ツリーは、異なるルートノードを持つBKツリーの束を生成し、それらを分散させることで改善できると思います。

これが、VPツリーのパフォーマンスがBKツリーよりも(わずかに)優れている理由だと思います。 「shallower」ではなく「deeper」であるため、より少ないポイントに対するきめ細かい比較を使用するのではなく、より多くのポイントに対して比較します。高次元の空間では違いがより極端になると思います。

最後のヒント:ツリー内のリーフノードは、線形スキャンの整数のフラット配列である必要があります。小さいセット(おそらく1000ポイント以下)の場合、これはより高速で、メモリ効率が向上します。

100
Dietrich Epp

2のビットセットで入力数を表すソリューションを作成しました32 ビットなので、O(1)入力に特定の数値があるかどうかをチェックできます。クエリされた数値と最大距離については、その距離内のすべての数値を再帰的に生成し、チェックしますビットセット。

たとえば、最大距離5の場合、これは242825の数値です( sumd = 0から5 {32 dを選択} )。比較のために、たとえば、ディートリッヒエップのVPツリーソリューションは、1億の数字の22%、つまり2,200万の数字を通過します。

Dietrichのコード/ソリューションを基礎として使用して、ソリューションを追加し、彼と比較しました。最大距離10の場合、1秒あたりのクエリでの速度は次のとおりです。

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)

距離が短い場合、ビットセットソリューションは4つの中で最速です。質問作者のエリックは、関心の最大距離はおそらく4-5になるとコメントしました。当然、私のビットセットソリューションは、距離が長いほど遅くなり、線形検索よりもさらに遅くなります(距離32の場合、232 数字)。しかし、距離9の場合は、まだ簡単につながります。

ディートリッヒのテストも修正しました。上記の各結果は、アルゴリズムに少なくとも3つのクエリと約15秒でできる限り多くのクエリを解決させるためのものです(少なくとも10秒になるまで、1、2、4、8、16などのクエリでラウンドを実行します)合計で渡されます)。それはかなり安定しており、たった1秒間で同様の数値を得ることができます。

私のCPUはi7-6700です。 ディートリッヒに基づく)私のコードはここにあります (少なくとも今のところそこにあるドキュメントは無視してください、どうすればいいのかわかりませんが、tree.cにはすべてのコードとtest.batは、どのようにコンパイルして実行したかを示しています(DietrichのMakefileのフラグを使用しました)。 私のソリューションへのショートカット

注意点:クエリの結果には数字が1回しか含まれていないため、入力リストに重複した数字が含まれている場合、それが望ましい場合とそうでない場合があります。質問作者のエリックの場合、重複はありませんでした(以下のコメントを参照)。いずれにせよ、このソリューションは、入力に重複がないか、クエリ結果に重複を必要としない、または必要としない人に適している可能性があります(純粋なクエリ結果は、目的を達成するための手段にすぎないと思われます他のいくつかのコードは、数字を別の何かに変換します。たとえば、数字をその数字のハッシュを持つファイルのリストにマッピングするマップなどです。

9
Stefan Pochmann

一般的なアプローチ(少なくとも私には一般的)は、ビット文字列をいくつかのチャンクに分割し、これらのチャンクでクエリを実行して、プレフィルターステップとして完全に一致するようにします。ファイルを操作する場合、各チャンクを前に並べ替えたチャンク(たとえば、ここでは4)と同じ数のファイルを作成し、ファイルを並べ替えます。バイナリ検索を使用できます。また、ボーナスのために一致するチャンクの上下に検索を展開することもできます。

その後、返された結果に対してビット単位のハミング距離の計算を実行できます。これは、データセット全体の小さいサブセットのみである必要があります。これは、データファイルまたはSQLテーブルを使用して実行できます。

要約すると、DBまたはファイルに32ビット文字列の束があり、「クエリ」ビット文字列の3ビット以下のハミング距離内にあるすべてのハッシュを検索するとします。

  1. 4列のテーブルを作成します。各列には、32ビットハッシュの8ビット(文字列またはint)スライス、islice 1〜4が含まれます。または、ファイルを使用する場合は、4つのファイルを作成します。各「行」の前に1つの「スライス」

  2. qslice 1から4で同じようにクエリビット文字列をスライスします。

  3. いずれかのqslice1=islice1 or qslice2=islice2 or qslice3=islice3 or qslice4=islice4。これにより、7ビット(8 - 1)クエリ文字列。ファイルを使用する場合、同じ結果を得るために、4つの並べ替えられたファイルのそれぞれでバイナリ検索を実行します。

  4. 返された各ビット文字列について、クエリビット文字列を使用して、ペアごとに正確なハミング距離を計算します(DBまたは置換ファイルからの4つのスライスからインデックス側のビット文字列を再構築します)

ステップ4の操作の数は、テーブル全体の完全なペアワイズハミング計算よりもはるかに少なくする必要があり、実際には非常に効率的です。さらに、並列処理を使用してより高速にする必要があるため、ファイルを小さなファイルに分割するのは簡単です。

もちろん、あなたの場合は、ある種の自己結合を探しています。それは、互いにある程度の距離内にあるすべての値です。同じアプローチは私見でも機能しますが、開始チャンクを共有し、結果のクラスターのハミング距離を計算する順列(ファイルまたはリストを使用)の開始点から上下に拡張する必要があります。

ファイルではなくメモリで実行する場合、100M 32ビット文字列データセットは4 GBの範囲になります。したがって、4つの並べ替えられたリストには、約16GB以上のRAMが必要になる場合があります。代わりに、メモリマップファイルで優れた結果が得られますが、同様のサイズのデータ​​セットの場合はRAM.

利用可能なオープンソースの実装があります。空間で最高のものは、IMHOである MozによるSimhash 、C++ですが、32ビットではなく64ビット文字列用に設計されています。

この制限されたハッピング距離のアプローチは、最初に Moses Charikar の「simhash」精液 paper および対応するGoogle 特許

  1. ハミングスペースでの最も近い近隣検索

[...]

それぞれがdビットで構成されるビットベクトルが与えられた場合、ビットのN = O(n 1 /(1+))ランダム順列を選択します。各ランダム置換σについて、ビットベクトルのソートされた順序Oσを、σで置換されたビットの辞書式順序で維持します。クエリビットベクトルqが与えられた場合、次の操作を実行して近似の最近傍を見つけます。

各順列σについて、Oσのバイナリ検索を実行して、qに最も近い2つのビットベクトルを見つけます(σで順列したビットによって得られる辞書式順序で)。次に、ソートされた各順序Oσで検索し、qに一致する最長のプレフィックスの長さの順にバイナリ検索によって返された位置の上下の要素を調べます。

Monika Henziger 彼女の論文でこれについて詳しく説明しました "ほぼ重複したWebページを見つける:アルゴリズムの大規模な評価"

3.3アルゴリズムCの結果

各ページのビット文字列を12バイトの重複しない4バイトの断片に分割し、20Bの断片を作成し、少なくとも1つの断片が共通するすべてのページのC相似性を計算しました。このアプローチでは、最大11の差があるすべてのページペア、つまりC相似性373が見つかることが保証されていますが、より大きな差があると見落とす場合があります。

これは、Gurmeet Singh Manku、Arvind Jain、およびAnish Das Sarmaによる論文 Webクロールのほぼ重複の検出 でも説明されています。

  1. ハミング距離の問題

定義:fビットフィンガープリントのコレクションとクエリフィンガープリントFを指定すると、既存のフィンガープリントが最大でkビットでFと異なるかどうかを識別します。 (上記の問題のバッチモードバージョンでは、単一のクエリフィンガープリントではなく、クエリフィンガープリントのセットがあります)

[...]

直感:2 d fビットの真にランダムなフィンガープリントのソートされたテーブルを考えます。表の最上位dビットのみに注目してください。これらのdビット数のリストは、(a)2 dビットの組み合わせが非常に多く、(b)複製されるdビットの組み合わせが非常に少ないという意味で、「ほぼカウンター」になります。一方、最下位f-dビットは「ほぼランダム」です。

ここで、| d − d |となるようにdを選択しますは小さな整数です。テーブルはソートされているため、d個の最上位ビット位置でFに一致するすべての指紋を識別するには、1つのプローブで十分です。 | d − d |以来が小さい場合、そのような一致の数も少ないと予想されます。一致する指紋ごとに、最大でkビットの位置でFと異なるかどうかを簡単に判断できます(これらの違いは、当然f-dの最下位ビット位置に制限されます)。

上記の手順は、kビット位置でFと異なる既存のフィンガープリントを見つけるのに役立ちます。それらはすべて、Fの最下位f-dビットに制限されています。これにより、かなりの数のケースが処理されます。すべてのケースをカバーするには、次のセクションで正式に概説されているように、少数の追加のソート済みテーブルを作成するだけで十分です。

注: 関連するDBのみの質問 に同様の回答を投稿しました

3

指定したハミング距離内で元のリストの可能なバリエーションをすべて事前に計算し、ブルームフィルターに保存できます。これにより、高速な「NO」が得られますが、必ずしも「YES」について明確な答えが得られるわけではありません。

YESの場合、ブルームフィルターの各位置に関連付けられているすべての元の値のリストを保存し、一度に1つずつ調べます。ブルームフィルターのサイズを最適化して、速度とメモリのトレードオフを実現します。

すべてが正確に機能するかどうかはわかりませんが、ランタイムRAMを書き込み、非常に長い時間を事前計算に費やすことを望んでいる場合は、良いアプローチのようです。

1
Leopd

この問題を解決するための1つの可能なアプローチは、 結合されていないデータ構造 を使用することです。アイデアは、同じセットでハミング距離<= kのリストメンバーをマージすることです。アルゴリズムの概要は次のとおりです。

  • リストメンバー可能なすべての計算ハミング距離<= kで。 k = 1の場合、32個の値があります(32ビット値の場合)。 k = 2、32 + 32 * 31/2値の場合。

    • 計算された各valueについて、それが元の入力にあるかどうかをテストします。サイズ2 ^ 32の配列またはハッシュマップを使用して、このチェックを行うことができます。

    • valueが元の入力にある場合、list memberを使用して「結合」操作を実行します。

    • 実行されたユニオン操作の数を変数に保持します。

N個の互いに素なセットでアルゴリズムを開始します(Nは入力内の要素の数です)。ユニオン操作を実行するたびに、互いに素なセットの数が1つ減ります。アルゴリズムが終了すると、disjoint-setデータ構造には、ハミング距離<= kのすべての値がdisjointセットにグループ化されます。この互いに素なデータ構造は almostlinear time で計算できます。

1
Marcio Fonseca

リストを並べ替えて、その並べ替えられたリストで、ハミング距離内のさまざまな可能な値でバイナリ検索を実行するのはどうですか?

1
borrible