高速な類似検索 ― ハミング距離と鳩ノ巣原理
タイトルにあるハミング距離とは文字列間の類似度の一つで、2つの文字列間のハミング距離が小さいとき、それらの文字列は似ているとされます。実際に、たくさんある文字列の中からクエリの文字列に似た(すなわち、ハミング距離が小さい)文字列を取得したいという問題はたくさんあります。例えば、Googleは大量のWebページをSimHashとよばれるアルゴリズムを用いて長さ64のビット列に写像し、ハミング距離を用いて内容が重複するWebページを高速に検出しています。[1]
本記事では、このような問題を高速に解くために広く用いられるマルチインデックスと呼ばれる手法について解説します。また、マルチインデックスを説明する上では欠かせない鳩ノ巣原理について説明します。
あと、せっかくなので最後にState-of-the-Artな内容として、今年のICDE(2018年時点)で提案された一般鳩ノ巣原理についても簡単に紹介させてもらってます。
ハミング距離
Googleの例で示したように、よくビット列間のハミング距離を利用したいケースに遭遇します。なので、本記事でも対象をビット列に限定し、長さ
すなわち、ビット列
により求めることができます。例えば、
XOR演算も
問題設定
例えば、以下の例では
上の問題をどう効率的に解くかというのが本記事のテーマです。最も単純な解法は、上の例で示したように各
転置インデックスを用いた解法
別の解法としてよく知られる?のが、転置インデックスを用いた解法です。転置インデックスを簡単に説明すると、上の例で示したような
例えば、上の例のデータセットから転置インデックスを作ると以下のようになります。このデータ構造は、例えばクエリとして
さて、この転置インデックスを使ってどのように問題を解くかというと、ビット列
例えば、上で作った転置インデックスに対し、クエリ
問題点
多くの方が、上の例でクエリとして 9 個ビット列が生成されたことに嫌な予感がしたと思います。そうです。この解法の問題点は、ビット列長
二項係数が出てきたということは、結構なでかさになり得るということです。イメージが湧きづらい人のために、
|
|
---|---|
0 | 1 |
1 | 64 |
2 | 2,079 |
3 | 43,742 |
4 | 679,117 |
5 | 8,303,629 |
6 | 83,277,996 |
7 | 704,494,187 |
8 | 5,130,659,554 |
9 | 32,671,244,065 |
10 | 184,144,458,880 |
11 | 927,740,240,703 |
12 | 4,211,954,943,758 |
13 | 17,348,813,755,981 |
14 | 65,204,513,714,796 |
15 | 224,723,513,577,515 |
16 | 713,250,450,657,094 |
17 | 2,092,620,625,940,610 |
18 | 5,694,309,416,958,690 |
19 | 14,414,187,542,581,400 |
20 | 34,033,913,325,232,500 |
21 | 75,141,910,203,168,200 |
22 | 155,489,358,646,406,000 |
23 | 302,210,786,238,405,000 |
24 | 552,859,891,708,071,000 |
25 | 953,898,460,459,537,000 |
26 | 1,555,456,313,586,730,000 |
27 | 2,402,093,292,062,050,000 |
28 | 3,520,863,585,047,290,000 |
29 | 4,909,681,879,787,590,000 |
30 | 6,529,969,890,317,930,000 |
見ての通り地獄です。
そもそも、線形探索より高速に検索することが目的でしたので、
でもまぁ安心してください。「だから皆さん線形探索を使いましょう!」なんてオチではないですし、まだ鳩も出てきてないです。以下では、この転置インデックスを用いた解法を上手く活用し、高速に問題を解くマルチインデックスという手法を紹介します。
マルチインデックス
ここからは、転置インデックスによる解法を大きいパラメータに対しても有効に使うために用いられるマルチインデックスという手法を紹介していきます。マルチインデックスは、その名の通り複数のインデックスを作る手法です。構築段階と検索段階に分けて、具体的にそのアプローチを見ていきましょう。
構築
マルチインデックスでは、まず各ビット列
後は、それぞれのブロックに対して、同じように転置インデックスを構築するだけです。今回の例では、図のように2つの転置インデックスが構築されます。
検索
マルチインデックスでは、これらの複数の転置インデックスに対し検索を実行します。このとき、Filter-and-Verification戦略[5]というものに基づいて解を求めます。この戦略は、FilterステップとVerificationステップの二段階に分かれます。
Filterステップ
クエリとしてビット列
例えば、これまでと同じようにビット列
Verificationステップ
Verificationステップは読んで字のごとくです。先程得られた解候補に対して実際にクエリとのハミング距離を計算し検証することで真の解を見つけます。
上の例では解候補として {1,3,4} が得られたので、クエリビット列
閾値の設定条件
マルチインデックス。聞いてみればえらくあっさりとした内容だったと思います。でもまだ棚上げしてることが一つ残ってますよね。そうです。各ブロックに対する閾値として割り当てられた
ここで改めて各ブロックに割り当てる閾値に求められる要件を考えてみると、以下の2つになります。
- この閾値を用いて得られる解候補が真の解を全て含んでいること。
- 高速に検索するためや解候補を少なくするために小さな閾値であること。
とりわけ1つ目の要件は重要です。例えば、上の例で得られた解候補が {1,3} で 4 が含まれてなかったとすれば、Verificationステップでいくら頑張ろうとも真の解を得ることができません。このように真の解を見逃すことを、False Negativeといいます。一方で、1 のような解ではないものが含まれてる分には、Verificationステップで検出できるので(最終的に真の解が得られるという点では)問題ありません。このような誤検出をFalse Positiveといいます。
改めて、閾値の設定条件はFalse Negativeを許してはいけないということです。
鳩ノ巣原理(Pigeonhole Principle)
てことでようやく鳩ノ巣原理について解説します。といっても簡単な原理ですし、調べればいろんな応用が出てくると思います。
鳩ノ巣原理はいろんな説明の仕方があると思いますが、今回のシナリオに沿って説明すると以下のような原理になります。
【鳩ノ巣原理】
羽の鳩が M 個の鳩ノ巣に割り振られているとき、$\lfloor M / N \rfloor $羽以下の鳩を含む巣が少なくとも1つ存在する。 N
例えば、
実はこの鳩と鳩ノ巣の関係は、ビット列間の不一致文字とブロックの関係に置き換えることができます。例えば、以下の図のように
以上のことから、ブロック数
よって、マルチインデックスの閾値として
一般鳩ノ巣原理(General Pigeonhole Principle)
おかげさまで基本的なことは一通り言い終わったのですが、せっかくなのでState-of-the-Artなことも書こうと思います。
マルチインデックスは非常に実用的な手法で、今までにいくつかの亜種が発表されてますが、ずっとそれらは鳩ノ巣原理に基づいて設計されてきました。しかし、False Negativeを起こらないようにするとき、
実は今年のICDEというデータベース系の有名会議で、一般鳩ノ巣原理(General Pigeonhole Principle)[6]が提案され、従来の鳩ノ巣原理に基づく割り当てより小さい閾値の割り当てが存在し、且つ一般鳩ノ巣原理に基づく割り当てより小さい閾値の割り当てが他に存在しないことが示されました。
Jianbin Qin, Yaoshu Wang, Chuan Xiao, Wei Wang, Xuemin Lin, and Yoshiharu Ishikawa, "GPH: Similarity Search in Hamming Space", 34th International Conference on Data Engineering (ICDE), pp. 29-40, 2018.
恐縮ながら、簡単に解説させていただきます。ただし本当は、これを議論する上で「小さい閾値の割り当てとは何か」を論文に従ってちゃんと定義する必要があります。でもちょっと長くなるし面倒なので、今回はその辺を端折って一般鳩ノ巣原理を示し、分かりやすい一例を示すだけに留めたいと思います。
また、一般鳩ノ巣原理との違いを明らかにするため、以下ではこれまで説明した従来の鳩ノ巣原理を基本鳩ノ巣原理とよびます。
【定理:基本鳩ノ巣原理】
互いに疎な個のブロックに分割された2つのビット列 b と q = q^1,...,q^b が与えられたとき、 x = x^1,...,x^b を閾値として各ブロックに割り当てる。もし \lfloor k / b \rfloor なら、 Ham(q,x) \leq k を満たすブロック Ham(q^j,x^j) \leq \lfloor k / b \rfloor が少なくとも1つは存在する。 j
補題
まずは準備として柔軟鳩ノ巣原理(Flexible Pigeonhole Principle)[7]を導入します。基本鳩ノ巣原理では、どのブロックにも同じ閾値を割り当ててました。この補題では、各ブロックの閾値の合計値がある基準を満たしていれば、柔軟に違う閾値を割り当ててもFalse Negativeが起こらないことを証明します。
【補題1:柔軟鳩ノ巣原理】
互いに疎な個のブロックに分割された2つのビット列 b と q = q^1,...,q^b が与えられたとき、 x = x^1,...,x^b となるような整数値の閾値 \sum_{1\leq j \leq b} k^j = k をそれぞれのブロックに割り当てる。もし k^1,...,k^b なら、 Ham(q,x) \leq k となるようなブロック Ham(q^j,x^j) \leq k^j が少なくとも1つは存在する。 j
改めて確認しておくと、「
この補題は背理法を使って簡単に証明できます。
(証明)
となるようなブロックが1つも存在しないことを仮定する。 Ham(q^j,x^j) \leq k^j 個のブロックは互いに疎なので b が成り立つが、仮定より Ham(q,x)=\sum_{1\leq j \leq b} Ham(q^j,x^j) が成り立つので、矛盾が生じる。 \sum_{1\leq j \leq b} Ham(q^j,x^j) > k
例えば、以下のように
この補題の面白いところは、実は整数値に限らず実数値を閾値に割り当てても補題が成り立つところにあります。すなわち、以下のような補題も成り立ちます。
【補題2:柔軟鳩ノ巣原理(実数値版)】
互いに疎な個のブロックに分割された2つのビット列 b と q = q^1,...,q^b が与えられたとき、 x = x^1,...,x^b となるような実数値の閾値 \sum_{1\leq j \leq b} k^j = k をそれぞれのブロックに割り当てる。もし k^1,...,k^b なら、 Ham(q,x) \leq k となるようなブロック Ham(q^j,x^j) \leq k^j が少なくとも1つは存在する。 j
これは同じように背理法で証明できるので、証明は省略します。
一見、補題1と補題2の違いは「整数値」と「実数値」だけのように思えますが、実は暗に大きく変わってるところがあります。それは
となるようなブロック Ham(q^j,x^j) \leq k^j が少なくとも1つは存在する。 j
という部分です。今回のケースにおいて、
となるようなブロック Ham(q^j,x^j) \leq \lfloor k^j \rfloor が少なくとも1つは存在する。 j
すなわち、小数点以下を切り捨てることで、補題1と
例えば、先程の例に対して、以下のように実数値の閾値を割り当てることができます。閾値の合計値は同じく 4 です。しかし、小数点以下を切り捨てられるので、実質
定理
準備も整ったので、一般鳩ノ巣原理を以下に示します。
【定理:一般鳩ノ巣原理】
互いに疎な個のブロックに分割された2つのビット列 b と q = q^1,...,q^b が与えられたとき、 x = x^1,...,x^b となるような整数値の閾値 \sum_{1\leq j \leq b} k^j = k - b + 1 をそれぞれのブロックに割り当てる。もし k^1,...,k^b なら、 Ham(q,x) \leq k となるようなブロック Ham(q^j,x^j) \leq k^j が少なくとも1つは存在する。 j
証明
一般鳩ノ巣原理は上記の補題を用いて証明できます。
まず、
続いて以下の図のように、ものすごく小さい実数値
そして、補題2のときと同じように、
よって、一般鳩ノ巣原理が証明されました。めでたしめでたし。
例
本当は「めでたしめでたし」ではなく、一般鳩ノ巣原理に基づく閾値の割り当てよりも他に小さい割り当てが存在しないことや、基本鳩ノ巣原理より大きくならないことを示さないといけないのですが、それは論文参照でお願いします。
ここでは最後に、この割り当てが基本鳩ノ巣原理の割り当てより小さくなってる一例を示します。
例えば、
おわりに
というわけで、重要なことを論文に投げた形で本記事は終わります。完全なる体力切れです。すみません。でもいっぱい図を作ったんで許してください。
もしここまで読んでくれた方いらっしゃいましたら、ありがとうございました。
マルチインデックスは非常に実用的な手法なので、もし今回設定したような問題を解きたい方にはオススメします。問い合わせ時間に関する理論的な解析については、「Fast search in hamming space with multi-index hashing, CVPR2012」などを参照すると良いです。
一般鳩ノ巣原理の論文については、この他にもビット列中の1と0の出現に偏りがあった場合の適した閾値の割り振り方なども提案しているので、興味がある方は読んでみてはいかがでしょうか。あと今回は特に触れませんでしたが、一般鳩ノ巣原理は閾値を非負整数に制限していません。すなわち、負の値も閾値として設定できるということです。偏りに応じた割り振りでは、この辺りの特性も考慮したりしていて面白いです。
あと、これの他にPigeonring Principle[8]とかもあったりします。これも面白いです。てか凄いです。
-
Gurmeet Singh Manku, Arvind Jain, and Anish Das Sarma, "Detecting near-duplicates for web crawling", 16th International Conference on World Wide Web (WWW), pp. 141–150, 2007. ↩︎
-
についてはpopcountで調べればいろんな記事がヒットすると思います。例えば、http://www.nminoru.jp/~nminoru/programming/bitcount.html ↩︎popcnt -
インパクトを演出するために
まで示しましたが、こんな大きな閾値で実際に検索したいかは別の話です。 ↩︎k = 30 -
どうでもいいことですが、以下のサイトで数字から漢数字に変換しました。https://www.sljfaq.org/cgi/numbers_ja.cgi ↩︎
-
他にも、「Filter-and-Refine」や「Search-and-Check」とよばれたりしてます。 ↩︎
-
勝手に直訳してます。一般化鳩ノ巣原理にすべきだったでしょうか。 ↩︎
-
これも勝手に直訳してます。もっとセンスの良いのがありそうですが。 ↩︎
-
Jianbin Qin and Chuan Xiao, "Pigeonring: A Principle for Faster Thresholded Similarity Search", 44th International Conference on Very Large Data Bases (VLDB), pp. 28–42, 2018. ↩︎
Discussion