数组中的加权随机选择


72

我想从数组中随机选择一个元素,但是每个元素都有一个已知的选择概率。

(在数组中)所有机会的总和为1。

您会建议哪种算法最快,最适合进行大量计算?

例:

id => chance
array[
    0 => 0.8
    1 => 0.2
]

对于此伪代码,所讨论的算法应在多个调用上统计地返回id上的四个元素,以id上0的一个元素1

Answers:


73

计算列表的离散累积密度函数(CDF)-或简单地说就是权重的累积和数组。然后生成一个介于0和所有权重之和之间的随机数(在您的情况下可能为1),进行二进制搜索以在离散的CDF数组中找到该随机数,并获取与该条目对应的值-是您的加权随机数。


5
@Mikulas Dite:此二进制搜索将在log2(500) = 9每次查找中采取步骤。
thejh 2010年

2
生成一个介于0和权重之和之间的随机数,谁能保证所生成的随机数位于cdf数组中?假设权重数组为[0.1 0.2 0.4 0.3]。cdf数组将为[0.1 0.3 0.7 1.0]。rand值必须在0到1.0之间生成。那么可能是例如0.62,但该值不在cdf数组中。
马齐

2
@Mazzy:您正在寻找包含您生成的随机数的间隔-在这种情况下,间隔的形式为0.3到0.7。当然,您不能指望会出现确切的值,但是无论如何都可以使用二进制搜索来查找间隔。
Sven Marnach 2015年

1
@SvenMarnach也许我不清楚。当我对cdf数组[0.1 0.3 0.7 0.1]应用二进制搜索时,我期望的是在数组中找到rand值。在该示例中,rand值为0.62。应用于cdf数组的二进制搜索算法将在数组中查找0.62值,如果找不到该值,它将退出“未找到”状态。我的意思是二进制搜索必须找到正确的值,否则将不返回任何值
Mazzy

2
@Mazzy:二进制搜索可以轻松地找到您要查找的值所在的时间间隔,这就是您所需要的。标准编程语言库中的大多数二进制搜索实现都不需要找到确切的值,例如lower_bound()在C ++bisect_left()Python中
Sven Marnach

14

该算法很简单

rand_no = rand(0,1)
for each element in array 
     if(rand_num < element.probablity)
          select and break
     rand_num = rand_num - element.probability

这是行不通的,因为我有机会,而不是该地区。| 即使有人拒绝了这个答案,它也给了我一个可行的主意。限制非常简单地计算出来,不应影响性能。
Mikulas Dite 2010年

@Mikulas假设您有离散的机会,并且随机数平均分布在0和1之间,它将给出等于其权重的概率。对于您的情况,有80%的机会随机数会小于.8,因此将选择第一个元素,而有20%的机会其随机数会大于.8,在这种情况下,将选择第二个元素。

这将要求从最小的机会开始对数组进行排序,不是吗?那是我负担不起的计算。(请注意,我没有保留以前选择的元素的列表)
Mikulas Dite 2010年

1
不可以,它无需排序即可工作,并且如果要在选择元素后将其删除,则其速度比二进制搜索更快。

6
抱歉,如果我有两个重量相同的元素怎么办?在这种情况下,我只会得到数组中两个元素的第一个,否则我错了吗?
arpho 2014年

8

我发现本文对于全面了解此问题最有用。 这个stackoverflow问题也可能是您要寻找的。


我相信最佳解决方案是使用Alias方法(维基百科)。它需要O(n)的时间进行初始化,需要O(1)的时间进行选择,还需要O(n)的内存。

这是生成滚动加权n边模具的结果的算法(从这里开始,很难从长度为n的数组中选择一个元素),如本文所述。作者假设您具有滚动公平的骰子(floor(random() * n))和翻转有偏见的硬币(random() < p)的功能。

算法:Vose的别名方法

