生成加权随机数

IT技术 javascript groovy actionscript
2021-01-21 05:48:04

我正在尝试设计一种(好的)方法来从一系列可能的数字中选择一个随机数,其中该范围内的每个数字都被赋予一个权重。简单地说:给定数字范围 (0,1,2),选择一个数字,其中 0 有 80% 的概率被选中,1 有 10% 的几率被选中,2 有 10% 的几率被选中。

我的大学统计课已经过去了大约 8 年,所以你可以想象此刻我无法找到正确的公式。

这是我想出的“又便宜又脏”的方法。此解决方案使用 ColdFusion。您可以使用任何您喜欢的语言。我是程序员,我想我可以处理移植它。最终我的解决方案需要在 Groovy 中 - 我在 ColdFusion 中编写了这个,因为它很容易在 CF 中快速编写/测试。

public function weightedRandom( Struct options ) {

    var tempArr = [];

    for( var o in arguments.options )
    {
        var weight = arguments.options[ o ] * 10;
        for ( var i = 1; i<= weight; i++ )
        {
            arrayAppend( tempArr, o );
        }
    }
    return tempArr[ randRange( 1, arrayLen( tempArr ) ) ];
}

// test it
opts = { 0=.8, 1=.1, 2=.1  };

for( x = 1; x<=10; x++ )
{
    writeDump( weightedRandom( opts ) );    
}

我正在寻找更好的解决方案,请提出改进​​或替代方案。

6个回答

拒绝抽样(例如在您的解决方案中)是第一个想到的事情,即您构建一个查找表,其中包含按权重分布填充的元素,然后在表中随机选择一个位置并返回它。作为一种实现选择,我会创建一个高阶函数,它接受一个规范并返回一个函数,该函数根据规范中的分布返回值,这样您就不必为每次调用构建表。缺点是构建表格的算法性能与项目数量呈线性关系,并且对于大型规范(或那些具有非常小或精确权重的成员,例如 {0:0.99999, 1 :0.00001})。好处是选择一个值有恒定的时间,如果性能至关重要,这可能是可取的。在 JavaScript 中:

function weightedRand(spec) {
  var i, j, table=[];
  for (i in spec) {
    // The constant 10 below should be computed based on the
    // weights in the spec for a correct and optimal table size.
    // E.g. the spec {0:0.999, 1:0.001} will break this impl.
    for (j=0; j<spec[i]*10; j++) {
      table.push(i);
    }
  }
  return function() {
    return table[Math.floor(Math.random() * table.length)];
  }
}
var rand012 = weightedRand({0:0.8, 1:0.1, 2:0.1});
rand012(); // random in distribution...

另一种策略是选择一个随机数[0,1)并迭代权重规范求和,如果随机数小于总和,则返回相关值。当然,这假设权重总和为 1。该解决方案没有前期成本,但平均算法性能与规范中的条目数量呈线性关系。例如,在 JavaScript 中:

function weightedRand2(spec) {
  var i, sum=0, r=Math.random();
  for (i in spec) {
    sum += spec[i];
    if (r <= sum) return i;
  }
}
weightedRand2({0:0.8, 1:0.1, 2:0.1}); // random in distribution...
如果我使用这些参数 arr = {0:0.1, 1:0.7, 2:0.9} 运行该函数 10000 次,它会给我这个输出: 0 : 983 、 1 : 7011 和 2 : 2006 这都是错误的,因为 2 有大于 1 的概率而 outout 表明一些不同的东西。
2021-03-17 05:48:04
@maerics 嘿,请快速检查一下,权重之和是否需要正好为 1?我试过这个 weightedRand({0:0.350, 1:0.200, 2:0.010, 3:0.150 , 4:0.010, 5:0.200, 6:0.150 }); 但我意识到数字 4 经常会出现一个非常大的数字
2021-03-28 05:48:04
@maerics 感谢您的澄清!但是我可以知道常量值 1000 而不是 10 是什么意思吗?
2021-03-30 05:48:04
请注意,您可以存储一个给出累积总和的数组,即执行一次,然后在log n每次生成数字时使用二分查找。但这仅对大 n 有意义。
2021-04-01 05:48:04
@hyperfkcb 是的,权重之和必须为 1,对于这些权重,您需要使用常数值 1000 而不是 10。
2021-04-02 05:48:04

生成一个介于 0 和 1 之间的随机数 R。

如果 R 在 [0, 0.1) -> 1

如果 R 在 [0.1, 0.2) -> 2

如果 R 在 [0.2, 1] -> 3

如果您不能直接获得 0 到 1 之间的数字,请生成一个范围内的数字,该数字将产生所需的精度。例如,如果你有权重

