基數估計算法

譯註:給定一個數據集,求解數據集的基數(Cardinality,也譯作“勢”,表示一個數據集中不同數據項的數量)是非常普遍的一個需求。許多業務需求最終可以歸結爲基數求解,如網站訪問分析中的UV(訪客數,指一段時間內訪問網站的不同用戶的數量)。由於數據集基數是不可聚集指標(兩個數據集總的基數無法通過分別的基數簡單計算),因此如果要得到N個數據集任意組合的基數,需要 2^N

次數據集去重計算,是一個複雜度非常高的計算過程。當數據量較小時,可以採取bitmap“按位或”方法獲得較高的計算速度;而當數據量很大時,一般會採取概率算法對基數進行估計。這篇文章是對基數估計算法的一個非常好的概覽。
以下爲譯文


假如你有一個巨大的含有重複數據項數據集,這個數據集過於龐大以至於無法全部放到內存中處理。現在你想知道這個數據集裏有多少不同的元素,但是數據集沒有排好序,而且對如此大的一個數據集進行排序和計數幾乎是不可行的。你要如何估計數據集中有多少不同的數據項?很多應用場景都涉及這個問題,例如設計數據庫的查詢策略:一個良好的數據庫查詢策略不但和總的數據量有關,同時也依賴於數據中不同數據項的數量。

我建議在繼續閱讀本文前你可以稍微是思考一下這個問題,因爲接下來我們要談的算法相當有創意,而且實在是不怎麼直觀。

一個簡單直觀的基數估計方法
讓我們從一個簡單直觀的例子開始吧。假設你通過如下步驟生成了一個數據集:

1、隨機生成n個服從均勻分佈的數字

2、隨便重複其中一些數字,重複的數字和重複次數都不確定

3、打亂這些數字的順序,得到一個數據集

我們要如何估計這個數據集中有多少不同的數字呢?因爲知道這些數字是服從均勻分佈的隨機數字,一個比較簡單的可行方案是:找出數據集中最小的數字。假如m是數值上限,x是找到的最小的數,則 m/x

是基數的一個估計。例如,我們掃描一個包含0到1之間數字組成的數據集,其中最小的數是0.01,則一個比較合理的推斷是數據集中大約有100個不同的元素,否則我們應該預期能找到一個更小的數。注意這個估計值和重複次數無關:就如最小值重複多少次都不改變最小值的數值。
這個估計方法的優點是十分直觀,但是準確度一般。例如,一個只有很少不同數值的數據集卻擁有很小的最小值;類似的一個有很多不同值的數據集可能最小值並不小。最後一點,其實只有很少的數據集符合隨機均勻分佈這一前提。儘管如此,這個原型算法仍然是瞭解基數估計思想的一個途徑;後面我們會了解一些更加精巧的算法。

基數估計的概率算法
最早研究高精度基數估計的論文是Flajolet和Martin的 Probabilistic Counting Algorithms for Data Base Applications ,後來Flajolet又發表了 LogLog counting of large cardinalities 和 HyperLogLog: The analysis of a near-optimal cardinality estimation algorithm 兩篇論文對算法進行了進一步改進。通過逐篇閱讀這些論文來了解算法的發展和細節固然有趣,不過在這篇文章中我會忽略一些算法的理論細節,把精力主要放在如何通過論文中的算法解決問題。有興趣的讀者可以讀一下這三篇論文;本文不會介紹其中的數學細節。

Flajolet和Martin最早發現通過一個良好的哈希函數,可以將任意數據集映射成服從均勻分佈的(僞)隨機值。根據這一事實,可以將任意數據集變換爲均勻分佈的隨機數集合,然後就可以使用上面的方法進行估計了,不過只是這樣是遠遠不夠的。

接下來,他們陸續發現一些其它的基數估計方法,而其中一些方法的效果優於之前提到的方法。Flajolet和Martin計算了哈希值的二進制表示的0前綴,結果發現在隨機數集合中,通過計算每一個元素的二進制表示的0前綴,設k爲最長的0前綴的長度,則平均來說集合中大約有 2^k

個不同的元素;我們可以用這個方法估計基數。但是,這仍然不是很理想的估計方法,因爲和基於最小值的估計一樣,這個方法的方差很大。不過另一方面,這個估計方法比較節省資源:對於32位的哈希值來說,只需要5比特去存儲0前綴的長度。
值得一提的是,Flajolet-Martin在最初的論文裏通過一種基於bitmap的過程去提高估計算法的準確度。關於這點我就不再詳述了,因爲這種方法已經被後續論文中更好的方法所取代;對這個細節有興趣的讀者可以去閱讀原始論文。

到目前爲止,我們這種基於位模式的估計算法給出的結果仍然不夠理想。如何進行改進呢?一個直觀的改進方法就是使用多個相互獨立的哈希函數:通過計算每個哈希函數所產生的最長0前綴,然後取其平均值可以提高算法的精度。

