在包括范围内生成随机浮点数(双精度)。

30

以下函数可以很容易地在所需范围内获取随机浮点数[X,Y)(注意X是包括的,Y是不包括的),因为Math.random()(以及大多数伪随机数生成器,据我所知)产生[0,1)中的数字:

function randomInRange(min, max) {
  return Math.random() * (max-min) + min;
}
// Notice that we can get "min" exactly but never "max".

如何获得一个在指定范围内的随机数,范围包括两个端点,例如[X,Y]

我想我们可以通过将Math.random()(或等价物)的值“增加”,再通过滚动IEE-754浮点双精度的位来将最大可能值精确地放在1.0处,但是这似乎很难正确实现,特别是在不适合位操作的语言中。有更简单的方法吗?

(顺便问一句,为什么随机数生成器产生的数字是在[0,1)而不是[0,1]范围内?)

[编辑] 请注意,我并没有需要这个问题,我完全意识到这个区别非常微小。只是出于好奇想要一些有趣的回答。如果此问题不合适,请随意投票关闭。


2
你能解释一下如何使用最大包含会有任何显著的区别吗?这些是浮点数。0.499999999不应该与0.5有太大的区别。 - Kendall Frey
@KendallFrey:不,我对我的问题没有实际应用。我只是好奇。如果我只是在胡闹,请随意投票关闭。 - maerics
请查看以下链接:https://dev59.com/hXRC5IYBdhLWcg3wP-j4。 - Thomas
1
回答你的“旁白”,很可能是因为它们是从整数PRNG生成的(其范围将在0->2^n-1之间),然后缩放为浮点数。 - Oliver Charlesworth
13个回答

16

我相信有更好的决策,但这个应该有效 :)

function randomInRange(min, max) {
  return Math.random() < 0.5 ? ((1-Math.random()) * (max-min) + min) : (Math.random() * (max-min) + min);
}

4
+1 很好!除了0和1的概率都是其余数字的一半之外。 :) - Kendall Frey
+1 对称,但要注意隐含的假设,即 1-Math.random() 不会丢失精度。 - tc.

6

首先,你的代码存在一个问题:尝试使用randomInRange(0,5e-324)或直接在浏览器的JavaScript控制台中输入Math.random()*5e-324

即使没有溢出/下溢/非规格化数,理性地推断浮点运算仍然很困难。经过一番挖掘,我找到了一个反例:

>>> a=1.0
>>> b=2**-54
>>> rand=a-2*b
>>> a
1.0
>>> b
5.551115123125783e-17
>>> rand
0.9999999999999999
>>> (a-b)*rand+b
1.0

这个问题可以通过a=253和b=0.5来更好地解释:253-1是下一个可表示的数字。默认的舍入模式(“四舍五入到最近的偶数”)将253-0.5向上舍入(因为253是“偶数”[LSB = 0],而253-1是“奇数”[LSB = 1]),所以您减去b得到253,乘以得到253-1,再加上b得到253


回答您的第二个问题:因为底层PRNG几乎总是在区间[0,2n-1]中生成随机数,即生成随机位。很容易选择一个合适的n(浮点表示中的精度位数)并除以2n以获得可预测的分布。请注意,使用此方法您将永远不会生成[0,1)中的某些数字(使用IEEE双精度浮点数,在(0,2-53)中的任何内容都不会生成)。

这也意味着您可以执行a[Math.floor(Math.random()*a.length)]而不必担心溢出(作业:在IEEE二进制浮点数中,证明b < 1意味着对于正整数aa*b < a)。

另一个好处是您可以将每个随机输出x视为表示间隔[x,x+2-53)的区间(不太好的是返回的平均值略小于0.5)。如果您返回[0,1],您是否应该像其他所有内容一样具有同等概率地返回端点,还是它们只应该具有一半的概率,因为它们只代表了其他所有内容的一半间隔?

为了回答返回[0,1]中的数字的简单问题,下面的方法有效地生成一个整数[0,2n](通过在[0,2n+1-1]中生成一个整数并在它太大时丢弃它),然后除以2n

function randominclusive() {
  // Generate a random "top bit". Is it set?
  while (Math.random() >= 0.5) {
    // Generate the rest of the random bits. Are they zero?
    // If so, then we've generated 2^n, and dividing by 2^n gives us 1.
    if (Math.random() == 0) { return 1.0; }
    // If not, generate a new random number.
  }
  // If the top bits are not set, just divide by 2^n.
  return Math.random();
}