初始化:

  1. 创建数组AliasProb,每个数组的大小为n
  2. 创建两个工作清单SmallLarge
  3. 将每个概率乘以n
  4. 对于每个缩放的概率p i
    1. 如果p i <1,则将i加到Small
    2. 否则(p≥1 ),添加
  5. 大号小号不为空时:(大号可能会先清空)
    1. Small中删除第一个元素;称它为l
    2. Large删除第一个元素; 称它为g
    3. 设置Prob [l] = p l
    4. 设置Alias [l] = g
    5. 设置p g:=(p g + p l)-1。(这是一个在数值上更稳定的选项。)
    6. 如果p g <1,则将g加到Small
    7. 否则(p≥1 ),添加
  6. 虽然不为空:
    1. Large删除第一个元素; 称它为g
    2. 设置Prob [g] = 1
  7. Small不为空时:仅由于数字不稳定,才有可能。
    1. Small中删除第一个元素;称它为l
    2. 设置Prob [l] = 1

代:

  1. n面模具产生公平的模具辊;叫一边。
  2. 翻转以概率Prob [i]出现的偏向硬币。
  3. 如果硬币“冒出”,则返回i
  4. 否则,返回Alias [i]

8

另一个Ruby示例:

def weighted_rand(weights = {})
  raise 'Probabilities must sum up to 1' unless weights.values.inject(&:+) == 1.0
  raise 'Probabilities must not be negative' unless weights.values.all? { |p| p >= 0 }
  # Do more sanity checks depending on the amount of trust in the software component using this method
  # E.g. don't allow duplicates, don't allow non-numeric values, etc.

  # Ignore elements with probability 0
  weights = weights.reject { |k, v| v == 0.0 }   # e.g. => {"a"=>0.4, "b"=>0.4, "c"=>0.2}

  # Accumulate probabilities and map them to a value
  u = 0.0
  ranges = weights.map { |v, p| [u += p, v] }   # e.g. => [[0.4, "a"], [0.8, "b"], [1.0, "c"]]

  # Generate a (pseudo-)random floating point number between 0.0(included) and 1.0(excluded)
  u = rand   # e.g. => 0.4651073966724186

  # Find the first value that has an accumulated probability greater than the random number u
  ranges.find { |p, v| p > u }.last   # e.g. => "b"
end

如何使用:

weights = {'a' => 0.4, 'b' => 0.4, 'c' => 0.2, 'd' => 0.0}

weighted_rand weights

大致期望:

sample = 1000.times.map{ weighted_rand weights }
sample.count('a') # 396
sample.count('b') # 406
sample.count('c') # 198
sample.count('d') # 0

刚刚使用了这个,就实现了一个公认的名字!谢谢@ wolfgang-teuber!
安倍·佩特里罗

1
使用此方法的一个警告是,如果您的权重为1.0,而其余的权重为0.0,则此方法将无法正常工作。我们将权重作为ENV变量,当我们将权重之一切换为1.0时(即使其始终为真),其影响相反。只是供其他使用此方法的人参考!
安倍·佩特里罗

@AbePetrillo我更新了weighted_rand方法来解决您描述的问题。
knugie

6

红宝石的一个例子

#each element is associated with its probability
a = {1 => 0.25 ,2 => 0.5 ,3 => 0.2, 4 => 0.05}

#at some point, convert to ccumulative probability
acc = 0
a.each { |e,w| a[e] = acc+=w }

#to select an element, pick a random between 0 and 1 and find the first   
#cummulative probability that's greater than the random number
r = rand
selected = a.find{ |e,w| w>r }

p selected[0]

6
在该算法中,最后一个元素将永远不会被选择的,因为它的概率是1.0,和RAND永远是0和1之间
马特达比

6

可以按每个样本O(1)的预期时间进行操作,如下所示。

将每个元素i的CDF F(i)计算为小于或等于i的概率之和。

将元素i的范围r(i)定义为区间[F(i-1),F(i)]。

对于每个间隔[(i-1)/ n,i / n],创建一个存储桶,该存储桶由范围与该间隔重叠的元素列表组成。只要您相当谨慎,整个阵列总共要花费O(n)时间。

当您对数组进行随机采样时,您只需计算随机数位于哪个存储桶中,然后与列表中的每个元素进行比较,直到找到包含它的间隔。

