海量数据处理利器之Hash——在线邮件地址过滤
标题用了了海量数据(Massive datasets)而不用大数据(Big data)。感觉大数据还是略微有点虚,来点实际的。
一、需求
现在我们需要设计一个在线过滤垃圾邮件地址的方案,我们的数据库里面已经有10亿个合法的邮件地址(称为合法地址集S),当有新的邮件发过来时,要检查这个邮件地址是不是在我们的数据库里面,如果在,我们接收邮件,如果不在,我们就把它当做垃圾邮件过滤掉。
二、直觉想到的方法
一拿到这个问题,我就想到了用log(n)的折半查找,先将10亿个邮件地址排序,当收到一个邮件地址时,我利用折半查找,看邮件地址是否在S中,log(1,000,000,000) = 29.89约等于30,对每一个邮件地址我最多也只需要查找30次,感觉也挺快的,应该能满足要求。仔细想一下,折半查找必须放入内存,10个邮件地址还差不多,10亿个邮件地址我们来算一算有多大,邮件地址平均长度按20个字符计算,一个字符占用1Byte,一个email就占了20B,1,000,000,000X20B = 20GB,内存顶得住么,当然可以分多段进行折半,当分段需要多次I/O操作,需要的时间已经不是在线过滤所能承受的了。
三、利用hash处理问题
当数据量很大时,我们一些快速的方法已经不是多给点时间就能解决的,是压根解决不了,折半查找的方法在可接受的范围内是不可行的。我们来介绍一种神奇的方法,利用hash和位图实现常量时间确定邮件地址是否在S中。
1、过滤器初步设计
我们申请一个1GB的内存(虽然大了点,但现在的PC都顶得住了),1B共8位,1GB共有80亿位(实际应该为8X2^30=8,589,934,592位,但为了方便叙述,我们这里用80亿位)这个位图用B表示,B[i]表示位图的第i位;设计一个hash函数,将邮件地址映射到1-80亿的整数空间上。先将80亿位全部置为0,然后对S中的每一个邮件地址进行hash,hash得到一个整数k,就将第k位 置为1,即B[k]=1,如果hash函数设计得好,hash完S后,80亿的位图应该有10亿个(实际值比10亿小,后面会详细分析)位的值为1,当收到邮件时,对邮件地址进行hash,记hash得到的结果为p,如果B[p]=0,则邮件地址一定不再S中,即当作垃圾邮件过滤;如果B[p]=1,则邮件通过过滤,接收邮件。可以结合下图理解:

注意当B[p]=1时,我们并不是说新邮件地址一定在S中,而是说很可能在S中。B[p]=1只能说明S中一定有一个邮件地址URL,使得hash(URL)=p,而这不能保证其他垃圾邮件地址的hash值不等于p。B[p]=0说明S中不存在邮件地址URL,使得hash(URL)=p,从而新邮件地址一定不在S中。因此,被当作垃圾邮件过滤的邮件一定不包含合法的地址,而通过过滤的地址仍然有可能是垃圾邮件地址,大约有1/8(1/8=10亿/80亿,不过实际值比1/8小,后面会详细分析)的垃圾邮件通过过滤,1/8也称为伪阳率。
这样的方案直接上线还是有问题的,因为伪阳率比较高,我们来计算一下照这种方案我们接收的邮件中垃圾邮件的比率是多少。根据新闻报道全球80%的邮件是垃圾邮件,于是P(接收到的垃圾邮件/接收的邮件)=(1/8*80%)/(20%+1/8*80%)=1/3(33.33%),也就是说平均收三封邮件就有一封是垃圾邮件,这对用户来说是不能忍受的,方案必须优化,不过我们应该看到,我们在不漏掉任何一个合法邮件的前提下过滤了7/8的垃圾邮件,已经初显hash利器之锋芒了。
2、伪阳率分析
前面我们提到过伪阳率并不是1/8,它比1/8略小,现在我们从概率的角度来计算一下伪阳率的真实值。
先来看另外一个简单的例子,假设我们有m个飞镖、n个靶,一个射击高手(所谓高手就是无论怎么射击都不会脱靶的神人)把这m个飞镖一个接一个的射向这n个靶,假设一个飞镖击中每个耙的概率相等,问高手射击完之后,某一个靶上面一个飞镖也没有的概率是多少?这个计算并不难,对于任意的一个耙W,被某一个飞镖击中的概率P(耙W被某飞镖击中)=1/n,那么不被某一飞镖击中的概率P(耙W不被某飞镖击中)=1-1/n,射击m个飞镖可以看做是m次独立重复事件,于是P(耙W不被任何一个飞镖击中) = (1-1/n)^m,这也就是某一个耙上面一个飞镖也没有的概率。现在我们再问,某一个飞镖至少有一个飞镖的概率?有了上一步的分析,可以知道p(耙W上至少有一个飞镖)=1-P(耙W不被任何一个飞镖击中)=1- (1-1/n)^m。
现在回到我们之前的问题,我们的80亿个位相当于上例中的n个耙,10亿个合法邮件地址相当于m个飞镖,hash函数相当于射击高手,集合S经过hash后,某一位值为1(即至少被击中一次)的概率P=1-((1-1/8,000,000,000)^1,000,000,000),直接用计算器计算这个值是不现实的,因为1除以80亿已经下溢出为0,再进行10亿次累乘已经没有意义。这个值的计算需要用到极限的知识,伟大的数学家已经帮我们算好了,我们只需要套一下公式,还记得下面这个公式吗:


