使用浮点精度时,C++ 随机为相同的 Mersenne Twister 种子产生不同的数字

C++ random yields different numbers for same Mersenne Twister seed when using float precision

我需要 运行 可重现 Monte Carlo 运行s。这意味着我使用与我的结果一起存储的已知种子,如果我需要使用相同的 运行dom 数字 运行 相同的问题实例,则使用该种子。这是常见的做法。

在调查数值精度的影响时,我 运行 遇到以下问题:对于相同的 Mersenne Twister 种子,std::uniform_real_distribution<float>(-1, 1) returns 与 std::uniform_real_distribution<double>(-1, 1)std::uniform_real_distribution<double>(-1, 1) 不同的数字std::uniform_real_distribution<long double>(-1, 1),如下例所示:

#include <iomanip>
#include <iostream>
#include <random>

template < typename T >
void numbers( int seed ) {
  std::mt19937                        gen( seed );
  std::uniform_real_distribution< T > dis( -1, 1 );
  auto p = std::numeric_limits< T >::max_digits10;
  std::cout << std::setprecision( p ) << std::scientific << std::setw( p + 7 )
            << dis( gen ) << "\n"
            << std::setw( p + 7 ) << dis( gen ) << "\n"
            << std::setw( p + 7 ) << dis( gen ) << "\n"
            << "**********\n";
}

int main() {
  int seed = 123;
  numbers< float >( seed );
  numbers< double >( seed );
  numbers< long double >( seed );
}

结果:

$ /usr/bin/clang++ -v
Apple LLVM version 10.0.0 (clang-1000.11.45.5)
Target: x86_64-apple-darwin18.2.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

$ /usr/bin/clang++ bug.cpp -std=c++17
$ ./a.out 
 3.929383755e-01
 4.259105921e-01
-4.277213216e-01
**********
 4.25910643160561708e-01
-1.43058149942132062e-01
 3.81769702875451866e-01
**********
 4.259106431605616525145e-01
-1.430581499421320209545e-01
 3.817697028754518623166e-01
**********

如您所见,doublelong double 都从相同的数字开始(保留精度差异)并继续产生相同的值。另一方面,float 以完全不同的数字开始,它的第二个数字类似于 doublelong double.

产生的第一个数字

您在编译器中看到相同的行为吗?这种(对我来说)出乎意料的差异是否有原因?

方法

回复清楚地表明,没有理由期望使用不同的基础精度生成的值会相同。

我将采用生成可重现的 运行 的方法是始终以尽可能高的精度生成值,并根据需要将它们转换为较低的精度(例如,float x = y,其中 ydoublelong double,视情况而定)。

每个分布将通过从底层 Mersenne Twister 中获取足够数量的(伪)随机位然后从中生成均匀分布的浮点数来生成浮点数。

只有两种实现方式可以满足您对 "same algorithm, therefore same results (minus precision)" 的期望:

  1. std::uniform_real_distribution<long double>(-1, 1)std::uniform_real_distribution<float>(-1, 1) 一样随机。更重要的是,前者与后者具有完全一样多的可能输出。如果后者可以产生比前者更多不同的值,那么它需要从底层 Mersenne Twister 消耗更多的随机性位。如果不能 - 那么,使用它有什么意义(它仍然是 "uniform")?

  2. std::uniform_real_distribution<float>(-1, 1) 从底层 Mersenne Twister 中消耗(并且大部分丢弃)与 std::uniform_real_distribution<long double>(-1, 1) 一样多的随机性位。那将是非常浪费和低效的。

由于没有理智的实现会执行上述任一操作,因此对于每个生成的数字,std::uniform_real_distribution<long double>(-1, 1) 将比 std::uniform_real_distribution<float>(-1, 1) 将基础 Mersenne Twister 推进更多的步骤。这当然会改变随机数的进程。这也解释了为什么 long doubledouble 变体相对靠近:它们最初共享大部分随机位(而 float 可能需要更少的位,因此发散更快)。

将随机数生成器初始化为特定种子将指定它输出的随机位序列。但是,您在每种情况下都不会以相同的方式使用这些位。 std::uniform_real_distribution<double>std::uniform_real_distribution<float> 有更大的可能性 space(假设在你的平台上是 sizeof(double) > sizeof(float))所以它需要消耗更多的随机位来生成完全均匀的分布.

第一个结果是伪随机位序列对于不同的分布类型会有不同的解释。第二个结果是,每当产生一个值时,每个分布都会在伪随机序列中向下移动不同数量的位,这意味着后面的数字不会在伪随机位序列中的同一点。

您的问题的解决方案是始终使用相同类型的分布。如果要比较使用较低精度值与使用较高精度值的结果,请仅生成具有最高精度的值并在需要时将其截断。

只是为了添加更具体的优秀@MaxLanghof 答案:

对于双重代码会做这样的事情 - 生成 u64 整数,并使用它的 53 位来生成浮点数,沿线

double r = (u64 >> 11) * (1.0 / (uint64_t(1) << 53));

对于 long double,假设 Intel 80 位格式,带 64 位尾数,它会做同样的事情,得到 64 位,return 返回 long double。

long double r = u64 * (1.0 / (uint64_t(1) << 64)); // pseudocode

两种情况下都消耗了 64 位随机性,因此您会看到相同的值。

在浮点数的情况下,32位用于制作单个浮点数

float r = (u32 >> 8) * (1.0f / (uint32_t(1) << 24));

消耗了 32 位的随机性,另外 32 位用于下一个数字,这与字节顺序一起使第二个浮点数与第一个 double/long double 大致相同。

Link: http://xoshiro.di.unimi.it/