伪随机数生成器给出相同的第一个输出,但随后的行为和预期的一样

Pseudo random number generator gives same first output but then behaves as expected

本文关键字:一样 随机数生成器 第一个 输出      更新时间:2023-10-16

使用随机类和时间种子(NULL),均匀分布总是给出相同的第一个输出,即使有不同的compiling,但在第一个输出之后,行为与您期望的伪随机数生成器的行为相同。

这是由于结构原因,还是我使用错误?

MWE:

#include <ctime>
#include <iostream>
#include <random>
using namespace std;
default_random_engine gen(time(NULL));
uniform_int_distribution<int> dist(10,200);
int main()
{
    for(int i = 0; i < 5; i++)
        cout<<dist(gen)<<endl;
    return 0;
}

前三次我运行这个程序,我得到的输出:

57
134
125
136
112

在第二次尝试之前,我决定删除uniform_int_distributionint main()之间的空行,只是为了看看种子是否基于编译时,正如您所看到的,这无关紧要。

57
84
163
42
146

只是再次运行:

57
73
181
160
46

所以在我的跑步中,我总是先得到57,这当然不是世界末日,如果我想要不同的输出,我可以放弃第一个输出。但这让人怀疑这是故意的(如果是为什么?)还是我以某种方式滥用了生成器(如果是如何使用的?)。

我还不确定出了什么问题(还!),但您仍然可以按如下时间进行初始化,而不会遇到问题(从这里借用)。

#include <ctime>
#include <iostream>
#include <random>
#include <chrono>
using namespace std;
unsigned seed1 = std::chrono::system_clock::now().time_since_epoch().count();
default_random_engine gen(seed1); //gen(time(NULL));
uniform_int_distribution<int> dist(10,200);
int main()
{
    for(int i = 0; i < 5; i++)
        cout<<dist(gen)<<endl;
    return 0;
}

你也可以使用随机设备,它是非决定性的(它从你的按键、鼠标移动和其他来源窃取时间信息,以生成不可预测的数字)。这是你可以选择的最强种子,但如果你不需要强有力的保证,计算机的时钟是更好的选择,因为如果你经常使用它,计算机可能会耗尽"随机性"(需要多次按键和鼠标移动才能生成一个真正的随机数)。

std::random_device rd;
default_random_engine gen(rd());

运行

cout<<time(NULL)<<endl;
cout<<std::chrono::system_clock::now().time_since_epoch().count()<<endl;
cout<<rd()<<endl;

在我的机器上生成

1413844318
1413844318131372773
3523898368

因此CCD_ 4库提供了比CCD_。random_device正在产生遍布地图的非确定性数字。因此,似乎ctime产生的种子可能在某种程度上过于紧密,从而部分映射到相同的内部状态?

我做了另一个程序,看起来像这样:

#include <iostream>
#include <random>
using namespace std;
int main(){
  int oldval           = -1;
  unsigned int oldseed = -1;
  cout<<"SeedtValuetSeed Difference"<<endl;
  for(unsigned int seed=0;seed<time(NULL);seed++){
    default_random_engine gen(seed);
    uniform_int_distribution<int> dist(10,200);
    int val = dist(gen);
    if(val!=oldval){
      cout<<seed<<"t"<<val<<"t"<<(seed-oldseed)<<endl;
      oldval  = val;
      oldseed = seed;
    }
  }
}

正如您所看到的,这只是打印出截至当前时间的每个可能的随机种子的第一个输出值,以及具有相同值的种子和先前种子的数量。输出摘录如下:

Seed  Value Seed Difference
0 10  1
669 11  669
1338  12  669
2007  13  669
2676  14  669
3345  15  669
4014  16  669
4683  17  669
5352  18  669
6021  19  669
6690  20  669
7359  21  669
8028  22  669
8697  23  669
9366  24  669
10035 25  669
10704 26  669
11373 27  669
12042 28  669
12711 29  669
13380 30  669
14049 31  669

因此,对于每一个新的第一个数字,都有669个种子给出第一个数字。因为第二个和第三个数字不同,我们仍然在生成独特的内部状态。我认为我们必须更多地了解default_random_engine,才能了解669的特殊之处(可以将其分解为3和223)。

考虑到这一点,chronorandom_device库工作得更好的原因就很清楚了:它们生成的种子总是相距669以上。请记住,即使第一个数字是相同的,在许多程序中重要的是由不同的数字生成的序列。

使用std::default_random_engine就像在一家糟糕的餐厅里说"给我惊喜!"。你唯一可以确定的是,结果会很差——因为<random>提供的生成器都有缺陷——但你甚至不知道你必须处理哪些特定的缺陷。

Mersenne Twister是一个不错的选择,如果——而且只有当——它被正确地播种,并且存在摩擦。理想情况下,种子的每一个比特都应该以相等的概率影响结果生成器状态的每一比特;正如您所发现的,std::mersennetwister_engine的常见实现中并非如此。

Mersenne Twister通常用一个更简单的PRNG的输出进行初始化,而PRNG又被任何可用的熵播种。这有效地将更简单的PRNG的种子熵扩展到龙卷风的巨大状态上。该标准的制定者为此深思熟虑地提供了seed_seq接口;然而,该库似乎不包含任何适配器,用于将生成器用作种子序列。

两种不同的播种概念之间也存在差异。在生成器方面,种子函数应该获取传递的熵,并将其忠实地映射到生成器状态,确保在这个过程中不会丢失熵。在用户端,它是"取这些数字,给我不同的序列",其中"这些数字"是{1,2,3,…}或clock()输出。

换句话说,种子熵以不适合直接初始化生成器状态的形式提供;小的种子差异产生小的状态差异。这对于像Mersenne Twister或为std::ranluxXX生成器供电的滞后Fibonacci这样的巨大滞后生成器来说尤其有问题。

比特混合函数是一种双射函数,其中输出的每个比特都以相等的概率依赖于输入的每个比特。比特混合函数可以帮助使1、2、3或clock()输出等种子对种子更有用。杂音散列混合器通过实现几乎完美的扩散(显示的是32位版本)接近这一理想:

uint32_t murmur_mix32 (uint32_t x)
{
   x ^= x >> 16;
   x *= 0x85EBCA6B;
   x ^= x >> 13;
   x *= 0xC2B2AE35;
   x ^= x >> 16;
   return x;
}

该函数是双射的,因此它根本不会失去任何熵。这意味着你可以用它来改善任何种子,而不会有让事情变得更糟的危险。

另一个快速解决方案是在生成器上调用discard(),该参数取决于(杂音混合)种子,而无需制作seed_seq。然而,对像Mersenne Twister这样的大型生成器的影响是有限的,因为它们的状态发展非常缓慢,需要数十万次迭代才能从缺陷状态中完全恢复。

您正在使用的种子可能会引入偏差,如果使用不同的种子产生相同的结果,则生成器本身没有正确写入。

我建议用不同的种子进行测试以得出结论。