可以看到0.1175与我们初步估计的1/8=0.125相差并不大,所以我们之前用1/8分析是合理的。
p(某一位值为1)也就是伪阳率,即垃圾邮件被接收的概率,这里再解释一下为什么p(某一位值为1)就是垃圾邮件被接收的概率:我们收到一个新邮件,将地址hash为p,B[p]为1的概率即为我们接受邮件的概率,显然P(B[p]=1)=p(某一位值为1)。
3、优化过滤器,降低伪阳率
前面我们计算过了,按上面的方案,用户平均每接受3封邮件就有1封是垃圾邮件。我们对过滤器进行改进,设置k个hash函数h1,h2,...,hk,每一个hash函数的映射空间都是1-80亿的整数集。对S中的每一个邮件地址都在k个hash函数进行计算,记hash的结果为p1,p2,...,pk,则将对于位 置1,即B[pi]=1(i=1,2,..,k), 当新邮件发来时,我们对新邮件地址也在k个hash函数上计算,同样记hash的结果为p1,p2,...,pk,如果hash结果的所有位都为1,则接受邮件,否则新地址一定不再S中,当作垃圾邮件过滤。当k=2时的示意图如下:

跟1个hash函数一样,设置k个hash函数也不会漏掉任何一个合法邮件,而接受的邮件里仍然还是会有垃圾邮件,现在我们来计算此时的伪阳率,根据前面的分析,对单个hash函数的情况下,一个位为1的概率为P=1-e^(-m/n)。
现在有k个hash函数,相当于我们现在有k*m个飞镖,于是此时某一位为1的概率为P=1-e^(-k*m/n),但此时伪阳率不再等于p(某一位值为1),只有在k个hash结果所在位都为1的情况下我们才接收邮件,而P(k个hash结果所在位都为1)=(1-e^(-k*m/n))^k,因此伪阳率为(1-e^(-k*m/n))^k,当k=2时,伪阳率P=(1-e^(-1/4))^2=0.048929094,这个值比一个hash函数下的0.1175小了很多,那是不是伪阳率也越低呢,我们来看一下曲线图:

发现伪阳率随着k的增大先减小后增大,最后趋于1,最优的k值在5,6之间,但k取整数,因此k=6时伪阳率最小,即设置6个hash函数,可以得到最小的伪阳率,此时伪阳率的值为0.0216。一般情况下,最优值k=ln(2)*n/m,记住这个答案就可以了,如果感兴趣,可以参看下面的计算过程:

现在我们再来计算一下最优情况下k=6时,接收到的垃圾邮件在接收的邮件中的比例 P(接收到的垃圾邮件/接收的邮件)=80%*0.0216/(20%+80%*0.0216)=0.077490775=1/13,即平均每接收13封邮件有一封是垃圾邮件,我感觉这跟我的邮箱情况差不多,达到了用户可以承受的范围。
如果我们申请2G的内存,那么我们有160亿位,k=ln(2)*n/m=ln(2)*16=11.09,即k=10,此时伪阳率p=0.0004587, P(接收到的垃圾邮件/接收的邮件)=80%*0.0004587/(20%+80%*0.0004587)=0.00183144=1/546,也就是说平均接收546封邮件才收到一封垃圾邮件,这已经完全符合业务要求了,要知道这个方案处理速度很快,只需要计算几个hash函数,然后查位图,可在线计算(当然必须提前hash映射S)。
这个多个hash函数的顾虑器称为布隆过滤器(Bloom Filter)。
当有新的合法邮件添加时,将新合法邮件地址添加到S中,并进行k次hash,并将对应位置为1,这样新的合法邮件就不会被过滤,当新合法邮件添加到一定数量时,需要重新计算k,重新hash一遍S。
事实上,更多的垃圾邮件过滤是基于邮件内容的,可以在我们的过滤器过滤之后,进行自然语言处理内容分析过滤,我们的过滤器以及过滤了绝大部分垃圾邮件了。
我们见证了hash了处理海量数据的威力,这只是一个例子,相信大家可以在其他地方也能用的。
最后,我想说,终于发现以前的数学分析、高等代数、概率论有用了,不是吗?
参考资料:
[1].Anand Rajaraman, Jeffrey davaid UIIman. Mining of Massive Datasets. Cambridge University Presss 2012.
[2].维基百科.e (数学常数).https://zh.wikipedia.org/wiki/E_(%E6%95%B0%E5%AD%A6%E5%B8%B8%E6%95%B0). 2013.5.27
[3].Jure Leskovec.Stanford CS246 Mining Massive Data Sets.http://www.stanford.edu/class/cs246/.2013(很好的一门课,推荐)
[4].维基百科.Email spam.https://en.wikipedia.org/wiki/Email_spam.2013.6.21
ps:画图工具:word2010
数据处理:excel2010
感谢条子同学提供的数学证明指导!
感谢关注,欢迎评论。
海量数据处理利器之Hash——在线邮件地址过滤的更多相关文章
- 海量数据处理算法—Bloom Filter
		
