HyperLogLog算法

项目在统计UV/PV时用到了Druid的Hyper hyperunique算法,书上介绍这种算法求出的UV/PV存在一定误差,因此需要了解下误差来自哪里。

实现去重功能,最简单的就是使用set记录集合本身,缺点与前面Bloom Filter差不多,显而易见,需要大量内存空间。HyperLogLog为解决这个问题而生。

另外redis也实现了HyperLogLog的结构,所以可以从redis源码上分析下其实现。

1。基数计数

基数是指一个集合中不同元素的个数。假设有一组数据{1, 2, 3, 3, 4, 5},除去重复的数字之后,该组数据中不同的数有5个,则该组数据的基数为5。

那什么是基数统计呢?基数统计是指在误差允许的情况下估算出一组数据的基数。

2。伯努利过程

投掷一次硬币出现正、反两面的概率均为1/2。如果我们不断的投掷硬币,直到出现一次正面,在这样的一个过程中,投掷一次得到正面的概率为1/2,投掷两次才得到正面的概率为1/2^2….依次类推,投掷k次才得到一次正面的概率为1/2^k。这个过程在统计学上称为伯努利问题。

思考下面两个问题:

  • 进行n次伯努利过程,所有投掷次数都小于k的概率

  • 进行n次伯努利过程,所有投掷次数都大于k的概率

针对第一个问题,在一次伯努利过程中,投掷次数大于k的概率为1/2^k,也就是投了k次反面的概率。因此,在一次过程中投掷次数不大于k的概率为1-1/2^k。因此n次伯努利过程所有投掷次数都不大于k的概率为 

很显然,第二个问题,n次伯努利过程,所有投掷次数都不小于k的概率为

从上述公式中可得出结论:当n远小于2^k时,P(x>=k)几乎为0,即所有投掷次数都小于k;当n远大于2^k时,P(x <= k)几乎为0,即所有投掷次数都大于k。因此,当x=k的情况下,我们可以把2^k当成n的一个粗糙估计。

3。基数统计和HyperLogLog算法

将上述伯努利过程转换到比特位串上,假设我们有8位比特位串,每一位上出现0或者1的概率均为1/2,投掷k次才得到一次正面的过程可以理解为第k位上出现第一个1的过程。

那么针对一个数据集来说,我们用某种变换将其转换成一个比特子串,就可以根据上述理论来估算出该数据集的技术。例如数据集转换成00001111,第一次出现1的位置为4,那么该数据集的基数为16。

于是现在的问题就是如何将数据集转换成一个比特位串?很明显,哈希变换可以帮助我们解决这个问题。

选取一个哈希函数,该函数满足一下条件:

  • 具有很好的均匀性,无论原始数据集分布如何,其哈希值几乎服从均匀分布。这就保证了伯努利过程中的概率均为1/2

  • 碰撞几乎忽略不计,也就是说,对于不同的原始值,其哈希结果相同的概率几乎为0

  • 哈希得出的结果比特位数是固定的。

有了以上这些条件,就可以保证“伯努利过程”的随机性和均匀分布了。

接下来,对于某个数据集,其基数为n,将其中的每一个元素都进行上述的哈希变换,这样就得到了一组固定长度的比特位串,设f(i)为第i个元素比特位上第一次出现”1“的位置,简单的取其最大值f_max为f(i)的最大值,这样,我们就可以得出以下结论:

  • 当n远小于2^f_max时,f_max为当前值的概率为0

  • 当n远大于2^f_max时,f_max为当前值的概率为0

这样一来,我们就可以将f_max作为n的一个粗糙估计。当然,在实际应用中,由于数据存在偶然性,会导致估计量误差较大,这时候需要采用分组估计来消除误差,并且进行偏差修正。

HyperLogLog算法原理通俗版:Here I’ll cover only the basic idea using a very clever example found at [3]. Imagine you tell me you spent your day flipping a coin, counting how many times you encountered a non interrupted run of heads. If you tell me that the maximum run was of 3 heads, I can imagine that you did not really flipped the coin a lot of times. If instead your longest run was 13, you probably spent a lot of time flipping the coin.

所谓分组估计就是,每一个数据进行hash之后存放在不同的桶中,然后计算每一个桶的f_max,最后对这些值求一个平均f_avg,即可得到基数的粗糙估计2^f_avg。

误差消减原理通俗版:However if you get lucky and the first time you get 10 heads, an event that is unlikely but possible, and then stop flipping your coin, I’ll provide you a very wrong approximation of the time you spent flipping the coin. So I may ask you to repeat the experiment, but this time using 10 coins, and 10 different piece of papers, one per coin, where you record the longest run of heads. This time since I can observe more data, my estimation will be better.

实际实现中,分组估计也不能很好的解决误差问题,还需要额外的偏差修正工作。

http://algo.inria.fr/flajolet/Publications/DuFl03-LNCS.pdf

4。Redis实现

  1. 对于输入,计算64位hash

  2. hash最右14bit来进行分桶,桶的个数是2 ^14=16384个

  3. 统计hash左侧50bit,第一次1出现的位置(用bit存储需要6bit:2^6 = 64 > 50)

  4. 与对应桶的6bit值比较,若大于原来值,则更新之

redis HyperLogLog内存占用:6(存储第一次出现位置) * 16384(桶个数) / 8 (1byte = 8bit)/ 1024(bytes) = 12K

因此redis使用12K固定大小内存即可完成对一个Key的Unique统计,空间效率极高。

原文地址:https://www.cnblogs.com/gm-201705/p/10765489.html