有和没有replace的加权随机select

最近,我需要从列表中加权随机select元素,无论是否有replace。 虽然有一些众所周知的好的algorithm用于未加权的select,有些用于加权select而没有replace(例如resevoiralgorithm的修改),但我找不到任何用于replace的加权select的好algorithm。 我也想避免使用藏库方法,因为我正在select一个很小的列表中的一小部分,这个列表足够小,可以放在内存中。

在这种情况下有没有人有最好的方法build议? 我有我自己的解决scheme,但我希望find更有效率,更简单,或两者兼而有之。

使用不变列表replace样本的最快方法之一是别名方法。 核心的直觉是,我们可以为加权列表创build一组相同大小的bin,可以通过位操作非常高效地索引,从而避免二分search。 结果是,如果做得正确的话,我们将只需要从每个分箱的原始列表中存储两个项目,从而可以用单个百分比表示分割。

让我们以五个加权的select为例(a:1, b:1, c:1, d:1, e:1)

要创build别名查找:

  1. 将权重归一化为1.0(a:0.2 b:0.2 c:0.2 d:0.2 e:0.2)这是select每个重量的概率。

  2. 找出2的最小幂数大于或等于variables个数,并创build这个分区数, |p| 。 每个分区代表1/|p|的概率质量 。 在这种情况下,我们创build了8分区,每个分区可以包含0.125

  3. 以剩余重量最小的variables,把尽可能多的质量放在一个空的分区中。 在这个例子中,我们看到a填满了第一个分区。 (p1{a|null,1.0},p2,p3,p4,p5,p6,p7,p8) (a:0.075, b:0.2 c:0.2 d:0.2 e:0.2) (p1{a|null,1.0},p2,p3,p4,p5,p6,p7,p8)

  4. 如果分区没有被填充,取最重的variables,并用该variables填充分区。

重复步骤3和4,直到没有任何来自原始分区的权重需要分配给列表。

例如,如果我们运行另一个3和4的迭代,我们可以看到

(p1{a|null,1.0},p2{a|b,0.6},p3,p4,p5,p6,p7,p8) (a:0, b:0.15 c:0.2 d:0.2 e:0.2) (p1{a|null,1.0},p2{a|b,0.6},p3,p4,p5,p6,p7,p8)留下来分配

在运行时:

  1. 得到一个U(0,1)随机数,比如说二进制0.001100000

  2. lg2(p)lg2(p) ,find索引分区。 因此,我们将它移动3 ,产生001.1 ,或者位置1,从而分割2。

  3. 如果分区被分割,则使用移位的随机数的小数部分来决定分割。 在这种情况下,值是0.5 ,并且0.5 < 0.6 ,所以返回a

这里有一些代码和另一个解释 ,但不幸的是,它不使用偏移技术,也没有实际validation它。

我build议你先看一下 Donald Knuth的algorithmalgorithm 3.4.2节。

如果你的数组很大,John Dagpunar 在随机variables生成原理的第3章中有更高效的algorithm。 如果你的arrays不是非常大,或者你不关心尽可能多的效率,Knuth中更简单的algorithm可能是好的。

以下是我想出的无需更换的加权select:

 def WeightedSelectionWithoutReplacement(l, n): """Selects without replacement n random elements from a list of (weight, item) tuples.""" l = sorted((random.random() * x[0], x[1]) for x in l) return l[-n:] 

这是要从中select的列表中的项目数O(m log m)。 我相当肯定,这将正确地加权项目,虽然我没有任何正式的意义上的validation。

以下是我想出的replace加权select:

 def WeightedSelectionWithReplacement(l, n): """Selects with replacement n random elements from a list of (weight, item) tuples.""" cuml = [] total_weight = 0.0 for weight, item in l: total_weight += weight cuml.append((total_weight, item)) return [cuml[bisect.bisect(cuml, random.random()*total_weight)] for x in range(n)] 

这是O(m + n log m),其中m是input列表中的项目数量,n是要select的项目数量。

这里没有提到的简单方法是在Efraimidis和Spirakis中提出的方法 。 在python中,你可以从n> = m个加权项目中selectm个项目,权重存储在严格正的权重中,返回选定的索引:

 import heapq import math import random def WeightedSelectionWithoutReplacement(weights, m): elt = [(math.log(random.random()) / weights[i], i) for i in range(len(weights))] return [x[1] for x in heapq.nlargest(m, elt)] 

