利用bloom filter算法處理大規模數據過濾

Bloom Filter是由Bloom在1970年提出的一種快速查找算法,通過多個hash算法來共同判斷某個元素是否在某個集合內。可以用於網絡爬蟲的url重複過濾、垃圾郵件的過濾等等。


它相比hash容器的一個優勢就是,不需要存儲元素的實際數據到容器中去來一個個的比較是否存在。

只需要對應的位段來標記是否存在就行了,所以想當節省內存,特別適合海量的數據處理。並且由於省去了存儲元素和比較操作,所以性能也比基於hash容器的高了很多。


但是由於bloom filter沒有去比較元素,只通過多個hash來判斷唯一性,所以存在一定的hash衝突導致誤判。誤判率的大小由hash函數的個數、hash函數優劣、以及存儲的位空間大小共同決定。


並且刪除也比較困難,解決辦法是使用其變種,帶計數的bloom filter,這個這裏就不多說了。


對於bloom filter算法的實現,相當簡單:

首先分配一塊固定的連續空間,大小是m個比特位(m/8+1個字節),然後再提供k個不同hash函數,同時對每個元素進行計算位索引。如果每個位索引對應的位都爲1,則存在該元素,否則就是不存在。


可以看出,如果判斷爲不存在,那麼肯定是不存在的,只有在判斷爲存在的時候,纔會存在誤判。


bloom filter主要的難點其實在於估算:

保證指定誤判率的情況下,到底需要多少個hash函數,多少的存儲空間。


首先來看下bloom filter的誤判率計算公式:


假定有k個hash函數,m個比特位的存儲空間,n個集合元素,則有誤判率p:


    p = (1 - ((1 - 1/ m) ^ kn))^k ~= (1 - e^(-kn/m))^k

 

根據這個,官方給出了一個計算k的最優解公式,使其滿足給定p的情況下,存儲空間達到最小:


    k = (m / n) * ln2


把它帶入概率公式得到:


    p = (1 - e ^-((m/nln2)n/m))^(m/nln2)


簡化爲:


    lnp = -m/n * (ln2)^2


因此,如果指定p,只需要滿足如果公式,就可以得到最優解:


    s = m/n = -lnp / (ln2 * ln2) = -log2(p) / ln2

    k = s * ln2 = -log2(p)


理論值:


    p < 0.1: k = 3.321928, m/n = 4.79

    p < 0.01: k = 6.643856, m/n = 9.58

    p < 0.001: k = 9.965784, m/n = 14.37

    p < 0.0001: k = 13.287712, m/n = 19.170117


可以看出,這個確實能夠在保證誤判率的前提下,使其存儲空間達到最小,但是使用的hash函數個數k

相對較多,至少也得4個,要滿足p < 0.001,需要10個才行,這個對於字符串hash的計算來講,性能損耗相當大的,實際使用中根本沒法接受。


因此我們需要另外一種推到公式,可以認爲指定p和k的情況下,來計算空間使用s=m/n的大小,這樣我們在實際使用的時候,靈活性就大大提高了。


下面來看下,我自己推到出來的公式,首先還是依據誤判率公式:


    p = (1 - e^(-kn/m))^k


假定s=m/n,則有


    p = (1 - e^(-k/s))^k


兩邊取導,得到:


    lnp = k * ln(1 - e^(-k/s))


交換k:


    (lnp) / k = ln(1 - e^(-k/s))


重新上e:


    e^((lnp) / k) = 1 - e^(-k/s)


化簡:


    e^(-k/s) = 1 - e^((lnp) / k) = 1 - (e^lnp)^(1/k) = 1 - p^(1/k)


再求導:


    -k/s = ln(1 - p^(1/k))


得出:


    s = -k / ln(1 - p^(1/k))


假定`c = p^(1/k)`:


    s = -k / ln(1 - c)

    

利用泰勒展開式:`ln(1 + x) ~= x - 0.5x^2 while x < 1` 化簡得到:


    s ~= -k / (-c-0.5c^2) = 2k / (2c + c * c)


最後得出公式:


    c = p^(1/k)

    s = m / n = 2k / (2c + c * c)


假定有n=10000000的數據量,則有理論值:


    p < 0.1 and k = 1: s = m/n = 9.523810

    p < 0.1 and k = 2: s = m/n = 5.461082

    p < 0.1 and k = 3: s = m/n = 5.245850, space ~= 6.3MB

    p < 0.1 and k = 4: s = m/n = 5.552045, space ~= 6.6MB


    p < 0.01 and k = 1: s = m/n = 99.502488

    p < 0.01 and k = 2: s = m/n = 19.047619

    p < 0.01 and k = 3: s = m/n = 12.570636, space ~= 15MB

    p < 0.01 and k = 4: s = m/n = 10.922165, space ~= 13MB


    p < 0.001 and k = 1: s = m/n = 999.500250

    p < 0.001 and k = 2: s = m/n = 62.261118

    p < 0.001 and k = 3: s = m/n = 28.571429, space ~= 34MB

    p < 0.001 and k = 4: s = m/n = 20.656961, space ~= 24.6MB


    p < 0.0001 and k = 1: s = m/n = 9999.500025

    p < 0.0001 and k = 2: s = m/n = 199.004975

    p < 0.0001 and k = 3: s = m/n = 63.167063, space ~= 75.3MB

    p < 0.0001 and k = 4: s = m/n = 38.095238, space ~= 45.4MB

    p < 0.0001 and k = 5: s = m/n = 29.231432, space ~= 24.8MB


