大數據流的在線Heavy Hitters算法(上篇):基於計數器的方法

Question!

有海量(e.g. 日均千億級別)的訪問日誌流,如何在不要求結果100%精確的前提下,儘量快速地統計出被訪問次數最多的一些域名,以及它們的訪問頻率?

Heavy Hitters(頻繁項)以及它衍生出來的Top-K(前K最高頻項)是大數據和流式計算領域非常經典的問題,並且在海量數據+內存有限+在線計算的前提下,傳統的HashMap + Heap-Sort方式幾乎不可行,需要利用更加高效的數據結構和算法來解決。好在大佬們對Heavy Hitters問題進行了深入的研究,並總結出了很多有效的方案,本文簡要介紹一種主流的類別,即基於計數器(Counter)的方法,包括:

  • Misra-Gries算法
  • Lossy Counting算法
  • Space Saving算法

在下篇文章中(計劃明天寫),會繼續介紹另一類,即基於摘要(Sketch)的方法。

Majority問題

先看一個非常經典的問題。

數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。

思路很簡單:遍歷數組,如果前後遇到的兩個數不相等,就將這兩個數消去,最終剩下的那個肯定是出現次數超過一半的那個數。具體到操作上,可以設定一個候選值與一個計數器,在遍歷過程中,如果遇到的數與候選值相同則增加計數,不同則減少計數。候選值的計數減爲0時,表示它肯定不是所求的結果,選取下一個數作爲候選值,直到遍歷完畢。

Misra-Gries算法

將Majority問題推廣,就會變成:

數據流中一共有m個元素,請找出出現頻率超過m / k的k - 1個元素。

可見,Majority問題就是上述問題k = 2時的特例。套用上面的計數器思路,就是Misra-Gries算法,該算法早在1982年就提出了。

如圖所示,維護k - 1個候選值與計數器的集合:

  • 如果元素在集合中,將其對應的計數器自增;
  • 如果元素不在集合中且集合未滿,就將元素加入集合,計數器設爲1;
  • 如果元素不在集合中且集合已滿,將集合內所有計數器自減,計數器減爲0的元素被移除。

Misra-Gries算法可以利用O(k)的空間對元素j的出現頻率 fj 做出如下的估計:

  • fj - (m - m')/k <= fj <= fj
    (其中fj是j的真實出現頻率,m'則是集合中的所有計數器之和)

爲什麼會有這樣的結果呢?因爲計數器自減只會發生在集合滿時,且觸發計數器自減的那個元素也不會被統計到,所以相當於少統計了(k - 1) + 1 = k個元素。也就是說,計數器自減的操作最多能發生(m - m')/k輪——即fjfj之間的最大差值。由此可以總結出:

  • Misra-Gries算法對元素出現頻率的估計總是偏低的;
  • k越大(即計數器的集合越大),頻率的估計誤差越小;
  • 最終結果能夠保證沒有假陰性(false negative),即不會漏掉實際頻率高於m / k的元素。但可能會出現假陽性(false positive),即混入實際頻率低於m / k的元素。

Lossy Counting算法

Lossy Counting算法在2002年提出,與Misra-Gries算法的思路不太相同,但也很簡單。其流程如下。

  1. 將數據流劃分爲固定大小的窗口。
  1. 統計每一個窗口中元素的頻率,維護在計數器的集合中。然後將所有計數器的值自減1,將計數器減爲0的元素從集合中移除。
  1. 重複上述步驟,每次都統計一個窗口中的元素,將頻率值累加到計數器中,並將所有計數器自減1,並將計數器減爲0的元素從集合中移除。

在窗口大小爲1/ε的情況下,套用Misra-Gries算法的誤差分析思路,容易得出Lossy Counting算法對元素出現頻率的估計同樣是偏低的,會出現假陽性,且誤差在εm的範圍內。換句話說,如果我們希望得出頻率超過Fm的所有元素(F是個比例,如20%),那麼我們最終得到的是頻率超過(F - ε)m的結果。原作論文內建議F大約設爲ε的10倍。

論文也指出Lossy Counting算法的空間佔用爲O(1/ε · log εm),可見它是以比Misra-Gries算法更多的空間作爲trade-off換來了更低的誤差。

話說回來,Misra-Gries和Lossy Counting這樣的算法爲什麼具有實用價值呢?根據著名的Zipf's Law思想,元素在數據流中的分佈往往高度傾斜,少數頻繁出現的元素佔據了數據流中的大部分空間(考慮一下“二八定律”)。所以,即使它們是不精準的,但仍然能夠給出大致正確的、有意義的統計結果。

Space Saving算法

Space Saving算法在2005年提出,本質上是Misra-Gries和Lossy Counting算法的折衷,也是目前應用最廣泛的Heavy Hitters算法之一。它維護k = 1/ε個候選值與計數器的集合,操作流程如下圖所示。

  • 如果元素在集合中,將其對應的計數器自增;
  • 如果元素不在集合中且集合未滿,就將元素加入集合,計數器設爲1;
  • 如果元素不在集合中且集合已滿,將集合內計數器值最小的元素移除,將新元素插入到它的位置,並且在原計數值的基礎上自增。(這裏維護計數值最小的元素可以用傳統的堆)

可見,Space Saving算法構建在Misra-Gries算法的基礎上,且只有第三種情況的處理方式是不一樣的——借鑑了Lossy Counting的合併思路。除了只需要O(k)的空間之外,這樣操作的好處是,所有計數器的和一定等於數據流的總元素數m(因爲不需要做減法,只需要自增),且那些沒有被移除過的元素的計數值是準確的。容易分析得出:

  • 集合中最小的計數值min一定不會大於m / k = εm,同時能夠保證找出所有頻率大於εm的元素;
  • 元素出現頻率的估計誤差同樣在εm的範圍內,不過會偏高;
  • Space Saving算法也有假陽性的問題,特別是在非頻繁項集中位於流的末尾時。

Space Saving算法在貼近實際應用的Zipfian數據集上的benchmark如下圖所示,可見與其他算法相比,無論在準確率方面還是效率方面都幾乎是最優的。

在大數據相關的組件中,筆者所熟知的Space Saving算法應用有兩處:一是Apache Kylin中的Top-N近似預計算特性;二是ClickHouse函數庫中的anyHeavy()函數,它能夠返回數據集中任意一個頻繁項。特別地,它們使用的都是並行化的Space Saving算法,能夠顯著提升多線程環境下的計算效率。

The End

明天早起搬磚,民那晚安晚安。

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