带或不带替换的加权随机选择

时间:2008-12-09 13:15:01

标签: python algorithm random random-sample

最近我需要对列表中的元素进行加权随机选择,无论是否有替换。虽然有未知加权选择的众所周知和良好的算法,有些用于无替换的加权选择(例如修改算法),我找不到任何好的算法用于替换加权选择。我也想避免使用resevoir方法,因为我选择了列表中的一小部分,这个数据小到足以保存在内存中。

有没有人对这种情况下的最佳方法有任何建议?我有自己的解决方案,但我希望找到更高效,更简单或两者兼而有之的方法。

9 个答案:

答案 0 :(得分:31)

使用不变列表替换样本的最快方法之一是别名方法。核心直觉是我们可以为加权列表创建一组大小相等的二进制位,可以通过位操作非常有效地索引,以避免二进制搜索。结果表明,正确完成后,我们只需要从每个bin的原始列表中存储两个项目,因此可以用一个百分比表示拆分。

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

创建别名查找:

  1. 将权重标准化,使它们总和为1.0(a:0.2 b:0.2 c:0.2 d:0.2 e:0.2)这是选择每个体重的概率。

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

  3. 获取剩余重量最少的变量,并将尽可能多的质量放在空分区中。在此示例中,我们看到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)

  4. 如果未填充分区,请使用权重最大的变量,并使用该变量填充分区。

  5. 重复步骤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)

    在运行时:

    1. 获取U(0,1)随机数,比如二元0.001100000

    2. 将它移位lg2(p),找到索引分区。因此,我们将其移至3,产生001.1或位置1,从而产生分区2.

    3. 如果分割分区,请使用移位的随机数的小数部分来决定分割。在这种情况下,值为0.50.5 < 0.6,因此请返回a

    4. 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)时间。

它包括实现二进制搜索树,按要素排序 选中,树的每个节点包含:

  1. 元素本身(元素
  2. 元素的非标准化权重(元素权重)和
  3. 左子节点和所有子节点的所有未规范化权重的总和 它的孩子( leftbranchweight )。
  4. 右子节点和所有子节点的所有未规范化权重的总和 它的孩子( rightbranchweight )。
  5. 然后我们通过沿树下降来从BST中随机选择一个元素。一个 该算法的粗略描述如下。该算法给出了一个节点 那个树。然后是 leftbranchweight rightbranchweight 的值, 将节点 elementweight 求和,并将权重除以此 sum,得到值 leftbranchprobability rightbranchprobability elementprobability 分别。然后一个 获得0到1之间的随机数( randomnumber )。

    • 如果数字小于元素概率
      • 正常删除BST中的元素,更新 leftbranchweight 和所有必要节点的 rightbranchweight ,并返回 元件。
    • 如果数字小于(元素概率 + leftbranchweight
      • 递归 leftchild (使用 leftchild 作为节点运行算法)
    • 否则
      • 右边的
      • 上递言

    当我们最终使用这些权重找到要返回的元素时,我们要么只返回它(带替换),要么删除它并更新树中的相关权重(无需替换)。

    免责声明:该算法粗略,并且正确实施的论文 这里没有尝试过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 允许在有或没有替换和给定权重的情况下进行采样。