Bloom Filter算法


Bloom Filter算法詳解

什么是布隆過濾器


布隆過濾器(Bloom Filter)是 1970 年由布隆提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數 (下面詳細說),實際上你也可以把它簡單理解為一個不怎么精確的set結構,當你使用它的contains方法判斷某個對象是否存在時,它可能會誤判。但是布隆過濾器也不是特別不精確,只要參數設置的合理,它的精確度可以控制的相對足夠精確,只會有小小的誤判概率。

當布隆過濾器說某個值存在時,這個值可能不存在;但是當它說不存在時,那么這個值一定不存在。打個比方,當它說不認識你時,那就是真的不認識,但是當它說認識你時,可能是因為你長得像他認識的另一個朋友(臉長得有些相似),所以誤判認識你。

image

布隆過濾器的使用場景


在程序的世界中,布隆過濾器是程序員的一把利器,利用它可以快速地解決項目中一些比較棘手的問題。

如網頁URL的去重、垃圾郵件識別、大集合中重復元素的判斷和緩存穿透等問題。

布隆過濾器的典型應用有:

  • 大數據判斷是否存在
    如果你的服務器內存足夠大的話,那么使用HashMap可能是一個不錯的解決方案,理論上時間復雜度可以達到O(1)級別,但是當數據量起來之后還是只能考慮布隆過濾器。
  • 解決緩存穿透
    我們通常會把一些經常訪問的數據放在Redis中當作緩存,例如產品詳情。通常一個請求過來之后,我們會先查詢緩存,而不用直接讀取數據庫,這是提升性能最簡單,也是最普遍的做法,但是如果一直請求一個不存在的緩存,那就會有大量的請求被直接打到數據庫上,造成緩存穿透,布隆過濾器也可以用來解決此類問題。
  • 爬蟲|郵箱等系統的過濾
    對爬蟲網址進行過濾,已經存在布隆中的網址,不再爬取。
    對於垃圾郵件進行過濾,對每一個發送郵件的地址進行判斷是否在布隆的黑名單內,如果在就判斷為垃圾郵件。
  • 業務場景判斷
    判斷用戶是否閱讀過某視頻或文章,比如抖音或頭條,當然會導致一定的誤判,但不會讓用戶看到重復的內容。
  • Web攔截器
    如果是相同的請求則進行攔截,防止被重復攻擊。
    用戶第一次請求,將請求參數放入布隆過濾器中,當第二次請求時,先判斷請求參數是否被布隆過濾器命中。可以提高緩存命中率。Squid 網頁代理緩存服務器在 cache digests 中就使用了布隆過濾器。Google Chrome瀏覽器使用了布隆過濾器加速安全瀏覽服務

為什么使用布隆過濾器


下面舉一個實例說明我們為什么要學習BloomFilter

image

假設我們要寫一個爬蟲程序。由於網絡間的鏈接錯綜復雜,蜘蛛在網絡間爬行很可能會形成“環”,爬蟲就會進入一個無限怪圈,找不到出路,程序出現崩潰。

所以為了避免形成“環”,就需要知道蜘蛛已經訪問過那些URL,也就是如何判重。

給一個URL,怎樣知道蜘蛛是否已經訪問過呢?按照我們的常識,就會有如下幾種方案:
  1. 將訪問過的URL保存到數據庫,數據庫管理系統可以為你去重。
  2. 用Set將訪問過的URL保存起來。那只需接近O(1)的代價就可以查到一個URL是否被訪問過了。
  3. URL經過MD5SHA-1等單向哈希后再保存到Set數據庫
  4. Bit-Map方法。建立一個BitSet,將每個URL經過一個哈希函數映射到某一位。

方法1~3都是將訪問過的URL完整保存,方法4則只標記URL的一個映射位。

以上方法在數據量較小的情況下都能完美解決問題,但是當數據量變得非常龐大時問題就來了。

方法1的缺點:數據量變得非常龐大后關系型數據庫查詢的效率會變得很低。而且每來一個URL就啟動一次數據庫查詢是不是太小題大做了?

方法2的缺點:太消耗內存。隨着URL的增多,占用的內存會越來越多。就算只有1億個URL,每個URL只算50個字符,至少需要5GB內存,還不包括Set數據結構中的內存浪費。

方法3的缺點:由於字符串經過MD5處理后的信息摘要長度只有128Bit,SHA-1處理后也只有160Bit,因此方法3比方法2節省了好幾倍的內存。

方法4的缺點:消耗內存是相對較少的,但缺點是單一哈希函數發生沖突的概率太高。

若要降低沖突發生的概率到1%,有種辦法就是就要將BitSet的長度設置為URL個數的100倍。

假設一億條URL,就得把BitSet長度設為100億,過於稀疏也是很費內存的

