大数据处理中基于概率的数据结构

Probabilistic Data Structures for Web Analytics and Data Mining

 

对于big data经常需要做如下的查询和统计,

Cardinality Estimation (基数或势), 集合中不同元素的个数, 比如, 独立访客(Unique Visitor,简称UV)统计

Frequency Estimation, 估计某个element重复出现次数, 比如, 某个用户对网站访问次数

Heavy Hitters, top-k elements, 比如, 销量top-100的商铺

Range Query, 比如找出年龄在20~30之间的用户

Membership Query, 是否包含某个element, 比如, 该用户名是否已经被注册.

当然你可以采用精确的数据结构, sorted table或hash table, 结果是需要耗费的空间比较大, 如图中对于40M数据, 需要4~7M的数据.
但是其实在很多情况下, 我们不需要很精确的结果, 可以容忍较小的误差, 那么在这种情况下, 我们就可以使用些基于概率的数据结构来大大提高时空效率.

image

 

1 Cardinality Estimation

解读Cardinality Estimation算法(第一部分:基本概念)

Big Data Counting: How to count a billion distinct objects using only 1.5KB of Memory

1.1 Cardinality Estimation: Linear Counting

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

image

优点, 简单, 便于多集合合并(多个bitset直接or即可)

缺点, 空间效率不够理想, m大约为n的十分之一, 空间复杂度仍为O(Nmax)

Case Study, 收到各个网站的用户访问log, 需要支持基于时间范围和网站范围的UV查询
对于每个网站的每个时间单元(比如小时)建立Linear Counting, 然后根据输入的时间和网站范围进行or合并, 最终计算出近似值

 

1.2 Cardinality Estimation: Loglog Counting

这个数据结构和算法比较复杂, 但基于的原理还是可以说的清楚的
首先, 需要将集合里面所有的element进行hash, 这里的hash函数必须要保证服从均匀分布(即使集合里面的element不是均匀的), 这个前提假设是Loglog Counting的基础

在均匀分布的假设下, 产生的hash value就有如下图中的分布比例, 因为每个bit为0或1的概率都是1/2, 所以开头连续出现的0的个数越多, 出现概率越小, 需要尝试伯努利过程的次数就越多
Loglog Counting就是根据这个原理, 根据出现的最大的rank数, 来estimate伯努利过程的次数(即Cardinality)

image

参考, 第三部分:LogLog Counting

假设设ρ(a)为a的比特串中第一个"1”出现的位置, 即前面出现连续ρ(a)-1个0, 其实这是个伯努利过程
集合中有n个elements, 而每个element的ρ(a)都小于k的概率为, 当n足够大(>>2^k)的时候接近0
 image

反之, 至少有一个element大于k的概率为, 当n足够小(<<2^k)的时候接近0
image

所以当在集合中出现ρ(a) = k时, 说明n不可能远大于或远小于2^k(从概率上讲)
故当取得一个集合中的Max(ρ(a))时, 可以将2^Max(ρ(a))作为Cardinality的近似值
image

但这样的方案的问题是, 偶然性因素影响比较大, 因为小概率事件并不是说不会发生, 从而带来较大的误差
所以这里采用分桶平均的方式来平均误差,

将哈希空间平均分成m份,每份称之为一个桶(bucket)。对于每一个元素,其哈希值的前k比特作为桶编号,其中2^k=m,而后L-k个比特作为真正用于基数估计的比特串。桶编号相同的元素被分配到同一个桶,在进行基数估计时,首先计算每个桶内元素最大的第一个“1”的位置,设为M[i],然后对这m个值取平均后再进行估计,

image

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就是这类改进算法

 

2 Frequency Estimation

估计某个element的出现次数
正常的做法就是使用sorted table或者hash table, 问题当然就是空间效率
所以我们需要在牺牲一定的准确性的情况下, 优化空间效率

2.1 Frequency Estimation: Count-Min Sketch

这个方法比较简单, 原理就是, 使用二维的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来降低冲突带来的影响

image

 

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, 大量重复的数据集, 测试结果准确度比较高

2.2 Frequency Estimation: Count-Mean-Min Sketch

前面说了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)

    }

}

 

3 Heavy Hitters (Top Elements)

3.1 Heavy Hitters: Count-Min Sketch

首先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访问数的排名

3.2 Heavy Hitters: Stream-Summary

另外一种获取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}.

image

 

4 Range Query

4.1 Range Query: Array of Count-Min Sketches

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个红线部分的值相加就可以了

image

MADlib (a data mining library for PostgreSQL and Greenplum) implements this algorithm to process range queries and calculate percentiles on large data sets.

 

5 Membership Query

查询某个element在不在, 典型的Bloom Filter的应用

你可能感兴趣的:(数据结构)