给定二进制随机数生成器生成随机浮点数的正确方法

Proper way to generate a random float given a binary random number generator?

本文关键字:浮点数 方法 随机 二进制 随机数生成器      更新时间:2023-10-16

假设我们有一个二进制随机数生成器,int r();它将返回一个零或一个,概率为0.5。

我看了一下 Boost.Random,他们生成了 32 位并做了这样的事情(伪代码(:

x = double(rand_int32());
return min + x / (2^32) * (max - min);

我对此有一些严重的怀疑。一个替身有 53 位尾数,32 位永远无法正确生成完全随机的尾数,其中包括舍入误差等。

假设IEEE754,在半开放范围内创建均匀分布的floatdouble [min, max)的快速方法是什么?这里的重点是分发的正确性,而不是速度。

为了正确定义 right,正确的分布将等于我们采用无限精确的均匀分布随机数生成器时得到的分布,并且对于每个数字,我们将四舍五入到最接近的IEEE754表示,如果该表示仍在 [min, max) 范围内,否则该数字将不计入分布。

PS:我也会对开放范围的正确解决方案感兴趣。

AFAIK,正确(也可能也是最快的(方法是首先创建一个 64 位无符号整数,其中 52 个分数位是随机位,指数是 1023,如果类型双关语为 (IEEE 754( 双精度,将是 [1.0, 2.0] 范围内的均匀分布随机值。因此,最后一步是从中减去 1.0,从而在 [0.0, 1.0] 范围内得到均匀分布的随机双精度值。

在伪代码中:

rndDouble = bitCastUInt64ToDouble(1023 <<52 | rndUInt64 & 0xfffffffffffff( - 1.0

这里提到了此方法:http://xoroshiro.di.unimi.it(参见"在单位区间内生成均匀双精度值"(

编辑:推荐的方法已更改为:(x>> 11( * (1./(UINT64_C(1( <<53((

有关详细信息,请参阅上面的链接。

这是一个正确的方法,没有试图提高效率。

我们从一个 bignum 类开始,然后是所述 bignum 的理性包装器。

我们产生一个"足够大[min, max)"的范围,以便四舍五入我们的smaller_minbigger_max产生超出该范围的浮点值,这是我们建立在 bignum 上的理性。

现在我们将范围完全细分为中间的两个部分(我们可以这样做,因为我们有一个合理的 bignum 系统(。 我们随机选择两个部分中的一个。

如果在四舍五入后,拾取范围的顶部和底部将 (A( 超出[min, max)(在同一侧,请注意!(,则拒绝并从头开始。

如果 (B( 范围的顶部和底部舍入到相同的double(如果您返回浮点数,则float(,则完成,并返回此值。

否则(C(你递归这个新的,较小的范围(细分,随机选择,测试(。

不能保证此过程会停止,因为您可以不断向下钻取到两个舍入double之间的"边缘",也可以不断选取[min, max)范围之外的值。 但是,发生这种情况的概率是(永远不会停止(,但是,零(假设有一个好的随机数生成器,并且[min, max)大小为非零(。

这也适用于(min, max),甚至在四舍五入的足够胖的康托尔集合中选择一个数字。 只要四舍五入到正确浮点值的实数的有效范围的度量不为零,并且该范围具有紧凑的支持,此过程就可以运行并且终止概率为 100%,但不能对所需时间进行硬性上限。

这里的问题是,在IEEE754中,可能表示的双打不是平均分布的。也就是说,如果我们有一个生成器生成实数,比如 (0,1(,然后映射到IEEE754可表示的数字,结果将不会是等分布的。

因此,我们必须定义"平均分配"。也就是说,假设每个IEEE754数只是在IEEE754舍入定义的区间内撒谎概率的代表,首先生成等分布"数字"和四舍五入到IEEE754的过程将生成(根据定义(IEEE754数字的"均等分布"。

因此,我相信,如果我们选择足够高的精度,上述公式将变得任意接近这样的分布。如果我们将问题限制为在 [0,1] 中查找一个数字,这意味着限制为一组去命名的 IEEE 754 数字,这些数字是一对一的 53 位整数。因此,通过 53 位二进制随机数生成器仅生成尾数应该是快速和正确的。

IEEE 754 算术始终是"无限精度的算术,然后四舍五入",即表示 b 的IEEE754数是最接近 b 的数字(换句话说,您可以认为以无限精度计算的 a*b,然后四舍五入到收盘IEEE754数字(。因此,我相信min + (max-min( * x,其中x是一个面额化的数字,是一种可行的方法。

(注意:从我的评论中可以清楚地看出,我首先不知道您指出最小值和最大值与 0,1 不同的情况。非规范化数字具有均匀分布的属性。因此,您可以通过将 53 位映射到尾数来获得等分布。接下来,您可以使用浮点运算,因为它在机器精度之前是正确的。如果使用反向映射,则将恢复均等分布。

有关此问题的另一个方面,请参阅此问题:将 Int 均匀随机范围缩放为双精度

std::uniform_real_distribution .

在今年的Go Native大会上,S.T.L.有一个非常好的演讲,解释了为什么你应该尽可能使用标准发行版。简而言之,手工卷制代码的质量往往很差(想想std::rand() % 100(,或者有更微妙的均匀性缺陷,比如在(std::rand() * 1.0 / RAND_MAX) * 99中,这是演讲中给出的例子,也是问题中发布的代码的一个特例。

编辑:我看了一下libstdc++的std::uniform_real_distribution实现,这是我发现的:

该实现通过使用范围[0, 1)中生成的某个数字的简单线性变换来生成[dist_min, dist_max)范围内的数字。它使用 std::generate_canonical 生成此源编号,我的实现可在此处找到(在文件末尾(。 std::generate_canonical确定分布范围(表示为整数,此处表示为 r *(适合目标类型的尾数的次数(表示为 k(。然后,它所做的基本上是为尾数的每个r大小的片段生成一个[0, r)数字,并使用算术相应地填充每个段。结果值的公式可以表示为

Σ(i=0, k-1, X/(r^i))

其中X[0, r) 中的随机变量。每个除以范围相当于用于表示它的位数(即log2(r)(的偏移,因此填充相应的尾数段。这样,使用目标类型的整个精度,并且由于结果的范围是 [0, 1) ,指数保持0**(模偏差(,并且当您开始弄乱指数时,您不会遇到均匀性问题。

不会相信这种方法在加密上是安全的隐含性(并且我怀疑在计算r大小时可能存在一个错误(,但我想它在统一性方面比你发布的 Boost 实现可靠得多,绝对比摆弄std::rand要好。

值得注意的是,Boost代码实际上是该算法的退化情况,其中k = 1,这意味着如果输入范围需要至少23位来表示其大小(IEE 754单精度(或至少52位(双精度(,它是等效的。这意味着最小范围分别为~840万或~4.5e15。鉴于这些信息,我不认为如果你使用二进制生成器,Boost实现会削减它。

在简要了解了libc ++的实现之后,看起来他们使用的是相同的算法,但实现方式略有不同。

(*( r实际上是输入加一的范围。这允许使用骨灰盒的max值作为有效输入。

(**( 严格来说,编码指数不是0的,因为 IEEE 754 在有效数的基数之前编码隐式前导 1。然而,从概念上讲,这与该算法无关。