最近我需要对列表中的元素进行加权随机选择,无论是否有替换。虽然有未知加权选择的众所周知和良好的算法,有些用于无替换的加权选择(例如修改算法),我找不到任何好的算法用于替换加权选择。我也想避免使用resevoir方法,因为我选择了列表中的一小部分,这个数据小到足以保存在内存中。
有没有人对这种情况下的最佳方法有任何建议?我有自己的解决方案,但我希望找到更高效,更简单或两者兼而有之的方法。
答案 0 :(得分:31)
使用不变列表替换样本的最快方法之一是别名方法。核心直觉是我们可以为加权列表创建一组大小相等的二进制位,可以通过位操作非常有效地索引,以避免二进制搜索。结果表明,正确完成后,我们只需要从每个bin的原始列表中存储两个项目,因此可以用一个百分比表示拆分。
让我们以五个同等加权的选择为例(a:1, b:1, c:1, d:1, e:1)
创建别名查找:
将权重标准化,使它们总和为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
。
Here is some code and another explanation,但不幸的是它没有使用比特移位技术,也没有实际验证过它。
答案 1 :(得分:5)
以下是我提出的无需替换的加权选择:
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)。我相当肯定这会正确地加权项目,但我还没有在任何正式意义上验证它。
以下是我为替换加权选择提出的建议:
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是输入列表中的项目数,n是要选择的项目数。
答案 2 :(得分:5)
这里没有提到的简单方法是Efraimidis and Spirakis中提出的方法。在python中,您可以从n> = m加权项目中选择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证明他们的方法相当于随机抽样而无需替换链接论文。
答案 3 :(得分:4)
我建议你先看看Donald Knuth Seminumerical Algorithms的第3.4.2节。
如果您的阵列很大,John Dagpunar在Principles of Random Variate Generation的第3章中提供了更有效的算法。如果您的阵列不是非常大或者您不关心尽可能多地挤出效率,那么Knuth中的简单算法可能就好了。
答案 4 :(得分:4)
以下是对a元素的随机加权选择的描述 set(或multiset,如果允许重复),在O(n)空间中有和没有替换 和O(log n)时间。
它包括实现二进制搜索树,按要素排序 选中,树的每个节点包含:
然后我们通过沿树下降来从BST中随机选择一个元素。一个 该算法的粗略描述如下。该算法给出了一个节点 那个树。然后是 leftbranchweight , rightbranchweight 的值, 将节点的 elementweight 求和,并将权重除以此 sum,得到值 leftbranchprobability , rightbranchprobability 和 elementprobability 分别。然后一个 获得0到1之间的随机数( randomnumber )。
当我们最终使用这些权重找到要返回的元素时,我们要么只返回它(带替换),要么删除它并更新树中的相关权重(无需替换)。
免责声明:该算法粗略,并且正确实施的论文 这里没有尝试过BST;相反,希望这个答案会有所帮助 真正的人需要快速加权选择而不需要替换(就像我一样)。
答案 5 :(得分:3)
在O(N)时间内首次创建额外的O(N)大小的数据结构之后,可以在O(1)时间内进行加权随机选择。该算法基于Walker和Vose开发的Alias Method,其中有很好的描述here。
基本思想是直方图中的每个区间将通过统一的RNG以1 / N的概率选择。因此,我们将逐步完成它,并且对于任何可能会收到过多命中的人口不足的垃圾箱,将多余的垃圾分配给人口过多的垃圾箱。对于每个箱,我们存储属于它的命中百分比,以及超出的伙伴箱。此版本可跟踪小型和大型垃圾箱,无需额外堆叠。它使用合作伙伴的索引(存储在bucket[1]
中)作为已经处理过的指标。
这是一个基于the C implementation here
的最小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 = [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]))
答案 6 :(得分:0)
假设您想要使用概率从列表['white','blue','black','yellow','green']中替换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
答案 7 :(得分:0)
我们面临一个问题,即按照每个时期按比例向K
个候选者中随机选择N
个验证者。但这给我们带来了以下问题:
想象每个候选人的概率:
0.1
0.1
0.8
从2
中选择1 {000}次3
而不进行替换后,每个候选人的概率变为:
0.254315
0.256755
0.488930
您应该知道,对于2
中的3
个选择而不进行替换的原始概率是无法实现的。
但我们希望初始概率为利润分配概率。否则,小型候选人池将更有利可图。因此,我们意识到替换的随机选择将帮助我们–从>K
中随机选择N
,并存储每个验证者的权重以分配奖励:
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
答案 8 :(得分:0)
这是一个老问题,numpy 现在提供了一个简单的解决方案,所以我想我会提到它。 numpy 的当前版本是 1.2 版,numpy.random.choice
允许在有或没有替换和给定权重的情况下进行采样。