再谈基数估计之HyperLogLog算法

前言

在很久(好像也没多久,4个月)之前,我曾经写了一篇和主业无关的有点意思的小文章《基数估计探秘:Linear Counting与Flajolet-Martin算法》。但是这篇文章讲的两个算法都已经老掉牙了,实际应用最广泛的基数估计算法是HyperLogLog(HLL)算法。

最近笔者基于Flink搞了些从超大行为数据集计算UV的工作,用到了Redis的HyperLogLog,感觉是时候写个续篇了。在读本文之前,强烈建议看官先读之前的那篇,就算不深挖细节,也能够获得一些基数估计方面的背景知识。

LogLog Counting(LLC)算法

等等,不是叫HyperLogLog么,怎么这里变LogLog了?很简单,HLL是基于LLC的改进,所以我们有必要了解LLC。该算法由M. Durand、P. Flajolet在2003年的论文《Loglog Counting of Large Cardinalities》中首次提出。下面看看这个算法是怎么操作的。

首先我们得有一个哈希函数H(x),并且满足如下条件:

  • 哈希结果尽量服从均匀分布;
  • 碰撞概率尽量小;
  • 结果是长度固定为L的二进制串。

然后定义符号ρ(y),它代表将数y表示成二进制串后,从左向右读遇到的第一个“1”的位置下标(下标从1开始,忽略全0的情况),也就是前导0的个数+1。

LogLog Counting算法的流程如下:

  1. 对每个待测集合中的元素x,计算它的哈希值y=H(x)。
  2. 将哈希值y通过它们的前k=logm个比特分组(即分桶),作为桶的编号,即一共有m个桶。
  3. 将y的后L - k个比特作为真正参与基数估计的串s,计算并记录下所有桶内的ρ(s)。
  4. 令M[k]表示第k个桶内所有元素中最大的那个ρ值,那么该集合基数的估计量为:

Ñ = αm · m · 2ΣM[i] / m

其中αm是修正参数。有没有觉得这个算法流程有点似曾相识的意思?

实际上,它与前面说过的Flajolet-Martin算法的PCSA变种非常相似,也采用了分桶平均的思想,只不过在精确度方面又做了一些其他工作。因此与它相关的细节(比如为什么ρmax可以作为估计量)可以参考上一篇文章。

那么α是怎么来的呢?这个过程极其复杂,直接说结论吧。根据之前对伯努利实验的分析,可以知道:

Pn{X=k} = (1 - 1/2k)n - (1 - 1/2k-1)n

它是一个无穷递推数列,采用指数生成函数和泊松化的方法处理,得到估计量的泊松期望和方差如下:

进而推出算法流程第4步的估计量。这是一个渐进无偏估计量,其中:

这是LLC比F-M算法更精细的地方之一。

LLC的空间复杂度是多少呢?不难得知,在F-M算法中,我们可以用logN个比特来存储哈希值,而LLC算法只存储下标(即ρ值)就够用了,所以可以降低为log(logN)个比特。再加上分桶数为m,亦即它的空间复杂度是:

O[m · log(logN)]

这也就是LogLog Counting这个名字的由来。

根据论文中给出的数据,LLC的误差在m不算太小(大于64)时,大概是:

StdError ≈ 1.30 / √m

虽然LLC的误差常数比F-M算法的还大(F-M是0.78),但是由于空间复杂度从log级别降低到了loglog级别,所以相同误差下实际的空间开销要更小。

分桶数m完全由可接受的精确度决定。但这个误差的前提是实际基数N要远远大于分桶数m(因为Ñ是渐近无偏的),所以在集合比较小时,LLC算法的效果要打折扣,这点与F-M-PCSA是相同的。

LLC算法本质上不是个新的算法,并且也从未大规模地使用过。它的主要意义有三:

  • 与F-M算法相比,经过了非常完备的实验分析与验证;
  • 在精确度和空间占用之间取得了更好的平衡;
  • 作为后续HyperLogLog算法产生的基础。

