基于随机位流生成随机浮点数值

6

如何从随机源(生成随机比特流的生成器)中生成给定范围内均匀分布的随机浮点数?

假设我的随机源看起来像:

unsigned int GetRandomBits(char* pBuf, int nLen);

我希望你能帮我实现这个功能。
double GetRandomVal(double fMin, double fMax);

注:

  • 我不希望结果的精度受到限制(例如仅限于5位数字)。
  • 必须严格满足均匀分布。
  • 我不要求引用现有库。 我想“知道如何”从头开始实现它。
  • 如果需要伪代码/代码,最好使用C++。

3
为什么要重复造轮子? 所有平台都有好的(伪)随机源。 - the JinX
你有没有考虑到在给定的范围内可能并不恰好有2^n个可能的值?“均匀”到底有多均匀呢? :) - Karl Knechtel
2
@the JinX:伪随机和真随机是完全不同的东西...考虑制作一次性加密密码本——你不会用rand()来做这个的;-P - Tony Delroy
非常正确,但是自己创建随机内容通常更糟糕。 大多数系统也有一个好的“真”随机源。 - the JinX
2
@the JinX:这个问题是关于如何将现有的“好的随机源”比特转换为均匀分布的双精度浮点数随机源。 - j_random_hacker
显示剩余2条评论
8个回答

9

我不认为我会被说服你真的需要这个,但写这个确实很有趣。

#include <stdint.h>

#include <cmath>
#include <cstdio>

FILE* devurandom;

bool geometric(int x) {
  // returns true with probability min(2^-x, 1)
  if (x <= 0) return true;
  while (1) {
    uint8_t r;
    fread(&r, sizeof r, 1, devurandom);
    if (x < 8) {
      return (r & ((1 << x) - 1)) == 0;
    } else if (r != 0) {
      return false;
    }
    x -= 8;
  }
}

double uniform(double a, double b) {
  // requires IEEE doubles and 0.0 < a < b < inf and a normal
  // implicitly computes a uniform random real y in [a, b)
  // and returns the greatest double x such that x <= y
  union {
    double f;
    uint64_t u;
  } convert;
  convert.f = a;
  uint64_t a_bits = convert.u;
  convert.f = b;
  uint64_t b_bits = convert.u;
  uint64_t mask = b_bits - a_bits;
  mask |= mask >> 1;
  mask |= mask >> 2;
  mask |= mask >> 4;
  mask |= mask >> 8;
  mask |= mask >> 16;
  mask |= mask >> 32;
  int b_exp;
  frexp(b, &b_exp);
  while (1) {
    // sample uniform x_bits in [a_bits, b_bits)
    uint64_t x_bits;
    fread(&x_bits, sizeof x_bits, 1, devurandom);
    x_bits &= mask;
    x_bits += a_bits;
    if (x_bits >= b_bits) continue;
    double x;
    convert.u = x_bits;
    x = convert.f;
    // accept x with probability proportional to 2^x_exp
    int x_exp;
    frexp(x, &x_exp);
    if (geometric(b_exp - x_exp)) return x;
  }
}

int main() {
  devurandom = fopen("/dev/urandom", "r");
  for (int i = 0; i < 100000; ++i) {
    printf("%.17g\n", uniform(1.0 - 1e-15, 1.0 + 1e-15));
  }
}

一个方法是通过将frexp替换为读取实际指数的代码片段来使得它适用于非规范化或零。此后,负数a和b也不会太难(尽管您必须考虑到double以符号-幅度顺序排列并相应地调整x_bits的采样)。 - a dabbler
难以置信!这太棒了! - Lior Kogan

5
这是一种做法。
IEEE Std 754双精度格式如下:
[s][     e     ][                          f                         ]

s为符号位(1位),e为偏置指数(11位),f为小数部分(52位)。

请注意,在小端机器上,内存中的布局将不同。

对于0 < e < 2047,所表示的数字为

(-1)**(s)   *  2**(e – 1023)  *  (1.f)

通过将s设置为0,e设置为1023,f从您的位流中获取52个随机位,您可以在区间[1.0, 2.0)中获得一个随机双精度数。该区间独特之处在于它包含2 ** 52个双精度数,并且这些双精度数是等距的。如果您从构造的双精度数中减去1.0,则可以获得一个在区间[0.0, 1.0)内的随机双精度数。此外,等距性质得以保留。 从那里开始,您应该能够根据需要进行缩放和平移。

那真的很酷。虽然我必须承认我不太理解 :D - tenfour

4

我很惊讶这个问题这么老了,竟然没有最佳答案的实际代码。User515430的答案是正确的——你可以利用IEEE-754双精度格式,直接将52位放入双精度浮点数中,完全不需要进行任何数学计算。但他没有给出代码。因此,这里有来自我公共领域ojrandlib的代码:

double ojr_next_double(ojr_generator *g) {
    uint64_t r = (OJR_NEXT64(g) & 0xFFFFFFFFFFFFFull) | 0x3FF0000000000000ull;
    return *(double *)(&r) - 1.0;
}

NEXT64()函数用于获取一个64位的随机数。如果您有更高效的方法只获取52位,请使用该方法。


