再来看看第二个问题,这就涉及到了带权重的概率抽样问题了。那有没有在蓄水池算法基础上的带权重概率的抽样算法呢?当然是有的,想要详细了解的可以直接看paper《Weighted random sampling with a reservoir》。
首先对于每个样本,都具有一个权重Wi,我们可以针对这个权重值做一个变换作为每个样本的得分:sampleScore = random(0, 1)^(1/Wi)。然后采样过程与之前的一致,也是对每个样本进行顺序读取。对前k个样本维护一个最小堆(针对sampleScore排序),然后对于后续的样本,每次来一个样本,都将这个新样本的sampleScore与之前的最小样本的sampleScore进行比较,如果比最小sampleScore要大,则推出这个最小值,压入这个新样本并继续维护这个最小堆,直到所有样本都被遍历过一次。
具体的代码实现如下:
Comparator
3. A-Res算法
摘自:http://live.aulddays.com/tech/17/weighted-random-sampling-reservoir-algorithm.htm
最近,Aulddays 遇到一个随机抽样任务。有一个对象集合,由于整个集合非常大,希望考虑每个对象的热门程度抽样出一部分对象来进行分析。把这个任务抽象出来,其实就对应了一个带概率加权的随机抽样 (Weighted Random Sampling) 问题。对应到不同的应用场景,可以对应解决搜索query抽样、商品抽样、网页抽样等任务。对于不加权的普通随机抽样,其实并不难解决,在样本集合非常大的情况下,还可以使用经典的蓄水池算法 (Reservoir Sampling) 来高效实现的抽样。但对于带概率加权的情况,就不太容易了,特别的,在 Aulddays 的任务中,结果集合中每个对象只有1次,对应了无放回的情况,则更困难一些。在内存足够的情况下,似乎还能想到接近于 O(mlogn) 的思路,但大数据上就没什么好的思路了。Survey 了一番之后,发现这个问题其实已经被研究了很久,但直到 2006 年才得到较好的解决:Pavlos S. Efraimidis and Paul G. Spirakis, 2006, Weighted random sampling with a reservoir。这里介绍一下他们提出的 A-Res 算法。
问题定义
(简单)随机抽样
给定 n 个样本(样本集合),从中随机抽出 m 个样本(抽样集合),样本集合中每个样本出现在抽样集合的概率相等(=m/n)。
加权随机抽样
样本集合中每个样本附加一个权重 wi >0,每个样本被抽中的概率由 wi 确定。wi 有两种指定方法:
- 概率值,即所有样本的权重 wi 加和为 1
- 相对权重,wi 只表明样本之间被抽中概率的相对关系,但事先并不知道每个样本具体的概率是多少。在大数据的场景下,这个是更常见的情况
显然,概率值是相对权重的一个特例,解决了相对权重的情况,拿概率值作为输入也是直接可运行的。
有放回/无放回 (with/without replacement)
顾名思义,无放回就是被抽中之后就不会被再次加入候选,也就是一个样本在最终的抽样结果中最多只出现1次。有放回则可能在结果中出现多次。更一般的,还可以定义 k-1-放回,即一个样本最多被抽中 k 次(被放回 k-1 次)。不难看出,无放回对应 k=1,而有放回对应 k=m (m是抽样集合的大小)
这里首先重点研究无放回的情况。对于有放回的情况,可以在无放回算法的基础上做简单扩展来解决。
A-Res 算法
A-Res 算法也利用了蓄水池 (reservoir) 的思想来解决大数据的问题,也就是说,事先不用知道样本集合的大小 n 和样本权重的情况(i.e. 权重之和),只需要依次遍历一次样本集合即可以得到结果,空间复杂度是 O(m),正比于结果集合的大小。
出人意料的,A-Res 算法相当简洁,只需要计算和记录一个参数即可。具体算法如下:
Algorithm: A-Res
Input: 样本序列 V,长度未知,第 i 个样本 vi 的权重为 wi
Output: 长度为 m 的结果集合 R
foreach vi in V (i = 1, 2, ...):
ki = rand(0, 1) ^ (1 / wi)
if i <= m:
(vi, ki) 加入 R
else:
(vt, kt) = min k ∈ R // Aulddays: 选出 R 中 k 最小的那个样本
if ki > kt:
(vi, ki) 替换 (vt, kt)
提示:当
wi 值为一般权重而非概率值时,可能会是一个很大的数值,从而使得
ki 的指数操作可能会丢失精度。这种情况下可以对
ki 取 log() 而变成
ki = log(rand(0, 1)) / wi
,因为后续在各个
ki之间只涉及比较相对大小而不是绝对值,所以可以保证精度的同时不影响结果。
循环中的每一轮需要查找/更新蓄水池中 ki
特征值的最小值,显然这里用一个最小堆来维护是一个较优化的选择。于是整个算法的时间复杂度就是 O(m*log(n))
显然,整个算法的核心在于 ki = (rand(0, 1)) ^ (1 / wi)
这个特征值,这里做一个简单证明:
- 令
U1
, U2
为两个相互独立的随机变量且均服从 [0, 1] 区间上的均匀分布
- 令
k1 = (U1)1/w1
, k2 = (U2)1/w2
, 其中 w1, w2 > 0
(在算法中 w1
, w2
就是对应目标样本的权重)
- 可以推出概率关系:
P(k1≤k2) = w2/(w1+w2)
- 因此比较
ki
特征值确实实现了按 wi
的概率加权随机抽样
扩展(分布式、有放回)
因为所有数据只需过一轮,A-Res 可以比较容易的扩展到并行或分布式的情况。
对于有放回的情况,只需要这样进行修改:创建 m 个大小为 1 的蓄水池,对于每个样本 vi,分别在每个蓄水池上独立的运行 A-Res 算法。
4.总结篇
转自:https://xiaochai.github.io/2018/03/12/weighted-random-sampling-paper/
问题定义
不放回随机抽样(random sampling without replacement(RS))问题要求从大小为nn的集合中,随机抽取mm个不同元素。如果所有的元素被抽取出来的概率一致,则称之为均匀随机抽样(uniform RS)。一次遍历解决均匀随机抽样问题在推荐阅读的[1,5,10]中有讨论。在数据流上使用蓄水池类型(Reservoir-type)均匀随机抽样算法在推荐阅读[11]给出。推荐阅读[9]给出了一种可并行计算的均匀随机抽样算法。对于加权随机抽样(weighted random smapling(WSR)),它指的是所有元素都含有权重,每一个元素被取出的概率是由元素本身的权重决定的。WSR问题可以使用以下算法D来定义:
算法D,WRS定义
输入:含有nn个带权重元素的集合VV
输出:含有mm个元素的加权随机抽样结果集SS
1: for k=1k=1 to mm do
2: 元素vivi在第kk回合被选出来的概率为pi(k)=wi∑sj∈V−Swjpi(k)=wi∑sj∈V−Swj
3: 从V−SV−S中选出vivi,然后插入到S中
4: End-For
加权随机抽样(WRS)中最重要的算法有Alias Method, Partial Sum Trees 和 the Acceptance/Rejection method(推荐阅读[8]包含了WRS问题的解决算法汇总)。但这些算法皆非一次遍历算法(one-pass)。这篇论文中将展示一种简单的,非常灵活的解决WRS问题的方法。该方法可以基于数据流运行,并且此方法支持并行和分布式计算。以作者所知(To the best knowledge of the entry authors),这是第一个可以解决数据流的WRS算法,也是第一个能够支持并行、分布式运算的WRS算法。
定义:所谓一次遍历WRS(One-pass WRS)是指只对元素集合进行一次遍历即可完成加权随机抽样的WRS算法。如果这个集合的大小是不确定的(例如数据流),可以使用蓄水池类型抽样算法来解决。这种算法使用一组额外空间(蓄水池)来保存最后结果的候选元素。
符号与假设:开始时所有元素的权重是未知的正实数。总集合的大小为nn,抽样mm个元素。元素vivi的权重为wiwi。函数random(L,H)random(L,H)产生(L,H)(L,H)之间的随机数。XX表示随机变量。假设支持无限精度计算。在无特别说明时,本文所指的抽样都是不放回抽样。WRS这个名词可以用作抽样的结果,或者抽样的操作过程,视上下文而定。
关键结果
WRS方法的关键工作如下,姑且称之为算法A:
算法A
输入:含有nn个带权重元素的集合VV
输出:mm个元素的加权随机抽样结果
1:对于任意的vi∈Vvi∈V, ui=random(0,1)ui=random(0,1),ki=u1wiki=ui1w
2:选取mm个kiki最大的元素做为WRS
定理1. 算法A可获得一个WRS结果
以下使用蓄水池类型适配的算法A,我们称之为A-Res算法(algorithm A-Res):
使用蓄水池适配的算法A(Algorithm A With a Reservoir(A-Res))
输入:含有nn个带权重元素的集合VV
输出:存储有mm个元素的加权随机抽样结果的蓄水池空间RR
1: 将VV的前m个元素直接插入RR
2: 对于vi∈Rvi∈R:计算关键值(key)ki=u1wiiki=ui1wi,其中ui=random(0,1)ui=random(0,1)
3: 让ii依次等于m+1,m+2,...,nm+1,m+2,...,n,执行4到7步骤
4: 计算RR中的最小关键值做为当前的阈值TT
5: 计算vivi的关键值ki=u1wiiki=ui1wi,其中ui=random(0,1)ui=random(0,1)
6: 如果ki>Tki>T
7: 则将R中关键值最小的元素使用vivi代替
算法A-Res使用了算法A来计算生成了加权抽样结果。下面的命题给出此算法集合RR的操作次数:
定理2. 如果A-Res作用在nn个权重(wiwi)大于0的元素上,且wiwi为一般连续分布(common continuous distribution)的独立随机数,则集合RR的插入操作次数预期为(不包括前m个元素的插入):
n∑i=m+1P[第i个元素被插入S]=n∑i=m+1mi=O(m×log(nm))∑i=m+1nP[第i个元素被插入S]=∑i=m+1nmi=O(m×log(nm))
令SwSw为被A-Res算法连续跳过的元素(没有插入到S)的权重之和,如果TwTw为当前进入S集合的阈值,则SwSw为服从指数分布的连续随机变量。所以可以使用SwSw随机跳过若干元素,这样就不用对所有元素生成随机数了。同样的方法已经在均匀随机抽样中得到 应用(推荐阅读[3]中有对应的例子)。以下算法A-ExpJ是使用指数跳跃类型适配的算法A:
使用指数跳跃的算法A(Algorithm A with exponential jumps(A-ExpJ)
输入:含有nn个带权重元素的集合VV
输出:存储有mm个元素的加权随机抽样结果的蓄水池空间RR
1: VV的前mm个元素直接插入RR
2: 计算RR中各个元素的关键值(即key值,ki=u1wiiki=ui1wi,其中ui=random(0,1)ui=random(0,1)
3: RR中最小的关键值为阈值TwTw
4: 对VV中剩下的元素执行5~10步骤
5: 令r=random(0,1)r=random(0,1), Xw=log(r)log(Tw)Xw=log(r)log(Tw)
6: 从当前的元素vcvc一直跳过,直到vivi满足以下条件:
7: wc+wc+1+...+wi−1 8: 使用vivi替换掉RR中关键值最小的元素
9: 令tw=Twiwtw=Twwi, r2=random(tw,1)r2=random(tw,1) , vivi的关值为: ki=r1wi2ki=r21wi
10: 算出新的阈值TwTw为当前RR中最小的关键值
定理3. 算法A-ExpJ可获得一个WRS结果
定理2给出了使用A-ExpJ跳过的元素数量。所以使用A-ExpJ可使得随机数的生成量从原来的O(n)O(n)减少到O(mlog(nm))O(mlog(nm))。生成高质量的随机数有可能会有较大的性能开销,所以这个算法优化在复杂的随机抽样场景中可带来可观的性能改善
应用
随机抽样在计算机科学的许多应用领域都是一个基础的问题,如数据库领域(推荐阅读[4],[8]),数据挖掘,近似算法和随机算法([6])。通用工具算法A在随机算法设计中可以找到其对应的应用。如在k-Median算法的近似算法中使用了算法A(推荐阅读[6])。
基于蓄水池方式的算法A,A-Res和A-ExpJ只需要很少的额外存储(m个元素的堆),就可以使得抽样处理期间始终存储着对于已经处理过的元素的加权随机抽样,所以在数据流处理这一领域中这些算法都可以得到应用(推荐阅读[2],[7])。
算法A-Res和A-ExpJ也可以应用于对数据流的放回加权随机抽样(weighted random sampling with replacement)中。开k个A-Res和A-ExpJ进程,并行计算目标数据流的结果为1不放回随机抽样,最终的k个结果合并,即为k个放回加权随机抽样的结果。
推荐阅读
[1] J. H. Ahrens and U. Dieter, Sequential random sampling, ACM Trans. Math. Softw., 11 (1985), pp. 157–169.
[2] B. Babcock, S. Babu, M. Datar, R. Motwani, and J. Widom, Models and issues in data stream systems, in Proceedings of the twenty-first ACM SIGMOD-SIGACT-SIGART symposium on Principles of database systems, ACM Press, 2002, pp. 1–16.
[3] L. Devroye, Non-uniform Random Variate Generation, Springer Verlag, New York, 1986.
[4] C. Jermaine, A. Pol, and S. Arumugam, Online maintenance of very large random samples, in SIGMOD ’04: Proceedings of the 2004 ACM SIGMOD international conference on Management of data, New York, NY, USA, 2004, ACM Press, pp. 299–310.
[5] D. Knuth, The Art of Computer Programming, vol. 2 : Seminumerical Algorithms, Addison- Wesley Publishing Company, second ed., 1981.
[6] J.-H. Lin and J. Vitter, ǫ-approximations with minimum packing constraint violation, in 24th ACM STOC, 1992, pp. 771–782.
[7] S. Muthukrishnan, Data streams: Algorithms and applications, Foundations & Trends in Theoretical Computer Science, 1 (2005).
[8] F. Olken, Random Sampling from Databases, PhD thesis, Department of Computer Science, University of California at Berkeley, 1993.
[9] V. Rajan, R. Ghosh, and P. Gupta, An efficient parallel algorithm for random sampling, Information Processing Letters, 30 (1989), pp. 265–268.
[10] J. Vitter, Faster methods for random sampling, Communications of the ACM, 27 (1984), pp. 703–718.
[11] —, Random sampling with a reservoir, ACM Trans. Math. Softw., 11 (1985), pp. 37–57.
以上为简化版论文,文中省略了算法的证明和详细说明。只是把实现思路陈述了下。
更详细的论文里解释了概率计算与各个定理的完整计算和证明,我对其中的推倒有许多不明白的地方,所以就不给予列举,只是给出对应算法的代码实现。
定义数据结构和最小堆
// 元素数组结构
type Data struct {
Condition int
Value int
Weight float64 // 原生的权重
Key float64 // 关键值
}
// 最小堆的实现
type PHeap []*Data
func (h PHeap) Len() int{
return len(h)
}
func (h PHeap) Less(i, j int)bool {
return h[i].Key < h[j].Key
}
func (h PHeap)Swap(i, j int){
h[i], h[j] = h[j], h[i]
}
func (h *PHeap)Push(x interface{}){
d := x.(*Data)
*h = append(*h, d)
}
func (h *PHeap)Pop()interface{}{
item := (*h)[h.Len()-1]
*h = (*h)[0:h.Len()-1]
return item
}
A-Res
// 加权随机抽样
func WSRARes(l *list.List, m int, cond int) []*Data{
h := PHeap{}
heap.Init(&h)
for e := l.Front(); e != nil; e = e.Next() {
data, ok := e.Value.(*Data);
if !ok || data.Condition <= cond {
continue;
}
data.Key = math.Pow(rand.Float64(), 1/data.Weight)
if h.Len() < m {
heap.Push(&h, data)
} else if h[0].Key < data.Key {
heap.Pop(&h)
heap.Push(&h, data)
}
}
return h
}
A-ExpJ
func WSRExpJ(l *list.List, m int, cond int ) []*Data {
h := PHeap{}
heap.Init(&h)
var XW float64
var flightSum float64
for e := l.Front(); e != nil; e = e.Next() {
data, ok := e.Value.(*Data);
if !ok || data.Condition <= cond {
continue;
}
data.Key = math.Pow(rand.Float64(), 1/data.Weight)
if h.Len() < m {
heap.Push(&h, data)
if h.Len() == m { // 当插入完所有元素之后,算出xw值
XW = math.Log(rand.Float64())/ math.Log(h[0].Key)
}
} else if flightSum += data.Weight; flightSum >= XW {
tw := math.Pow(h[0].Key, data.Weight)
r2 := rand.Float64()*(1- tw) + tw
data.Key = math.Pow(r2, 1/data.Weight)
heap.Pop(&h)
heap.Push(&h, data)
flightSum = 0;
XW = math.Log(rand.Float64())/ math.Log(h[0].Key)
}
}
return h
}