这些评论暗示了使用基数2,但我认为假设如下:
  • 0和1应该等概率返回(即Math.random()不利用浮点数在0附近的更紧密间距)。
  • Math.random() >= 0.5的概率为1/2(对于偶数基数应该成立)
  • 底层PRNG足够好,我们可以这样做。
请注意,随机数总是成对生成的:while中的一个随机数(a)总是后面跟着if或末尾的另一个随机数(b)。通过考虑返回0或0.5的PRNG,很容易验证它是合理的:
  • a=0   b=0  : 返回0
  • a=0   b=0.5: 返回0.5
  • a=0.5 b=0  : 返回1
  • a=0.5 b=0.5: 循环
问题:
  • 假设可能不成立。特别是,常见的PRNG是采取48位LCG的前32位(Firefox和Java这样做)。要生成double,您需要从两个连续输出中获取53位,然后除以253,但某些输出是不可能的(您无法使用48位状态生成253个输出!)。我怀疑其中一些从不返回0(假设单线程访问),但我现在不想检查Java的实现。
  • 需要获取额外的位,因此Math.random()对于每个潜在输出都是两倍,但这会对PRNG施加更多限制(要求我们推理上述LCG的四个连续输出)。
  • 平均每个输出调用Math.random()约四次。有点慢。
  • 它确定性地丢弃结果(假设单线程访问),因此几乎可以保证减少输出空间。

5

我对这个问题的解决方案一直是使用以下方法来替换您的上限。

Math.nextAfter(upperBound,upperBound+1)

或者

upperBound + Double.MIN_VALUE

所以你的代码应该是这样的:
double myRandomNum = Math.random() * Math.nextAfter(upperBound,upperBound+1) + lowerBound;

或者

double myRandomNum = Math.random() * (upperBound + Double.MIN_VALUE) + lowerBound;

这只是通过最小的双精度(Double.MIN_VALUE)增加您的上限,以便您的上限将成为随机计算的可能性之一。

这是一个很好的方法,因为它不会偏向任何一个数字而扭曲概率。

唯一无法奏效的情况是当你的上限等于Double.MAX_VALUE时。


而正确的代码是什么来处理 Double.MAX_VALUE 作为上限? - elect

4
只需将您选择的半开区间略微扩大,使其成为所选闭区间的子集。然后,不断生成随机变量,直到它落在该闭区间中。
例如:如果您想要在 [3,8] 中获得均匀分布的值,则需要在 [3,9) 中重复生成均匀随机变量,直到它恰好落在 [3,8] 中。
function randomInRangeInclusive(min,max) {
 var ret;
 for (;;) {
  ret = min + ( Math.random() * (max-min) * 1.1 );
  if ( ret <= max ) { break; }
 }
 return ret;
}

注意:生成半开区间随机变量的次数是随机的,可能是无限次的,但你可以使期望调用次数尽可能接近1,我认为没有不可能无限调用的解决方案。

在数学上它是可行的,但在浮点数世界中,无法保证它会返回“max”。 - tc.

1

给定在0到1之间的“极大”值数量,这真的很重要吗?实际命中1的机会微乎其微,所以它对你正在做的任何事情几乎不会产生显著影响。


2
不好意思,我对我的问题没有实际应用。我只是好奇 =) - maerics

1

在某个范围内生成“均匀”的浮点数是非常棘手的。例如,将随机整数乘以一个常数或除以一个常数,或者通过缩放“均匀”的浮点数以达到所需的范围,这些都有劣势,因为不能以这种方式覆盖范围内浮点格式可以表示的所有数字,并且可能存在微妙的偏差问题。这些问题在 F. Goualard 的 "Generating Random Floating-Point Numbers by Dividing Integers: a Case Study" 中详细讨论。

只是为了展示问题有多么棘手,以下伪代码在闭区间 [lo, hi] 中生成一个随机的“均匀行为”浮点数,其中该数字采用 FPSign * FPSignificand * FPRADIX^FPExponent 的形式。以下伪代码摘自我的 floating-point number generation 部分。请注意,它适用于任何精度和任何基数(包括二进制和十进制)的浮点数。

