夜深人靜寫算法(十四)- 基數估計 (Cardinality Estimation)

目錄

一、概述
      1、最小值估值法
      2、哈希法
      3、k-前綴法
二、Linear Counting
      1、算法思路
      2、算法證明
三、LogLog Counting
      1、算法思路
      2、算法證明
      3、誤差消減
四、HyperLogLog Counting
      1、算法思路
      2、並行化
五、參考資料

 

一、概述

        基數估計算法是爲了解決這樣一個問題:設想你有一個巨大的含有重複項的數據集合,這個數據大到無法完全存儲到內存中,但是你想知道這個數據集合中有多少不同的元素,這個不同元素的個數就叫基數(Cardinality)。
        舉個簡單的例子:統計一個遊戲所有服務器上的註冊用戶總數。傳統的方法是給每個用戶分配一個唯一標識,然後用一個數據結構(哈希表、平衡二叉樹、紅黑樹 等等)來維護這個唯一標識的插入,最後統計這個數據結構的元素個數。這種方法是最常用的,但是在大數據下,內存佔用會隨着玩家的增多呈線性增長。所以這種方法是不可行的。

        1、最小值估計法
        我們把問題簡化一下,假設集合中的元素都是整數,數值上限爲 M,x 爲目前找到的數字的最小值,那麼我們可以估計這個集合的基數爲 M / x。
        例如,一個集合的數值上限 100,找到最小的數是 2,那麼估計這個集合的基數爲 100 / 2 = 50。這個方法很直觀,然而準確度一般。但是,它可以作爲基數估計的切入點,從而引入更加複雜的算法。

        2、哈希法
        最小值估計法雖然直觀,但是侷限性很大。我們可能遇到的情況諸如:集合中不同元素個數很少,但是最小值非常小;集合中不同元素個數很大,但是最小值並不小;集合元素是字符串 等等。這些問題源於集合元素並非均勻分佈。解決這個的辦法是:我們可以利用一些良好的哈希函數,將任意數據集映射到隨機分佈的下標中。然後再利用簡單估計法進行計算。

        k-前綴法
        隨機數集合中,通過計算每一個元素哈希值的二進制表示的 0 前綴,設 k 爲當前集合中最長的 0 前綴的長度,則平均來說集合中大約有 2^k 個不同的元素;我們可以用這個方法估計基數。同樣,這種估計方法準確性也不高,但是這個估計方法比較節省資源:對於 16 位的哈希值來說,只需要 4 比特去存儲 0 前綴的長度。

        爲了提高準確性,我們可以採用多個相互獨立的哈希函數,計算每個哈希函數產生的最長 0 前綴,然後取平均值來提高算法精度。

二、Linear Counting

        Linear Counting 簡稱 LC,在 1990 年的一篇論文 “A linear-time probabilistic counting algorithm for database applications” 中被提出。作爲一個早期的基數估計算法,實際空間複雜度並沒有太大改善,但是可以作爲更復雜的基數估計算法的基礎,還是有必要了解一下。
        1、算法思路
        假設一個哈希函數 H,它的映射值域爲 [0, M),且哈希函數服從均勻分佈。使用一個長度爲 M 的 bitmap ,每個比特代表一個桶 (bucket),初始化每個桶的值爲 0。一個集合的基數爲 n,將集合中所有元素用哈希函數 H 映射到這個 bitmap 中,如果一個元素被哈希到第 i 個桶,則將它對應的比特位的值置爲 1。當集合中所有元素哈希完畢,bitmap 中還剩餘 x 個 0 (這裏的 x 稱爲 空桶數),則有集合的基數的近似估計值(最大似然估計)如下:

集合基數近似值

        2、算法證明
        由於哈希函數 H 服從均勻分佈,所以一個元素經過哈希函數映射後映射到任何一個桶的概率都爲 1 / M。那麼,經過 n 個元素哈希後,第 i 個桶爲 0 的概率 p 如下:

n 次哈希都避開了第 i 個桶

        M 是個固定值,所以當 n 爲常量時,p 就是一個常量。從而可知,經過 n 個元素哈希後,每個桶爲 0 的概率相等,即它們是相互獨立的事件。
       根據組合原理,當有 x 個桶爲 0 的方案數就是 C(M,x)。從而得到 x 個桶爲 0 的概率如下:

        很明顯,它服從二項分佈,所以期望 μ 就是獨立事件次數 M 和該事件發生概率 p 的乘積,有:

