神奇的HyperLogLog算法
2019獨角獸企業重金招聘Python工程師標準>>>
原文鏈接:http://rainybowe.com/blog/2017/07/13/%E7%A5%9E%E5%A5%87%E7%9A%84HyperLogLog%E7%AE%97%E6%B3%95/index.html?utm_source=tuicool&utm_medium=referral
神奇的HyperLogLog算法
?
基數計數基本概念
基數計數(cardinality counting)通常用來統計一個集合中不重復的元素個數,例如統計某個網站的UV,或者用戶搜索網站的關鍵詞數量。數據分析、網絡監控及數據庫優化等領域都會涉及到基數計數的需求。 要實現基數計數,最簡單的做法是記錄集合中所有不重復的元素集合S_uS?u??,當新來一個元素x_ix?i??,若S_uS?u??中不包含元素x_ix?i??,則將x_ix?i??加入S_uS?u??,否則不加入,計數值就是S_uS?u??的元素數量。這種做法存在兩個問題:
大數據量背景下,要實現基數計數,首先需要確定存儲統計數據的方案,以及如何根據存儲的數據計算基數值;另外還有一些場景下需要融合多個獨立統計的基數值,例如對一個網站分別統計了三天的UV,現在需要知道這三天的UV總量是多少,怎么融合多個統計值。
基數計數方法
B樹
B樹最大的優勢是插入和查找效率很高,如果用B樹存儲要統計的數據,可以快速判斷新來的數據是否已經存在,并快速將元素插入B樹。要計算基數值,只需要計算B樹的節點個數。 將B樹結構維護到內存中,可以快速統計和計算,但依然存在問題,B樹結構只是加快了查找和插入效率,并沒有節省存儲內存。例如要同時統計幾萬個鏈接的UV,每個鏈接的訪問量都很大,如果把這些數據都維護到內存中,實在是夠嗆。
bitmap
bitmap可以理解為通過一個bit數組來存儲特定數據的一種數據結構,每一個bit位都能獨立包含信息,bit是數據的最小存儲單位,因此能大量節省空間,也可以將整個bit數據一次性load到內存計算。 如果定義一個很大的bit數組,基數統計中每一個元素對應到bit數組的其中一位,例如bit數組?001101001001101001代表實際數組[2,3,5,8][2,3,5,8]。新加入一個元素,只需要將已有的bit數組和新加入的數字做按位或?(or)(or)計算。bitmap中1的數量就是集合的基數值。
bitmap有一個很明顯的優勢是可以輕松合并多個統計結果,只需要對多個結果求異或就可以。也可以大大減少存儲內存,可以做個簡單的計算,如果要統計1億個數據的基數值,大約需要內存: 100000000/8/1024/1024?\approx≈?12M
如果用32bit的int代表每個統計數據,大約需要內存:
32*100000000/8/1024/1024?\approx≈?381M
bitmap對于內存的節約量是顯而易見的,但還是不夠。統計一個對象的基數值需要12M,如果統計10000個對象,就需要將近120G了,同樣不能廣泛用于大數據場景。
概率算法
實際上目前還沒有發現更好的在大數據場景中準確計算基數的高效算法,因此在不追求絕對準確的情況下,使用概率算法算是一個不錯的解決方案。概率算法不直接存儲數據集合本身,通過一定的概率統計方法預估基數值,這種方法可以大大節省內存,同時保證誤差控制在一定范圍內。目前用于基數計數的概率算法包括:
- Linear Counting(LC):早期的基數估計算法,LC在空間復雜度方面并不算優秀,實際上LC的空間復雜度與上文中簡單bitmap方法是一樣的(但是有個常數項級別的降低),都是O(N_{max})O(N?max??);
- LogLog Counting(LLC):LogLog Counting相比于LC更加節省內存,空間復雜度只有O(log_2(log_2(N_{max})))O(log?2??(log?2??(N?max??)))
- HyperLogLog Counting(HLL):HyperLogLog Counting是基于LLC的優化和改進,在同樣空間復雜度情況下,能夠比LLC的基數估計誤差更小。
下面將著重講HLL的原理和計算過程。
HyperLogLog的驚人表現
上面我們計算過用bitmap存儲1一億個統計數據大概需要12M內存;而在HLL中,只需要不到1K內存就能做到;redis中實現的HyperLogLog,只需要12K內存,在標準誤差0.81%的前提下,能夠統計2^{64}2?64??個數據。首先容我感嘆一下數學的強大和魅力,那么概率算法是怎樣做到如此節省內存的,又是怎樣控制誤差的呢?
首先簡單展示一下HLL的基本做法,HLL中實際存儲的是一個長度為mm的大數組SS,將待統計的數據集合劃分成mm組,每組根據算法記錄一個統計值存入數組中。數組的大小mm由算法實現方自己確定,redis中這個數組的大小是16834,mm越大,基數統計的誤差越小,但需要的內存空間也越大。
這里有個HLL demo可以看一下HLL到底是怎么做到這種超乎想象的事情的。
看到這里心里應該有無數個問號,這樣真的就能統計到上億條數據的基數了嗎?我總結一下,先拋出三個疑問:
hyperloglog原理理解
舉一個我們最熟悉的拋硬幣例子,出現正反面的概率都是1/2,一直拋硬幣直到出現正面,記錄下投擲次數kk,將這種拋硬幣多次直到出現正面的過程記為一次伯努利過程,對于nn次伯努利過程,我們會得到nn個出現正面的投擲次數值k_1k?1??,k_2k?2??……k_nk?n??,其中最大值記為k_{max}k?max??,那么可以得到下面結論:
對于第一個結論,nn次伯努利過程的拋擲次數都不大于k_{max}k?max??的概率用數學公式表示為:?
P_n(X \le k_{max})=(1-1/2^{k_{max}})^nP?n??(X≤k?max??)=(1?1/2?k?max????)?n??
第二個結論至少有一次等于k_{max}k?max??的概率用數學公式表示為:?
P_n(X \ge k_{max})=1-(1-1/2^{k_{max}-1})^nP?n??(X≥k?max??)=1?(1?1/2?k?max???1??)?n??
當n\ll 2^{k_{max}}n?2?k?max????時,P_n(X \ge k_{max})\approx0P?n??(X≥k?max??)≈0,即當nn遠小于2^{k_{max}}2?k?max????時,上述第一條結論不成立;?
當n\gg 2^{k_{max}}n?2?k?max????時,P_n(X \le k_{max})\approx0P?n??(X≤k?max??)≈0,即當nn遠大于2^{k_{max}}2?k?max????時,上述第二條結論不成立。 因此,我們似乎就可以用2^{k_{max}}2?k?max????的值來估計nn的大小。
以上結論可以總結為:進行了nn次進行拋硬幣實驗,每次分別記錄下第一次拋到正面的拋擲次數kk,那么可以用n次實驗中最大的拋擲次數k_{max}k?max??來預估實驗組數量nn:?\hat{n} = 2^{k_{max}}?n?^??=2?k?max????可以通過一組小實驗驗證一下這種估計方法是否基本合理。
回到基數統計的問題,我們需要統計一組數據中不重復元素的個數,集合中每個元素的經過hash函數后可以表示成0和1構成的二進制數串,一個二進制串可以類比為一次拋硬幣實驗,1是拋到正面,0是反面。二進制串中從低位開始第一個1出現的位置可以理解為拋硬幣試驗中第一次出現正面的拋擲次數kk,那么基于上面的結論,我們可以通過多次拋硬幣實驗的最大拋到正面的次數來預估總共進行了多少次實驗,同樣可以可以通過第一個1出現位置的最大值k_{max}k?max??來預估總共有多少個不同的數字(整體基數)。
這種通過局部信息預估整體數據流特性的方法似乎有些超出我們的基本認知,需要用概率和統計的方法才能推導和驗證這種關聯關系。HyperLogLog核心在于觀察集合中每個數字對應的比特串,通過統計和記錄比特串中最大的出現1的位置來估計集合整體的基數,可以大大減少內存耗費。
現在回到第二節中關于HyperLogLog的第一個疑問,為什么要統計hash值中第一個1出現的位置?
第一個1出現的位置可以類比為拋硬幣實驗中第一次拋到正面的拋擲次數,根據拋硬幣實驗的結論,記錄每個數據的第一個出現的位置kk,就可以通過其中最大值{k_{max}}k?max??推導出數據集合的基數:\hat{n} = 2^{k_{max}}?n?^??=2?k?max????。
hyperloglog算法講解
分桶平均
HLL的基本思想是利用集合中數字的比特串第一個1出現位置的最大值來預估整體基數,但是這種預估方法存在較大誤差,為了改善誤差情況,HLL中引入分桶平均的概念。?
同樣舉拋硬幣的例子,如果只有一組拋硬幣實驗,運氣較好,第一次實驗過程就拋了10次才第一次拋到正面,顯然根據公式推導得到的實驗次數的估計誤差較大;如果100個組同時進行拋硬幣實驗,同時運氣這么好的概率就很低了,每組分別進行多次拋硬幣實驗,并上報各自實驗過程中拋到正面的拋擲次數的最大值,就能根據100組的平均值預估整體的實驗次數了。
分桶平均的基本原理是將統計數據劃分為mm個桶,每個桶分別統計各自的{k_{max}}k?max??并能得到各自的基數預估值?\hat{n}?n?^???,最終對這些?\hat{n}?n?^???求平均得到整體的基數估計值。LLC中使用幾何平均數預估整體的基數值,但是當統計數據量較小時誤差較大;HLL在LLC基礎上做了改進,采用調和平均數,調和平均數的優點是可以過濾掉不健康的統計值,具體的計算公式為:
回到第二節中關于HLL的第二個疑問,為什么要有分桶數組???分桶數組是為了消減因偶然性帶來的誤差,提高預估的準確性。那么分桶數組的大小怎么確定呢??
這是由算法實現方自己設定的,例如上面HLL demo中,設定統計數組的大小,如果函數得到的比特串是32位,需要其中6()位定位分桶數組中的桶的位置,還剩下26位(需要記錄的出現1的位置的最大值是26),那么數組中每個桶需要5()位記錄1第一次出現的位置,整個統計數組需要花費的內存為:?
?
也就是用32bit的內存能夠統計的基數數量為。
偏差修正
上述經過分桶平均后的估計量看似已經很不錯了,不過通過數學分析可以知道這并不是基數n的無偏估計。因此需要修正成無偏估計。這部分的具體數學分析在“Loglog Counting of Large Cardinalities”中。
其中系數由統計數組的大小??決定,具體的公式為:
根據論文中分析結論,HLL與LLC一樣是漸進無偏估計,漸進標準誤差表示為:
因此,統計數組大小??越大,基數統計的標準誤差越小,但需要的存儲空間也越大,在?的情況下,HLL的標準誤差為1.1%。
雖然調和平均數能夠適當修正算法誤差,但作者給出一種分階段修正算法。當HLL算法開始統計數據時,統計數組中大部分位置都是空數據,并且需要一段時間才能填滿數組,這種階段引入一種小范圍修正方法;當HLL算法中統計數組已滿的時候,需要統計的數據基數很大,這時候hash空間會出現很多碰撞情況,這種階段引入一種大范圍修正方法。最終算法用偽代碼可以表示為如下。
?m = 2^b # with b in [4...16]
if m == 16:
alpha = 0.673
elif m == 32:
alpha = 0.697
elif m == 64:
alpha = 0.709
else:
alpha = 0.7213/(1 + 1.079/m)
registers = [0]*m # initialize m registers to 0
###########################################################################
# Construct the HLL structure
for h in hashed(data):
register_index = 1 + get_register_index( h,b ) # binary address of the rightmost b bits
run_length = run_of_zeros( h,b ) # length of the run of zeroes starting at bit b+1
registers[ register_index ] = max( registers[ register_index ], run_length )
##########################################################################
# Determine the cardinality
DV_est = alpha * m^2 * 1/sum( 2^ -register ) # the DV estimate
if DV_est < 5/2 * m: # small range correction
V = count_of_zero_registers( registers ) # the number of registers equal to zero
if V == 0: # if none of the registers are empty, use the HLL estimate
DV = DV_est
else:
DV = m * log(m/V) # i.e. balls and bins correction
if DV_est <= ( 1/30 * 2^32 ): # intermediate range, no correction
DV = DV_est
if DV_est > ( 1/30 * 2^32 ): # large range correction
DV = -2^32 * log( 1 - DV_est/2^32)
redis中hyperloglog實現
redis正是基于以上的HLL算法實現的HyperLogLog結構,用于統計一組數據集合中不重復的數據個數。 redis中統計數組大小設置為,hash函數生成64位bit數組,其中??位用來找到統計數組的位置,剩下50位用來記錄第一個1出現的位置,最大位置為50,需要?位記錄。
那么統計數組需要的最大內存大小為:??基數估計的標準誤差為。可以學習一下redis中HyperLogLog的源碼實現。
參考閱讀
Redis new data structure: the HyperLogLog
HyperLogLog — Cornerstone of a Big Data Infrastructure
解讀Cardinality Estimation算法(第四部分:HyperLogLog Counting及Adaptive Counting)
轉載于:https://my.oschina.net/u/2330181/blog/1926470
總結
以上是生活随笔為你收集整理的神奇的HyperLogLog算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云计算之KVM虚拟化实战
- 下一篇: 第58件事 借势文案创作实例