METHOD RNDRANGE(lo, hi)
  losgn = FPSign(lo)
  hisgn = FPSign(hi)
  loexp = FPExponent(lo)
  hiexp = FPExponent(hi)
  losig = FPSignificand(lo)
  hisig = FPSignificand(hi)
  if lo > hi: return error
  if losgn == 1 and hisgn == -1: return error
  if losgn == -1 and hisgn == 1
    // Straddles negative and positive ranges
    // NOTE: Changes negative zero to positive
    mabs = max(abs(lo),abs(hi))
    while true
       ret=RNDRANGE(0, mabs)
       neg=RNDINT(1)
       if neg==0: ret=-ret
       if ret>=lo and ret<=hi: return ret
    end
  end
  if lo == hi: return lo
  if losgn == -1
    // Negative range
    return -RNDRANGE(abs(lo), abs(hi))
  end
  // Positive range
  expdiff=hiexp-loexp
  if loexp==hiexp
    // Exponents are the same
    // NOTE: Automatically handles
    // subnormals
    s=RNDINTRANGE(losig, hisig)
    return s*1.0*pow(FPRADIX, loexp)
  end
  while true
    ex=hiexp
    while ex>MINEXP
      v=RNDINTEXC(FPRADIX)
      if v==0: ex=ex-1
      else: break
    end
    s=0
    if ex==MINEXP
      // Has FPPRECISION or fewer digits
      // and so can be normal or subnormal
      s=RNDINTEXC(pow(FPRADIX,FPPRECISION))
    else if FPRADIX != 2
      // Has FPPRECISION digits
      s=RNDINTEXCRANGE(
        pow(FPRADIX,FPPRECISION-1),
        pow(FPRADIX,FPPRECISION))
    else
      // Has FPPRECISION digits (bits), the highest
      // of which is always 1 because it's the
      // only nonzero bit
      sm=pow(FPRADIX,FPPRECISION-1)
      s=RNDINTEXC(sm)+sm
    end
    ret=s*1.0*pow(FPRADIX, ex)
    if ret>=lo and ret<=hi: return ret
  end
END METHOD

0
问题类似于问:“在1.0之前的浮点数是什么?” 这样的浮点数存在,但对于IEEE float来说是2^24中的一个,而对于double来说是2^53中的一个。

实际上,差异微不足道。


@OliCharlesworth:哪一个是sweeping - wallyk
实际上,这两者之间的差别微不足道。或许可以想象出一种情况,其中这个差别非常重要。 - Oliver Charlesworth
1
@OliCharlesworth,实际上你可能做不到。如果你对“1”和次小浮点数之间的差异敏感,那么你的结果将被浮点数精度问题所影响,并且在你使用高精度的工具之前将不代表任何有意义的东西(此时你已经不再对“1”和次小浮点数之间的差异敏感)。 - Aaron Dufour
其实更像是1/2**53的结果;) - tc.
@tc:是的,你说得很对。我在写2^56时比我想象中快了一些,并且忘记了指数是11位,而不是IEEE-754 shortreal的8位。我已经修复了它,尽管这对我的观点没有任何影响。 - wallyk

0

在什么情况下,您需要将浮点值包含在上限内?对于整数我理解,但对于浮点数,包含和不包含之间的差异是多少,像1.0e-32这样。


0

这样想吧。如果你想象浮点数具有任意精度,那么得到恰好min的机会为零。得到max的机会也是如此。我会让你自己得出结论。

这个“问题”相当于在0和1之间的实线上随机取一个点。没有“包含”和“不包含”的概念。


而且得到任何特定值的概率也是零,所以总结起来你不会得到任何结果? - Bergi
实际上,有无限多的实数,所以 0 * 无穷大 = 未定义。事实上,结果是1。这是因为上述方程中的0由 1(概率总和)/ 无穷大(可能性数量)= 0(任何一个数字的概率) 确定。使用这个逻辑,你可以简化 0 * 无穷大 = 1 / 无穷大 * 无穷大 = 1 - Kendall Frey
1
这个论点的问题在于浮点数并没有任意精度!在任何真实世界的实验中,你都可以通过统计学方法来证明差异。 - Oliver Charlesworth
在任何真实的实验中,要获得足够数量的随机数样本需要耗费太多的 CPU 使用年数。 - Kendall Frey
@KendallFrey:单精度浮点数只有2^32个不同的值(其中相当一部分不在范围[0,1]内);20亿次迭代并不是很长时间。(但对于双精度浮点数,您可能是安全的。) - Oliver Charlesworth
我同意你关于单精度的观点。由于问题提到了“一个IEE-754浮点双精度”,所以我假设是双精度。 - Kendall Frey

0

我经验相对较少,因此我也在寻找解决方案。

这是我的初步想法:

随机数生成器产生的数字是 [0,1) 而不是 [0,1],

因为 [0,1) 是一个单位长度,可以跟随 [1,2) 等而不重叠。

对于 random[x, y],您可以这样做:

float randomInclusive(x, y){

    float MIN = smallest_value_above_zero;
    float result;
    do{
        result = random(x, (y + MIN));
    } while(result > y);
    return result;
}

当[x,y]中的所有值具有相同的选择可能性,并且您现在可以到达y时。


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