繁体   English   中英

生成加权随机数

[英]Generate A Weighted Random Number

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

自从我的大学统计数据 class 以来已经过去了大约 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 ) );    
}

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

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

生成一个介于 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。

我使用以下

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 |
==================

这里有 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]

这或多或少是@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));
    }
}

怎么样

int [] numbers = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 2 } ;

然后您可以从数字中随机选择,0 将有 80% 的机会,1 10% 和 2 10%

晚了 8 年,但这是我的 4 行解决方案。

  1. 准备一个概率质量函数数组,使得

pmf[array_index] = P(X=array_index):

var pmf = [0.8, 0.1, 0.1]
  1. 为相应的累积分布函数准备一个数组,使得

cdf[array_index] = F(X=array_index):

var cdf = pmf.map((sum => value => sum += value)(0))
// [0.8, 0.9, 1]

3a) 生成一个随机数。

3b) 获取大于或等于该数字的元素数组。

3c) 返回其长度。

var r = Math.random()
cdf.filter(el => r >= el).length

这个在 Mathematica 中,但很容易复制到另一种语言,我在我的游戏中使用它,它可以处理十进制权重:

weights = {0.5,1,2}; // The weights
weights = N@weights/Total@weights // Normalize weights so that the list's sum is always 1.
min = 0; // First min value should be 0
max = weights[[1]]; // First max value should be the first element of the newly created weights list. Note that in Mathematica the first element has index of 1, not 0.
random = RandomReal[]; // Generate a random float from 0 to 1;
For[i = 1, i <= Length@weights, i++,
    If[random >= min && random < max,
        Print["Chosen index number: " <> ToString@i]
    ];
    min += weights[[i]];
    If[i == Length@weights,
        max = 1,
        max += weights[[i + 1]]
    ]
]

(现在我正在谈论列表第一个元素的索引等于 0)这背后的想法是,拥有标准化列表权重有机会weights[n]返回索引n ,因此 min 和 max 之间的距离在步骤n应该是weights[n] 与最小最小值(我们将其设为 0)和最大值最大值的总距离是列表权重的总和。

这背后的好处是您不会附加到任何数组或嵌套 for 循环,这会大大增加执行时间。

这是 C# 中的代码,无需标准化权重列表并删除一些代码:

int WeightedRandom(List<float> weights) {
    float total = 0f;
    foreach (float weight in weights) {
        total += weight;
    }

    float max = weights [0],
    random = Random.Range(0f, total);

    for (int index = 0; index < weights.Count; index++) {
        if (random < max) {
            return index;
        } else if (index == weights.Count - 1) {
            return weights.Count-1;
        }
        max += weights[index+1];
    }
    return -1;
}

我建议使用对概率和其余随机数的连续检查。

此函数首先将返回值设置为最后一个可能的索引并迭代,直到剩余的随机值小于实际概率。

概率之和必须为 1。

 function getRandomIndexByProbability(probabilities) { var r = Math.random(), index = probabilities.length - 1; probabilities.some(function (probability, i) { if (r < probability) { index = i; return true; } r -= probability; }); return index; } var i, probabilities = [0.8, 0.1, 0.1], count = probabilities.map(function () { return 0; }); for (i = 0; i < 1e6; i++) { count[getRandomIndexByProbability(probabilities)]++; } console.log(count);
 .as-console-wrapper { max-height: 100% !important; top: 0; }

这是输入和比率:0 (80%), 1(10%), 2 (10%)

让我们把它们画出来,这样很容易形象化。

                0                       1        2
-------------------------------------________+++++++++

让我们将总重量相加,并将其称为总比率的 TR。 所以在这种情况下 100. 让我们从 (0-TR) 或 (0 到 100 在这种情况下) 随机获取一个数字。 100 是您的总重量。 将其称为随机数 RN。

所以现在我们将 TR 作为总权重,RN 作为 0 和 TR 之间的随机数。

所以让我们想象一下,我们从 0 到 100 中随机选择了一个#。比如说 21。所以实际上是 21%。

我们必须将其转换/匹配到我们的输入数字,但如何转换?

让我们循环遍历每个权重 (80, 10, 10) 并保留我们已经访问过的权重之和。 当我们循环的权重总和大于随机数 RN(在本例中为 21)时,我们停止循环并返回该元素位置。

double sum = 0;
int position = -1;
for(double weight : weight){
position ++;
sum = sum + weight;
if(sum > 21) //(80 > 21) so break on first pass
break;
}
//position will be 0 so we return array[0]--> 0

