一个集合里有 n
个元素,每个元素有不同的权重,现在要不放回地随机抽取 m
个元素,每个元素被抽中的概率为元素的权重占总权重的比例。要怎么做呢?
简单的解法
现在考虑只抽取一个元素,假设权重之和为 1
。我们可以从 [0, 1]
中随机得到一个权重,假设为 0.71
,而后从第一个元素开始,不断累加它们的权重,直到有一个元素的累加权重包含 0.71
,则选取该元素。下面是个示意图:
要选取 m 个元素,则可以按上面的方法先选取一个,将该元素从集合中去除,再反复按上面的方法抽取剩余的元素。这种方法的复杂度是 O(mn)
,并且将元素从集合中删除其实不太方便实现。
当然,最重要的是这个算法需要多次遍历数据,不适合用在流处理的场景中。
Algorithm A
Algorithm A 是论文 Weighted Random Sampling 中提出的,步骤如下:
- 对于集合 $V$ 中的元素 $v_i \in V$,选取均匀分布的随机数 $u_i = rand(0, 1)$ ,计算元素的特征 $k_i = u_i^{(1/w_i)}$
- 将集合按 $k_i$ 排序,选取前 $m$ 大的元素。
算法的正确性在作者 2006 年的论文 Weighted random sampling with a
reservoir
里给了详细的证明。论文中给出了算法的两个变种 A-Res 与 A-ExpJ,它们都能在一次扫描中得到 m
个样本。非常适合在流处理的场合中。
A-Res 算法
A-Res(Algorithm A With a Reservoir) 是 Algorithm 的“蓄水池”版本,即维护含有
m
个元素的结果集,对每个新元素尝试去替换结果集中权重最小的元素。步骤如下:
- 将集合 $V$ 的前 $m$ 个元素放入结果集合 $R$。
- 对于结果集里的每个元素,计算特征值 $k_i = u_i^{(1/w_i)}$,其中 $u_i = rand(0, 1)$
- 对 $i = m+1, m+2, \dots, n$ 重复步骤 4 ~ 6
- 将结果集中最小的特征 $k$ 作为当前的阈值 $T$
- 对于元素 $v_i$,计算特征 $k_i = u_i^{(1/w_i)}$,其中 $u_i = rand(0, 1)$
- 如果 $k_i > T$ 则将 $R$ 中拥有最小 $k$ 值的元素替换成 $v_i$。
论文证明了如果权重 $w_i$ 是一般连续分布上的随机变量,则上面的算法中插入 $R$ 的次数为 $O(m \log(\frac{n}{m}))$。该算法用 Python 实现如下:
import heapq |
A-ExpJ 算法
A-Res 需要对每个元素产生一个随机数,而生成高质量的随机数有可能会有较大的性能开销,,所以论文中给出了 A-ExpJ 算法,能将随机数的生成量从 $O(n)$ 减少到 $O(m\log(\frac{n}{m})))$。从步骤上看,很像我们最开始提出的简单版本,设定一个阈值并跳过一些元素。具体步骤如下:
- 将集合 $V$ 的前 $m$ 个元素放入结果集合 $R$。
- 对于结果集里的每个元素,计算特征值 $k_i = u_i^{(1/w_i)}$,其中 $u_i = rand(0, 1)$
- 将 $R$ 中小最的特征值记为阈值 $T_w$
- 对剩下的元素重复步骤 5 ~ 10
- 令 $r = rand(0, 1)$ 且 $X_w = \log( r )/\log(T_w)$
- 从当前元素 $v_c$ 开始跳过元素,直到遇到元素 $v_i$,满足
- $w_c + w_{c+1} + \dots + w_{i-1} \lt X_w \le w_c + w_{c+1} + \dots + w_{i-1} + w_{i}$
- 使用 $v_i$ 替换 $R$ 中特征值最小的元素。
- 令 $t_w = T_w^{w_i}$, $r2 = rand(t_w, 1)$, $v_i$ 的特征 $k_i = r_2^{(1/w_i)}$
- 令新的阈值 $T_w$ 为此时 $R$ 中的最小特征值。
Python 实现如下:
def a_expj(samples, m): |
验证
我们用多次采样的方式来尝试验证算法的正确性。下面代码1中为 a
、b
、c
等元素赋予了不同的权重,采样 10 万次后计算被采样的次数与元素 a
被采样次数的比值。
overall = [('a', 10), ('b', 20), ('c', 50), ('d', 100), ('e', 200)] |
首先验证 A-Res 算法:
test_weighted_sampling(a_res, 1) |
看到,在采样一个元素时,b
被采样到的次数约为 a
的 2
倍,而 e
则约为 20
倍,与overall
数组中指定的权重一致。而采样 5 个元素时,所有元素都会被选中。
同理验证 A-ExpJ 算法:
test_weighted_sampling(a_expj, 1) |
与 A-Res 的结果类似。
小结
文章中介绍了 A-Res 与 A-ExpJ 两种算法,按照步骤用 Python 实现了一个简单的版本,最后用采样的方式验证了算法的正确性。
加权随机采样本身不难,但如果需要在一次扫描中完成就不容易了。难以想像上面的算法直到 2006 年才提出。算法本身如此之简单,也让不感叹数学与概率的精妙。
参考
- Weighted Random Sampling (2005; Efraimidis, Spirakis) 2015 年论文,大概介绍了本文中提到的算法
- Weighted random sampling with a reservoir 作者于 2016 年的论文,其中有详细的数学证明
- 加权随机抽样 有 2005 年论文的翻译
- 概率加权的随机抽样 (Weighted Random Sampling) – A-Res 蓄水池算法
- Metrics Core Java 的一个性能监控库,其中的
ExponentiallyDecayingReservoir
用到了 A-Res 算法。
- 1.修改自 https://blog.xingwudao.me/2017/09/26/sampling/ ↩