【问题标题】:Weighted random selection with and without replacement带和不带放回的加权随机选择
【发布时间】:2010-09-26 01:03:45
【问题描述】:

最近我需要对列表中的元素进行加权随机选择,包括替换和不替换。虽然有一些众所周知且很好的非加权选择算法,还有一些用于不带替换的加权选择(例如对 resevoir 算法的修改),但我找不到任何好的带替换加权选择算法。我还想避免使用 resevoir 方法,因为我选择了列表的很大一部分,它小到足以保存在内存中。

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

【问题讨论】:

  • 看到这个问题stackoverflow.com/q/10164303/112100,还以为是C#而不是python,代码很少,大家应该看懂
  • 对于其他必须查找它的人来说,“水库算法”位于维基百科的“reservoir sampling”下。引用的第一篇论文是 Jeffrey Scott Vitter 的“带有水库的随机抽样”,来自 ACM Transactions on Mathematical Software,Vol. 11,第 1 期,1985 年 3 月 1 日,第 37--57 页。
  • 对于加权无替换,其中权重意味着被选择的概率与权重成正比,请参阅我的答案:stackoverflow.com/a/27049669/262304 请注意,某些输入没有解决方案,例如从 {'a': 3, 'b': 1, 'c: 1} 中选择 2 应该产生 'a' 的频率是 b 或 c 的 3 倍,但这是不可能的。

标签: python algorithm random random-sample


【解决方案1】:

这是一个老问题,numpy 现在提供了一个简单的解决方案,所以我想我会提到它。 numpy 的当前版本是 1.2 版,numpy.random.choice 允许在有或没有替换的情况下以及给定的权重下进行采样。

【讨论】:

    【解决方案2】:

    我们遇到了一个问题,即在每个 epoch 按比例随机选择 N 候选者的 K 验证者一次。但这给我们带来了以下问题:

    想象一下每个候选人的概率:

    0.1
    0.1
    0.8
    

    23 没有替换 1'000'000 次选择之后,每个候选人的概率变为:

    0.254315
    0.256755
    0.488930
    

    您应该知道,对于23 选择没有替换,这些原始概率是无法实现的。

    但我们希望初始概率是利润分配概率。否则,它会使小型候选池更有利可图。所以我们意识到随机选择 with replacement 可以帮助我们 - 随机选择 >KN 并存储每个验证者的权重以进行奖励分配:

    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
    

    【讨论】:

      【解决方案3】:

      假设您想用概率从列表 ['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

      【讨论】:

        【解决方案4】:

        下面是对a元素的随机加权选择的描述 集(或多重集,如果允许重复),在 O(n) 空间中进行替换和不替换 和 O(log n) 时间。

        它包括实现一个二叉搜索树,按要排序的元素排序 已选中,其中树的每个 节点 包含:

        1. 元素本身(element
        2. 元素的非标准化权重(elementweight),以及
        3. 左子节点的所有未归一化权重的总和和所有的 它的孩子(leftbranchweight)。
        4. 右子节点的所有非归一化权重和所有的权重之和 它的孩子(rightbranchweight)。

        然后我们从 BST 中随机选择一个元素,沿着树向下。一种 算法的粗略描述如下。该算法被赋予一个节点 那个树。那么leftbranchweightrightbranchweight的值, 和nodeelementweight相加,权重除以这个 总和,产生值 leftbranchprobabilityrightbranchprobabilityelementprobability。然后一个 得到0到1之间的随机数(randomnumber)。

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

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

        免责声明:算法很粗糙,并且是关于正确实现的论文 此处不尝试使用 BST;相反,希望这个答案会有所帮助 那些真正需要快速加权选择而不需要替换的人(就像我一样)。

        【讨论】:

        【解决方案5】:

        使用不变列表中的替换样本生成多个样本的最快方法之一是别名方法。核心直觉是,我们可以为加权列表创建一组大小相等的 bin,可以通过位操作非常有效地对其进行索引,以避免二分查找。事实证明,如果操作正确,我们将只需要在每个 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. 如果分区未填充,则取权重最大的变量,并用该变量填充分区。

        重复第 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 &lt; 0.6,所以返回a

        Here is some code and another explanation,但不幸的是它没有使用位移技术,我也没有实际验证过。

        【讨论】:

        • 按位技巧很巧妙,但请记住,使用的随机数必须足够大才能选择分区并在该分区中选择一个值。我不确定如何计算计算第二部分所需的位数,但应该确保它们有足够的位数......(例如,在具有 2^32 个分区的 32 位机器上,你我需要比单个随机数更多的位!)我只为每个采样使用两个随机数。
        • 这是真的,您需要知道生成器为给定样本承诺了多少随机位才能正常工作。如果您不知道,请取两个,因为在现代生成器上,相位(或样本之间的均匀依赖性)非常大。
        • 这里也是 Walker Alias 方法的 Ruby 实现:github.com/cantino/walker_method
        • 您不需要下一个最大的二次幂限制。 N 个权重的 N 个 bin 工作正常。在陡峭的 3 中,您不需要剩余重量最少的物品,只需要小于平均重量的物品。这实际上大大加快了算法的速度,因为您不需要对权重进行排序,只需将它们划分为轻/重。
        • 2 的幂用于位移。你不必使用位移,如果你不使用,你就不会被限制为 2 的幂。此外,最轻的剩余权重是在查找构建时获取的,而不是采样时间,所以它没有太大区别。如果这是一个问题,请使用最小堆。我知道如果您不选择最小值,会有一些微妙的正确性案例,但我不记得它们了。换句话说,否则后果自负。
        【解决方案6】:

        在 O(N) 时间内首先创建一个额外的 O(N) 大小的数据结构之后,可以在 O(1) 时间内进行带替换的加权随机选择。该算法基于 Walker 和 Vose 开发的Alias Method,很好地描述了here

        基本思想是直方图中的每个 bin 将由统一的 RNG 以 1/N 的概率选择。因此,我们将遍历它,对于任何会收到过多命中的人口不足的 bin,将多余的 bin 分配给人口过多的 bin。对于每个 bin,我们存储属于它的命中百分比,以及超出部分的合作伙伴 bin。这个版本跟踪大小的箱子,无需额外的堆栈。它使用合作伙伴的索引(存储在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]))
        

        【讨论】:

          【解决方案7】:

          这里没有提到的一种简单方法是在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 提出的第一种方法非常相似。不幸的是,这种方法在选择元素方面存在偏差(参见方法中的 cmets)。 Efraimidis 和 Spirakis 在链接的论文中证明了他们的方法等同于无放回随机抽样。

          【讨论】:

          【解决方案8】:

          这是我想出的不带替换的加权选择:

          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 是要选择的项目数。

          【讨论】:

          • 第一个功能很棒,但可惜它没有正确地衡量物品的重量。考虑WeightedSelectionWithoutReplacement([(1, 'A'), (2, 'B')], 1)。它将以 1/4 而不是 1/3 的概率选择 A。很难修复。
          • 顺便说一句,更快但更复杂的算法在我的回答中:stackoverflow.com/questions/2140787/…
          • 很高兴找到@JasonOrendorff。事实上,差异是相当糟糕的。对于权重(1、2、3、4),您希望 1/10 的时间会选择“1”,但会选择 1/94 的时间。我真的很想让它起作用!
          • @JasonOrendorff:你是如何计算 1/4 的?如果您有一个公式,我们可以将其反转并用能给出正确结果的权重替换原始权重吗?
          • @LawrenceKesteloot – 对于 1/4,我是这样看的:(random()*1) 范围从 0 到 1。如果为 1,则大于 (random()*2) 的概率为 1/2。如果为0,则几率为0。平均几率为:1/4。
          【解决方案9】:

          我建议您首先查看 Donald Knuth 的 Seminumerical Algorithms 的第 3.4.2 节。

          如果您的数组很大,在 John Dagpunar 的Principles of Random Variate Generation 的第 3 章中有更有效的算法。如果您的数组不是特别大,或者您不关心尽可能多地提高效率,那么 Knuth 中更简单的算法可能就可以了。

          【讨论】:

          • 我刚刚看了第 3.4.2 节,它只涵盖了带替换和不带替换的无偏选择 - 没有提到加权选择。
          • §3.4.1 讨论了 Walker 的别名方法,该方法用于带替换的加权选择。
          猜你喜欢
          • 2012-05-20
          • 2012-01-26
          • 2015-07-05
          • 2010-09-08
          • 1970-01-01
          • 2017-12-26
          • 1970-01-01
          相关资源
          最近更新 更多