Probabilistic Data Structures for Web Analytics and Data Mining
對於big data經常需要做如下的查詢和統計,
Cardinality Estimation (基數或勢), 集合中不同元素的個數, 比如, 獨立訪客(Unique Visitor,簡稱UV)統計
Frequency Estimation, 估計某個element重復出現次數, 比如, 某個用戶對網站訪問次數
Heavy Hitters, top-k elements, 比如, 銷量top-100的商鋪
Range Query, 比如找出年齡在20~30之間的用戶
Membership Query, 是否包含某個element, 比如, 該用戶名是否已經被注冊.
當然你可以采用精確的數據結構, sorted table或hash table, 結果是需要耗費的空間比較大, 如圖中對於40M數據, 需要4~7M的數據.
但是其實在很多情況下, 我們不需要很精確的結果, 可以容忍較小的誤差, 那么在這種情況下, 我們就可以使用些基於概率的數據結構來大大提高時空效率.
1 Cardinality Estimation
解讀Cardinality Estimation算法(第一部分:基本概念)
Big Data Counting: How to count a billion distinct objects using only 1.5KB of Memory
1.1 Cardinality Estimation: Linear Counting
Linear Counting, 比較簡單的一種方法, 類似於Bitmap, 至少在實現上看沒有什么不同, 最終通過數有多少'1'來判斷個數
區別在於, Bitmap是精確的方法直接用'1'的個數來表示Cardinality, 所以必須要分配足夠的空間以避免沖突, 比如Cardinality上限為10000的集合, 就需要分配10000bit的bitmap
而Linear Counting, 是概率近似的方法, 允許沖突, 只要選取合適的m(bitset的大小), 就可以根據'1'的個數來推斷出近似的Cardinality.
class LinearCounter { BitSet mask = new BitSet(m) // m is a design parameter void add(value) { int position = hash(value) // map the value to the range 0..m mask.set(position) // sets a bit in the mask to 1 } }
所以接下來的問題就是,
如何根據'1'的個數來推斷出近似的Cardinality?
如何選取合適的m? m太大浪費空間, m太小會導致所有bit都被置1從而無法估計, 所以必須根據Cardinality上限n計算出合適的m
參考下面的公式, 第一個公式就是根據m和w(1的個數)來計算近似的Cardinality
優點, 簡單, 便於多集合合並(多個bitset直接or即可)
缺點, 空間效率不夠理想, m大約為n的十分之一, 空間復雜度仍為O(Nmax)
Case Study, 收到各個網站的用戶訪問log, 需要支持基於時間范圍和網站范圍的UV查詢
對於每個網站的每個時間單元(比如小時)建立Linear Counting, 然后根據輸入的時間和網站范圍進行or合並, 最終計算出近似值
1.2 Cardinality Estimation: Loglog Counting
這個數據結構和算法比較復雜, 但基於的原理還是可以說的清楚的
首先, 需要將集合里面所有的element進行hash, 這里的hash函數必須要保證服從均勻分布(即使集合里面的element不是均勻的), 這個前提假設是Loglog Counting的基礎
在均勻分布的假設下, 產生的hash value就有如下圖中的分布比例, 因為每個bit為0或1的概率都是1/2, 所以開頭連續出現的0的個數越多, 出現概率越小, 需要嘗試伯努利過程的次數就越多
Loglog Counting就是根據這個原理, 根據出現的最大的rank數, 來estimate伯努利過程的次數(即Cardinality)
假設設ρ(a)為a的比特串中第一個"1”出現的位置, 即前面出現連續ρ(a)-1個0, 其實這是個伯努利過程
集合中有n個elements, 而每個element的ρ(a)都小於k的概率為, 當n足夠大(>>2^k)的時候接近0
反之, 至少有一個element大於k的概率為, 當n足夠小(<<2^k)的時候接近0
所以當在集合中出現ρ(a) = k時, 說明n不可能遠大於或遠小於2^k(從概率上講)
故當取得一個集合中的Max(ρ(a))時, 可以將2^Max(ρ(a))作為Cardinality的近似值
但這樣的方案的問題是, 偶然性因素影響比較大, 因為小概率事件並不是說不會發生, 從而帶來較大的誤差
所以這里采用分桶平均的方式來平均誤差,
將哈希空間平均分成m份,每份稱之為一個桶(bucket)。對於每一個元素,其哈希值的前k比特作為桶編號,其中2^k=m,而后L-k個比特作為真正用於基數估計的比特串。桶編號相同的元素被分配到同一個桶,在進行基數估計時,首先計算每個桶內元素最大的第一個“1”的位置,設為M[i],然后對這m個值取平均后再進行估計,
class LogLogCounter { int H // H is a design parameter, hash value的bit長度 int m = 2^k // k is a design parameter, 划分的bucket數 etype[] estimators = new etype[m] // etype is a design parameter, 預估值的類型(ex,byte), 不同rank函數的實現可以返回不同的類型 void add(value) { hashedValue = hash(value) //產生H bits的hash value bucket = getBits(hashedValue, 0, k) //將前k bits作為桶號 estimators[bucket] = max( //對每個bucket只保留最大的預估值 estimators[bucket], rank( getBits(hashedValue, k, H) ) //用k到H bits來預估Cardinality ) } getBits(value, int start, int end) //取出從start到end的bits段 rank(value) //取出ρ(value) }
優點, 空間效率顯著優化, 可以支持多集合合並(對每個bucket的預估值取max)
缺點, n不是特別大時, 計誤差過大, HyperLogLog Counting和Adaptive Counting就是這類改進算法
Hyper loglog,https://www.jianshu.com/p/55defda6dcd2
一個hyper loglog的演示app
http://content.research.neustar.biz/blog/hll.html
2 Frequency Estimation
估計某個element的出現次數
正常的做法就是使用sorted table或者hash table, 問題當然就是空間效率
所以我們需要在犧牲一定的准確性的情況下, 優化空間效率
2.1 Frequency Estimation: Count-Min Sketch
這個方法比較簡單, 原理就是, 使用二維的hash table, w是hash table的取值空間, d是hash函數的個數
對某個element, 分別使用d個hash函數計算相應的hash值, 並在對應的bucket上遞增1, 每個bucket的值稱為sketch, 如圖
然后在查詢某個element的frequency時, 只需要取出所有d個sketch, 然后取最小的那個作為預估值, 如其名
因為為了節省空間, w*d是遠小於真正的element個數的, 所以必然會出現很多的沖突, 而最小的那個應該是沖突最少的, 最精確的那個
這個方法的思路和bloom filter比較類似, 都是通過多個hash來降低沖突帶來的影響
class CountMinSketch { long estimators[][] = new long[d][w] // d and w are design parameters long a[] = new long[d] long b[] = new long[d] long p // hashing parameter, a prime number. For example 2^31-1 void initializeHashes() { //初始化hash函數family,不同的hash函數中a,b參數不同 for(i = 0; i < d; i++) { a[i] = random(p) // random in range 1..p b[i] = random(p) } } void add(value) { for(i = 0; i < d; i++) estimators[i][ hash(value, i) ]++ //簡單的對每個bucket經行疊加 } long estimateFrequency(value) { long minimum = MAX_VALUE for(i = 0; i < d; i++) minimum = min( //取出最小的估計值 minimum, estimators[i][ hash(value, i) ] ) return minimum } hash(value, i) { return ((a[i] * value + b[i]) mod p) mod w //hash函數,a,b參數會變化 } }
優點, 簡單, 空間效率顯著優化
缺點, 對於大量重復的element或top的element比較准確, 但對於較少出現的element准確度比較差
實驗, 對於Count-Min sketch of size 3×64, i.e. 192 counters total
Dataset1, 10k elements, about 8500 distinct values, 較少重復的數據集, 測試結果准確度很差
Dataset2, 80k elements, about 8500 distinct values, 大量重復的數據集, 測試結果准確度比較高
2.2 Frequency Estimation: Count-Mean-Min Sketch
前面說了Count-Min Sketch只對重度重復的數據集有比較好的效果, 但對於中度或輕度重復的數據集, 效果就很差
因為大量的沖突對較小頻率的element的干擾很大, 所以Count-Mean-Min Sketch就是為了解決這個問題
原理也比較簡單, 預估sketch上可能產生的noise
怎么預估? 很簡單, 比如1000數hash到20個bucket里面, 那么在均勻分布的條件下, 一個bucket會被分配50個數
那么這里就把每個sketchCounter里面的noise減去
最終是取所有sketch的median(中位數), 而不是min
class CountMeanMinSketch { // initialization and addition procedures as in CountMinSketch // n is total number of added elements long estimateFrequency(value) { long e[] = new long[d] for(i = 0; i < d; i++) { sketchCounter = estimators[i][ hash(value, i) ] noiseEstimation = (n - sketchCounter) / (w - 1) e[i] = sketchCounter – noiseEstimator } return median(e) } }
3 Heavy Hitters (Top Elements)
3.1 Heavy Hitters: Count-Min Sketch
首先top element應該是重度重復的element, 所以使用Count-Min Sketch是沒有問題的
方法,
1. 建個Count-Min Sketch不斷的給所有的element進行計數
2. 需要取top的時候, 對集合中每個element從Count-Min Sketch取出近似的frequency, 然后放到heap中
其實這里使用Count-Min Sketch只是計算frequency, Top-n問題仍然是依賴heap來解決
use case, 比如網站IP訪問數的排名
3.2 Heavy Hitters: Stream-Summary
另外一種獲取top的思路,
維護一組固定個數的slots, 比如你要求Top-10, 那么維護10個slots
當elements過來, 如果slots里面有, 就遞增, 沒有就替換solts中frequency最小的那個
這個算法沒有講清楚, 給的例子也太簡單, 不太能理解e(maximum potential error)干嗎用的, 為什么4替換3后, 3的frequency作為4的maximum potential error
我的理解是, 因為3的frequency本身就是最小的, 所以4繼承3的frequency不會影響實際的排名,
這樣避免3,4交替出現所帶來的計數問題, 但這里的frequency就不是精確的, 3的frequency被記入4是potential error
The figure below illustrates how Stream-Summary with 3 slots works for the input stream {1,2,2,2,3,1,1,4}.
4 Range Query
4.1 Range Query: Array of Count-Min Sketches
RangeQuery, 毫無疑問需要類似B-tree這樣排序的索引, 對於大部分NoSql都很難支持
這里要實現的是, SELECT count(v) WHERE v >= c1 AND v < c2, 在一定范圍內的element的個數和
簡單的使用Count-Min Sketch的方法, 就是通過v的索引找出所有在范圍內的element, 然后去Count-Min Sketch中取出每個element的近似frequency, 然后相加
這個方法的問題在於, 在范圍內的element可能非常多, 並且那么多的近似值相加, 誤差會被大大的放大
解決辦法就是使用多個Count-Min Sketch, 來提供更粗粒度的統計
如圖, sketch1就是初始的, 以element為單位的統計, 沒一個小格代表一個element
sketch2, 以2個element為單位統計, 實際的做法就是truncate a one bit of a value, 比如1110111, 前綴匹配111011.
sketch3, 以4個element為單位統計......
最終sketchn, 所有element只會分兩類統計, 1開頭或0開頭
這樣再算范圍內的count, 就不需要一個個element加了, 只需要從粗粒度開始匹配查詢
如下圖, 只需要將4個紅線部分的值相加就可以了
MADlib (a data mining library for PostgreSQL and Greenplum) implements this algorithm to process range queries and calculate percentiles on large data sets.
5 Membership Query
查詢某個element在不在, 典型的Bloom Filter的應用






