【原創】演算法分享(4)Cardinality Estimate 基數計數概率演算法
讀過《程式設計珠璣》(<Programming Pearls>)的人應該還對開篇的Case記憶猶新,大概的場景是:
作者的一位在電話公司工作的朋友想要統計一段時間內不同的電話號碼的個數,電話號碼的數量很大,當時的記憶體很小,所以不能把所有的電話號碼全部放到記憶體來去重統計,他的朋友很苦惱。
作者聰明的想到了用bit陣列來解決問題,每個電話號碼可以對映為bit陣列的index,bit陣列初始狀態所有位為0,所有電話號碼逐一處理:將bit陣列對應位置為1,處理完之後統計bit陣列中有多少個1即可。
示例:[0,1,0,0,0,1,0,...] 這個bit陣列表示2和5存在
不得不說這種想法非常精妙,即減少了記憶體佔用(8位電話號碼如果全部放到記憶體需要381M(每個電話號碼存成Integer佔4Byte計算),而使用bit陣列只需要11M),而且只需要兩次迴圈就可以得到結果;
這是一個基數計數的問題,Cardinality:estimating the number of distinct elements.
1 Bitmap
上邊提到的方式是使用bitmap,思路是將dataset中的每一個element對映到一個bit位,不允許衝突,所需要的記憶體空間大概為基數*1bit(上例中是100,000,000bit),並且計數精準;
但是當基數非常大時,即使bitmap記憶體也放不下!
好訊息是如果不要求計數精準(允許一定範圍內的誤差),可以採用概率估算演算法:
2 LC:Linear Counting
公式
其中:n是估算值,m是bitmap大小,Vn是bitmap中0出現的比率,比如0.1;
原理
使用固定大小的hashtable來存放dataset,可以想見:
- dataset的基數越小,hashtable越空,當基數為0時hashtable所有的key都是空的
- dataset的基數越大,hashtable越滿,衝突也越多,當基數無窮大時hashtable所有的key都被佔用並且每個key都有大量衝突
所以可以根據hashtable中key被佔用的情況來估算dataset的基數,這裡主要用到了對數曲線的特性(0<x<1這一段):
過程
初始化一個bitmap,所有位為0,dataset的每一個element都hash到bitmap的一個bit位,hash之後將對應的bit位置為1,hash允許衝突,最後根據bitmap中0的數量和bitmap大小由公式來估算基數;
注意:雖然LC用的也是bitmap,但是相比原始的bitmap演算法,LC的bitmap大小可以比基數小很多,因為LC的對映允許衝突,另外可以設定bitmap大小來決定誤差的大小;
參考:A linear-time probabilistic counting algorithm for database applications
https://wenku.baidu.com/view/9c4489ee0975f46527d3e1d1.html
3 LLC:LogLog Counting
公式
其中:m是桶的數量,M為桶的集合,k是用於分桶的位數,ρ為bit陣列中第一個為1的下標即index,E是估算值;
原理
來看拋硬幣的過程,拋硬幣的過程是伯努利Bernoulli過程,每次的結果要麼是0,要麼是1,並且概率均為1/2,假設一次拋硬幣game定義為拋到1為止,可能第一次就拋到1(P=0.5),也可能前邊拋了i次0最後才拋到1(P=1-0.5^i);拋硬幣game玩的次數越多,越容易出現前邊很多次都是0的情況,這是因為開頭連續出現的0的個數越多,出現概率越小,需要嘗試伯努利過程的次數就越多,所以可以利用概率根據結果(開頭出現0的個數,即i)來反推出條件(game玩了多少次,即n=2^i);
但是由於隨機性的存在導致誤差較大,所以通過將dataset分為m份,每份單獨統計,最後取算數平均值的方式來降低隨機性從而減小誤差;
過程
dataset的每一個element先對映到一個bit陣列,比如32位bit陣列,將這個bit陣列的1到k位的值作為桶的bucket_index(即第幾個桶),將k+1到32位中第一個為1的index作為value放到桶中,如果桶裡已經有value,桶會儲存一個最大的value,資料集元素全部對映完之後,將所有桶的value取算數平均值,根據n=2^i,這樣可以得到每個桶內的基數,再乘以m可以得到整個dataset的基數,公式最前邊的α是修正引數;
比如k=2,則m=2^k=4,即4個桶,dataset中一個element對映的bit陣列為[1,0,0,0,0,1,0,...],取前兩位[1,0]對應的值是2,即第2個桶,取第3位之後的陣列[0,0,0,1,0,...]可見第一個位1的index是3,將3放到桶2中,以此類推;
參考:Loglog Counting of Large Cardinalities
http://algo.inria.fr/flajolet/Publications/DuFl03-LNCS.pdf
4 HLLC:Hyper LogLog Counting
公式
原理
同LLC,只有一點不同:取均值的時候不使用算數平均數而改用調和平均數
參考:HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm
http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf
5 最優實踐
公式
過程
在大量實踐中根據各個引數和結果的情況進行調優,綜合使用HLLC和LC等演算法
參考:
http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/40671.pdf
總結
目前HLLC在很多開源元件中都有應用,比如redis、druid等
其他:
https://research.neustar.biz/2012/10/25/sketch-of-the-day-hyperloglog-cornerstone-of-a-big-data-infrastructure/