基数估计算法简介
基数估计算法简介
注1:本文是之前工作时在团队内分享的一个PPT的文字版本.
注2 :我有了新的个人博客地址
下文中的sqrt表示开根号(sqrt(4)=2),m^n表示m的n次方
什么是基数(Cardinality)
基数指的是一个可重复集合中不重复元素的个数。
什么是基数计算
给定一个含有重复元素的有限集合,计算其不重复元素的个数。
应用场景举例:
- 某家店铺今天有多少不同用户访问
- 某家店铺今天接待了多少不同买家
简单来说就是各种UV的计算
常见的实现方式
Hash集合 + 计数 或者 BitMap + 计数
缺陷:占用空间过大
什么是基数估计算法
基数估计是一类概率算法,可以在有一定误差的前提下以远低于精确计算的时间和空间消耗对基数进行估计
特点
- 有误差
- 时间复杂度和空间复杂度仅与误差和基数上限有关
- 易于合并
有哪些基数估计算法
- Linear Counting
- LogLog Counting
- Adaptive Counting
- HyperLogLog Counting
- HyperLogLog++ Counting
它们之间的关系如下图所示:
Linear Counting
Linear Counting是在1990年的一篇论文
A linear-time probabilistic counting algorithm for database applications 中被提出。
作为一个早期的基数估计算法,Linear Counting的空间复杂度方面与简单bitmap方法是一样的(但是有个常数项级别的降低,约1/10),都是O(N)。 因此很少单独使用。目前只在Adaptive Counting中被混合使用
特点:在基数较小时比较准确
具体算法如下:
设有一哈希函数H,其哈希结果空间有m个值,并且哈希结果服从均匀分布。
使用一个长度为m的bitmap,每个bit为一个桶,均初始化为0
设一个集合的基数为n,此集合所有元素通过H哈希到bitmap中,如果某一个元素被哈希到第k个比特并且第k个比特为0,则将其置为1。
当集合所有元素哈希完成后,设bitmap中还有u个bit为0
估计值 n = -mLn(u/m)
误差 = (e^t-t-1)/2n 其中 t = n/m 也就是基数与哈希空间的比值
标准差 = (sqrt(m)*(et-t-1)0.5)/n
误差控制,以标准差作为误差,记容许误差为E,则 m > (e^t -t -1)/(E*t)^2
例子:
n=11(实际数目) m=8(哈希空间) u=2(0的个数)
估计值=-8*ln(0.25)=11.04
满桶控制:
如果m比n小太多,则很有可能所有桶都被哈希到了,此时u的值为0,估计公式就不起作用了(变成无穷大)。
因此m的选择除了要满足上面误差控制的需求外,还要保证满桶的概率非常小。
m的选取应该遵从:
m > beta(e^t-t-1) 其中 beta=max(5,1/((E*t)^2))
LogLog Counting
LogLog Counting 出自论文Loglog Counting of Large Cardinalities
Loglog Counting的空间复杂度仅有O(log2(log2(Nmax))),所以叫LogLog Counting
算法前提
均匀随机化:与LC一样,在使用LLC之前需要选取一个哈希函数H应用于所有元素,然后对哈希值进行基数估计。H必须满足如下条件
-
H的结果具有很好的均匀性,无论原始集合元素的值分布如何,其哈希结果的值几乎服从均匀分布
-
H的碰撞几乎可以忽略不计。认为对于不同的原始值,其哈希结果相同的概率非常小以至于可以忽略不计。
-
H的哈希结果是固定长度的
算法基本思路
设a为待估集合(哈希后)中的一个元素,由上面对H的定义可知,a可以看做一个长度固定的比特串(也就是a的二进制表示).
设H哈希后的结果长度为L比特,将这L个比特位从左到右分别编号为1、2、…、L:
又因为a是从服从均与分布的样本空间中随机抽取的一个样本,因此a每个比特位服从如下分布且相互独立。
P(x=k)=0.5
就是a的每个比特位为0和1的概率各为0.5,且相互之间是独立的
设ρ(a)为a的比特串中第一个“1”出现的位置,显然1≤ρ(a)≤L,这里我们忽略比特串全为0的情况(概率为1/2^L)。如果我们遍历集合中所有元素的比特串,取ρmax 为所有ρ(a) 的最大值。
此时我们可以将2^ρmax 作为基数的一个粗糙估计,即
n 约为 2^ρmax
减小误差
- 分桶平均
- 偏差修正
分桶平均
将哈希空间平均分成m份,每份称之为一个桶
对于每一个元素,其哈希值的前k比特作为桶编号, 2^k = m
后L-k个比特作为真正用于基数估计的比特串
桶编号相同的元素被分配到同一个桶,在进行基数估计时,首先计算每个桶内元素最大的第一个“1”的位置,设为M[j],然后对这m个值取平均后再进行估计
误差修正:
误差分析
当m不太小(不小于64)时:
StdError = 1.30/sqrt(m)
误差控制:
在应用LLC时,主要需要考虑的是分桶数m,而这个m主要取决于误差。根据上面的误差分析,如果要将误差控制在E之内,则:
m > (1.30/E)^2
内存使用分析:
假设H的值为32bit,由于ρmax≤32,因此每个桶需要5bit空间存储这个桶的ρmax,m个桶就是5m/8字节。
例如基数上限为一亿(约227),当分桶数m为1024时,每个桶的基数上限约为227/210=217
因为每个桶需要5bit,需要字节数就是5×1024/8=640,误差为1.30/sqrt(1024)=0.040625,也就是约为4%。
LLC标准差是渐近组合计数,也就是说,随着n趋向于无穷大,标准差趋向于1.30/sqrt(m),而不是说n多大时其值都一致为1.30/sqrt(m).
其无偏性也是渐近的,只有当n远远大于m时,其估计值才近似无偏。因此当n不太大时,LLC的效果并不好。
通过统计分析方法,我们可以得到n具体小到什么程度我们就不可忍受了,另外就是当n太小时可不可以用别的估计方法替代LLC来弥补LLC这个缺陷。Adaptive Counting 及HyperLogLog Counting都是基于这个思想实现的。
Adaptive Counting
Adaptive Counting在Fast and accurate traffic matrix measurement using adaptive cardinality counting中被提出。
Adaptive Counting只是简单将LC和LLC组合使用,根据基数量级决定是使用LC还是LLC。具体是通过分析两者的标准差,给出一个阈值,根据阈值选择使用哪种估计。
特点:在基数较大时比较准确
分析一下LC和LLC的存储结构,可以发现两者是兼容的,区别仅仅在于LLC关心每个桶的ρmax,而LC仅关心此桶是否为空。因此只要简单认为ρmax值不为0的桶为非空,0为空就可以使用LLC的数据结构做LC估计了。
联立linear counting和loglog Counting的误差
HyperLogLog Counting
HyperLogLog Counting的基本思想也是在LLC的基础上做改进, 来自论文 HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm
改进:用调和平均数替代几何平均数
调和平均数
LLC是对各个桶取算数平均数,最终被应用到2的指数上,所以LLC取得是几何平均数。由于几何平均数对于离群值(例如这里的0)特别敏感,因此当存在离群值时,LLC的偏差就会很大.
当n较小,可能存在较多空桶,而这些特殊的离群值强烈干扰了几何平均数的稳定性。
估值公式以及误差:
根据论文中的分析结论,与LLC一样HLLC是渐近无偏估计,且其渐近标准差为:
SE=1.04/sqrt(m)
分段偏差修正:
在HLLC的论文中,作者在实现建议部分还给出了在n相对于m较小或较大时的偏差修正方案
HyperLogLog Plus
HyperLogLog plus 是Google的改进版。来自论文 HyperLogLog in Practice: Algorithmic Engineering of a
State of The Art Cardinality Estimation Algorithm
是一个工程改进版,包括使用FNV64代替原始的32位Hash函数,分段偏差修正等工程上的改进,无法严格证明
Java中的相关实现
-
twitter/algebird a scala library from twitter which contains lots of different algorithms including HLL
-
prasanthj/hyperloglog a detached java library for HLL
-
addthis/stream-lib - another java lib(本人之前在项目中使用的这个库,还不错)
-
aggregateknowledge/java-hll a low-level java implementation of HLL
-
Comparison of HLL implementations 各种实现的相关比较