實質上,上面的算法都忽略了一個重要的隱含條件:允許小概率的出錯,不一定要100%准確!

也就是說少量URL實際上沒有沒被網絡爬蟲訪問,而將它們錯判為已訪問的代價是很小的——大不了少抓幾個網頁唄。

Bloom Filter算法原理


下面引入本篇的主角——Bloom Filter。其實上面方法4的思想已經很接近Bloom Filter了。

方法四的致命缺點是沖突概率高,為了降低沖突的概念,Bloom Filter使用了多個哈希函數,而不是一個。

為什么可以降低呢?我們知道Hash函數有一定幾率出現沖突,概率假設為 p1,我們知道p1是一個很小的幾率,但是在數據量大之后沖突就會變多,也就是上面第四種方法的問題。

BoomFilter使用 多個Hash函數 分別沖突概率為 p2 p3 p4 p5 … pn ,我們知道不同 Hash函數處理同一個字符串彼此獨立,所以沖突概率通過乘法公式得到為: p1p2p3p4p5p6…pn,是相當相當小的了。

Bloom Filter算法如下:

預操作
創建一個 m 位 BitSet(C++自帶,Python為bitarray),先將所有位初始化為0,然后選擇 k 個不同的哈希函數。第 i 個哈希函數對字符串 str 哈希的結果記為h(i, str),且h(i,str)的范圍是 0 到 m-1 。

image

Add操作
下面是每個字符串處理的過程,首先是將字符串str“記錄”到BitSet中的過程:

對於字符串str,分別計算h(1,str),h(2,str)…… h(k,str)。然后將BitSet的第h(1,str)、h(2,str)…… h(k,str)位設為1。

image

很簡單吧?這樣就將字符串str映射到BitSet中的k個二進制位了。

Check操作
根據上圖,我們對每個字符串采用同樣的算法。

下面是檢查字符串str是否被BitSet記錄過的過程:

  • 對於字符串str,分別計算h(1,str),h(2,str)…… h(k,str)。然后檢查BitSet的第h(1,str)、h(2,str)…… h(k,str)位是否為1,若其中任何一位不為1則可以判定str一定沒有被記錄過。若全部位都是1,則“認為”字符串str存在。
  • 若一個字符串對應的Bit不全為1,則可以肯定該字符串一定沒有被Bloom Filter記錄過。(這是顯然的,因為字符串被記錄過,其對應的二進制位肯定全部被設為1了)
  • 但是若一個字符串對應的Bit全為1,實際上是不能100%的肯定該字符串被Bloom Filter記錄過的。(因為有可能該字符串的所有位都剛好是被其他字符串所對應)這種將該字符串划分錯的情況,稱為wrong position。

Delete操作
字符串加入了就被不能刪除了,因為刪除會影響到其他字符串。實在需要刪除字符串的可以使用Counting bloomfilter(CBF),這是一種基本Bloom Filter的變體,CBF將基本Bloom Filter每一個Bit改為一個計數器,這樣就可以實現刪除字符串的功能了。

Bloom Filter跟單哈希函數Bit-Map不同之處在於:Bloom Filter使用了k個哈希函數,每個字符串跟k個bit對應。從而降低了沖突的概率。

Bloom Filter 優化


image

考慮到BoomFilter上面的指標,總結一下有以下幾個

m : BitSet 位數

n : 插入字符串個數

k :hash函數個數

當然,哈希函數也是影響的重要因素

從表格來看 m/n越大越准,k越大越准。

但是具體怎么設計呢?

哈希函數選擇

  • 哈希函數的選擇對性能的影響應該是很大的,一個好的哈希函數要能近似等概率的將字符串映射到各個Bit。
  • 選擇k個不同的哈希函數比較麻煩,一種簡單的方法是選擇一個哈希函數,然后送入k個不同的參數。

參數設計
相信大家對於 Bloom Filter 的工作原理都有了一個基本的了解,現在我們來看看在Bloom Filter 中涉及到的一些參數指標:

  • 欲插入Bloom Filter中的元素數目: n
  • Bloom Filter誤判率: P(true)
  • BitArray數組的大小: m
  • Hash Function的數目: k

欲插入Bloom Filter中的元素數目 n 是我們在實際應用中可以提前獲取或預估的;Bloom Filter的誤判率 P(true) 則是我們提前設定的可以接受的容錯率。所以在設計Bloom Filter過程中,最關鍵的參數就是BitArray數組的大小 m 和 Hash Function的數目 k,下面將給出這兩個關鍵參數的設定依據、方法

誤判率P(true)