(1, 83.7%) 和 (2, 16.3%) 掷出一个从 1 到 1000 的数字。1-837 是 1。838-1000 是 2。

除非您正在处理条件概率,否则我不建议这样做,这是最好的模型。
2021-03-13 05:48:04
我的一个朋友想出了这种方法的变体: return Math.random() < 0.8 ? 0 : ( Math.random() < 0.9 ? 1 : 2 );
2021-03-31 05:48:04
@ToddSharp 我知道它很古老,但是......你实际上想要使用相同的随机数,否则你会得到一个偏差:r = Math.random(); 返回 (r < 0.8) ? 0 : (r<.9) ? 1 : 2. 在您的代码中,仅当 r1>=.8 AND r2>=.9 时才会返回“2”,即 20% 的 10% 或 2% 的情况。
2021-04-04 05:48:04
这基本上是我要写的,但有代码。
2021-04-07 05:48:04

我使用以下

function weightedRandom(min, max) {
  return Math.round(max / (Math.random() * max + min));
}

这是我的首选“加权”随机数,我使用“x”的反函数(其中 x 是最小值和最大值之间的随机数)来生成加权结果,其中最小值是最重的元素,最大值是最轻(获得结果的机会最小)

所以基本上,使用weightedRandom(1, 5)意味着获得 1 的机会高于 2,高于 3,高于 4,高于 5。

可能对您的用例没有用,但可能对谷歌搜索同样问题的人有用。

经过 100 次迭代尝试,它给了我:

==================
| Result | Times |
==================
|      1 |    55 |
|      2 |    28 |
|      3 |     8 |
|      4 |     7 |
|      5 |     2 |
==================
呃,49 + weightedRandom(1, 51)是很明显的解决方案。谢谢你。
2021-03-22 05:48:04
这是一个顶级的解决方案!
2021-03-30 05:48:04
这有哪些用例?我试过了,weightedRandom(50, 100)但仍然收到 1s 等等,我显然没有抓住重点。
2021-03-31 05:48:04
使一些测试数据在图表中看起来更有说服力的完美解决方案。非常感谢这个聪明的小片段。
2021-04-06 05:48:04
@Solo 有几件事:(1)这种方法非常具体,因为它为最低数字赋予了巨大的权重(优先级),接近f(x)=1/x ......(2)鉴于它使用随机,不能保证它至少会使用每个数字一次......和(3)最后但并非最不重要的,49 + weightedRandom(1, 51)如果你想获得 50 到 100 之间的数字,你应该使用
2021-04-07 05:48:04

这里有 3 种 javascript 解决方案,因为我不确定您想要哪种语言。根据您的需要,前两种中的一种可能有效,但第三种可能最容易用大量数字实现。

function randomSimple(){
  return [0,0,0,0,0,0,0,0,1,2][Math.floor(Math.random()*10)];
}

function randomCase(){
  var n=Math.floor(Math.random()*100)
  switch(n){
    case n<80:
      return 0;
    case n<90:
      return 1;
    case n<100:
      return 2;
  }
}

function randomLoop(weight,num){
  var n=Math.floor(Math.random()*100),amt=0;
  for(var i=0;i<weight.length;i++){
    //amt+=weight[i]; *alternative method
    //if(n<amt){
    if(n<weight[i]){
      return num[i];
    }
  }
}

weight=[80,90,100];
//weight=[80,10,10]; *alternative method
num=[0,1,2]
我喜欢第二个!
2021-04-01 05:48:04

这或多或少是@trinithis 在 Java 中编写的内容的通用版本:我使用整数而不是浮点数来完成它以避免混乱的舍入错误。

static class Weighting {

    int value;
    int weighting;

    public Weighting(int v, int w) {
        this.value = v;
        this.weighting = w;
    }

}

public static int weightedRandom(List<Weighting> weightingOptions) {

    //determine sum of all weightings
    int total = 0;
    for (Weighting w : weightingOptions) {
        total += w.weighting;
    }

    //select a random value between 0 and our total
    int random = new Random().nextInt(total);

    //loop thru our weightings until we arrive at the correct one
    int current = 0;
    for (Weighting w : weightingOptions) {
        current += w.weighting;
        if (random < current)
            return w.value;
    }

    //shouldn't happen.
    return -1;
}

public static void main(String[] args) {

    List<Weighting> weightings = new ArrayList<Weighting>();
    weightings.add(new Weighting(0, 8));
    weightings.add(new Weighting(1, 1));
    weightings.add(new Weighting(2, 1));

    for (int i = 0; i < 100; i++) {
        System.out.println(weightedRandom(weightings));
    }
}