JavaScript随机数与加权概率

3
我将尝试创建一个带有以下签名的函数:

function weightedRandom (target, probability) {
  // calculate weighted random number that is more likely to be near target value
  return value; // number from 0 to 1
}

它应该做到以下几点:
  • 生成从0到1之间的随机数,但不包括1
  • 在该范围内选择任意给定数字的概率并非均匀分布
  • 被选中的数字靠近目标值(目标值也是0到1之间的值)的概率更大
  • 概率曲线看起来像钟形曲线,其中目标值具有最高概率,其周围的值逐渐减少,但在0到1范围内的所有值仍然有可能被选中。
  • 这种机会的权重可以使用"probability"值进行调整,其中0表示没有对随机性施加任何加权,而1则表示几乎所有选定的数字都将聚集在目标值周围。
例如,weightedRandom(0.8, 0.2)会产生一个随机值,有20%的可能性聚集在值0.8周围,但可以是从0到1的任何数字。如果概率为0.5,则返回的随机值更多地接近0.8。我认为可能需要另一个参数来定义群集的宽度(标准差?)。
我不是数学家,但被告知可以考虑Beta分布作为可能的工具。

enter image description here

我找到了一些拥有“beta”功能的NPM模块,但我不确定如何使用它们来解决这个问题:
3个回答

6

TLDR: 随机选择两个较简单的分布,也称反变换抽样。

结合两个分布

如果您有一个平坦分布,您可以等概率地选择范围内的任何值。如果您有一个高斯分布,则可以选择接近高斯均值的值。因此,请考虑随机选择执行其中之一。

如果您希望随机值在目标t 80%的时间接近该值,在其他时间接近另一个值20%的时间,则假设“接近”意味着在2个标准差内,并且我们将方差取为v。因此,范围(t-2*v)(t+2*v)需要覆盖P(0.8)。

假设我们将随机使用平坦分布或高斯分布;一个随机值落在给定范围内的概率是两个分布的加权和,权重为分布选择的偏差。如果我们选择高斯分布,则最终得到的值95.45%的时间在2个标准差内。如果我们X%的时间采用高斯分布,则近似概率Pn = P(t-2v至t+2v)= 0.9545*X +(1-X)(4v / r),其中r为完整范围,(4v / r)是范围内平坦分布的比例。
要使Pn达到80%:
0.8 = 0.9545*X + (1-X)(4v/r). 

我们有两个未知数,因此如果我们还需要一个非常接近的概率,即值在目标值的1个标准差范围内60%的时间内,则
0.6 = 0.6827*X + (1-X)(2v/r). 

重新排列成 (2v/r):
(0.8 - 0.9545*X)/(1-X)*2 = (2v/r)
(0.6 - 0.6826*x)/(1-X) = (2v/r)

等式化简

X = 0.81546

因此:
var range = [0, 10];
var target = 7.0;
var stddev = 1.0;
var takeGauss = (Math.random() < 0.81546);
if(takeGauss) {
  // perform gaussian sampling (normRand has mean 0), resample if outside range
  while(1) {
    var sample = ((normRand()*stddev) + target);
    if(sample >= range[0] && sample <= range[1]) {
      return sample;
    }
  }
} else {
  // perform flat sampling
  return range[0]+(Math.random()*(range[1]-range[0]));
}

我认为这个方法可以帮助您得到所需的形状,让您选择近似概率和非常接近概率的两个概率,但避免了过多的复杂性。

由于要求我提供更多实现,我找到了一个正常变量生成器(感谢Ian Neath教授)

function normRand() {
    var x1, x2, rad;

    do {
        x1 = 2 * Math.random() - 1;
        x2 = 2 * Math.random() - 1;
        rad = x1 * x1 + x2 * x2;
    } while(rad >= 1 || rad == 0);

    var c = Math.sqrt(-2 * Math.log(rad) / rad);

    return x1 * c;
};

反变换抽样

我考虑的第一种方法是使用反变换抽样,在这里我将尝试解释它。

假设我们有一个分布,其中从0到4的值是等可能的,但是从4到10的值只有从0到4的一半可能性大。总概率为4a + 6(2*a) = 1,因此a=1/16:

step probability function doubling at 4 from a sixteenth to an eighth

假设您有一个函数,当给定0到1之间的值时,会产生0到10之间的值;它仍然是单调的(没有最小/最大值),但如果您从0到1每0.01递增地提供它,您将得到4:6*2的比率= 1:3,因此超过4的值是低于4的值的3倍。该函数如下所示:

enter image description here