样本成本为O(随机选择的列表的预期长度)<= 2。


如果权重的大小差异很大,则此算法的最坏情况复杂度为O(n)。所有间隔都可能属于同一存储桶。没有权重的其他限制,这绝对不是O(1)甚至不是O(log n)。
斯文·马纳赫

最坏的情况很少发生。如果所有n个间隔都与一个存储桶重叠,那么几乎所有查询都需要将其与一个间隔进行比较。实际上,这将比二进制搜索快得多。如果您坚持针对最坏的情况进行优化,则可以在每个存储桶内进行二进制搜索,从而使最坏情况下的每个查询成本为O(lg(最大存储桶的长度))和O(lg的期望值(随机选择的列表的长度)),其期望值仍为O(1)。
jonderry 2010年

谢谢,看起来真的很好。我将不得不进行一些试验,以确定在我的解决方案中它是否真的比CDF-way更快。
Mikulas Dite 2010年

1
@Mikulas Dite,值得强调的是,这也是一个CDF数组解决方案,与纯二进制搜索的区别有点像执行二进制搜索和哈希搜索数组中的元素之间的区别。另一种看待它的方法是计算CDF数组,而不是对其进行二进制搜索,而是将随机数散列到与存储桶开头相对应的数组索引中。然后,您可以使用所需的任何搜索策略(例如,蛮力线性搜索或二进制搜索)将范围进一步缩小至正确的采样元素。
jonderry 2010年

1
请注意,与通常的“最坏情况”评估相比,您在这里拥有更好的保证,因为根据构造,您的访问被认为是随机的……
comestorm 2010年

5

这是我在生产中使用的PHP代码:

/**
 * @return \App\Models\CdnServer
*/
protected function selectWeightedServer(Collection $servers)
{
    if ($servers->count() == 1) {
        return $servers->first();
    }

    $totalWeight = 0;

    foreach ($servers as $server) {
        $totalWeight += $server->getWeight();
    }

    // Select a random server using weighted choice
    $randWeight = mt_rand(1, $totalWeight);
    $accWeight = 0;

    foreach ($servers as $server) {
        $accWeight += $server->getWeight();

        if ($accWeight >= $randWeight) {
            return $server;
        }
    }
}

3

使用拾音器gem的Ruby解决方案:

require 'pickup'

chances = {0=>80, 1=>20}
picker = Pickup.new(chances)

例:

5.times.collect {
  picker.pick(5)
}

给出了输出:

[[0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 0], 
 [0, 0, 0, 1, 1], 
 [0, 0, 0, 0, 0], 
 [0, 0, 0, 0, 1]]

2

如果数组很小,在这种情况下,我将给数组一个长度为5的值,并根据需要分配值:

array[
    0 => 0
    1 => 0
    2 => 0
    3 => 0
    4 => 1
]

这是最明显的解决方案,但是我不能真正将其用于我要处理的数据量。
Mikulas Dite 2010年

1

诀窍可能是使用元素重复来采样一个辅助数组,以反映概率

给定与其概率相关的元素,以百分比表示:

h = {1 => 0.5, 2 => 0.3, 3 => 0.05, 4 => 0.05 }

auxiliary_array = h.inject([]){|memo,(k,v)| memo += Array.new((100*v).to_i,k) }   

ruby-1.9.3-p194 > auxiliary_array 
 => [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,                                 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4] 

auxiliary_array.sample

如果要尽可能通用,则需要根据最大小数位数计算乘数,并使用它代替100:

m = 10**h.values.collect{|e| e.to_s.split(".").last.size }.max

1

“命运之轮” O(n),仅用于小型阵列:

function pickRandomWeighted(array, weights) {
    var sum = 0;
    for (var i=0; i<weights.length; i++) sum += weights[i];
    for (var i=0, pick=Math.random()*sum; i<weights.length; i++, pick-=weights[i])
        if (pick-weights[i]<0) return array[i];
}

0

我可以想象,大于或等于0.8但小于1.0的数字将选择第三个元素。

换句话说:

x是0到1之间的随机数

