zoukankan      html  css  js  c++  java
  • 大数据处理中基于概率的数据结构

    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的应用

  • 相关阅读:
    什么是二进制补码
    第四章 VB程序的控制结构
    第三章 VB的程序设计基础
    C#學習基礎方法
    C#學習基礎繼承
    C#學習基礎變量和常量
    C#學習基礎域和屬性
    第八章 VB中ActiveX控件的使用
    写给WEB2.0的站长 不仅仅是泼冷水(转)
    常见错误和难点分析
  • 原文地址:https://www.cnblogs.com/fxjwind/p/3289221.html
Copyright © 2011-2022 走看看