我认为你的方法不会导致严格的均匀分布。在范围[1..2]中的2^52个值与范围[0..1]之间没有一对一的映射,可以使其均匀。有些随机值比其他值更常见。 - Lior Kogan
在IEEE-754双精度浮点数中,它尽可能地均匀。这里的2^52个双精度浮点数实际上是等距的,范围从1.0到最大值<2.0,可以表示为双精度浮点数(在我的机器上,这将打印为1.999999999999999778)。 - Lee Daniel Crocker
我还应该指出,我已经对这段代码进行了广泛的一致性测试,并发现它非常健壮。我的库包括这些测试,您可以随意查看。 - Lee Daniel Crocker
你说得对,在区间[0,1)中有超过2^52个可表示的值,但它们并不是均匀分布的,因此编写能够生成所有这些值且仍然均匀的代码将非常复杂。我想这是可行的,并且可能是一个有趣的练习,但与给定的代码相比,你只会获得一位额外的精度,所以我怀疑它是否值得。 - Lee Daniel Crocker
你也说得对;-) 对于区间[0,1),这并不值得麻烦,但是对于任意区间,比如[0,1e6),将结果乘以1e6会非常低效。 - Lior Kogan
显示剩余2条评论

3

只要您拥有一个与double相同位数精度的整数类型,这就很容易。例如,IEEE双精度浮点数具有53位精度,因此64位整数类型就足够了:

#include <limits.h>
double GetRandomVal(double fMin, double fMax) {
  unsigned long long n ;
  GetRandomBits ((char*)&n, sizeof(n)) ;
  return fMin + (n * (fMax - fMin))/ULLONG_MAX ;
}

1
实际上,您正在从2 ^ 64个可能的值映射到2 ^ 53个可能的值。这样的映射不会提供均匀分布(是的,对我来说这样的精度很重要)。 - Lior Kogan
4
"浮点数"并非均匀分布。如果您需要更好的结果,就需要建造神话般的“真实RAM”。 - a dabbler
@a dabbler:谢谢,这确实是正确的。但是,在给定的狭窄范围内(例如从34到35),浮点数不会均匀分布吗? - Lior Kogan
@Lior Kogan:为什么它不会是均匀的?通过压缩值,您不会改变均匀性,就像IEEE浮点数的基本颗粒度一样。 - tenfour
打个反对的腔调:这个函数在接近零的值附近没有充分利用有效数字,也许提问者想在随机数非常小相对于边界时执行额外的、精度敏感的处理。做正确的事情可能是再生成一个随机值,但谁知道呢?像往常一样,我们不知道真正的问题是什么。 - a dabbler
@Lior Kogan:好的,请告诉我们fMin和fMax是什么,我会更新我的答案。否则这个问题很难:例如,假设fMin = 0且fMax = 1,并且您要求在[0,1]中选择每个可能的数字x的表示方式的概率等于其所代表区间的大小。因此,大约有2^63个这样的区间,但它们的大小从约2^-1023到约2^-53不等。因此,在最坏的情况下,您可能需要1023个随机位。 - TonyK

2
这可能不是你想要的答案,但是这里的规范:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3225.pdf在 [rand.util.canonical] 和 [rand.dist.uni.real] 部分包含了足够实现你想要的内容的信息,只是语法略有不同。虽然不容易,但是可以做到。我从个人经验中说话。一年前我什么都不懂关于随机数,但我还是能做到。尽管花了我一段时间... :-)

1
这个问题提出得不够明确。"uniform distribution over floats" 是什么意思?
借鉴 discrepancy 的方法,我们可以将您的问题具体化为:寻找最小化以下值的分布:

\int_{t=fmin}^{fmax} \left(p\left(x \leq \text{t} \right ) - \frac{t-fmin}{fmax-fmin} \right )^2dt

其中x是您使用GetRandomVal(double fMin, double fMax)函数进行抽样的随机变量,而p(x <= t表示随机x小于或等于t的概率。

现在,您可以继续尝试评估例如一个业余者的答案。 (提示:所有未使用整个精度并坚持例如52位的答案都将无法满足此最小化准则。)

然而,如果您只想生成所有浮点位模式,并使其落入指定范围内的可能性相等,即使这意味着例如请求 GetRandomVal(0,1000) 会创建更多值在0到1.5之间而不是1.5到1000之间,那很容易:将任何IEEE浮点数间隔解释为位模式后,映射到一小部分unsigned int64间隔。请参见例如此question。在给定间隔内生成等分布的unsigned int64随机值很容易。

0

要在 [0..1[ 范围内获取一个随机值,你可以这样做:

double value = 0;
for (int i=0;i<53;i++)
   value = 0.5 * (value + random_bit());  // Insert 1 random bit
   // or value = ldexp(value+random_bit(),-1);
   // or group several bits into one single ldexp
return value;

0

我可能误解了这个问题,但您为什么不直接从随机比特流中采样下一个n位,并将其转换为介于0到2^n - 1之间的十进制数呢?


... 这将是一个整数范围从0到2^n-1。我想要一个浮点数范围从最小值到最大值。 - Lior Kogan
@Lior Kogan为什么假设Ben在谈论整数。数字流也可以是浮点数。对于整数部分采样n位,对于小数部分采样n位。 - tenfour
随机选择两个整数并将较小的数除以较大的数。 - image_doctor

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