空桶個數的期望

        數學中的第二重要極限表示如下(證明可以採用 洛必達法則):

自然常數 e

那麼,當 M 的值足夠大的時候,我們可以把上面的期望計算進行一個變形,得到如下近似式:

然後移項,將 n 轉化成期望 μ 和 M 的表達式如下(其中 ln 爲自然對數):

集合基數近似表達式

        二項分佈的極限分佈爲正態分佈。故當 M 很大時,二項分佈的概率可用正態分佈的概率作爲近似值。何謂 M 很大呢 ? 一般規定:當 p < 0.5 且 Mp ≥ 5,這時的 M 就被認爲很大,可以用正態分佈的概率作爲近似值了。
        正態分佈的期望 μ 的最大似然估計爲樣本均值(可用利用偏導進行推導),如下圖所示,表示的是 M = 100,n = 65 的空桶個數的分佈曲線圖:

M=100,n=65 的空桶個數分佈曲線

        空桶個數大概率出現在正態分佈密度函數圖像的中軸所在位置,所以我們可以將上面的式子再做一個變形,也就是剛開始提到的集合基數近似值:

其中 x 爲統計所得空桶數


        再來看幾個例子加深下理解,我們令 M = 1000,然後 n 取不同值,採用 Python 的 matplotlib.pyplot 繪製函數曲線。下圖展示了 n 爲 50、200、500、1000、2000 時的函數曲線:

M = 1000 的情況下空桶個數的概率分佈呈二項分佈

        我們發現:n 越小, 空桶個數相對越多;反之,則個數相對越少。這也是符合正常思維邏輯的,在運用這個算法的時候,如果 M 比 n 小太多,可能導致所有的桶都被哈希到,這樣空桶數爲 0,估算公式的值爲無窮大。所以在取值的時候需要保證滿桶的概率非常小。而實際情況是 M 是固定值,n 會隨着加入的元素增加而呈線性增長,所以 LC 算法往往在數據量較小時發揮作用,一般用來配合 LogLog 或者 HyperLogLog 使用 ( Redis 中就是用 Linear Counting 來 配合 HyperLogLog)。

三、LogLog Counting

        LogLog Counting 簡稱 LLC,出自論文“Loglog Counting of Large Cardinalities”。LLC的空間複雜度僅爲 O( log2( log2(N) )),故此得名。數億級別的數據量可以在 KB 級的內存中得到良好的估值。
        1、算法思路
       