海量数据处理算法—Bloom Filter 1. Bloom-Filter算法简介 Bloom-Filter,即布隆过滤器,1970年由Bloom中提出.它可以用于检索一个元素是否在一个集合中. Bl ...
 - 【转】海量数据处理算法-Bloom Filter
		
1. Bloom-Filter算法简介 Bloom Filter(BF)是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合.它是一个判断元素是否存在于 ...
 - (面试)Hash表算法十道海量数据处理面试题
		
Hash表算法处理海量数据处理面试题 主要针对遇到的海量数据处理问题进行分析,参考互联网上的面试题及相关处理方法,归纳为三种问题 (1)数据量大,内存小情况处理方式(分而治之+Hash映射) (2)判 ...
 - july教你如何迅速秒杀掉:99%的海量数据处理面试题
		
作者:July出处:结构之法算法之道blog 以下是原博客链接网址 http://blog.csdn.net/v_july_v/article/details/7382693 微软面试100题系列 h ...
 - 海量数据处理面试题学习zz
		
来吧骚年,看看海量数据处理方面的面试题吧. 原文:(Link, 其实引自这里 Link, 而这个又是 Link 的总结) 另外还有一个系列,挺好的:http://blog.csdn.net/v_jul ...
 - 海量数据处理的 Top K 相关问题
		
Top-k的最小堆解决方法 问题描述:有N(N>>10000)个整数,求出其中的前K个最大的数.(称作Top k或者Top 10) 问题分析:由于(1)输入的大量数据:(2)只要前K个,对 ...
 - 海量数据处理之Tire树(字典树)
		
参考博文:http://blog.csdn.net/v_july_v/article/details/6897097 第一部分.Trie树 1.1.什么是Trie树 Trie树,即字典树,又称单词查找 ...
 - php调试利器之phpdbg
		
信海龙的博客 php调试利器之phpdbg 简介 PHPDBG是一个PHP的SAPI模块,可以在不用修改代码和不影响性能的情况下控制PHP的运行环境. PHPDBG的目标是成为一个轻量级.强大.易用的 ...
 - 海量数据处理面试题(2) 将用户的query按出现频度排序
		
问题描述: 有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复.要求你按照query的频度排序. 分析:一般海量数据采用分治法时,都要用到哈希,将相 ...
 
随机推荐
- Oracle数据库十大常见性能问题
			
错误的连接管理 oracle的连接是耗时耗力的操作,不应像sqlserver那样使用连接 错误的使用游标和共享池 一般是没有使用绑定变量 不好的SQL语句 使用大量资源的SQL语句都应该好好检查是否可 ...
 - jQuery 常用的代码片段
			
1. 官网下载链接: http://jquery.com/download/ 2. 引入jquery 文件方式: (1) <script src="http://libs.baid ...
 - 烂泥:centos安装及配置DNS服务器
			
本文由秀依林枫提供友情赞助,首发于烂泥行天下. 要在centos配置DNS服务器,要先安装DNS软件BIND.当然我们也可以安装其他的DNS软件,比如国内的开源DNS软件DNSPod. 在此我们以通过 ...
 - C++中对象初始化
			
在C++中对象要在使用前初始化,永远在使用对象之前先将它初始化. 1.对于无任何成员的内置类型,必须手工完成此事. 例如: int x=0; double d; std::cin>>d; ...
 - 初学嵌入式STM32基础下选哪款开发板适合学习
			
iTOP-4412开发板 目前为止,在用户网盘上已经积累了多达100G以上资料, 这些资料都是和4412相关的,并不是随便拼凑起来的!同时我们也完全开放原厂资料. 鉴于用户对于海量资料无从下手的问题, ...
 - docker containerd shim分析
			
// containerd-shim is a small shim that sits in front of a runtime implementation that allows it to ...
 - selenium如何识别验证码
			
一:前面的文章写了如何右键另存为图片,把验证码存为图片后,接下来就是要做,怎么把图片上的内容获取到,借住tesseract工具 1.下载tesseract:http://sourceforge.net ...
 - 欧拉函数 cojs 2181. 打表
			
cojs 2181. 打表 ★☆ 输入文件:sendtable.in 输出文件:sendtable.out 简单对比时间限制:1 s 内存限制:256 MB [题目描述] 有一道比赛题 ...
 - [ubuntu]中文用户目录路径改英文
			
ubuntu中文版home文件夹里用目录的"桌面"."图片"."视频"虽然对于中文使用者看起来舒适直观,但是对于bash使用来说简直是障碍. ...
 - poj1274  The Perfect Stall (二分最大匹配)
			
Description Farmer John completed his new barn just last week, complete with all the latest milking ...