「Wikipedia の 600 万記事から、ある記事に似たものを見つける」「ECサイトの 1 億商品から、ある商品に似たものを推薦する」「論文の剽窃検査で、過去 100 億件と照合する」。これらすべて 「類似ペアを大量データから高速に発見する」 問題。MinHash + LSH がこれを O(n) で解く。
1. これで何が動いているか
- Google の重複コンテンツ検出 (Web ページのほぼコピーを除外)
- Spotify の楽曲類似性 (Discover Weekly の一部)
- Turnitin などの剽窃検査ツール
- メルカリ・Yahoo!オークション の類似商品検出
- Wikipedia の編集履歴差分検出
- Apache Solr / Elasticsearch の moreLikeThis
2. 仕組みのざっくり
- 1Step 1: 文書を集合化 (例: 単語、3-gram のシングル)
- 2Step 2: MinHash signature 計算 — 集合の類似度を圧縮 (128 個の整数)
- 3Step 3: LSH バケット に振り分け — signature を b バンドに分割、各バンドのハッシュをキーに
- 4Step 4: 同じバケットに入った候補ペア だけを Jaccard 推定で精査
なぜ Jaccard を MinHash で推定できるか
集合 A の全要素を 1 つのハッシュ関数 h でハッシュし、その最小値 min(h(A)) を取る。A と B の最小値が等しい確率は Jaccard(A,B) と等しい、という美しい数学的性質がある (1997 年 Andrei Broder 発見)。これを多数のハッシュで並列実行することで類似度を推定。
3. Python 実装: datasketch (動作確認済)
MinHash + LSH で類似文書検出
Python
# pip install datasketchfrom datasketch import MinHash, MinHashLSH
def make_minhash(text, num_perm=128): '''テキストを MinHash signature に変換''' m = MinHash(num_perm=num_perm) for word in text.lower().split(): m.update(word.encode('utf-8')) return m
# サンプル文書documents = { 'doc1': 'the quick brown fox jumps over the lazy dog', 'doc2': 'the quick brown fox jumps over the lazy cat', # 1 word 違い 'doc3': 'a completely different document about machine learning', 'doc4': 'the lazy dog sleeps under the warm sun', 'doc5': 'the quick brown fox jumps over the sleepy dog', # 2 words 違い}
# MinHash 計算minhashes = {k: make_minhash(v) for k, v in documents.items()}
# LSH index に投入lsh = MinHashLSH(threshold=0.5, num_perm=128)for k, m in minhashes.items(): lsh.insert(k, m)
# クエリ: doc1 と類似するものresult = lsh.query(minhashes['doc1'])print(f'doc1 と類似 (threshold=0.5): {result}')
# Jaccard 推定値の確認print('\nJaccard 類似度 (推定):')for a in documents: for b in documents: if a < b: j = minhashes[a].jaccard(minhashes[b]) print(f' {a} vs {b}: {j:.3f}')
# 実機実行結果:# doc1 と類似 (threshold=0.5): ['doc5', 'doc1', 'doc2']# doc1 vs doc2: 0.758# doc1 vs doc5: 0.789# doc1 vs doc3: 0.000 (全く違う)4. パラメータ調整
| num_perm | メモリ/文書 | Jaccard 推定誤差 |
|---|---|---|
| 64 | 256 B | 12.5% |
| 128 (推奨) | 512 B | 8.8% |
| 256 | 1 KB | 6.2% |
| 512 | 2 KB | 4.4% |
5. 大規模データでの実用パターン
1 億文書を分散処理
Python
# 各文書を MinHash に変換 (Spark / Dask で並列化)# 1 文書 ≈ 512B、1 億で 50 GB → 分散ストア
# datasketch の MinHashLSH は Redis backend にも対応from datasketch import MinHashLSH
# Redis をストレージとして使うlsh = MinHashLSH( threshold=0.7, num_perm=128, storage_config={'type': 'redis', 'redis': {'host': 'localhost', 'port': 6379}})
# 100 万件投入後、クエリ# similar = lsh.query(query_minhash)6. 計算量比較
| 手法 | 前処理 | クエリ | 1 億件で |
|---|---|---|---|
| 全ペア比較 | なし | O(n²) | 100 京演算 (現実的でない) |
| MinHash + LSH | O(n) | O(1) per query | 実時間 |
7. メリットとデメリット
- メリット: O(n) で類似ペア発見、メモリ効率良い、分散しやすい
- メリット: Jaccard 類似度が信号源、文書だけでなく「ユーザの好み集合」「タグの集合」等にも応用可
- デメリット: 「集合」表現に向く問題のみ (順序・頻度が重要なケースは苦手)
- デメリット: 偽陽性・偽陰性が一定割合発生
- デメリット: 短い文書 (10 単語以下) では精度が落ちる
8. 関連ツール
- datasketch (Python): MinHash / LSH / Theta Sketch 統合
- Apache DataSketches (Java): Yahoo 製、本番向け
- spark-mllib: Spark の LSH 実装
- simhash (Google 系): 似た目的の別アルゴリズム
9. 次の話
EP.07 では CRDT (Conflict-free Replicated Data Types) を扱います。Notion / Figma / Linear のリアルタイム同時編集 を可能にする分散データ構造の実装。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。