布隆過濾器的概述

周建丁發表於2016-08-01

原文:A Gentle Introduction to Bloom Filter
翻譯:KK4SBB
責編:周建丁(zhoujd@csdn.net)

布隆過濾器

布隆過濾器(Bloom Filter)是一種概率空間高效的資料結構。它與hashmap非常相似,用於檢索一個元素是否在一個集合中。它在檢索元素是否存在時,能很好地取捨空間使用率與誤報比例。正是由於這個特性,它被稱作概率性資料結構(probabilistic data structure)。

空間效率

我們來仔細地看看它的空間效率。如果你想在集合中儲存一系列的元素,有很多種不同的做法。你可以把資料儲存在hashmap,隨後在hashmap中檢索元素是否存在,hashmap的插入和查詢的效率都非常高。但是,由於hashmap直接儲存內容,所以空間利用率並不高。

如果希望提高空間利用率,我們可以在元素插入集合之前做一次雜湊變換。還有其它方法呢?我們可以用位陣列來儲存元素的雜湊值。還有嗎,還有嗎?我們也允許在位陣列中存在雜湊衝突。這正是布隆過濾器的工作原理,它們就是基於允許雜湊衝突的位陣列,可能會造成一些誤報。在布隆過濾器的設計階段就允許雜湊衝突的存在,否則空間使用就不夠緊湊了。

當使用列表或者集合時,空間效率都是重要且顯著的,那麼布隆過濾器就應當被考慮。

布隆過濾器基礎

布隆過濾器是N位的位陣列,其中N是位陣列的大小。它還有另一個引數k,表示使用雜湊函式的個數。這些雜湊函式用來設定位陣列的值。當往過濾器中插入元素x時,h1(x), h2(x), …, hk(x)所對應索引位置的值被置“1”,索引值由各個雜湊函式計算得到。注意,如果我們增加雜湊函式的數量,誤報的概率會趨近於0.但是,插入和查詢的時間開銷更大,布隆過濾器的容量也會減小。

為了用布隆過濾器檢驗元素是否存在,我們需要校驗是否所有的位置都被置“1”,與我們插入元素的過程非常相似。如果所有位置都被置“1”,那也就意味著該元素很有可能存在於布隆過濾器中。若有位置未被置“1”,那該元素一定不存在。

簡單的Python實現

如果想實現一個簡單的布隆過濾器,我們可以這樣做:

    from bitarray import bitarray

    # 3rd party
    import mmh3


    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 ii in range(self.hash_count):
                index = mmh3.hash(item, ii) % self.size
                self.bit_array[index] = 1

            return self

        def __contains__(self, item):
            out = True
            for ii in range(self.hash_count):
                index = mmh3.hash(item, ii) % self.size
                if self.bit_array[index] == 0:
                    out = False

            return out


    def main():
        bloom = BloomFilter(100, 10)
        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,它們會調整雜湊函式的數量。

誤報

布隆過濾器能夠拍著胸脯說某個元素“肯定不存在”,但是對於一些元素它們會說“可能存在”。針對不同的應用場景,這有可能會是一個巨大的缺陷,亦或是無關緊要的問題。如果在檢索元素是否存在時不介意引入誤報情況,那麼你就應當考慮用布隆過濾器。

另外,如果隨意地減小了誤報比率,雜湊函式的數量相應地就要增加,在插入和查詢時的延時也會相應地增加。本節的另一個要點是,如果雜湊函式是相互獨立的,並且輸入元素在空間中均勻的分佈,那麼理論上真實誤報率就不會超過理論值。否則,由於雜湊函式的相關性和更頻繁的雜湊衝突,布隆過濾器的真實誤報比例會高於理論值。

在使用布隆過濾器時,需要考慮誤報的潛在影響。

確定性

當你使用相同大小和數量的雜湊函式時,某個元素通過布隆過濾器得到的是正反饋還是負反饋的結果是確定的。對於某個元素x,如果它現在可能存在,那五分鐘之後、一小時之後、一天之後、甚至一週之後的狀態都是可能存在。當我得知這一特性時有一點點驚訝。因為布隆過濾器是概率性的,那其結果顯然應該存在某種隨機因素,難道不是嗎?確實不是。它的概率性體現在我們無法判斷究竟哪些元素的狀態是可能存在

換句話說,過濾器一旦做出可能存在的結論後,結論不會發生變化。

缺點

布隆過濾器並不十全十美。

布隆過濾器的容量

布隆過濾器需要事先知道將要插入的元素個數。如果你並不知道或者很難估計元素的個數,情況就不太好。你也可以隨機指定一個很大的容量,但這樣就會浪費許多儲存空間,儲存空間卻是我們試圖優化的首要任務,也是選擇使用布隆過濾器的原因之一。一種解決方案是建立一個能夠動態適應資料量的布隆過濾器,但是在某些應用場景下這個方案無效。有一種可擴充套件布隆過濾器,它能夠調整容量來適應不同數量的元素。它能彌補一部分短板。

布隆過濾器的構造和檢索

在使用布隆過濾器時,我們不僅要接受少量的誤報率,還要接受速度方面的額外時間開銷。相比於hashmap,對元素做雜湊對映和構建布隆過濾器時必然存在一些額外的時間開銷。

無法返回元素本身

布隆過濾器並不會儲存插入元素的內容,只能檢索某個元素是否存在,因為存在雜湊函式和雜湊衝突我們無法得到完整的元素列表。這是它相對於其它資料結構的最顯著優勢,空間的使用率也造成了這塊短板。

刪除某個元素

想從布隆過濾器中刪除某個元素可不是一件容易的事情,你無法撤回某次插入操作,因為不同專案的雜湊結果可以被索引在同一位置。如果想撤消插入,你只能記錄每個索引位置被置位的次數,或是重新建立一次。兩種方法都有額外的開銷。基於不同的應用場景,若要刪除一些元素,我們更傾向於重建布隆過濾器。

在不同語言中的實現

在產品中,你肯定不想自己去實現布隆過濾器。有兩個原因,其中之一是選擇好的雜湊函式和實現方法能有效改善錯誤率的分佈。其次,它需要通過實戰測試,錯誤率和容量大小都要經得起實戰檢驗。各種語言都有開源實現的版本,以我自己的經驗,下面的Node.js和Python版本實現非常好用:

還有更快版本的pybloomfilter(插入和檢索速度都比上面的Python庫快10倍),但它需要執行在PyPy環境下,並不支援Python3。


CCAI 2016中國人工智慧大會將於8月26-27日在京舉行,AAAI主席,多位院士,MIT、微軟、大疆、百度、滴滴專家領銜全球技術領袖和產業先鋒打造國內人工智慧前沿平臺,7+重磅大主題報告,4大專題論壇,1000+高質量參會嘉賓,探討人機互動、機器學習、模式識別及產業實戰。八折特惠門票截止8月12日24時

圖片描述

相關文章