我们有一个从z=0到z=1/3的线性段,其中x(1/3)=4,然后从z=1/3到z=1的线性段延续到x(1)=10。如果我们从0到1之间的平坦概率分布中选择一个随机数z,则x(z)将被分布,前1/3的范围内的值最高为4,其余部分在此之上。
因此,z(x)是一个反变换,它采用平坦分布并产生所需分布的值。如果您想绘制它,它是x<(1/3) ? 9*x : 12*x -1
游戏的目标是构建您满意的分布,并对其进行反转以获得反变换,可以通过使用以上部分或通过解析反转它或某些近似(高斯反转不能解析地写下)来实现。通过这个,您可以将任何平坦分布的样本转换为所需的分布。
从上述步骤分布中进行取样应该像这样进行:
// transform 0-1 flat to 0-10 stepped
function stepInvTransform(z) {
    return (3*z < 1 ? 9*z : (12*z - 1));
}

// sample via inv transform
var sample = stepInvTransform(Math.random());

我真希望我能为您深入且数学严谨的回答提供多个赞!但是,我不确定如何将其转换为实现。如果您能提供一些伪代码,那将非常有帮助。我需要一些时间来处理这个问题,并尝试将所有的数学内容转换成符合所需签名的JavaScript函数。 - Tauren
虽然你的回答很有趣且富有教育性,但我认为它更像是程序员/计算机科学交流风格的回答,而不是适合在SO上的回答。这个网站是用于解决与_具体的、与代码相关的_问题的问答。添加一些理论来解释你的答案是可以的,但仅提供理论会使这个答案不完整。 - Elias Van Ootegem
+1个好答案。对你而言,像“添加代码”这样的备注肯定让你很沮丧 —— 这绝不是重点 :) - Benjamin Gruenbaum
@EliasVanOotegem:我理解你的观点,尽管问题确实更偏向于抽象而非实现。无论如何,我已经找到了一个高斯样本生成器并填补了空白。BenjaminGruenbaum:我不会太在意 :) - Phil H
@PhilH:我知道我的评论可能有点学究,而且是的,这个问题本身更关注理论而不是实际代码,所以实际上这个问题并不适合这个网站。无论如何,你可能想要改变你添加到setInvTransform(x)的函数,而不是z,因为函数体始终使用x。无论如何还是+1。 - Elias Van Ootegem
显示剩余3条评论

1
我会将范围扩展 (Math.round()*2),然后使用一些其他函数 f: [0,2] -> [0,1],它不是1-1映射回0-1区间,但将更多的值映射到0.8的区域。例如,f(x) = c*x^2 - d*x^3,并调整c和d以适应您的分布情况。
请参见this。您可以随意玩弄数值或导出函数并查看c和d如何交互。当然,您可以使用其他函数来获得更高的精度,例如更高次数的多项式或将多项式与指数函数混合以获得各种行为,但这取决于您真正想要什么,以及您希望它如何表现。

好想法,但我不确定如何应用它们。问题在于0.8只是一个例子,而此函数需要支持从0到1的任何目标。我仍然需要找到一种自动化的方法来找到任何给定目标的适当cd值。您有没有关于如何做到这一点的想法? - Tauren
好的,我会拿一张纸,推导出函数,然后尝试用“0.8”和“0.2”表达cd的值。然后,你可以把这个放进代码里面,也就是CalcCD(0.8,0.2) - 这将输出cd的值。从那里开始,你只需要应用上述步骤。*注:数学部分可能有点烦人,但如果你找不到一个能完成工作的包,这样做还是值得的。 - Ramzi Khahil

1

好的,概率是一件相当容易的事情:

//get the probability factor:
var prob = +(probability.charAt(2));//0.[2] <-- coerces 2 to number

这是你将给予随机数生成器的尝试的最大次数。但是,考虑到你接近目标的程度,我们可能需要使用while循环来实现。
var targetDigit = (''+ target).charAt(2),
    prob = +((''+probability).charAt(2)),
    rn,
    closest = 0;//keep track of closest hit
if (probability == 0)
    return Math.random();
do
{
    rn = Math.random();
    //reduce chances for too great of distances, regenerate, but don't count!
    if (Math.abs(rn.toFixed(1).charAt(2)-targetDigit) >= target/2)
        rn = Math.random();
    if (rn.toFixed(1).charAt(2) === targetDigit)
        return rn;//on the money
    if (Math.abs(target - rn) < Math.abs(target - closest))
        closest = rn;
} while (prob--);//while probability left
return closest;//return the closest generated value

当我使用参数 .8 作为目标,.4 作为概率尝试运行此代码时,我得到了以下返回值:
0.8257349738919855 // hit!
0.7696360136541552 // -0.1, close
0.7542727420001457 // -0.1, close
0.9837384401271013 // +0.1, close
0.3078854698178465 // -0.5, far
0.8178311114230021 // hit!
0.6079441227457084 // -0.2, fairly close

我理解你的意思是,这与你预期的相近。

有趣的方法,我会尝试一下,看看结果是否足够接近满足我的需求。 - Tauren

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接