可以看到,在k=3的情況下,其實已經可以達到我們平常使用所能的接受範圍內了,沒必要非得

使用最優解,除非在空間使用極爲苛刻的情況下,而且這個公式,針對程序空間使用的調整,更加的靈活智能。


特別提下,經過實測,如果每個hash的實現非常優質,分佈很均勻的情況下,其實際的誤判率比理論值低很多:


就拿TBOX的bloom filter的實現做測試,n=10000000:


    p < 0.01 and k = 3的情況下,其實際誤判率爲:0.004965

    p < 0.001 and k = 3的情況下,其實際誤判率爲:0.000967


所以好的hash函數算法也是尤爲的重要。


下面來看下TBOX提供的bloom filter的使用,用起來也是相當的方便:



// 總的元素個數

tb_size_t count = 10000000;



/* 初始化bloom filter

 *

 * TB_BLOOM_FILTER_PROBABILITY_0_01: 預定義的誤判率,接近0.01

 * 注:由於內部使用位移數來表示:1 / 2^6 = 0.015625 ~= 0.01

 * 所以實際傳入的誤判率,有可能稍微大一點,但是還是相當接近的

 *

 * 3:爲k值,hash函數的個數,最大不超過15個

 *

 * count:指定的元素規模數

 *

 * tb_item_func_long():容器的元素類型,主要是用其內定的hash函數,如果要自定義hash函數,可以替換:

 *

 * tb_size_t tb_xxxxxx_hash(tb_item_func_t* func, tb_cpointer_t data, tb_size_t mask, tb_size_t index)

 * {

 * // mask爲hash掩碼,index爲第index個hash算法的索引

 * return compute_hash(data, index) & mask;

 * }

 *

 * tb_item_func_t func = tb_item_func_long();

 * func.hash = tb_xxxxxx_hash;

 *

 * 來進行

 */

tb_bloom_filter_ref_t filter = tb_bloom_filter_init(TB_BLOOM_FILTER_PROBABILITY_0_01, 3, count, tb_item_func_long());



if (filter)

{

    tb_size_t i = 0;

    for (i = 0; i < count; i++)

    {

        // 產生隨機數

        tb_long_t value = tb_random();

        

        // 設置值到filter內,如果不存在,則返回tb_true表示設置成功

        if (tb_bloom_filter_set(filter, (tb_cpointer_t)value))

        {

             // 添加元素成功,之前元素不存在

             // 不會存在誤判

        }

        else

        {

             // 添加失敗,添加的元素已經存在

             // 這裏可能會存在誤判

        }

        

        // 僅僅判斷元素是否存在

        if (tb_bloom_filter_get(filter, (tb_cpointer_t)data)

        {

             // 元素已經存在

             // 這裏可能會存在誤判

        }

        else

        {

             // 元素不存在

             // 不會存在誤判

        }

    }

    

    // 退出filter

    tb_bloom_filter_exit(filter);

}



// 常用預定義的誤判率,也可以指定其他值,注:必須是位移數,而不是實際值

typedef enum __tb_bloom_filter_probability_e

{

    TB_BLOOM_FILTER_PROBABILITY_0_1 = 3 ///!< 1 / 2^3 = 0.125 ~= 0.1

, TB_BLOOM_FILTER_PROBABILITY_0_01 = 6 ///!< 1 / 2^6 = 0.015625 ~= 0.01

, TB_BLOOM_FILTER_PROBABILITY_0_001 = 10 ///!< 1 / 2^10 = 0.0009765625 ~= 0.001

, TB_BLOOM_FILTER_PROBABILITY_0_0001 = 13 ///!< 1 / 2^13 = 0.0001220703125 ~= 0.0001

, TB_BLOOM_FILTER_PROBABILITY_0_00001 = 16 ///!< 1 / 2^16 = 0.0000152587890625 ~= 0.00001

, TB_BLOOM_FILTER_PROBABILITY_0_000001 = 20 ///!< 1 / 2^20 = 0.00000095367431640625 ~= 0.000001

        

}tb_bloom_filter_probability_e;


----------


* [TBOX項目詳情](http://www.oschina.net/p/tbox)

* [TBOX項目源碼](https://github.com/waruqi/tbox)

* [TBOX項目文檔](https://github.com/waruqi/tbox/wiki/%E7%9B%AE%E5%BD%95)


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