如果0.0> = x <0.2:项目1

如果0.2> = x <0.8:项目2

如果0.8> = x <1.0:项目3


0

我将在https://stackoverflow.com/users/626341/masciugo答案上进行改进。

基本上,您可以制作一个大数组,其中元素显示的次数与权重成正比。

它有一些缺点。

  1. 权重可能不是整数。想象一下,元素1的概率为pi,元素2的概率为1-pi。您如何划分?或想象是否有数百个这样的元素。
  2. 创建的数组可能非常大。想象一下,如果最小公倍数是100万,那么我们将需要选择一个数组,其中包含100万个元素。

为了解决这个问题,这就是您要做的。

创建这样的数组,但只能随机插入一个元素。元素插入的概率与权重成正比。

然后从通常选择随机元素。

因此,如果有3个具有不同权重的元素,则只需从1-3个元素的数组中选择一个元素。

如果构造的元素为空,则可能会出现问题。碰巧的是,没有元素出现在数组中,因为它们的骰子滚动不同。

在这种情况下,我建议插入一个元素的概率为p(inserted)= wi / wmax。

这样,将插入一个元素,即概率最高的元素。其他元素将通过相对概率插入。

说我们有2个对象。

元素1出现的时间为.20%。元素2出现的时间为.40%,并且概率最高。

在数组中,元素2将一直显示。元素1将显示一半的时间。

因此,元素2的调用次数将是元素1的2倍。一般而言,所有其他元素的调用将与其权重成比例。它们的所有概率之和也为1,因为该数组将始终至少包含1个元素。


我的数学不对了。看起来使用此技术,具有较高编号的元素将具有较高的实际概率。我建议现在投票最多的答案。
user4951 '16

0

另一种可能性是将与指数分布相关的随机数与该数组的每个元素相关联,该随机数与该元素的权重给出的参数有关。然后选择具有最低“订购号”的元素。在这种情况下,特定元素具有数组最低排序数的概率与数组元素的权重成正比。

这是O(n),不涉及任何重新排序或额外的存储,并且选择可以在单次通过数组的过程中完成。权重必须为正且大于零,但不必求和为任何特定值。

这样做还有一个好处,如果您将顺序号与每个数组元素一起存储,则可以选择通过增加顺序号来对数组进行排序,以获得对数组的随机排序,其中权重较高的元素具有较高的概率早点到来(在确定选择哪个DNS SRV记录,确定要查询的计算机时,我发现这很有用)。

重复进行随机抽样替换需要每次重新通过阵列。对于无选择的随机选择,可以按增加的订购数对数组进行排序,并且k读取元素。

看到关于指数分布维基页(特别是关于这样个变量的集合的最小值的分布的话),用于证明的是,上述是真实的,并且还用于对生成这样的变元的技术的指针:如果Ť在[0,1)中具有均匀的随机分布,则Z = -log(1-T)/ w(其中w是分布的参数;此处是关联元素的权重)具有指数分布。

那是:

  1. 对于数组中的每个元素i,计算zi = -log(T)/ wi(或zi = -log(1-T)/ wi),其中T是从[0,1)中的均匀分布得出的,并且wi是第i个元素的权重。
  2. 选择具有最低zi的元素。

将以概率wi /(w1 + w2 + ... + wn)选择元素i

请参阅下面的Python说明,每10000次试验都通过权重数组一次。

import math, random

random.seed()

weights = [10, 20, 50, 20]
nw = len(weights)
results = [0 for i in range(nw)]

n = 10000
while n > 0: # do n trials
    smallest_i = 0
    smallest_z = -math.log(1-random.random())/weights[0]
    for i in range(1, nw):
        z = -math.log(1-random.random())/weights[i]
        if z < smallest_z:
            smallest_i = i
            smallest_z = z

    results[smallest_i] += 1 # accumulate our choices

    n -= 1

for i in range(nw):
    print("{} -> {}".format(weights[i], results[i]))

编辑(用于历史记录):发布此内容后,我确定无法成为第一个想到这一点的人,考虑到此解决方案的另一次搜索表明确实如此。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.