實踐表明從統計意義來說這種方法確實可以提高估計的準確度,但是計算哈希值的消耗比較大。另一個更高效的方法就是隨機平均(stochastic averaging)。這種方法不是使用多個哈希函數,而是使用一個哈希函數,但是將哈希值的區間按位切分成多個桶(bucket)。例如我們希望取1024個數進行平均,那麼我們可以取哈希值的前10比特作爲桶編號,然後計算剩下部分的0前綴長度。這種方法的準確度和多哈希函數方法相當,但是比計算多個哈希效率高很多。

根據上述分析,我們可以給出一個簡單的算法實現。這個實現等價於Durand-Flajolet的論文中提出的LogLog算法;不過爲了方便,這個實現中統計的是0尾綴而不是0前綴;其效果是等價的。

def trailing_zeroes(num):
“”“Counts the number of trailing 0 bits in num.”“”
if num == 0:
return 32 # Assumes 32 bit integer inputs!
p = 0
while (num >> p) & 1 == 0:
p += 1
return p

def estimate_cardinality(values, k):
“”“Estimates the number of unique elements in the input set values.

Arguments:
values: An iterator of hashable elements to estimate the cardinality of.
k: The number of bits of hash to use as a bucket number; there will be 2**k buckets.
“””
num_buckets = 2 ** k
max_zeroes = [0] * num_buckets
for value in values:
h = hash(value)
bucket = h & (num_buckets - 1) # Mask out the k least significant bits as bucket ID
bucket_hash = h >> k
max_zeroes[bucket] = max(max_zeroes[bucket], trailing_zeroes(bucket_hash))
return 2 * (float(sum(max_zeroes)) / num_buckets) num_buckets * 0.79402
這段代碼實現了我們上面討論的估計算法:我們計算每個桶的0前綴(或尾綴)的最長長度;然後計算這些長度的平均數;假設平均數是x,桶數量是m,則最終的估計值是 2^x \times m

。其中一個沒提過的地方是魔法數字0.79402。統計分析顯示這種預測方法存在一個可預測的偏差;這個魔法數字是對這個偏差的修正。實際經驗表明計算值隨着桶數量的不同而變化,不過當桶數量不太小時(大於64),計算值會收斂於估計值。原論文中描述了這個結論的推導過程。
這個方法給出的估計值比較精確 —— 在分桶數爲m的情況下,平均誤差爲 1.3/\sqrt{m}

。因此對於分桶數爲1024的情況(所需內存1024*5 = 5120位,或640字節),大約會有4%的平均誤差;每桶5比特的存儲已經足以估計 2^{27} 的數據集,而我們只用的不到1k的內存!
讓我們看一下試驗結果:

[100000/estimate_cardinality([random.random() for i in range(100000)], 10) for j in range(10)]
[0.9825616152548807, 0.9905752876839672, 0.979241749110407, 1.050662616357679, 0.937090578752079, 0.9878968276629505, 0.9812323203117748, 1.0456960262467019, 0.9415413413873975, 0.9608567203911741]
不錯!雖然有些估計誤差大於4%的平均誤差,但總體來說效果良好。如果你準備自己做一下這個試驗,有一點需要注意:Python內置的 hash() 方法將整數哈希爲它自己。因此諸如 estimate_cardinality(range(10000), 10) 這種方式得到的結果不會很理想,因爲內置 hash() 對於這種情況並不能生成很好的散列。但是像上面例子中使用隨機數會好很多。

提升準確度:SuperLogLog和HyperLogLog
雖然我們已經有了一個不錯的估計算法,但是我們還能進一步提升算法的準確度。Durand和Flajolet發現離羣點會大大降低估計準確度;如果在計算平均值前丟棄一些特別大的離羣值,則可以提高精確度。特別的,通過丟棄最大的30%的桶的值,只使用較小的70%的桶的值來進行平均值計算,則平均誤差可以從 1.3/\sqrt{m}

降低到 1.05/\sqrt{m} !這意味着在我們上面的例子中,使用640個字節可情況下可以將平均誤差從4%降低到3.2%,而所需內存並沒有增加。
最後,Flajolet等人在HyperLogLog論文中給出一種不同的平均值,使用調和平均數取代幾何平均數(譯註:原文有誤,此處應該是算數平均數)。這一改進可以將平均誤差降到 1.04/\sqrt{m}

,而且並沒不需要額外資源。但是這個算法比前面的算法複雜很多,因爲對於不同基數的數據集要做不同的修正。有興趣的讀者可以閱讀原論文。
並行化
這些基數估計算法的一個好處就是非常容易並行化。對於相同分桶數和相同哈希函數的情況,多臺機器節點可以獨立並行的執行這個算法;最後只要將各個節點計算的同一個桶的最大值做一個簡單的合併就可以得到這個桶最終的值。而且這種並行計算的結果和單機計算結果是完全一致的,所需的額外消耗僅僅是小於1k的字節在不同節點間的傳輸。

結論
基數估計算法使用很少的資源給出數據集基數的一個良好估計,一般只要使用少於1k的空間存儲狀態。這個方法和數據本身的特徵無關,而且可以高效的進行分佈式並行計算。估計結果可以用於很多方面,例如流量監控(多少不同IP訪問過一個服務器)以及數據庫查詢優化(例如我們是否需要排序和合並,或者是否需要構建哈希表)。

轉載自 http://www.tuicool.com/articles/mANFru

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章