假设随机数(0 到 100 之间)是 83。让我们再做一次:

double sum = 0;
int position = -1;
for(double weight : weight){
position ++;
sum = sum + weight;
if(sum > 83) //(90 > 83) so break
break;
}

//we did two passes in the loop so position is 1 so we return array[1]---> 1

我有一台老虎机,我使用下面的代码来生成随机数。 在 probabilitiesSlotMachine 中,键是老虎机中的输出,值表示权重。

const probabilitiesSlotMachine         = [{0 : 1000}, {1 : 100}, {2 : 50}, {3 : 30}, {4 : 20}, {5 : 10}, {6 : 5}, {7 : 4}, {8 : 2}, {9 : 1}]
var allSlotMachineResults              = []

probabilitiesSlotMachine.forEach(function(obj, index){
    for (var key in obj){
        for (var loop = 0; loop < obj[key]; loop ++){
            allSlotMachineResults.push(key)
        }
    }
});

现在要生成随机输出,我使用以下代码:

const random = allSlotMachineResults[Math.floor(Math.random() * allSlotMachineResults.length)]

谢谢大家,这是一个有用的线程。 我把它封装成一个方便的函数(Typescript)。 下面的测试(sinon,jest)。 肯定会更紧一点,但希望它是可读的。

export type WeightedOptions = {
    [option: string]: number;
};

// Pass in an object like { a: 10, b: 4, c: 400 } and it'll return either "a", "b", or "c", factoring in their respective
// weight. So in this example, "c" is likely to be returned 400 times out of 414
export const getRandomWeightedValue = (options: WeightedOptions) => {
    const keys = Object.keys(options);
    const totalSum = keys.reduce((acc, item) => acc + options[item], 0);

    let runningTotal = 0;
    const cumulativeValues = keys.map((key) => {
        const relativeValue = options[key]/totalSum;
        const cv = {
            key,
            value: relativeValue + runningTotal
        };
        runningTotal += relativeValue;
        return cv;
    });

    const r = Math.random();
    return cumulativeValues.find(({ key, value }) => r <= value)!.key;
};

测试:

describe('getRandomWeightedValue', () => {
    // Out of 1, the relative and cumulative values for these are:
    //      a: 0.1666   -> 0.16666
    //      b: 0.3333   -> 0.5
    //      c: 0.5      -> 1
    const values = { a: 10, b: 20, c: 30 };

    it('returns appropriate values for particular random value', () => {
        // any random number under 0.166666 should return "a"
        const stub1 = sinon.stub(Math, 'random').returns(0);
        const result1 = randomUtils.getRandomWeightedValue(values);
        expect(result1).toEqual('a');
        stub1.restore();

        const stub2 = sinon.stub(Math, 'random').returns(0.1666);
        const result2 = randomUtils.getRandomWeightedValue(values);
        expect(result2).toEqual('a');
        stub2.restore();

        // any random number between 0.166666 and 0.5 should return "b"
        const stub3 = sinon.stub(Math, 'random').returns(0.17);
        const result3 = randomUtils.getRandomWeightedValue(values);
        expect(result3).toEqual('b');
        stub3.restore();

        const stub4 = sinon.stub(Math, 'random').returns(0.3333);
        const result4 = randomUtils.getRandomWeightedValue(values);
        expect(result4).toEqual('b');
        stub4.restore();

        const stub5 = sinon.stub(Math, 'random').returns(0.5);
        const result5 = randomUtils.getRandomWeightedValue(values);
        expect(result5).toEqual('b');
        stub5.restore();

        // any random number above 0.5 should return "c"
        const stub6 = sinon.stub(Math, 'random').returns(0.500001);
        const result6 = randomUtils.getRandomWeightedValue(values);
        expect(result6).toEqual('c');
        stub6.restore();

        const stub7 = sinon.stub(Math, 'random').returns(1);
        const result7 = randomUtils.getRandomWeightedValue(values);
        expect(result7).toEqual('c');
        stub7.restore();
    });
});

现代 JavaScript 中最短的解决方案

注意:所有权重都必须是整数

function weightedRandom(items){

  let table = Object.entries(items)
    .flatMap(([item, weight]) => Array(item).fill(weight))

  return table[Math.floor(Math.random() * table.length)]
}

const key = weightedRandom({
  "key1": 1,
  "key2": 4,
  "key3": 8
}) // returns e.g. "key1"

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM