再談基數估計之HyperLogLog演算法

2021-10-02 15:40:16 字數 3952 閱讀 3557

在很久(好像也沒多久,4個月)之前,我曾經寫了一篇和主業無關的有點意思的小文章《基數估計探秘:linear counting與flajolet-martin演算法》。但是這篇文章講的兩個演算法都已經老掉牙了,實際應用最廣泛的基數估計演算法是hyperloglog(hll)演算法。

最近筆者基於flink搞了些從超大行為資料集計算uv的工作,用到了redis的hyperloglog,感覺是時候寫個續篇了。在讀本文之前,強烈建議看官先讀之前的那篇,就算不深挖細節,也能夠獲得一些基數估計方面的背景知識。

等等,不是叫hyperloglog麼,怎麼這裡變loglog了?很簡單,hll是基於llc的改進,所以我們有必要了解llc。該演算法由m. durand、p. flajolet在2023年的**《loglog counting of large cardinalities》中首次提出。下面看看這個演算法是怎麼操作的。

首先我們得有乙個雜湊函式h(x),並且滿足如下條件:

然後定義符號ρ(y),它代表將數y表示成二進位制串後,從左向右讀遇到的第乙個「1」的位置下標(下標從1開始,忽略全0的情況),也就是前導0的個數+1。

loglog counting演算法的流程如下:

對每個待測集合中的元素x,計算它的雜湊值y=h(x)。

將雜湊值y通過它們的前k=logm個位元分組(即分桶),作為桶的編號,即一共有m個桶。

將y的後l - k個位元作為真正參與基數估計的串s,計算並記錄下所有桶內的ρ(s)。

令m[k]表示第k個桶內所有元素中最大的那個ρ值,那麼該集合基數的估計量為:

ñ = αm · m · 2σm[i] / m

其中αm是修正引數。有沒有覺得這個演算法流程有點似曾相識的意思?

那麼α是怎麼來的呢?這個過程極其複雜,直接說結論吧。根據之前對伯努利實驗的分析,可以知道:

pn = (1 - 1/2k)n - (1 - 1/2k-1)n

它是乙個無窮遞推數列,採用指數生成函式和泊松化的方法處理,得到估計量的泊松期望和方差如下:

進而推出演算法流程第4步的估計量。這是乙個漸進無偏估計量,其中:

這是llc比f-m演算法更精細的地方之一。

llc的空間複雜度是多少呢?不難得知,在f-m演算法中,我們可以用logn個位元來儲存雜湊值,而llc演算法只儲存下標(即ρ值)就夠用了,所以可以降低為log(logn)個位元。再加上分桶數為m,亦即它的空間複雜度是:

o[m · log(logn)]

這也就是loglog counting這個名字的由來。

根據**中給出的資料,llc的誤差在m不算太小(大於64)時,大概是:

stderror ≈ 1.30 / √m

雖然llc的誤差常數比f-m演算法的還大(f-m是0.78),但是由於空間複雜度從log級別降低到了loglog級別,所以相同誤差下實際的空間開銷要更小。

分桶數m完全由可接受的精確度決定。但這個誤差的前提是實際基數n要遠遠大於分桶數m(因為ñ是漸近無偏的),所以在集合比較小時,llc演算法的效果要打折扣,這點與f-m-pcsa是相同的。

llc演算法本質上不是個新的演算法,並且也從未大規模地使用過。它的主要意義有三:

hll由四位大佬p. flajolet、é. fusy、o. gandouet、f. meunier(全是法語名字)在2023年的**《hyperloglog: the analysis of a near-optimal cardinality estimation algorithm》中提出。從**題目可以得知,這個演算法已經相當優化了。實際上它的演算法流程與llc仍然幾乎相同,那麼它到底「hyper」在**呢?

一是分桶平均的時候,採用調和平均數。我們知道,調和平均數的定義是:

n / [1/x1 + 1/x2 + ... + 1/xn] = n / [σ 1/xi]

