最近在推荐系统项目中遇到一个问题,就是希望能过滤用户最近看过的item, 否则给用户推荐的如果都是他们看过的,体验会大受影响;于是就去调研了下Bloom Filter过滤器的一些知识,想看看是否适用于推荐场景及如何应用,后者将会在未来的尝试中得到答案。
然而Bloom Filter过滤器本身的学习过程,就已经十分受用;最受启发的一点思想就是:
在容错率上做一个很小的妥协,即不要求100%准确(但仍然十分近似),就能换取以前在资源限制条件下不可能完成的目标最终完美解决
这一近似思想在统计中也经常用到,所以有必要对Bloom Filter过滤器做一个较全面的梳理总结,于是有了下文:)
Bloom Filter介绍
Bloom Filter的中文翻译叫做布隆过滤器,是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
应用场景
1. 黑名单,垃圾邮件过滤
最典型的一个应用就是黑名单功能,对用户名称或者IP或者Email进行过滤,每次检查时用key进行hash后,如果不在黑名单内的,肯定可以通行,如果在的则不允许通过,误判情况增加一个排除名单来进行排除。
误判情况:将正常用户判定为黑名单用户
2. 爬虫重复URL检测
在爬取网站URL时,要检测这条URL是否已经访问过。
误判情况:没有访问过的误判为访问过
3. 字典纠错
检查单词拼写是否正确
误判情况:错误的单词误判为正确。
4. 磁盘文件检测
将磁盘中或者数据库中数据key存入该结构中,检测要访问的数据是否在磁盘或数据库中,然后再发起访问,避免空查询造成磁盘或数据库压力。
误判情况:不存在该数据却误判为有该数据。
上图分别对应了三种情况:
1. filter返回说没有找到key1, 那么key1真的不在storage里,这样就避免了数据库的空查询,减轻了数据库的压力;
2. filter返回说找到key2, 于是放行key2到storage中进行查询,成功查到后返回正确结果;
3. filter返回说找到key3, 于是放行key3到storage中进行查询,但由于Bloom Filter 本身有一定的False Positive Rate(误识率), 实际上在Storage运行查询后没有,返回结果为没有找到
尽管不可避免地会有一些空查询操作(比如3),但相对不做过滤来讲,Bloom Filter 已经抵挡住了99.99...%的空操作,在使用极小内存的情况下极大地减轻了数据库的压力,作用非常明显。
5. CDN(squid)代理缓存技术
先查找本地有无cache,如果没有则到其他兄弟 cache服务器上去查找。为了避免无谓的查询,在每个cache服务器上保存其兄弟服务器的缓存关键字,以bloomfilter方式存储,再去其他cache服务器查找之前,先检查该结构是否有url,如果有存在url,再去对应服务器查找。
误判情况: 对应服务器不存在该URL的缓存。
下面是一个综合应用场景的链接:
http://www.quora.com/What-are-the-best-applications-of-Bloom-filters
几个专业术语
这里有必要介绍一下False Positive和False Negative的概念
False Positive中文可以理解为“假阳性”,形象的一点说就是“误报”,后面将会说道Bloom Filter存在误报的情况,现实生活中也有误报,比如说去体检的时候,医生告诉你XXX检测是阳性,而实际上是阴性,也就是说误报了,是假阳性,杀毒软件误报也是同样的概念。
False Negative,中文可以理解为“假阴性”,形象的一点说是“漏报”。医生告诉你XXX检测为阴性,实际上你是阳性,你是有病的(Sorry, it’s just a joke),那就是漏报了。同样杀毒软件也存在漏报的情况。
Bloom Filter算法
初始状态下,Bloom Filter是一个m位的位数组,且数组被0所填充。同时,我们需要定义k个不同的hash函数,每一个hash函数都随机的将每一个输入元素映射到位数组中的一个位上。那么对于一个确定的输入,我们会得到k个索引。
插入元素:经过k个hash函数的映射,我们会得到k个索引,我们把位数组中这k个位置全部置1(不管其中的位之前是0还是1)
查询元素:输入元素经过k个hash函数的映射会得到k个索引,如果位数组中这k个索引任意一处是0,那么就说明这个元素不在集合之中;如果元素处于集合之中,那么当插入元素的时候这k个位都是1。但如果这k个索引处的位都是1,被查询的元素就一定在集合之中吗?答案是不一定,也就是说出现了False Positive的情况(但Bloom Filter不会出现False Negative的情况)
在上图中,当插入x、y、z这三个元素之后,再来查询w,会发现w不在集合之中,而如果w经过三个hash函数计算得出的结果所得索引处的位全是1,那么Bloom Filter就会告诉你,w在集合之中,实际上这里是误报,w并不在集合之中。
Bloom Filter算法的False Positive Rate
Bloom Filter的误报率到底有多大?下面在数学上进行一番推敲。假设HASH函数输出的索引值落在m位的数组上的每一位上都是等可能的。那么,对于一个给定的HASH函数,在进行某一个运算的时候,一个特定的位没有被设置为1的概率是
那么,对于所有的k个HASH函数,都没有把这个位设置为1的概率是
如果我们已经插入了n个元素,那么对于一个给定的位,这个位仍然是0的概率是
那么,如果插入n个元素之后,这个位是1的概率是
如果对一个特定的元素存在误报,那么这个元素的经过HASH函数所得到的k个索引全部都是1,概率也就是
根据常数e的定义,可以近似的表示为:
关于误报
有时候误报对实际操作并不会带来太大的影响,比如对于HTTP缓存服务器,如果一条URL被误以为存在与缓存服务器之中,那么当取数据的时候自然会无法取到,最终还是要从原始服务器当中获取,之后再把记录插入缓存服务器,几乎没有什么不可以接受的。
对于安全软件,有着“另可错报,不可误报”的说法,如果你把一个正常软件误判为病毒,对使用者来说不会有什么影响(如果用户相信是病毒,那么就是删除这个文件罢了,如果用户执意要执行,那么后果也只能由用户来承担);如果你把一个病毒漏判了,那么对用户造成的后果是不可设想的……更有甚者,误报在某种程度上能让部分用户觉得你很专业……
Bloom Filter实现代码
下面给出一个简单的Bloom Filter的Java实现代码:
import java.util.BitSet;
publicclass BloomFilter
{
/* BitSet初始分配2^24个bit */
privatestaticfinalint DEFAULT_SIZE =1<<25;
/* 不同哈希函数的种子,一般应取质数 */
privatestaticfinalint[] seeds =newint[] { 5, 7, 11, 13, 31, 37, 61 };
private BitSet bits =new BitSet(DEFAULT_SIZE);
/* 哈希函数对象 */
private SimpleHash[] func =new SimpleHash[seeds.length];
public BloomFilter()
{
for (int i =0; i < seeds.length; i++)
{
func[i] =new SimpleHash(DEFAULT_SIZE, seeds[i]);
}
}
// 将字符串标记到bits中
publicvoid add(String value)
{
for (SimpleHash f : func)
{
bits.set(f.hash(value), true);
}
}
//判断字符串是否已经被bits标记
publicboolean contains(String value)
{
if (value ==null)
{
returnfalse;
}
boolean ret =true;
for (SimpleHash f : func)
{
ret = ret && bits.get(f.hash(value));
}
return ret;
}
/* 哈希函数类 */
publicstaticclass SimpleHash
{
privateint cap;
privateint seed;
public SimpleHash(int cap, int seed)
{
this.cap = cap;
this.seed = seed;
}
//hash函数,采用简单的加权和hash
publicint hash(String value)
{
int result =0;
int len = value.length();
for (int i =0; i < len; i++)
{
result = seed * result + value.charAt(i);
}
return (cap -1) & result;
}
}
}
Bloom Filter参数选择
1. 哈希函数选择
哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。
2. Bit数组大小选择
哈希函数个数k、位数组大小m、加入的字符串数量n的关系可以参考[5]。该文献证明了对于给定的m、n,当 k = ln(2)* m/n 时出错的概率是最小的。
同时该文献还给出特定的k,m,n的出错概率。例如:根据参考文献[5],哈希函数个数k取10,位数组大小m设为字符串个数n的20倍时,false positive发生的概率是0.0000889 ,这个概率基本能满足网络爬虫的需求了。
参考:
[1] http://www.programlife.net/bloom-filter.html 综述
[2] http://blog.csdn.net/lovingprince/article/details/6632328 应用
[3] http://www.cnblogs.com/heaad/archive/2011/01/02/1924195.html
代码实现
[4] Pei Cao. Bloom Filters - the math.
http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html 参数最优化