最近,我需要对列表进行带权重的随机选择,包括有放回和无放回的情况。虽然有一些已知且有效的算法可以进行等权重选择,以及一些可以进行带权重无放回选择的算法(如修改后的蓄水池算法),但我找不到任何好的算法可以进行带权重有放回选择。同时,我也想避免使用蓄水池方法,因为我需要选择列表中相当大的一部分,该部分足够小而可容纳在内存中。
请问在这种情况下最佳的解决方案是什么?我自己有一些解决方案,但我希望能找到更高效、更简单或两者兼备的东西。
将权重归一化,使它们总和为1.0
。(a:0.2 b:0.2 c:0.2 d:0.2 e:0.2)
这是选择每个权重的概率。
找到大于或等于变量数的最小2的幂,并创建此数量的分区|p|
。每个分区表示1/|p|
的概率质量。在本例中,我们创建了8个分区,每个分区能够包含0.125
。
取剩余权重最小的变量,并尽可能多地将其质量放入空分区。在这个例子中,我们看到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)
如果分区未填满,则取剩余权重最大的变量,并用该变量填充分区。
重复步骤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)
未被分配。
在运行时:
获得一个U(0,1)
的随机数,例如二进制0.001100000
将其左移lg2(p)
位,找到索引分区。因此,我们将其左移3
位,得到001.1
,即位置1,因此是分区2。
如果分区被拆分,则使用移位后的随机数的小数部分来决定拆分。在这种情况下,该值为0.5
,且0.5 < 0.6
,因此返回a
。
这里有一些代码和另一个解释,但不幸的是它没有使用位移技术,我也没有验证过。
这里没有提到的一种简单方法是由Efraimidis和Spirakis提出的。在Python中,您可以从权重为正且至少有m个加权项存储在weights中的n个加权项中选择m个项目,并返回所选索引,如下所示:
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)]
这种方法在结构上与Nick Johnson提出的第一种方法非常相似。不幸的是,那种方法在选择元素时存在偏差(请参见该方法的评论)。Efraimidis和Spirakis在相关论文中证明,他们的方法等同于无重复随机抽样。
以下是我为不重复的加权选择所想出的方法:
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:]
这是一个O(m log m)的算法,其中m是要从中选择的列表项数。我相当确信它会正确地对项目进行加权,尽管我没有以任何正式的方式验证过。
以下是我为带有替换的加权选择所想出的方法:
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)]
WeightedSelectionWithoutReplacement([(1, 'A'), (2, 'B')], 1)
。它将有1/4的概率选择A,而不是1/3。难以修复。 - Jason Orendorffbucket[1]
中)作为已经处理它们的指示器。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 = [a-b 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]))
这是一个旧问题,但现在numpy已经提供了简单的解决方案,因此我想提一下。当前版本的numpy是1.2版本,numpy.random.choice
允许进行有或无替换的采样,并给出权重。
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
,您可以进行带替换的抽样。N
个候选人中的K
个验证者。但这会带来以下问题:0.1
0.1
0.8
在进行100万次无放回的2
选3
后,每个候选人的概率如下:
0.254315
0.256755
0.488930
std::vector<int> validators;
std::vector<int> weights(n);
int totalWeights = 0;
for (int j = 0; validators.size() < m; j++) {
int value = rand() % likehoodsSum;
for (int i = 0; i < n; i++) {
if (value < likehoods[i]) {
if (weights[i] == 0) {
validators.push_back(i);
}
weights[i]++;
totalWeights++;
break;
}
value -= likehoods[i];
}
}
它在数百万个样本上提供了几乎原始的奖励分布:
0.101230
0.099113
0.799657
a
应该比b
或c
出现3倍,这是不可能的。另一种可能的解释是结果ab
/ba
/ac
/ca
应该具有相对权重31,而bc
/cb
应该具有相对权重11。还有一种可能的解释是我们通常选择第一个值,然后根据第一次未选择的任何内容的相对权重选择第二个值-这样ab
比ba
更有可能。 - Karl Knechtelnumpy.choice
支持这种方式 - 采用后一种解释。 - Karl Knechtel