在llc演算法中,採用的是ρmax的算術平均數,並且它是作為2的指數,所以本質上是幾何平均數。但是幾何平均數受離群值(即偏離均值很大的值)的影響非常大,分桶的空桶越多,llc的估計值就越不準確。調和平均數就不太有這種困擾,所以集合基數的估計量就會變成:

ñ = αm · m2 / σ 2-m[i]

修正引數是:

為了實際應用起來方便,一般採用如下的近似值:

二是根據基數估計值的大小,採用不同的估計方法進行修正。**中給出了在常見情況——即被估集合的基數在億級別以下,m取值在2[4, 16]區間——下的修正的演算法,如下圖所示。

翻譯**話:

hll演算法的空間複雜度與llc相同,而標準差為:

stderror ≈ 1.04 / √m

可見,hll演算法的精度確實比llc更高。根據**中的描述,估計億級別集合的基數,在偏差大約2%的情況下,只需要耗費大約1.5kb記憶體。

hll演算法在很多框架中都有實現,其中尤以redis的實現方法最為有名,但它的**量極大,如果寫在文章裡的話會特別冗長,所以只是簡單說說吧。

redis使用了214=16384個桶,按照上面的標準差,誤差為0.81%,精度相當高。redis使用乙個long型雜湊值的前14個位元用來確定桶編號,剩下的50個位元用來做基數估計。而26=64,所以只需要用6個位元表示下標值,在一般情況下,乙個hll資料結構占用記憶體的大小為16384 * 6 / 8 = 12kb,redis將這種情況稱為密集(dense)儲存。

既然有了密集儲存,自然就會有稀疏(sparse)儲存。當多數桶的值為全0時,為了節省空間,redis會將連續的全0桶壓縮成0桶計數值。該計數值可以用單位元組或雙位元組表示,高2bit為標誌位,因此可以分別表示連續64個全0桶和連續16384個全0桶。

redis在hll稀疏儲存中用zero巨集表示單位元組全0桶,xzero巨集表示雙位元組全0桶,val表示中途的少數非0桶。舉個例子,假設只有第10001個桶和第10047個桶的值為1,其餘都為0,那麼整個儲存布局就是:

xzero (10000) | val (1) | zero (45) | val (1) | xzero (6337)

只需要5位元組就能儲存了。當稀疏儲存的總大小超過hll_sparse_max_bytes引數指定的大小(預設為3kb)時,就會自動轉換成密集儲存。redis還會在hll資料結構的頭部資訊中快取上一次計算出來的基數估計值,這樣可以避免不必要的重算,提高效率。

除了插入元素的pfadd指令之外,redis提供的計數指令pfcount和合併指令pfmerge都支援將多個hll合併起來。由於hll的桶只儲存下標值,因此合併時只需要按桶取最大值就可以了。redis的hll演算法在上述標準演算法的基礎上做了一點改進,比如預打表計算2-m[i]的值,以及用多項式回歸修正結果等。

晚安晚安。

基數估計演算法簡介

注1 本文是之前工作時在團隊內分享的乙個ppt的文字版本.注2 我有了新的個人部落格位址 下文中的sqrt表示開根號 sqrt 4 2 m n表示m的n次方 基數指的是乙個可重複集合中不重複元素的個數。給定乙個含有重複元素的有限集合,計算其不重複元素的個數。應用場景舉例 簡單來說就是各種uv的計算 ...

引數估計之點估計和區間估計

作者 cda資料分析師 引數估計 parameter estimation 是根據從總體中抽取的樣本估計總體分布中包含的未知引數的方法。人們常常需要根據手中的資料,分析或推斷資料反映的本質規律。即根據樣本資料如何選擇統計量去推斷總體的分布或數字特徵等。統計推斷是數理統計研究的核心問題。所謂統計推斷是...

Unity Shader之再談雜訊

對於雜訊其實我還有很多不懂的地方,比如random函式,比如雜訊的實際應用場景等等。於是在搜尋資料的時候我發現了兩個超超超牛b的 可以說是我這種想要去學習相關知識的人的福利 在這裡給自己標記一下同時也分享給大家 1.shadertoy創始人之一的奇淫巧計大集合 之前看shadertoy的時候,在驚嘆...