【问题标题】:Generate Random Weighted value生成随机加权值
【发布时间】:2011-05-01 03:13:06
【问题描述】:

编辑:我重写了这个问题,希望目标更清晰一些。

这是对here这个问题的扩展问题,我真的很喜欢this answer提供的功能。

在上面的答案中,可以设置达到极值的概率,数字越大获得更低数字的概率越高,反之亦然。问题是我必须设置 3 个组的概率。这些组是最低值 (LV)、最高值 (HV) 和中间值 (MV)。不过为了简化请求,我们可以考虑EVP=HVP=LVP

给定 任何 范围,HV/LV 应基于指定的 EVP 出现,并且随着您从每个极端在该范围内前进/下降,该范围内下一个值的概率将增加,或减少,取决于 EVP 和 MVP 之间的距离。

使用 1-6 的示例范围,其中 1 和 6 的权重为 5% (EVP),概率分布为 1/6 为 5%,2/4 为 15%,3/4 为 30 %(MVP),总计 100%。反过来也应该是可能的,交换 EVP 和 MVP 应该会产生下图的倒数。

这是一张我希望能传达给定示例预期结果的图像。

中等偏重:

奖励:如果我能够分别设置 HVP 和 LVP 产生类似于下图的结果,那就太好了(注意:该图与上面的规范不准确)。

中等加权(奖金):

谢谢!

【问题讨论】:

  • @Dr Herbie:我已经改写了这个问题。希望这能更好地描述我的目标。谢谢!
  • 我不确定我做对了:你给了一些百分比,你想取回一个随机数。对吗?
  • @nikic,看来您的回答正确。我正在寻找理论上更有活力的东西,但你的回答让我深思:)
  • 快速提问:您是在追求连续随机数(在端点之间随机选择任何十进制值)还是离散随机数(从数字列表中随机选择)?

标签: c# php random


【解决方案1】:

因为我今天因为流感被困在家里 :( 我决定尝试为你解决这个问题。基本上你要求的是某种插值。我使用了最简单的(线性)和这些是我的结果和代码。代码有点乱,我可能会在未来几天修复它..

<?php

// this function interpolates $a to $b over $steps steps, starting from key $k
// this can be cleaned up significantly
function interpolate($a, $b, $steps, $k) {
    @$per_step = abs($a - $b)/$steps; // suppress warnings in case of division by zero
    if ($a > $b)
        $decreasing = true;
    else
        $decreasing = false;
    $final = array();
    for ($i = 1; $i <= $steps-1; ++$i) {
        if ($decreasing)
            $final[$i+$k] = $a-=$per_step; // linear interpolation
        else
            $final[$i+$k] = $a+=$per_step; // linear interpolation
    }
    return $final;
}

// this function combines probability arrays after the interpolation occurs
// this may happen multiple times, think about 1, 3, 5. interpolation would have to occur
// from 1 -> 2 -> 3, and from 3 -> 4 -> 5.
function interpolateProbabilities ($nodes) {
    $pNodes = array();
    $pNodes = $nodes;
    $keys = array_keys($nodes);
    for ($i = 0; $i < count($keys); $i++) {
        if ($keys[$i+1] - $keys[$i] != 1) {
            $pNodes += interpolate($nodes[$keys[$i]], $nodes[$keys[$i+1]], $keys[$i+1] - $keys[$i], $keys[$i]);
        }
    }
    ksort($pNodes);
    return $pNodes;
}

// this generates a weighed random value and is pretty much copy-pasted from:
// http://w-shadow.com/blog/2008/12/10/fast-weighted-random-choice-in-php/
// it's robust and re-writing it would be somewhat pointless
function generateWeighedRandomValue($nodes) {
    $weights = array_values($nodes);
    $values = array_keys($nodes);
    $count = count($values);
    $i = 0;
    $n = 0;
    $num = mt_rand(0, array_sum($weights));
    while($i < $count) {
        $n += $weights[$i];
        if($n >= $num) {
            break;
           }
        $i++;
       }
    return $values[$i];
}

// two test cases
$nodes = array( 1 => 12, 5 => 22, 9 => 31, 10 => 35); // test 1
$nodes = array( 1 => 22, 3 => 50, 6 => 2, 7 => 16, 10 => 10); // test 2
$export = array();

// run it 1000 times
for ($i = 0; $i < 1000; ++$i) {
    $export[generateWeighedRandomValue(interpolateProbabilities($nodes))]++;
}

// for copy-pasting into excel to test out distribution
print_r($export);

?>

我认为,结果正是您想要的。 在这种情况下:

$nodes = array( 1 => 12, 5 => 22, 9 => 31, 10 => 35); // test 1

我得到了以下(最终)数组:

Array
(
    [5] => 92
    [7] => 94
    [10] => 162
    [8] => 140
    [3] => 71
    [6] => 114
    [2] => 75
    [4] => 69
    [9] => 131
    [1] => 52
)

也就是说,1 应该发生 12% 的时间,5 22%,9 31% 和 10 35% 的时间。让我们绘制它:

看起来很有希望,但让我们尝试一些更疯狂的东西......

$nodes = array( 1 => 22, 3 => 50, 6 => 2, 7 => 16, 10 => 10); // test 2

在这种情况下,3 应该出现 50% 的时间,然后急剧下降到 6。让我们看看发生了什么!这是数组(回想起来,我应该对这些数组进行排序):

Array
(
    [4] => 163
    [7] => 64
    [2] => 180
    [10] => 47
    [1] => 115
    [5] => 81
    [3] => 227
    [8] => 57
    [6] => 6
    [9] => 60
)

让我们看看图片:

它看起来有效:)

