Bloom Filter

一個經典的問題:
有1000瓶藥物,但是其中有一瓶是有毒的,小白鼠吃了一個星期以後就會死掉,請問,在一個星期內找出有毒的藥物,最少需要多少 只小白鼠?
如果一個人考慮問題是二進制的考慮方法,那麼肯定好不猶豫的會說10只,爲什麼呢?因爲小白鼠能夠有兩種狀態,1代表生,0代表死,那 麼10只能表示2的10次方種狀態,那麼也就是說能表示1024種狀態,那麼答案也就是10只。

 

任何perl使用者都熟悉hash查詢,一個存在測試的語句可以這樣寫:

foreach my $e ( @things ) { $lookup{$e}++ }

sub check {
my ( $key ) = @_;
print "Found $key!" if exists( $lookup{ $key } );
}

雖然hash查詢很有用,但對非常大的列表,或keys自身非常大時,這種查詢可能變得不實用。當查詢hash增長得太大,通常的做法是將它移到 數據庫或文件中,只在本地緩存裏保存最常用的關鍵字,這樣能改善性能。

許多人不知道有一種優雅的算法,用以代替hash查詢。它是一種古老的算法,叫做Bloom filter。 Bloom filter允許你在 有限的內存裏(你想在這塊內存裏存放關鍵字的完整列表),執行成員測試,這樣就能避開使用磁盤或數據庫進行查詢的性能瓶頸。也許你會認爲,空間的節省是有 代價的:存在着可大可小的假命中率風險,並且一旦你增加key到filter後,就不能刪除它。然而在許多情形下,這些侷限是可接受 的,bloom filter能編制有用工具。(仙子注:例如代理服務器軟件Squid就使用了bloom filter算法。)

例如,假如你運行了一個高流量的在線音樂存儲站點,並且如果你已知歌曲存在,就可以通過僅獲取歌曲信息的方法,來最大程度的減少數據庫壓力。你可 以在啓動時構建一個bloom filter,在試圖執行昂貴的數據庫查詢前,可以用它執行快速的成員存在測試。

use Bloom::Filter;

my $filter = Bloom::Filter->new( error_rate => 0.01, capacity => $SONG_COUNT );
open my $fh, "enormous_list_of_titles.txt" or die "Failed to open: $!";

while (<$fh>) {
chomp;
$filter->add( $_ );
}

sub lookup_song {
my ( $title ) = @_;
return unless $filter->check( $title );
return expensive_db_query( $title ) or undef;
}

在該示例裏,該測試給出假命中的機率是1%,在假命中率情況下程序會執行昂貴的數據庫索取操作,並最終返回空結果。儘管如此,你已避開了99%的 昂貴查詢時間,僅使用了用於hash查詢的一小片內存。更進一步,1%假命中率的filter,每個key的存儲空間在2字節以下。這比你執行完整的 hash查詢所需的內存少得多。

bloom filters在Burton Bloom之後命名,Burton Bloom 1970年首先在文檔裏描述了它們,文檔名 Space/time trade-offs in hash coding with allowable errors.在那些內存稀少的日子 裏,bloom filters因其簡潔而倍受重視。事實上,最早的應用之一是拼寫檢查程序。然而,由於有少數非常明顯的特性,該算法特別適合社會軟件應 用。

因爲bloom filters使用單向hash來存儲數據,因此不可能在不做窮舉搜索的情況下,重建filter裏的keys列表。甚至這點看 起來並非象很有用,既然來自窮舉搜索的假命中會覆蓋掉真正的keys列表。所以bloom filters能在不向全世界廣播完整列表的情況下,共享關於 已有資料的信息。因爲這個理由,它們在peer-to-peer應用中特別有用,在這個應用中大小和隱私是重要的約束。



bloom filters如何工作

bloom filter由2部分組成:1套k hash函數,1個給定長度的位向量。選擇位向量的長度,和hash函數的數量,依賴於我們想增 加多少keys到設置中,以及我們能容忍的多高的假命中率。