假設一個哈希函數 H,哈希結果服從均勻分佈,且哈希後的結果是一個長度爲 L 的比特串。令 g 爲服從均勻分佈的樣本空間中隨機抽取的一個樣本,那麼它的每個比特位服從 0-1 分佈。即每個比特位爲 0 和 1 的概率都爲 0.5,且各個比特位之間相互獨立。
        令 h(g) 爲 g 的比特串中第一個出現 "1" 的位置。那麼 h(g) 的值域爲 [1, L]。爲了排除 g 等於 0 的情況,可以在計算出的哈希值末尾 位或 上 1。然後,我們遍歷集合中所有元素計算哈希值,找到所有比特串中的 h(g) 最大值,記爲 hmax。
         此時,可以用 2 的 hmax 次冪作爲該集合基數的粗糙估計,即:

          2、算法證明
          哈希函數得到比特串的每一位都是相互獨立的,所以我們可以把每一位的結果看作是擲硬幣的過程,比特位的 0 代表硬幣反面,1 代表硬幣正面。假設我們的集合總共有 n 個元素,分兩種情況討論:
          a) 每個元素的 h(g) 都小於等於 k 的概率,記爲 P1;

          可以這麼考慮,h(g) 大於 k 這個事件可以描述成 “前 k 次擲硬幣得到都是反面”,這個概率就是 1/2 的 k 次冪;那麼,相反的,小於等於 k 的概率就是用 1 去減。然後 n 次獨立事件概率的乘積就是最後的結果了。
          b) 至少一個元素的 h(g) 大於等於 k 的概率,記爲 P2;

         利用 a) 的思路,可以得到每個元素的 h(g)  都小於k 的概率,然後在用 1 去減,就能得到 b) 的概率了。
         當 n>>2^k 時 a) 的概率幾乎爲 0;當n<<2^k 時 b) 的概率幾乎爲 0。所以一旦集合中出現 h(g) = k,那麼從概率上講 n 的值不可能遠大於2^k、也不可能遠小於 2^k。所以對於集合中最大的 h(g) = hmax,有集合基數近似值爲 2^hmax。

          3、誤差消減

         上述分析給出了 LLC 的基本思想,不過如果直接使用上面的單一估計量進行基數估計會由於偶然性而存在較大誤差。因此,LLC 採用了均值法來消減誤差。具體來說,就是將哈希空間平均分成 m 份,每份稱之爲一個桶(bucket)。對於每一個元素,其哈希值的前 k 比特作爲桶編號(即 m = 2^k),後 (L - k) 個比特作爲真正用於基數估計的比特串。桶編號相同的元素被分配到同一個桶,在進行基數估計時,首先計算第 i 個桶內元素最大的第一個 “1” 的位置 M[i],然後對這 m 個值取平均後再進行估計,即:

          這相當於物理試驗中經常使用的多次試驗取平均的做法,可以有效消減因偶然性帶來的誤差。這裏的 M[i] 相當於求了算數平均數,但是是作用在指數上的,所以其實真正做的是幾何平均數。
          假設 H 的哈希長度爲 16 比特,分桶數 m 定爲 32,那麼 k 就是 5。設一個元素哈希值的比特串爲“0011001010001110”,由於 m 爲 32,因此前 5 個爲桶編號,所以這個元素應該歸入“00110” 即 6 號桶(桶編號從0開始,最大編號爲 m - 1),而剩下部分是“01010001110”且顯然h(01010001110) = 2,所以桶編號爲 “00110” 的元素 M[6] 的值爲 2。
        相比 LC 其最大的優勢就是內存使用極少。不過LLC也有自己的問題,就是當 n 不是特別大時,其估計誤差過大,因此目前實際使用的基數估計算法都是基於 LLC 改進的算法。

四、HyperLogLog Counting

          HyperLogLog Counting 簡稱 HLLC,基本思想是在 LLC 的基礎上做改進,具體細節請參考“HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm”這篇論文。
          HLLC 在 LLC 基礎上的一個改進是用 “調和平均數” 取代 “幾何平均數”。所謂調和平均數,就是各個數倒數的平均數的倒數。表示如下:

        1、算法思路
       沿用了 LLC 的算法思路,只是在多個桶取平均值時,用  “調和平均數” 取代 “幾何平均數”,最終的代數表示如下:

        上述估值公式中的 C,是根據 m 得到的一直數值常量,具體值如下:

        Redis 的 HyperLogLog 算法中,m 的取值爲 2 的 14 次,即 16384。那麼常量 C 的值計算如下:

        HLLC 的另一項改進是在基數 n 相對與 m 較小或者較大時做了分支判斷,採用不同的策略計算基數值。主要分三種情況,下面給出 Python 僞代碼,其中 n 爲用調和平均計算出來的基數初始估值:

if n <= m*5/2:
	if V != 0:
		# V表示M[i]=0的個數,個數非0則採用 LC
		return m*log(m * 1.0 / V)
	else:
		# 否則,繼續採用HLLC
		return n
elif n <= (1<<32)/30.0:
	return n
else :
	# n 非常大的情況
	return -(1<<32) * log(1 - n * 1.0 / (1<<32))

        2、並行化

        以上幾種基數估計算法,存儲的實際數據結構都和本身集合的大小無關,所以無論集合多大,數據結構的大小都是固定的。這樣的數據結構下,能夠很好的實現兩個集合之間的合併。這樣就可以實現多個線程同時計算,最後合併統計,從而實現並行計算。

 

五、參考資料

基數估計算法概覽(推薦)

基數估計算法(英文)

LLC和HLLC的可視化數據調試程序

基數估計的概率算法

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

HyperlogLog 原理簡介

HyperlogLog 詳解(英文)

HyperlogLog 集合合併 

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