我希望我能够解决您的问题(或至少为您指明正确的方向)。请注意,我的代码目前有许多规定。也就是说,您提供的初始节点的概率必须为 100%,否则您可能会遇到一些不稳定的行为。

另外,代码有点乱,但概念相对简单。其他一些很酷的东西是尝试而不是使用线性插值,而是使用其他类型,这会给你带来更有趣的结果!


算法

为避免混淆,我将准确展示该算法的工作原理。 我给 PHP 一个 $node 数组,其形式为 integer =&gt; frequency in percentage,最终看起来类似于 array( 1 =&gt; 22, 3 =&gt; 50, 6 =&gt; 2, 7 =&gt; 16, 10 =&gt; 10),从上方看是 test 2

Test 2 基本上表示您希望将 5 个控制节点放置在1, 3, 6, 7, and 10,频率分别为22%, 50%, 2%, 16%, and 10%。首先,我需要准确地查看需要进行插值的何处。例如,我不需要在67 之间做,但我需要在13 之间做(我们需要插值2 ) 和 710(我们需要插入 89)。

1 -&gt; 3 之间的插值有(3 - 1) - 1 = 1 步长,应插入原始数组中的key[2]1 -&gt; 3 插值的值 (%) 是 abs($a - $b) / $steps,它转换为 1% 的绝对值减去 2% 除以 steps + 1,其中我们的例子恰好等于14。我们需要看看函数是增加还是减少(你好微积分)。如果函数正在增加,我们会继续添加% 到新的插值数组,直到我们填满所有空点(如果函数正在减少,我们减去步 % value。因为我们只需要填一个位置,我们返回2 =&gt; 36 (22 + 14 = 36)。

我们组合数组,结果是(1 =&gt; 22, 2 =&gt; 36, 3 =&gt; 50, 6 =&gt; 2, 7 =&gt; 16, 10 =&gt; 10)。程序插入了2,这是一个我们没有明确声明的百分比值。

7 -&gt; 10的情况下,有2个步骤,步骤百分比是2,来自(16-10) / (3 + 1) = 2。函数是递减的,所以我们需要反复减去2。最终的插值数组是(8 =&gt; 14, 9 =&gt; 12)。我们结合所有的数组,瞧。

下图显示了绿色(初始值)和红色(插值)。您可能必须“查看图像”才能清楚地看到整个事情。您会注意到我使用了±,因为算法需要确定我们应该在一段时间内增加还是减少。


这段代码可能应该以更多的 OOP 范式编写。我经常使用数组键(例如,我需要传递$k,因此一旦我从interpolate($a, $b, $steps, $k) 返回它们,组合数组就更容易了,因为它们自动拥有正确的键。这只是一个PHP 特质,回想起来,我可能应该从更易读的 OOP 方法开始。


这是我最后一次编辑,我保证 :) 因为我喜欢玩 Excel,所以这显示了在插入数字后百分比如何正常化。看到这一点很重要,特别是考虑到在您的第一张照片中,您所展示的内容在数学上有些不可能。

Test 1 Test 2

您会注意到百分比显着下降以适应插值。您在现实中的第二个图表看起来更像这样:

在这张图表中,我称重了1 = &gt; 1, 5 =&gt; 98, 10 =&gt; 1,您可以看到阻尼效果的极端情况。毕竟,根据定义,百分比加起来必须是 100!重要的是要意识到阻尼效果与极端之间的步数成正比。

【讨论】:

  • 当我试图解决这个问题时,我一直在问自己这相当于什么数学术语,但遗憾的是,我永远无法回答这个问题。总而言之,您的答案甚至比我提出的问题更可靠,我可以将范围的任何部分设置为概率,并根据设定值之间的距离获得所有其他值的随机数。我只是希望我能更多地了解它背后的巫术。让我回顾一下,我一定会在时间到之前回复你:) P.S.感觉好多了!
  • 谢谢,希望对您有所帮助!我添加了一个关于描述算法的部分,以便您了解引擎盖下发生的事情。 OOP 格式的代码可能更清晰易读,但我将把它作为练习留给读者;)
  • 我正要向你扔一个弧线球。但是,我将在另一个问题中问它,以免感觉我在这里抢劫你,毕竟你生病了;)。除非第二天之内出现如此惊人的世界,否则你就赢了。只是想亲自感谢你。
  • 谢谢,我刚刚添加了一些关于插值百分比的概率衰减行为及其数学工作原理的更多信息。
  • 只是想再次感谢您。今天再次偶然发现这个问题,这个答案太棒了:P