bloom filter中所有的hash函數被配置過,其範圍匹配位向量的長度。例如,假如向量是200位長,hash函數返回的值就在1到 200之間。在filter裏使用高質量的hash函數相當重要,它保證輸出等分在所有可能值上--hash函數裏的“熱點”會增加假命中率。(仙子注: 所謂“熱點”是指結果過分頻繁的分佈在某些值上。)

要將某個key輸入bloom filer中,我們在每個k hash函數裏遍歷它,並將結果作爲在位向量裏的offsets,並打開我們在該 offsets上找到的任何位。假如該位已經設置,我們繼續保留其打開。還沒有在bloom filter裏關閉位的機制。

在本示例裏,讓我們看看某個bloom filter,它有3個hash函數,並且位向量的長度是14。我們用空格和星號來表示位向量,以便於觀 察。你也許想到,空的bloom filter以所有的位關閉爲開始,如圖1所示。

圖1:空的bloom filter

現在我們將字符apples增加到filter中去。爲了做到這點,我們以apples爲參數來運行每個hash函數,並採集輸出:

hash1("apples") = 3
hash2("apples") = 12
hash3("apples") = 11

然後我們打開在向量裏相應位置的位--在這裏就是位3,11,和12,如圖2所示。

圖2:激活了3位的bloom filter

爲了增加另1個key,例如plums,我們重複hash運算過程:

hash1("plums") = 11
hash2("plums") = 1
hash3("plums") = 8

再次打開向量裏相應的位,如圖3裏的高亮度顯示。

圖3:增加了第2個key的bloom filter

注意位置11的位已被打開--在前面的步驟裏,當我們增加apples時已設置了它。位11現在有雙重義務,存儲apples和plums兩者的 信息。當增加更多的keys時,它也會存儲其他keys的信息。這種交迭讓bloom filters如此緊湊--任何位同時編碼多個keys。這種交迭 也意味着你永不能從filter裏取出key,因爲你不能保證你所關閉的位沒有攜載其他keys的信息。假如我們試圖執行反運算過程來從filter裏刪 除apples,就會不經意的關閉編碼plums的1個位。從bloom filter裏剝離key的唯一方法是重建filter,剔除無用key。

檢查是否某個key已經存在於filter的過程,非常類似於增加新key。我們在所有的hash函數裏遍歷key,然後檢查是否在那些 offsets上的位都是打開的。假如任何一位關閉,我們知道該key肯定不存在於filter中。假如所有位都打開,我們知道該key可能存在。

我說“可能”是因爲存在一種情況,該key是個假命中。例如,假如我們用字符mango來測試filter,看看會發生什麼情況。我們運行 mango遍歷hash函數:

hash1("mango") = 8
hash2("mango") = 3
hash3("mango") = 12

然後檢查在那些offsets上的位,如圖4所示。

圖4:bloom filter的假命中

所有在位置3,8,和12的位都是打開的,故filter會報告mango是有效key。

當然,mango並非有效key--我們構建的filter僅包含apples和plums。事實是mango的offsets非常巧合的指向了 已激活的位。這就找到了1個假命中--某個key看起來位於filter中,但實際不是。

正如你想的一樣,假命中率依賴於位向量的長度和存儲在filter裏的keys的數量。位向量越寬闊,我們檢查的所有k位被打開的可能性越小,除 非該key確實存在於filter中。在hash函數的數量和假命中率之間的關係更敏感。假如使用的hash函數太少,在keys之間的差別就很少;但假 如使用hash函數太多,filter會過於密集,增加了衝突的可能性。可以使用如下公式來計算任何filter的假命中率:

c = ( 1 - e(-kn/m) )k

這裏c是假命中率,k是hash函數的數量,n是filter裏keys的數量,m是filter的位長。

當使用bloom filters時,我們先要有個意識,期待假命中率多大;也應該有個粗糙的想法,關於多少keys要增加到filter裏。我 們需要一些方法來驗證需要多大的位向量,以保證假命中率不會超出我們的限制。下列方程式會從錯誤率和keys數量求出向量長度:

m = -kn / ( ln( 1 - c ^ 1/k ) )

請注意另1個自由變量:k,hash函數的數量。可以用微積分來得出k的最小值,但有個偷懶的方法來做它:

sub calculate_shortest_filter_length {
my ( $num_keys, $error_rate ) = @_;
my $lowest_m;
my $best_k = 1;

foreach my $k ( 1..100 ) {
my $m = (-1 * $k * $num_keys) / 
( log( 1 - ($error_rate ** (1/$k))));

if ( !defined $lowest_m or ($m < $lowest_m) ) {
$lowest_m = $m;
$best_k   = $k;
}
}
return ( $lowest_m, $best_k );
}

爲了給你直觀的感覺,關於錯誤率和keys數量如何影響bloom filters的存儲size,表1列出了一些在不同的容量/錯誤率組合下的 向量size。

ErrorRate   Keys   RequiredSize   Bytes/Key 
1%           1K       1.87 K         1.9 
0.1%         1K       2.80 K         2.9 
0.01%        1K       3.74 K         3.7 
0.01%       10K       37.4 K         3.7 
0.01%      100K        374 K         3.7 
0.01%        1M       3.74 M         3.7 
0.001%       1M       4.68 M         4.7 
0.0001%      1M       5.61 M         5.7 



在Perl裏構建bloom filter

爲了構建1個工作bloom filter,我們需要1套良好的hash函數。這些容易解決--在CPAN上有幾個優秀的hash算法可用。對我 們的目的來說,較好的選擇是Digest::SHA1,它是強度加密的hash,用C實現速度很快。通過對不同值的輸出列表進行排序,我們能使用該模塊來 創建任意數量的hash函數。如下是構建唯一hash函數列表的子函數:

use Digest::SHA1 qw/sha1/;

sub make_hashing_functions {
my ( $count ) = @_;
my @functions;

for my $salt (1..$count ) {
push @functions, sub { sha1( $salt, $_[0] ) };
}

return @functions;
}

爲了能夠使用這些hash函數,我們必須找到1個方法來控制其範圍。Digest::SHA1返回令人爲難的過長160位hash輸出,這僅在向 量長度爲2的160次方時有用,而這種情況實在罕見。我們結合使用位chopping和division來將輸出削減到可用大小。

如下子函數取某個key,運行它遍歷hash函數列表,並返回1個長度($FILTER_LENGTH)的位掩碼:

sub make_bitmask {
my ( $key ) = @_;
my $mask    = pack( "b*", '0' x $FILTER_LENGTH);

foreach my $hash_function ( @functions ){ 

my $hash       = $hash_function->($key);
my $chopped    = unpack("N", $hash );
my $bit_offset = $result % $FILTER_LENGTH;

vec( $mask, $bit_offset, 1 ) = 1;       
}
return $mask;
}

讓我們逐行分析上述代碼:

my $mask = pack( "b*", '0' x $FILTER_LENGTH);

我們以使用perl的pack操作來創建零位向量開始,它是$FILTER_LENGTH長。pack取2個參數,1個模型和1個值。b模型告訴 pack將值解釋爲bits,*指“重複任意多需要的次數”,跟正則表達式類似。perl實際上會補充位向量的長度爲8的倍數,但我們將忽視這些多餘位。

有1個空的位向量在手中,我們準備開始運行key遍歷hash函數:

my $hash = $hash_function->($key);
my $chopped = unpack("N", $hash );

我們保存首個32位輸出,並丟棄剩下的。這點可讓我們不必要求BigInt支持。第2行做實際的位chopping。模型裏的N告訴unpack 以網絡字節順序來解包32位整數。因爲未在模型裏提供任何量詞,unpack僅解包1個整數,然後終止。

假如你對位chopping過度狂熱,你可以將hash分割成5個32位的片斷,並對它們一起執行OR運算,將所有信息保存在原始hash裏:

my $chopped = pack( "N", 0 );
my @pieces  =  map { pack( "N", $_ ) } unpack("N*", $hash );
$chopped    = $_ ^ $chopped foreach @pieces;

但這樣作可能殺傷力過度。

現在我們有了來自hash函數的32位整數輸出的列表,下一步必須做的是,裁減它們的大小,以使其位於(1..$FILTER_LENGTH)範 圍內。

my $bit_offset = $chopped % $FILTER_LENGTH;