向Bloom Filter插入一個元素時,其一個Hash Function會將BitArray中的某Bit置為1,故對於任一Bit而言,其被置為1的概率\(P1=\frac{1}{m}\),那么其依然是0的概率\(P0=1-P1=1-\frac{1}{m}\);易知插入一個元素時,其k個Hash Function都未將該Bit置為1的概率\(P0^1=(1-\frac{1}{m})^{k}\)。則向Bloom Filter插入全部n個元素后,該Bit依然為0的概率即為\(P0^n=(1-\frac{1}{m})^{kn}\),反之,該Bit為1的概率則為\(P1^{n}=1-P0^{n}=1-(1-\frac{1}{m})^{kn}=1-(1-\frac{1}{m})^{mkn/m}\)

根據基本極限

\[\begin{gather*} \lim_{x \to \infty}{(1-\frac{1}{x})}^{-x}=e \end{gather*} \]

可得

\[\begin{gather*} P1^{n}≈1-e^{-\frac{kn}{m}} \end{gather*} \]

在已有n個元素進行過Hash操作的基礎上,當有新的元素y到來,如果該元素進行k個Hash操作后對應到位向量的位置均已被置1,那么元素y就會被認為已經存在於集合之中,但是其實其並不存在於集合之中,這種情況下就產生了誤判。我們用\(P(mis\_judge)\)表示誤判率,結合以上公式則:

\[\begin{gather*} P(mis\_judge)\approx (1-e^\frac{-nk}{m})^k \end{gather*} \]

從上式可以看出,當BitArray數組的大小m增大或欲插入Bloom Filter的元素數目n減小時,均可以使誤判率\(P(mis\_judge)\)下降,如果要保持\(P(mis\_judge)\)不變,則布隆過濾器位向量大小m應該和集合元素數量n保持同步增長。

Hash Function的數目k

前文已經看到Hash Function數目k的增加可以減小誤判率P(true),但是隨着Hash Function數目k的繼續增加,反而會使誤判率P(true)上升,即誤判率是一個關於Hash Function數目k的凸函數。所以當k在極值點時,此時誤判率即為最小值

\[\begin{gather*} f(k)=(1-e^\frac{-nk}{m})^k \end{gather*} \]

\(a=e^\frac{n}{m}\),則有:

\[\begin{gather*} f(k)=(1-a^{-k})^k \end{gather*} \]

分別對上式兩邊,先取對數然后再求導,可有:

