当需要对大量数据做去重计数, 例如统计一个页面的UV(Unique Visitor, 独立访客), 或者用户搜索的关键词数量, 比较容易想到的方案有
但都存在一些问题, 随着数据量增加, 存储空间占用越来越大; 统计速度慢, 性能并不理想
数据分析, 网络监控及数据库优化等领域都会涉及到基数计数的需求.
基数, 一个集合中不重复元素的个数.
目前还没有在大数据场景中准确计算基数的高效算法, 因此在允许一定误差的情况下, 使用概率算法是一个不错的选择
概率算法不直接存储数据本身, 而通过一定的概率统计方法预估基数值, 这种方法可以大大节省内存, 同时保证误差控制在一定范围内
目前用于基数计数的概率算法有
极大似然估计是建立在极大似然原理的基础上的一个统计方法, 是概率论在统计学中的应用
极大似然估计提供了一种给定观察数据来评估模型参数的方法, 即: 模型已定, 参数未知
通过若干次试验, 观察其结果, 利用试验结果得到某个参数值能够使样本出现的概率为最大, 则称为极大似然估计
目的就是: 利用已知的样本结果, 反推最有可能/最大概率导致这样结果的参数值
例如有两个箱子, 甲箱有99白球1个黑球, 乙箱有99黑球1白球, 现在取出了一个黑球
问黑球是从哪个箱子取出的
你的第一印象就是黑球最像是从乙箱取出的, 这个推断符合人们的经验事实, 最像就是最大似然之意, 这种想法常称为极大似然原理
伯努利试验(Bernoulli experiment), 是在同样的条件下重复地, 相互独立地进行的一种随机试验
其特点是该随机试验只有两种可能结果: 发生或者不发生
我们假设该项试验独立重复地进行了n次, 那么就称这一系列重复独立的随机试验为n重伯努利试验
单个伯努利试验是没有多大意义的, 然而当我们反复进行伯努利试验, 去观察这些试验有多少是成功的多少是失败的, 事情就变得有意义了, 这些累计记录包含了很多潜在的非常有用的信息
以抛硬币为例, 每次抛硬币出现正面的概率都是50%, 假设一直抛硬币, 直到出现正面, 则一个伯努利试验结束
假设第1次伯努利试验抛硬币的次数为K1, 第n次伯努利试验抛硬币的次数为Kn
我们记录这n次伯努利试验的最大抛硬币次数为Kmax
那么重点来了
在某次伯努利试验中, kmax出现的概率, 就是投掷了kmax-1次反面, 和1次正面, 概率就是 1 / 2 k m a x 1/2^{k_{max}} 1/2kmax
也就是说需要进行 2 k m a x 2^{k_{max}} 2kmax次试验可能会出现一次kmax, 那么根据极大似然估算法, 我们就粗略估计本次进行的n次伯努利试验的 n = 2 k m a x n=2^{k_{max}} n=2kmax
同理, 应用到基数统计中就是, 把集合中每个元素都经过hash后表示为二进制数串, 一个数串类比成一次抛硬币试验, 1是抛到正面, 0是反面
二进制串中从低位开始第一个1出现的位置, 理解为抛硬币中第一次出现正面的抛掷次数k, 依旧通过 2 k m a x 2^{k_{max}} 2kmax来估算集合中一共有多少不同的数字
因为重复的值hash后二进制数串一致, 得到的k值不会变化, 所以是天然去重的, 重复数据不会对估算值产生任何影响
很显然这个估算关系是不准确的
关于估值偏差较大的问题, 可以采用如下方式结合来缩小误差
选取的哈希函数必须满足以下条件, 优秀的哈希函数是后续概率分析的基础
以上直接采用单一估计量会由于偶然性存在较大误差, 因此采用分桶平均的思想来消除误差
以Redis为例, 哈希结果64位, 前14位来分桶, 共16384个桶, 后50位作为真正用于基数估计的比特串
桶编号相同的元素分配到一个桶, 先分别统计每个桶各自的kmax, 然后进行平均
相当于物理试验中多次试验取平均的做法, 可以有效消减因偶然性带来的误差
分桶取平均是LLC的算法实现, HLL区别在于采用了调和平均数,
调和平均数指的是倒数的平均数, 相比平均数能有效抵抗离群值对平均值的扰动
比如我和马云的平均资产, 我有两万, 他有两千亿, 平均一下我有一千亿
但是调和平均数, 平均一下我有2/(1/20000 + 1/200000000000)=40000
就和我的资产偏差不是很大
经过上述系列优化后的估计量看似已经不错了, 但通过数学分析可以知道这并不是基数n的无偏估计
因此需要修正成无偏估计, 此处涉及一系列数学公式
简单来说就是, 增加修正因子, 是一个不固定的值, 会根据实际情况来进行值的调整
详细可参考论文
具体数据结构, 密集存储结构, 稀疏存储结构及其转换, 可参考一下文章
玩转Redis-HyperLogLog原理探索
ps: 另附一个演示工具, 在明白以上原理以后, 可以直观的看到HLL的工作过程及误差变化情况