Probabilistic Data Structures for Web Analytics and Data Mininghtml
对于big data常常须要作以下的查询和统计,web
Cardinality Estimation (基数或势), 集合中不一样元素的个数, 好比, 独立访客(Unique Visitor,简称UV)统计算法
Frequency Estimation, 估计某个element重复出现次数, 好比, 某个用户对网站访问次数数据结构
Heavy Hitters, top-k elements, 好比, 销量top-100的商铺dom
Range Query, 好比找出年龄在20~30之间的用户数据结构和算法
Membership Query, 是否包含某个element, 好比, 该用户名是否已经被注册.wordpress
固然你能够采用精确的数据结构, sorted table或hash table, 结果是须要耗费的空间比较大, 如图中对于40M数据, 须要4~7M的数据.
可是其实在不少状况下, 咱们不须要很精确的结果, 能够容忍较小的偏差, 那么在这种状况下, 咱们就可使用些基于几率的数据结构来大大提升时空效率.函数
解读Cardinality Estimation算法(第一部分:基本概念)测试
Big Data Counting: How to count a billion distinct objects using only 1.5KB of Memory优化
Linear Counting, 比较简单的一种方法, 相似于Bitmap, 至少在实现上看没有什么不一样, 最终经过数有多少'1'来判断个数
区别在于, Bitmap是精确的方法直接用'1'的个数来表示Cardinality, 因此必需要分配足够的空间以免冲突, 好比Cardinality上限为10000的集合, 就须要分配10000bit的bitmap
而Linear Counting, 是几率近似的方法, 容许冲突, 只要选取合适的m(bitset的大小), 就能够根据'1'的个数来推断出近似的Cardinality.
class LinearCounter { BitSet mask = new BitSet(m) // m is a design parameter void add(value) { int position = hash(value) // map the value to the range 0..m mask.set(position) // sets a bit in the mask to 1 } }
因此接下来的问题就是,
如何根据'1'的个数来推断出近似的Cardinality?
如何选取合适的m? m太大浪费空间, m过小会致使全部bit都被置1从而没法估计, 因此必须根据Cardinality上限n计算出合适的m
参考下面的公式, 第一个公式就是根据m和w(1的个数)来计算近似的Cardinality
优势, 简单, 便于多集合合并(多个bitset直接or便可)
缺点, 空间效率不够理想, m大约为n的十分之一, 空间复杂度仍为O(Nmax)
Case Study, 收到各个网站的用户访问log, 须要支持基于时间范围和网站范围的UV查询
对于每一个网站的每一个时间单元(好比小时)创建Linear Counting, 而后根据输入的时间和网站范围进行or合并, 最终计算出近似值
这个数据结构和算法比较复杂, 但基于的原理仍是能够说的清楚的
首先, 须要将集合里面全部的element进行hash, 这里的hash函数必需要保证服从均匀分布(即便集合里面的element不是均匀的), 这个前提假设是Loglog Counting的基础
在均匀分布的假设下, 产生的hash value就有以下图中的分布比例, 由于每一个bit为0或1的几率都是1/2, 因此开头连续出现的0的个数越多, 出现几率越小, 须要尝试伯努利过程的次数就越多
Loglog Counting就是根据这个原理, 根据出现的最大的rank数, 来estimate伯努利过程的次数(即Cardinality)
假设设ρ(a)为a的比特串中第一个"1”出现的位置, 即前面出现连续ρ(a)-1个0, 其实这是个伯努利过程
集合中有n个elements, 而每一个element的ρ(a)都小于k的几率为, 当n足够大(>>2^k)的时候接近0
反之, 至少有一个element大于k的几率为, 当n足够小(<<2^k)的时候接近0
因此当在集合中出现ρ(a) = k时, 说明n不可能远大于或远小于2^k(从几率上讲)
故当取得一个集合中的Max(ρ(a))时, 能够将2^Max(ρ(a))做为Cardinality的近似值
但这样的方案的问题是, 偶然性因素影响比较大, 由于小几率事件并非说不会发生, 从而带来较大的偏差
因此这里采用分桶平均的方式来平均偏差,
将哈希空间平均分红m份,每份称之为一个桶(bucket)。对于每个元素,其哈希值的前k比特做为桶编号,其中2^k=m,然后L-k个比特做为真正用于基数估计的比特串。桶编号相同的元素被分配到同一个桶,在进行基数估计时,首先计算每一个桶内元素最大的第一个“1”的位置,设为M[i],而后对这m个值取平均后再进行估计,
class LogLogCounter { int H // H is a design parameter, hash value的bit长度 int m = 2^k // k is a design parameter, 划分的bucket数 etype[] estimators = new etype[m] // etype is a design parameter, 预估值的类型(ex,byte), 不一样rank函数的实现能够返回不一样的类型 void add(value) { hashedValue = hash(value) //产生H bits的hash value bucket = getBits(hashedValue, 0, k) //将前k bits做为桶号 estimators[bucket] = max( //对每一个bucket只保留最大的预估值 estimators[bucket], rank( getBits(hashedValue, k, H) ) //用k到H bits来预估Cardinality ) } getBits(value, int start, int end) //取出从start到end的bits段 rank(value) //取出ρ(value) }
优势, 空间效率显著优化, 能够支持多集合合并(对每一个bucket的预估值取max)
缺点, n不是特别大时, 计偏差过大, HyperLogLog Counting和Adaptive Counting就是这类改进算法
估计某个element的出现次数
正常的作法就是使用sorted table或者hash table, 问题固然就是空间效率
因此咱们须要在牺牲必定的准确性的状况下, 优化空间效率
这个方法比较简单, 原理就是, 使用二维的hash table, w是hash table的取值空间, d是hash函数的个数
对某个element, 分别使用d个hash函数计算相应的hash值, 并在对应的bucket上递增1, 每一个bucket的值称为sketch, 如图
而后在查询某个element的frequency时, 只须要取出全部d个sketch, 而后取最小的那个做为预估值, 如其名
由于为了节省空间, w*d是远小于真正的element个数的, 因此必然会出现不少的冲突, 而最小的那个应该是冲突最少的, 最精确的那个
这个方法的思路和bloom filter比较相似, 都是经过多个hash来下降冲突带来的影响
class CountMinSketch { long estimators[][] = new long[d][w] // d and w are design parameters long a[] = new long[d] long b[] = new long[d] long p // hashing parameter, a prime number. For example 2^31-1 void initializeHashes() { //初始化hash函数family,不一样的hash函数中a,b参数不一样 for(i = 0; i < d; i++) { a[i] = random(p) // random in range 1..p b[i] = random(p) } } void add(value) { for(i = 0; i < d; i++) estimators[i][ hash(value, i) ]++ //简单的对每一个bucket经行叠加 } long estimateFrequency(value) { long minimum = MAX_VALUE for(i = 0; i < d; i++) minimum = min( //取出最小的估计值 minimum, estimators[i][ hash(value, i) ] ) return minimum } hash(value, i) { return ((a[i] * value + b[i]) mod p) mod w //hash函数,a,b参数会变化 } }
优势, 简单, 空间效率显著优化
缺点, 对于大量重复的element或top的element比较准确, 但对于较少出现的element准确度比较差
实验, 对于Count-Min sketch of size 3×64, i.e. 192 counters total
Dataset1, 10k elements, about 8500 distinct values, 较少重复的数据集, 测试结果准确度不好
Dataset2, 80k elements, about 8500 distinct values, 大量重复的数据集, 测试结果准确度比较高
前面说了Count-Min Sketch只对重度重复的数据集有比较好的效果, 但对于中度或轻度重复的数据集, 效果就不好
由于大量的冲突对较小频率的element的干扰很大, 因此Count-Mean-Min Sketch就是为了解决这个问题
原理也比较简单, 预估sketch上可能产生的noise
怎么预估? 很简单, 好比1000数hash到20个bucket里面, 那么在均匀分布的条件下, 一个bucket会被分配50个数
那么这里就把每一个sketchCounter里面的noise减去
最终是取全部sketch的median(中位数), 而不是min
class CountMeanMinSketch { // initialization and addition procedures as in CountMinSketch // n is total number of added elements long estimateFrequency(value) { long e[] = new long[d] for(i = 0; i < d; i++) { sketchCounter = estimators[i][ hash(value, i) ] noiseEstimation = (n - sketchCounter) / (w - 1) e[i] = sketchCounter – noiseEstimator } return median(e) } }
首先top element应该是重度重复的element, 因此使用Count-Min Sketch是没有问题的
方法,
1. 建个Count-Min Sketch不断的给全部的element进行计数
2. 须要取top的时候, 对集合中每一个element从Count-Min Sketch取出近似的frequency, 而后放到heap中
其实这里使用Count-Min Sketch只是计算frequency, Top-n问题仍然是依赖heap来解决
use case, 好比网站IP访问数的排名
另一种获取top的思路,
维护一组固定个数的slots, 好比你要求Top-10, 那么维护10个slots
当elements过来, 若是slots里面有, 就递增, 没有就替换solts中frequency最小的那个
这个算法没有讲清楚, 给的例子也太简单, 不太能理解e(maximum potential error)干嘛用的, 为何4替换3后, 3的frequency做为4的maximum potential error
个人理解是, 由于3的frequency自己就是最小的, 因此4继承3的frequency不会影响实际的排名,
这样避免3,4交替出现所带来的计数问题, 但这里的frequency就不是精确的, 3的frequency被记入4是potential error
The figure below illustrates how Stream-Summary with 3 slots works for the input stream {1,2,2,2,3,1,1,4}.
RangeQuery, 毫无疑问须要相似B-tree这样排序的索引, 对于大部分NoSql都很难支持
这里要实现的是, SELECT count(v) WHERE v >= c1 AND v < c2, 在必定范围内的element的个数和
简单的使用Count-Min Sketch的方法, 就是经过v的索引找出全部在范围内的element, 而后去Count-Min Sketch中取出每一个element的近似frequency, 而后相加
这个方法的问题在于, 在范围内的element可能很是多, 而且那么多的近似值相加, 偏差会被大大的放大
解决办法就是使用多个Count-Min Sketch, 来提供更粗粒度的统计
如图, sketch1就是初始的, 以element为单位的统计, 没一个小格表明一个element
sketch2, 以2个element为单位统计, 实际的作法就是truncate a one bit of a value, 好比1110111, 前缀匹配111011.
sketch3, 以4个element为单位统计......
最终sketchn, 全部element只会分两类统计, 1开头或0开头
这样再算范围内的count, 就不须要一个个element加了, 只须要从粗粒度开始匹配查询
以下图, 只须要将4个红线部分的值相加就能够了
MADlib (a data mining library for PostgreSQL and Greenplum) implements this algorithm to process range queries and calculate percentiles on large data sets.
查询某个element在不在, 典型的Bloom Filter的应用