現在我們已轉換key爲位offsets列表,這正是我們所求的。

剩下唯一要做的事情是,使用vec來設置位,vec取3個參數:向量自身,開始位置,要設置的位數量。我們能象賦值給變量一樣來分配值給vec:

vec( $mask, $bit_offset, 1 ) = 1;

在設置了所有位後,我們以1個位掩碼來結束,位掩碼和bloom filter長度一樣。我們可以使用這個掩碼來增加key到filter中:

sub add {
my ( $key, $filter ) = @_;

my $mask = make_bitmask( $key );
$filter  = $filter | $mask;
}

或者我們使用它來檢查是否key已存在:

sub check {
my ( $key, $filter ) = @_;
my $mask  = make_bitmask( $key );
my $found = ( ( $filter & $mask ) eq $mask );
return $found;
}

注意這些是位邏輯運算符OR(|)和AND(&),而並非通用的邏輯OR(||)和AND(&&)運算符。將這兩者混在 一起,會導致數小時的有趣調試。第1個示例將掩碼和位向量進行OR運算,打開任何未設置的位。第2個示例將掩碼和filter裏相應的位置進行比較--假 如掩碼裏所有的打開位也在filter裏打開,我們知道已找到一個匹配。

一旦你克服了使用vec,pack和位邏輯運算符的難度,bloom filters實際非常簡單。http://www.perl.com /2004/04/08/examples/Filter.pm 這裏給出了Bloom::Filter模塊的完整信息。



分佈式社會網絡中的bloom filters

當前的社會網絡機制的弊端之一是,它們要求參與者泄露其聯繫列表給中央服務器,或公佈它到公共Internet,這2種情況下都犧牲了大量的用戶 隱私。通過交換bloom filters而不是暴露聯繫列表,用戶能參與社會網絡實踐,而不用通知全世界他們的朋友是誰。編碼了某人聯繫信息的 bloom filter能用來檢查它是否包含了給定的用戶名或email地址,但不能強迫要求它展示用於構建它的完整keys列表。甚至有可能將假命中 率(雖然它聽起來不像好特性),轉換爲有用工具。

假如我非常關注這些人,他們通過對bloom filter運行字典攻擊,來試圖對社會網絡進行反工程。我可以構建filter,它具備較高的假 命中率(例如50%),然後發送filter的多個拷貝給朋友,並變換用於構建每個filter的hash函數。我的朋友收集到的filters越多,他 們見到的假命中率越低。例如,在5個filters情況下,假命中率是0.5的5次方,或3%--通過發送更多filters,還能進一步減少假命中率。

假如這些filters中的任何一個被中途截取,它會展示全部50%的假命中率。所以我能隔離隱私風險,並且一定程度上能控制其他人能多清楚的

瞭解我的網絡。我的朋友能較高程度的確認是否某個人位於聯繫列表裏,但那些僅截取了1個或2個filters的人,幾乎不會獲取到什麼。如下是個 perl函數,它對1組嘈雜的filters檢查某個key:

use Bloom::Filter;
        
sub check_noisy_filters {
my ( $key, @filters ) = @_;
foreach my $filter ( @filters ) {
return 0 unless $filter->check( $key );
}
return 1;
}

假如你和你的朋友同意使用相同的filter長度和hash函數設置,你也能使用位掩碼對比來估計在你們的社會網絡之間的交迭程度。在2個 bloom filters裏的共享位數量會給出1個可用的距離度量。

sub shared_on_bits {
my ( $filter_1, $filter_2 ) = @_;
return unpack( "%32b*",  $filter_1 & $filter_2 )
}

另外,你能使用OR運算,結合2個有相同長度和hash函數的bloom filters來創建1個複合filter。例如,假如你參與某個小型 郵件列表,並希望基於組裏每個人的地址本來創建白名單,你可以爲每個參與者獨立的創建1個bloom filter,然後將filters一起進行OR運 算,將結果輸入Voltron-like主列表。組裏成員不會瞭解到其他成員的聯繫信息,並且filter仍能展示正確的行爲。

 

參考:http://www.perl.com/pub/a/2004/04/08/bloom_filters.html?page=1

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