Preface
本文主要介紹一種用於海量高維數據的近似最近鄰快速查找技術——局部敏感哈希 (Locality-Sensitive Hashing, LSH),內容包括了 LSH 的原理、LSH 哈希函數集、以及 LSH 的一些參考資料。
局部敏感哈希 LSH
在很多應用領域中,我們面對和需要處理的數據往往是海量並且具有很高的維度,怎樣快速地從海量的高維數據集合中找到與某個數據最相似(距離最近)的一個數據或多個數據成為了一個難點和問題。如果是低維的小數據集,我們通過線性查找(Linear Search)就可以容易解決,但如果是對一個海量的高維數據集採用線性查找匹配的話,會非常耗時,因此,為了解決該問題,我們需要採用一些類似索引的技術來加快查找過程,通常這類技術稱為最近鄰查找(Nearest Neighbor ,AN),例如Kd tree;或近似最近鄰查找(Approximate Nearest Neighbor, ANN),例如 Kd tree with BBF, Randomized Kd-trees, Hierarchical K-means Tree。而 LSH 是ANN中的一類方法。
我們知道,通過建立Hash Table的方式我們能夠得到 O(1) 的查找時間性能,其中關鍵在於選取一個 hash function,將原始數據映射到相對應的桶內(bucket, hash bin),例如對數據求模:h = x mod w,w通常為一個素數。在對數據集進行hash 的過程中,會發生不同的數據被映射到了同一個桶中(即發生了衝突collision),這一般通過再次哈希將數據映射到其他空桶內來解決。這是普通Hash方法或者叫傳統Hash方法,其與LSH有些不同之處。
局部敏感哈希示意圖(from: Here)
LSH的基本思想是:將原始數據空間中的兩個相鄰數據點通過相同的映射或投影變換(projection)後,這兩個數據點在新的數據空間中仍然相鄰的概率很大,而不相鄰的數據點被映射到同一個桶的概率很小。也就是說,如果我們對原始數據進行一些hash映射後,我們希望原先相鄰的兩個數據能夠被hash到相同的桶內,具有相同的桶號。對原始數據集合中所有的數據都進行hash映射後,我們就得到了一個hash table,這些原始數據集被分散到了hash table的桶內,每個桶會落入一些原始數據,屬於同一個桶內的數據就有很大可能是相鄰的,當然也存在不相鄰的數據被hash到了同一個桶內。因此,如果我們能夠找到這樣一些 hash functions,使得經過它們的哈希映射變換後,原始空間中相鄰的數據落入相同的桶內的話,那麼我們在該數據集合中進行近鄰查找就變得容易了,我們只需要將查詢數據進行哈希映射得到其桶號,然後取出該桶號對應桶內的所有數據,再進行線性匹配即可查找到與查詢數據相鄰的數據。換句話說,我們通過hash function映射變換操作,將原始數據集合分成了多個子集合,而每個子集合中的數據間是相鄰的且該子集合中的元素個數較小,因此將一個在超大集合內查找相鄰元素的問題轉化為了在一個很小的集合內查找相鄰元素的問題,顯然計算量下降了很多。
那具有怎樣特點的 hash functions 才能夠使得原本相鄰的兩個數據點經過 hash 變換後會落入相同的桶內?這些 hash function 需要滿足以下兩個條件:
其中 d(x,y) 表示 x 和 y 之間的距離,d1 < d2, h(x) 和 h(y) 分別表示對x和 y 進行 hash 變換。 滿足以上兩個條件的 hash functions 稱為 (d1,d2,p1,p2)-sensitive。而通過一個或多個 (d1,d2,p1,p2)-sensitive 的 hash function 對原始數據集合進行 hashing 生成一個或多個 hash table 的過程稱為 Locality-sensitive Hashing。
使用 LSH 進行對海量數據建立索引(Hash table)並通過索引來進行近似最近鄰查找的過程如下:
1. 離線建立索引
2. 在線查找
LSH 在線查找時間由兩個部分組成: (1)通過 LSH hash functions 計算hash值(桶號)的時間;(2)將查詢數據與桶內的數據進行比較計算的時間。因此,LSH 的查找時間至少是一個 sublinear 時間。為什麼是“至少”?因為我們可以通過對桶內的屬於建立索引來加快匹配速度,這時第(2)部分的耗時就從 O(N) 變成了O(logN) 或 O(1)(取決於採用的索引方法)。 LSH 為我們提供了一種在海量的高維數據集中查找與查詢數據點(query data point)近似最相鄰的某個或某些數據點。需要注意的是,LSH 並不能保證一定能夠查找到與query data point最相鄰的數據,而是減少需要匹配的數據點個數的同時保證查找到最近鄰的數據點的概率很大。
二、LSH的應用
LSH 的應用場景很多,凡是需要進行大量數據之間的相似度(或距離)計算的地方都可以使用 LSH 來加快查找匹配速度,下面列舉一些應用:
(1)查找網絡上的重複網頁
(2)查找相似新聞網頁或文章
(3)圖像檢索
(4)音樂檢索
(5)指紋匹配
三、LSH family
我們在第一節介紹了LSH的原理和LSH hash function需要滿足的條件,回顧一下: 滿足以下兩個條件的 hash functions 稱為 (d1,d2,p1,p2)-sensitive:
d(x,y) 是 x 和 y 之間的一個距離度量(distance measure),需要說明的是,並不是所有的距離度量都能夠找到滿足 locality-sensitive 的 hash functions。下面我們介紹一些滿足不同距離度量方式下的 locality-sensitive 的 hash functions:
1. Jaccard distance
2. Hamming distance
3. Cosine distance
4. normal Euclidean distance
四、增強LSH(Amplifying LSH)
通過 LSH hash functions 我們能夠得到一個或多個 hash table,每個桶內的數據之間是近鄰的可能性很大。我們希望原本相鄰的數據經過 LSH hash 後,都能夠落入到相同的桶內,而不相鄰的數據經過 LSH hash 後,都能夠落入到不同的桶中。如果相鄰的數據被投影到了不同的桶內,我們稱為 false negtive;如果不相鄰的數據被投影到了相同的桶內,我們稱為 false positive。因此,我們在使用 LSH 中,我們希望能夠盡量降低 false negtive rate 和 false positive rate。
通常,為了能夠增強 LSH,即使得 false negtive rate 和/或 false positive rate 降低,我們有兩個途徑來實現:1)在一個hash table內使用更多的 LSH hash functio n;2)建立多個hash table。下面介紹一些常用的增強LSH的方法:
1. 使用多個獨立的 hash table
每個 hash table 由 k 個LSH hash function創建,每次選用 k 個LSH hash function(同屬於一個LSH function family)就得到了一個 hash table,重複多次,即可創建多個 hash table。多個 hash table 的好處在於能夠降低 false positive rate。
2. AND 與操作
從同一個 LSH function family 中挑選出 k 個 LSH function,H(X) = H(Y) 有且僅當這 k 個 Hi(X) = Hi(Y) 都滿足。也就是說只有當兩個數據的這 k 個 hash 值都對應相同時,才會被投影到相同的桶內,只要有一個不滿足就不會被投影到同一個桶內。
AND與 操作能夠使得找到近鄰數據的 p1 概率保持高概率的同時降低 p2 概率,即降低了false negtive rate。
3. OR 或操作
從同一個 LSH function family 中挑選出 k 個 LSH function,H(X) = H(Y) 有且僅當存在一個以上的 Hi(X) = Hi(Y)。也就是說只要兩個數據的這 k 個 hash 值中有一對以上相同時,就會被投影到相同的桶內,只有當這 k 個 hash 值都不相同時才不被投影到同一個桶內。
OR或 操作能夠使得找到近鄰數據的p1概率變的更大(越接近1)的同時保持p2概率較小,即降低了 false positive rate。
4. AND 和 OR 的級聯
將與操作和或操作級聯在一起,產生更多的 hahs table,這樣的好處在於能夠使得 p1 更接近 1,而 p2 更接近0。
除了上面介紹的增強 LSH 的方法外,有時候我們希望將多個 LSH hash function 得到的 hash 值組合起來,在此基礎上得到新的hash值,這樣做的好處在於減少了存儲 hash table 的空間。下面介紹一些常用方法:
1. 求模運算
new hash value = old hash value % N
2. 隨機投影
假設通過k個LSH hash function得到了k個hash值:h1, h2..., hk。那麼新的hash值採用如下公式求得:
new hash value = h1*r1 + h2*r2 + ... + hk*rk,其中r1, r2, ..., rk是一些隨機數。
3. XOR異或
假設通過k個LSH hash function得到了k個hash值:h1, h2..., hk。那麼新的hash值採用如下公式求得:
new hash value = h1 XOR h2 XOR h3 ... XOR hk
Java - LSH
A Java implementation of Locality Sensitive Hashing (LSH). This library implements Locality Sensitive Hashing (LSH), as described in Leskovec, Rajaraman & Ullman (2014), "Mining of Massive Datasets", Cambridge University Press. Are currently implemented:
MinHash
MinHash is a hashing scheme that tents to produce similar signatures for sets that have a high Jaccard similarity. The Jaccard similarity between two sets is the relative number of elements these sets have in common: J(A, B) = |A ∩ B| / |A ∪ B|. A MinHash signature is a sequence of numbers produced by multiple hash functions hi. It can be shown that the Jaccard similarity between two sets is also the probability that this hash result is the same for the two sets: J(A, B) = Pr[hi(A) = hi(B)]. Therefore, MinHash signatures can be used to estimate Jaccard similarity between two sets. Moreover, it can be shown that the expected estimation error is O(1 / sqrt(n)), where n is the size of the signature (the number of hash functions that are used to produce the signature).
Binning
下面範例代碼示範該套件的使用, 流程如下:
- SimpleLSHMinHashExample.java
- package jlsh;
- import info.debatty.java.lsh.LSHMinHash;
- import jlsh.SimpleLSHMinHashEx2.DefaultHRMap;
- import jlsh.SimpleLSHMinHashEx2.DefaultSGMap;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Random;
- public class SimpleLSHMinHashExample {
- public static class DefaultHRMap
{ - protected V defaultValue;
- public DefaultHRMap() {}
- @Override
- public V get(Object k) {
- if(containsKey(k)) return super.get(k);
- else
- {
- List
v = new ArrayList (); - super.put((K)k, (V)v);
- return (V)v;
- }
- }
- }
- public static class DefaultSGMap
{ - protected V defaultValue;
- public DefaultSGMap() {}
- @Override
- public V get(Object k) {
- if(containsKey(k)) return super.get(k);
- else
- {
- Map
> v = new DefaultHRMap >(); - super.put((K)k, (V)v);
- return (V)v;
- }
- }
- }
- public static void main(String args[])
- {
- // proportion of 0's in the vectors
- // if the vectors are dense (lots of 1's), the average jaccard similarity
- // will be very high (especially for large vectors), and LSH
- // won't be able to distinguish them
- // as a result, all vectors will be binned in the same bucket...
- double sparsity = 0.75;
- // Number of sets
- int count = 10000;
- // Size of vectors
- int n = 50;
- // LSH parameters
- // the number of stages is also sometimes called thge number of bands
- int stages = 2;
- // Attention: to get relevant results, the number of elements per bucket
- // should be at least 100
- int buckets = 500;
- // Let's generate some random sets
- boolean[][] vectors = new boolean[count][n];
- Random rand = new Random();
- for (int i = 0; i < count; i++) {
- for (int j = 0; j < n; j++) {
- vectors[i][j] = rand.nextDouble() > sparsity;
- }
- }
- // Create and configure LSH algorithm
- LSHMinHash lsh = new LSHMinHash(stages, buckets, n);
- int[][] counts = new int[stages][buckets];
- for (int i = 0; i < stages; i++) {
- for(int j=0; j
0; - }
- // Perform hashing
- int vi = 0; // Vector index
- DefaultHRMap
> hrMap = new DefaultHRMap >(); - Map
>> sgMap = new DefaultSGMap >>(); - for (boolean[] vector : vectors) {
- int[] hash = lsh.hash(vector);
- //hrMap.get(String.valueOf(hash[0])).add(vi);
- hrMap.get(arr2str(hash)).add(vi);
- for (int i = 0; i < hash.length; i++) {
- counts[i][hash[i]]++;
- sgMap.get(i).get(hash[i]).add(vi);
- }
- print(vector);
- System.out.printf(" : %s", arr2str(hash));
- System.out.print("\n");
- vi++;
- }
- System.out.println("Number of elements per bucket at each stage:");
- for (int i = 0; i < stages; i++) {
- print(counts[i]);
- System.out.print("\n");
- }
- System.out.println();
- String bis = "(0 0)";
- int s=0, b=0;
- while(hrMap.get(bis).size()==0)
- {
- b++;
- if(b==buckets) {s++; b = 0;}
- bis = String.format("(%d %d)", s, b);
- }
- // Randomly show one bucket
- System.out.printf("Show records in bin%s (%d):\n", bis, hrMap.get(bis).size());
- for(int v:hrMap.get(bis))
- {
- print(vectors[v]); System.out.println();
- }
- b++;
- if(b==buckets) {s++; b = 0;}
- bis = String.format("(%d %d)", s, b);
- System.out.println();
- boolean tvec[] = new boolean[n]; // Target vector
- for (int j = 0; j < n; j++) {
- tvec[j] = rand.nextDouble() > sparsity;
- }
- System.out.printf("Target vector:\n"); print(tvec); System.out.println();
- int[] hash = lsh.hash(tvec);
- System.out.printf("Hash: %s\n", arr2str(hash));
- double cs = Double.MIN_VALUE; int tvi=0; int cc=0;
- for(int i=0; i
- {
- System.out.printf("Look for bucket=%d at stage=%d (%d)...\n", hash[i], i, sgMap.get(i).get(hash[i]).size());
- for(int j:sgMap.get(i).get(hash[i]))
- {
- double ccs = CosineSim(tvec, vectors[j]);
- if(ccs>cs)
- {
- cs = ccs; tvi = j;
- print(vectors[j]); System.out.printf(": %.02f\n", ccs);
- }
- cc++;
- }
- }
- System.out.printf("Search count=%,d!\n", cc);
- }
- static double CosineSim(boolean[] a, boolean[] b)
- {
- double pdv = 0.0;
- double av = 0.0, bv = 0.0;
- for(int i=0; i
- {
- if(a[i] && b[i]) pdv++;
- if(a[i]) av++; if(b[i]) bv++;
- }
- if(pdv==0) return 0.0;
- else
- {
- return pdv/(Math.sqrt(av)*Math.sqrt(bv));
- }
- }
- static void print(int[] array) {
- System.out.print("[ ");
- for (int v : array) {
- System.out.print("" + v + " ");
- }
- System.out.print("]");
- }
- static String arr2str(int[] arr)
- {
- StringBuffer strBuf = new StringBuffer();
- for(int v:arr) strBuf.append(String.format("%d ", v));
- return String.format("(%s)", strBuf.toString().trim());
- }
- static void print(boolean[] array) {
- System.out.print("[");
- for (boolean v : array) {
- System.out.print(v ? "1" : "0");
- }
- System.out.print("]");
- }
- }
透過 LSH 你不需要比對過 10,000 筆的 vectors, 就可以找出最相近的 vector.
Supplement
* LSH.9 Locality-sensitive hashing: how it works
* LSH.10 False positive and negative errors of LSH
* LSH.11 Hash-code length and number of hashtables
* How to understand Locality Sensitive Hashing?
* Java - LSH - A Java implementation of Locality Sensitive Hashing (LSH).
if you are looking for php interview Questions then you may click here
回覆刪除It is awesome! Thanks for sharing!
回覆刪除You are welcome.
刪除