【解决方案2】:

假设您可以处理百分比的整数,只需为 0 到 99 之间的每个值分配一个结果 - 例如0-9 的结果可能是 1,而 95-99 的结果可能是 6(给出 10%=1 和 5%=6 的情况)。一旦你获得了翻译功能(无论你如何实现——你可以使用多种方法),你只需要生成一个 0-99 范围内的随机数并将其翻译成结果。

就您想要的代码(甚至是哪种语言 - C# 或 PHP?)而言,您的问题并不是很清楚,但希望这会有所帮助。

这里有一些 C# 代码,可以让您在合理的范围内获得您喜欢的任何偏差 - 您不必将其表示为百分比,但您可以这样做:

static int BiasedRandom(Random rng, params int[] chances)
{
    int sum = chances.Sum();
    int roll = rng.Next(sum);
    for (int i = 0; i < chances.Length - 1; i++)
    {
        if (roll < chances[i])
        {
            return i;
        }
        roll -= chances[i];
    }
    return chances.Length - 1;
}

例如,您可以使用

int roll = BiasedRandom(rng, 10, 10, 10, 10, 10, 50) + 1;

这将为 1-5 中的每一个提供 10% 的机会,并有 50% 的机会获得 6。

【讨论】:

  • 您好,感谢您的快速回复。关于语言,我对 PHP 或 C# 都很满意,这就是我通过标签和问题请求的原因。然而,数学显然不是强项(至少目前不是)。我更喜欢类似于另一个问题(实际上是一个 PHP 示例)的答案中使用的“gamma”方法来简化概率级别。因此,通过将 1.5 指定为“gamma”(或者每个奖金可能为 1.5 gammaHigh 0.9 gammaLow),它会按照前面的答案中所述工作,但会影响上限和下限。
  • @Kevin:对我来说确切地并不明显概率与伽马有关。我使用了您将两个概率表示为百分比并从中推断的事实。您谈到使用“产生的级联” - 但它是如何定义的?您面临的真实场景是什么?
  • 嘿乔恩。我已经更新了我的问题。如果这是一个更好的解释,请告诉我。
【解决方案3】:

在 C# 中快速而肮脏的方式:

T PickWeightedRandom<T>(IEnumerable<Tuple<T,double>> items, Random r)
{
    var sum = 0.0;
    var rand = r.NextDouble();
    return items.First(x => { sum += x.Item2; return rand < sum; }).Item1;
}

测试代码:

var values = new [] { 
    Tuple.Create(1, 0.05),
    Tuple.Create(2, 0.15),
    Tuple.Create(3, 0.3),
    Tuple.Create(4, 0.3),
    Tuple.Create(5, 0.15),
    Tuple.Create(6, 0.05),
};

const int iterations = 1000;

var counts = new int[values.Length];
var random = new Random();

for (int i = 0; i < iterations; i++)
{
    counts[PickWeightedRandom(values, random)-1]++;
}

foreach (var item in counts)
{
    Console.WriteLine(item/(double)iterations);
}

输出(迭代次数 = 1000000):

0.050224
0.150137
0.300592
0.298879
0.150441
0.049727

看起来像:

【讨论】:

  • 你好。感谢您花时间回答。我在您的回答中看到的唯一问题是我必须知道我想要设定时间的概率。虽然示例是简单的 1-6,但实际模式很容易在 1-1,000,000,000 范围内。随着时间的推移,显然不可能设置 1,000,000,000 个概率(并更改它们)。
【解决方案4】:

生成非均匀随机数的一般技术是使用rejection sampling。即使在这种情况下它可能无效,您仍然应该知道如何执行此操作,因为它适用于您提供的任何密度函数。

function random($density, $max) {
    do {
        $rand = lcg_value();
        $rand2 = lcg_value() * $max;
    } while ($density($rand) < $rand2);
    return $rand;
}

$density 这是一个密度函数,它接受一个介于 0 和 1 之间的浮点数作为参数,并返回一个小于 $max 的值。对于您的示例,此密度函数可能是:

$density = function($x) {
    static $values = array(
        1 => 0.05,
        2 => 0.15,
        3 => 0.30,
        4 => 0.30,
        5 => 0.15,
        6 => 0.05,
    );

    return $values[ceil($x * 6)];
};

一个示例调用将是:

ceil(random($density, 0.3) * 6); // 0.3 is the greatest value returned by $density
// round and * 6 are used to map a 0 - 1 float to a 1 - 6 int.

如果您无法轻松计算分布的倒数,则拒绝抽样特别有用。在这种情况下,使用inverse transform sampling 计算逆很容易,这可能是更好的选择。但是Jon's answer 已经涵盖了这一点。

PS:上述实现是通用的,因此使用 0 到 1 之间的随机值。通过构建一个仅适用于您的方法的函数,一切都会变得更容易:

function random() {
    static $values = array(
        1 => 0.05,
        2 => 0.15,
        3 => 0.30,
        4 => 0.30,
        5 => 0.15,
        6 => 0.05,
    );

    do {
        $rand = mt_rand(1, 6);
        $rand2 = lcg_value() * 0.3;
    } while ($values[$rand] < $rand2);
    return $rand;
}

random();

【讨论】:

  • +1 用于拒绝采样的课程,并将其与 php 的新闭包结合起来以实现回调/密度。正如您所指出的,它可能无法 100% 满足我的需求,但我非常喜欢数学课与交出代码。
【解决方案5】:

首先,您需要表征您当前的随机数生成器。在 PHP 的情况下,rand() 函数返回一个漂亮的平面配置文件 - 所以不需要预处理。

重新映射输出分布函数,使其下的面积为单位,范围从0开始。然后计算它的积分。存储积分(例如,作为值数组)。然后当您需要一个随机数匹配配置文件时,首先从内置生成器中获取一个介于 0 和 1 之间的随机数,然后在积分上找到 Y 坐标,其中 X 坐标是您生成的值。最后,将值缩放到所需范围(例如,如果查找 0 到 10 之间的值,则乘以 10,如果查找 -8 和 +8 之间的值,则乘以 16 并减去 8)。

如果您的随机数生成器不生成平面配置文件,那么最简单的方法是使用与上述方法相反的方法将其转换为平面配置文件。

【讨论】:

    【解决方案6】:

    我没有尝试过,但我认为这可能有效:

    $random($probability)
    {
        $rnd = rand() / getrandmax();
    
        foreach($probability as $num => $prob)
        {
            $rnd -= $prob;
            if($rnd <=0)
                return $num;
        }
    
        return -1; //this should never happen
    }
    

    并这样称呼它(使用您的第二个示例):

    $distribution = array(
        1 => 0.10,
        2 => 0.15,
        3 => 0.30,
        4 => 0.27,
        5 => 0.14,
        6 => 0.04);
    
    $number = random($distribution);
    

    【讨论】:

      猜你喜欢
      • 2013-11-21
      • 1970-01-01
      • 2010-12-04
      • 2012-09-13
      • 2022-07-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多