HyperLogLog(HLL)算法

HLL由四位大佬P. Flajolet、É. Fusy、O. Gandouet、F. Meunier(全是法语名字)在2007年的论文《HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm》中提出。从论文题目可以得知,这个算法已经相当优化了。实际上它的算法流程与LLC仍然几乎相同,那么它到底“hyper”在哪里呢?

一是分桶平均的时候,采用调和平均数。我们知道,调和平均数的定义是:

n / [1/x1 + 1/x2 + ... + 1/xn] = n / [Σ 1/xi]

在LLC算法中,采用的是ρmax的算术平均数,并且它是作为2的指数,所以本质上是几何平均数。但是几何平均数受离群值(即偏离均值很大的值)的影响非常大,分桶的空桶越多,LLC的估计值就越不准确。调和平均数就不太有这种困扰,所以集合基数的估计量就会变成:

Ñ = αm · m2 / Σ 2-M[i]

修正参数是:

为了实际应用起来方便,一般采用如下的近似值:

二是根据基数估计值的大小,采用不同的估计方法进行修正。论文中给出了在常见情况——即被估集合的基数在亿级别以下,m取值在2[4, 16]区间——下的修正的算法,如下图所示。

翻译成人话:

  • 在估计值Ñ ≤ 5m/2时,使用前面讲过的Linear Counting算法估计,因为它在基数较小时偏差也较小;
  • 5m/2 < Ñ ≤ 232/30时,直接使用HLL的结果;
  • Ñ > 232/30时,结果为-232log(1 - Ñ / 232)。

HLL算法的空间复杂度与LLC相同,而标准差为:

StdError ≈ 1.04 / √m

可见,HLL算法的精度确实比LLC更高。根据论文中的描述,估计亿级别集合的基数,在偏差大约2%的情况下,只需要耗费大约1.5kB内存。

HLL算法在很多框架中都有实现,其中尤以Redis的实现方法最为有名,但它的代码量极大,如果写在文章里的话会特别冗长,所以只是简单说说吧。

Redis使用了214=16384个桶,按照上面的标准差,误差为0.81%,精度相当高。Redis使用一个long型哈希值的前14个比特用来确定桶编号,剩下的50个比特用来做基数估计。而26=64,所以只需要用6个比特表示下标值,在一般情况下,一个HLL数据结构占用内存的大小为16384 * 6 / 8 = 12kB,Redis将这种情况称为密集(dense)存储。

既然有了密集存储,自然就会有稀疏(sparse)存储。当多数桶的值为全0时,为了节省空间,Redis会将连续的全0桶压缩成0桶计数值。该计数值可以用单字节或双字节表示,高2bit为标志位,因此可以分别表示连续64个全0桶和连续16384个全0桶。

Redis在HLL稀疏存储中用ZERO宏表示单字节全0桶,XZERO宏表示双字节全0桶,VAL表示中途的少数非0桶。举个例子,假设只有第10001个桶和第10047个桶的值为1,其余都为0,那么整个存储布局就是:

XZERO (10000) | VAL (1) | ZERO (45) | VAL (1) | XZERO (6337)

只需要5字节就能存储了。当稀疏存储的总大小超过hll_sparse_max_bytes参数指定的大小(默认为3kB)时,就会自动转换成密集存储。Redis还会在HLL数据结构的头部信息中缓存上一次计算出来的基数估计值,这样可以避免不必要的重算,提高效率。

除了插入元素的PFADD指令之外,Redis提供的计数指令PFCOUNT和合并指令PFMERGE都支持将多个HLL合并起来。由于HLL的桶只存储下标值,因此合并时只需要按桶取最大值就可以了。Redis的HLL算法在上述标准算法的基础上做了一点改进,比如预打表计算2-M[i]的值,以及用多项式回归修正结果等。

最后推荐一个国外大佬做的HLL算法的动态Demo,简单直观,有助于理解,下面给出截图以及网页地址。

http://content.research.neustar.biz/blog/hll.html

The End

晚安晚安。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容