Reservoir Sampling
许多年以后,当听说蓄水池抽样算法 Reservoir Sampling 时,我将会想起,那个小学数学老师带他做“小明对水池边加水边放水,求何时能加满水”应用题的下午。
问题
一个数据抽样算法,寥寥几行,却暗藏玄机。主要用来解决如下问题。
给定一个数据流,数据流长度N很大,且N直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出m个不重复的数据。
这个场景强调了3件事:
- 数据流长度N很大且不可知,所以不能一次性存入内存。
- 时间复杂度为O(N)。
- 随机选取m个数,每个数被选中的概率为m/N。
第1点限制了不能直接取N内的m个随机数,然后按索引取出数据。第2点限制了不能先遍历一遍,然后分块存储数据,再随机选取。第3点是数据选取绝对随机的保证。讲真,在不知道蓄水池算法前,我想破脑袋也不知道该题做何解。
核心代码及原理
蓄水池抽样算法的核心如下:
1 | int[] reservoir = new int[m]; |
注:这里使用已知长度的数组dataStream来表示未知长度的数据流,并假设数据流长度大于蓄水池容量m。
算法思路大致如下:
- 如果接收的数据量小于m,则依次放入蓄水池。
- 当接收到第i个数据时,i >= m,在[0, i]范围内取以随机数d,若d的落在[0, m-1]范围内,则用接收到的第i个数据替换蓄水池中的第d个数据。
- 重复步骤2。
算法的精妙之处在于:当处理完所有的数据时,蓄水池中的每个数据都是以m/N的概率获得的。
下面用白话文推导验证该算法。假设数据开始编号为1.
第i个接收到的数据最后能够留在蓄水池中的概率=第i个数据进入过蓄水池的概率*之后第i个数据不被替换的概率(第i+1到第N次处理数据都不会被替换)。
- 当i<=m时,数据直接放进蓄水池,所以第i个数据进入过蓄水池的概率=1。
- 当i>m时,在[1,i]内选取随机数d,如果d<=m,则使用第i个数据替换蓄水池中第d个数据,因此第i个数据进入过蓄水池的概率=m/i。
- 当i<=m时,程序从接收到第m+1个数据时开始执行替换操作,第m+1次处理会替换池中数据的为m/(m+1),会替换掉第i个数据的概率为1/m,则第m+1次处理替换掉第i个数据的概率为(m/(m+1))(1/m)=1/(m+1),不被替换的概率为1-1/(m+1)=m/(m+1)。依次,第m+2次处理不替换掉第i个数据概率为(m+1)/(m+2)…第N次处理不替换掉第i个数据的概率为(N-1)/N。所以,之后**第i个数据不被替换的概率=m/(m+1)(m+1)/(m+2)…(N-1)/N=m/N**。
- 当i>m时,程序从接收到第i+1个数据时开始有可能替换第i个数据。则参考上述第3点,之后第i个数据不被替换的概率=i/N。
- 结合第1点和第3点可知,当i<=m时,第i个接收到的数据最后留在蓄水池中的概率=1m/N=m/N。结合第2点和第4点可知,当i>m时,第i个接收到的数据留在蓄水池中的概率=m/ii/N=m/N。综上可知,每个数据最后被选中留在蓄水池中的概率为m/N。
这个算法建立在统计学基础上,很巧妙地获得了“m/N”这个概率。
Distributed/Parallel Reservoir Sampling
一块CPU的计算能力再强,也总有内存和磁盘IO拖他的后腿。因此为提高数据吞吐量,分布式的硬件搭配软件是现在的主流。分布式蓄水池抽样。
如果遇到超大的数据量,即使是O(N)的时间复杂度,蓄水池抽样程序完成抽样任务也将耗时很久。因此分布式的蓄水池抽样算法应运而生。运作原理如下:
- 假设有K台机器,将大数据集分成K个数据流,每台机器使用单机版蓄水池抽样处理一个数据流,抽样m个数据,并最后记录处理的数据量为N1, N2, …, Nk, …, NK(假设m<Nk)。N1+N2+…+NK=N。
- 取[1, N]一个随机数d,若d<N1,则在第一台机器的蓄水池中等概率不放回地(1/m)选取一个数据;若N1<=d<(N1+N2),则在第二台机器的蓄水池中等概率不放回地选取一个数据;一次类推,重复m次,则最终从N大数据集中选出m个数据。
m/N的概率验证如下:
- 第k台机器中的蓄水池数据被选取的概率为m/Nk。
- 从第k台机器的蓄水池中选取一个数据放进最终蓄水池的概率为Nk/N。
- 第k台机器蓄水池的一个数据被选中的概率为1/m。(不放回选取时等概率的)
- 重复m次选取,则每个数据被选中的概率为m(m/Nk * Nk/N * 1/m) = m/N*。
算法验证
写一份完整的代码,用来验证蓄水池抽样的随机性。数据集大小为1000,蓄水池容量为10,做100 000次抽样。如果程序正确,那么每个数被抽中的次数接近1000次。
1 | package cn.edu.njupt.qyz; |
单机版输出和并行版的输出类似,截取片段如下:
1 | 948, 1006, 1014, 1019, 1033, 1040, 948, 1014, 1000, 951, |
此外,为了对比单机版与并行版(4线程)的性能差异,使用10_0000大小的数据集,蓄水池容量10,进行100_0000次重复抽样,对比两者的运行时间。结果如下
1 | ---------单机版---------- |
从输出结果可以看出,算法保证了数据选取的随机性。且并行版算法能够有效提高数据吞吐量。
应用场景
蓄水池抽样的O(N)时间复杂度,O(m)空间复杂度令其适用于对流数据、大数据集的等概率抽样。比如一个大文本数据,随机输出其中的几行。