大數據處理中基於機率的數據結構

Probabilistic Data Structures for Web Analytics and Data Mininghtml

 

對於big data常常須要作以下的查詢和統計,web

Cardinality Estimation (基數或勢), 集合中不一樣元素的個數, 好比, 獨立訪客(Unique Visitor,簡稱UV)統計算法

Frequency Estimation, 估計某個element重複出現次數, 好比, 某個用戶對網站訪問次數數據結構

Heavy Hitters, top-k elements, 好比, 銷量top-100的商鋪dom

Range Query, 好比找出年齡在20~30之間的用戶數據結構和算法

Membership Query, 是否包含某個element, 好比, 該用戶名是否已經被註冊.wordpress

固然你能夠採用精確的數據結構, sorted table或hash table, 結果是須要耗費的空間比較大, 如圖中對於40M數據, 須要4~7M的數據.
可是其實在不少狀況下, 咱們不須要很精確的結果, 能夠容忍較小的偏差, 那麼在這種狀況下, 咱們就可使用些基於機率的數據結構來大大提升時空效率.函數

image

 

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

image

優勢, 簡單, 便於多集合合併(多個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)

image

參考, 第三部分:LogLog Counting

假設設ρ(a)爲a的比特串中第一個"1」出現的位置, 即前面出現連續ρ(a)-1個0, 其實這是個伯努利過程
集合中有n個elements, 而每一個element的ρ(a)都小於k的機率爲, 當n足夠大(>>2^k)的時候接近0
 image

反之, 至少有一個element大於k的機率爲, 當n足夠小(<<2^k)的時候接近0
image

因此當在集合中出現ρ(a) = k時, 說明n不可能遠大於或遠小於2^k(從機率上講)
故當取得一個集合中的Max(ρ(a))時, 能夠將2^Max(ρ(a))做爲Cardinality的近似值
image

但這樣的方案的問題是, 偶然性因素影響比較大, 由於小几率事件並非說不會發生, 從而帶來較大的偏差
因此這裏採用分桶平均的方式來平均偏差,

將哈希空間平均分紅m份,每份稱之爲一個桶(bucket)。對於每個元素,其哈希值的前k比特做爲桶編號,其中2^k=m,然後L-k個比特做爲真正用於基數估計的比特串。桶編號相同的元素被分配到同一個桶,在進行基數估計時,首先計算每一個桶內元素最大的第一個「1」的位置,設爲M[i],而後對這m個值取平均後再進行估計,

image

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就是這類改進算法

 

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來下降衝突帶來的影響

image

 

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}.

image

 

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個紅線部分的值相加就能夠了

image

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的應用

相關文章
相關標籤/搜索