这与尼克·约翰逊提出的第一种方法在结构上非常相似。 不幸的是,这种方法在select元素方面存在偏差(请参阅方法的评论)。 Efraimidis和Spirakiscertificate,他们的方法相当于在相关论文中没有replace的随机抽样。

在O(N)时间首先创build一个额外的O(N)大小的数据结构之后,可以用O(1)时间replace来进行加权随机select。 该algorithm基于Walker和Vose开发的Alias方法 , 这里详细描述。

基本的想法是直方图中的每个bin将被一个统一的RNG以概率1 / N来select。 所以我们将通过它,并为任何可能会收到超额命中的人口过剩的垃圾箱,分配到一个人口过剩的垃圾箱。 对于每个垃圾箱,我们存储属于它的命中的百分比,以及多余的伙伴垃圾箱。 该版本跟踪小型和大型垃圾箱,无需额外的堆栈。 它使用伙伴的索引(存储在bucket[1] )作为已被处理的指示符。

这里是一个基于C实现的最小的Python实现

 def prep(weights): data_sz = len(weights) factor = data_sz/float(sum(weights)) data = [[w*factor, i] for i,w in enumerate(weights)] big=0 while big<data_sz and data[big][0]<=1.0: big+=1 for small,bucket in enumerate(data): if bucket[1] is not small: continue excess = 1.0 - bucket[0] while excess > 0: if big==data_sz: break bucket[1] = big bucket = data[big] bucket[0] -= excess excess = 1.0 - bucket[0] if (excess >= 0): big+=1 while big<data_sz and data[big][0]<=1: big+=1 return data def sample(data): r=random.random()*len(data) idx = int(r) return data[idx][1] if r-idx > data[idx][0] else idx 

用法示例:

 TRIALS=1000 weights = [20,1.5,9.8,10,15,10,15.5,10,8,.2]; samples = [0]*len(weights) data = prep(weights) for _ in range(int(sum(weights)*TRIALS)): samples[sample(data)]+=1 result = [float(s)/TRIALS for s in samples] err = [ab for a,b in zip(result,weights)] print(result) print([round(e,5) for e in err]) print(sum([e*e for e in err])) 

以下是在O(n)空间和O(log n)时间内有或没有replace的集合(或多重集,如果允许重复)的元素的随机加权select的描述。

它包括实现一个二叉search树,按照要select的元素sorting,树的每个节点包含:

  1. 元素本身( 元素
  2. 元素(元素重量)的非标准化权重,以及
  3. 左子节点及其所有子节点( 左分支 )的所有非标准化权重之和。
  4. 右子节点及其全部子( 右分支 )的所有非标准权重的总和。

然后我们通过从树中下降来随机地从BST中select一个元素。 该algorithm的粗略描述如下。 该algorithm被赋予树的节点 。 然后将左分枝 分枝节点 元素的值相加,并将权重除以该和,分别得到左分枝概率右分枝概率元素概率 。 然后获得0和1之间的随机数(随机数)。

  • 如果该数字小于元素概率
    • 正常情况下从BST中删除元素,更新所有必要节点的左分支右分支 ,并返回元素。
  • 否则如果数量小于( elementprobability + leftbranchweight
    • leftchild上recursion(使用leftchild作为节点运行algorithm)
  • 其他
    • recursion于右键

当我们最终发现,使用这些权重,要返回哪个元素,我们或者简单地返回它(用replace),或者我们删除它并更新树中的相关权重(没有replace)。

免责声明:该algorithm是粗糙的,并没有尝试在这里正确实施BST的论文; 相反,希望这个答案能帮助那些真正需要快速加权select而不需要replace的人(就像我一样)。

假设你想从列表中抽取3个元素,而不是从列表['白色','蓝色','黑色','黄色','绿色] 分布[0.1,0.2,0.4,0.1,0.2]。 使用numpy.random模块就像这样简单:

  import numpy.random as rnd sampling_size = 3 domain = ['white','blue','black','yellow','green'] probs = [.1, .2, .4, .1, .2] sample = rnd.choice(domain, size=sampling_size, replace=False, p=probs) # in short: rnd.choice(domain, sampling_size, False, probs) print(sample) # Possible output: ['white' 'black' 'blue'] 

设置replace标志为True ,你有一个替代抽样。

更多信息在这里: http : //docs.scipy.org/doc/numpy/reference/generated/numpy.random.choice.html#numpy.random.choice