\(\frac{1}{f(k)}f'(k)=ln(1-a^{-k})+\frac{ka^{-k}{lna}}{1-a^{-k}}\)

易知,當k取極值點時,有\(f'(k)=0\),故將其帶入上式即可求出k

\[\begin{gather*} ln(1-a^{-k})+\frac{ka^{-k}lna}{1-a^{-k}}=0 \\=>(1-a^{-k})ln(1-a^{-k})=-ka^{-k}lna \\=>(1-a^{-k})ln(1-a^{-k})=a^{-k}lna^{-k} \\=>1-a^{-k}=a^{-k} \\=>a^{-k}=\frac{1}{2} \\=>e^{\frac{-kn}{m}}=\frac{1}{2} \\=>k=\frac{m}{n}ln2\approx0.7\frac{m}{n} \end{gather*} \]

此時,誤判率\(f\)最小,約為:

\[\begin{gather*} f(k)=(\frac{1}{2})^k\approx(0.6185)^{\frac{m}{n}} \end{gather*} \]

由以上討論我們知道,k的值主要取決於布隆過濾器位向量大小m和集合中元素數量n的值。有研究表明,如果想要保持較低的誤判率,布隆過濾器的位向量使用空間應低於50%。

BitArray數組的大小 m

如何確定BitArray數組的大小 m 呢?這里我們固定k為最優的數目\(\frac{m}{n}ln2\)進行公式推導

由上面可知,

\[\begin{gather*} P(mis\_judge)=(\frac{1}{2})^k=(\frac{1}{2})^{\frac{mln2}{n}} \end{gather*} \]

對上式求解,可得:

\[\begin{gather*} =>lnP(mis\_judge)=(\frac{1}{2})^k=\frac{m}{n}ln2ln{\frac{1}{2}} \end{gather*} \\=>m=-\frac{nlnP(mis\_judge)}{(ln2)^{2}} \]

此時,我們即可以利用上式的結果,通過\(P(mis\_judge)\)\(n\)來確定最優的BitArray數組的大小 \(m\)

如何解決布隆過濾器不支持刪除的問題


(1)Counting Bloom Filter
Counting Bloom Filter將標准 Bloom Filter位數組的每一位擴展為一個小的計數器(counter),在插入元素時給對應的k(k為哈希函數個數)個Counter的值分別加1,刪除元素時給對應的k個Counter的值分別減1。Counting Bloom Filter通過多占用幾倍的存儲空間的代價,給Bloom Filter增加了刪除操作。

image

(2)布谷鳥過濾器
對於這種方式有興趣的讀者可以閱讀這篇文章:https://juejin.cn/post/6924636027948630029#heading-1

Python 代碼簡單實現


主體

from bitarray import bitarray # 產生BitSet

import mmh3 # 產生Hash函數
 
 
class BloomFilter(set):
 
    def __init__(self, size, hash_count):
        super(BloomFilter, self).__init__()
        self.bit_array = bitarray(size)
        self.bit_array.setall(0)
        self.size = size
        self.hash_count = hash_count
 
    def __len__(self):
        return self.size
 
    def __iter__(self):
        return iter(self.bit_array)
 
    def add(self, item):
        for seed in range(self.hash_count):
            index = mmh3.hash(item, seed) % self.size
            self.bit_array[index] = 1
 
        return self
 
    def __contains__(self, item):
        out = True
        for seed in range(self.hash_count):
            index = mmh3.hash(item, seed) % self.size
            if self.bit_array[index] == 0:
                out = False
 
        return out

測試

def main():
    bloom = BloomFilter(10000, 20)
    animals = ['dog', 'cat', 'giraffe', 'fly', 'mosquito', 'horse', 'eagle',
               'bird', 'bison', 'boar', 'butterfly', 'ant', 'anaconda', 'bear',
               'chicken', 'dolphin', 'donkey', 'crow', 'crocodile']
    # First insertion of animals into the bloom filter
    for animal in animals:
        bloom.add(animal)
 
    # Membership existence for already inserted animals
    # There should not be any false negatives
    for animal in animals:
        if animal in bloom:
            print('{} is in bloom filter as expected'.format(animal))
        else:
            print('Something is terribly went wrong for {}'.format(animal))
            print('FALSE NEGATIVE!')
 
    # Membership existence for not inserted animals
    # There could be false positives
    other_animals = ['badger', 'cow', 'pig', 'sheep', 'bee', 'wolf', 'fox',
                     'whale', 'shark', 'fish', 'turkey', 'duck', 'dove',
                     'deer', 'elephant', 'frog', 'falcon', 'goat', 'gorilla',
                     'hawk' ]
    for other_animal in other_animals:
        if other_animal in bloom:
            print('{} is not in the bloom, but a false positive'.format(other_animal))
        else:
            print('{} is not in the bloom filter as expected'.format(other_animal))
 
 
if __name__ == '__main__':
    main()

結果

dog is in bloom filter as expected
cat is in bloom filter as expected
giraffe is in bloom filter as expected
fly is in bloom filter as expected
mosquito is in bloom filter as expected
horse is in bloom filter as expected
eagle is in bloom filter as expected
bird is in bloom filter as expected
bison is in bloom filter as expected
boar is in bloom filter as expected
butterfly is in bloom filter as expected
ant is in bloom filter as expected
anaconda is in bloom filter as expected
bear is in bloom filter as expected
chicken is in bloom filter as expected
dolphin is in bloom filter as expected
donkey is in bloom filter as expected
crow is in bloom filter as expected
crocodile is in bloom filter as expected
badger is not in the bloom filter as expected
cow is not in the bloom filter as expected
pig is not in the bloom filter as expected
sheep is not in the bloom, but a false positive
bee is not in the bloom filter as expected
wolf is not in the bloom filter as expected
fox is not in the bloom filter as expected
whale is not in the bloom filter as expected
shark is not in the bloom, but a false positive
fish is not in the bloom, but a false positive
turkey is not in the bloom filter as expected
duck is not in the bloom filter as expected
dove is not in the bloom filter as expected
deer is not in the bloom filter as expected
elephant is not in the bloom, but a false positive
frog is not in the bloom filter as expected
falcon is not in the bloom filter as expected
goat is not in the bloom filter as expected
gorilla is not in the bloom filter as expected
hawk is not in the bloom filter as expected

從輸出結果可以發現,存在不少誤報樣本,但是並不存在假陰性。

不同於這段布隆過濾器的實現代碼,其它語言的多個實現版本並不提供哈希函數的參數。這是因為在實際應用中誤報比例這個指標比哈希函數更重要,用戶可以根據誤報比例的需求來調整哈希函數的個數。通常來說,sizeerror_rate是布隆過濾器的真正誤報比例。如果你在初始化階段減小了error_rate,它們會調整哈希函數的數量。

參考資料


https://cloud.tencent.com/developer/article/1731494 
https://blog.csdn.net/a745233700/article/details/113751718
https://juejin.cn/post/6924636027948630029#heading-1
https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/#%E4%B8%80%E3%80%81%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8%E7%AE%80%E4%BB%8B
https://segmentfault.com/a/1190000024566947
https://github.com/jaybaird/python-bloomfilter
https://blog.csdn.net/weixin_42081389/article/details/103137671


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM