伪随机数发生器给出相同的第一输出,但随后表现如预期

时间:2014-10-20 21:57:11

标签: c++ random

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

这是通过构造,还是我错误地使用它?

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,这当然不是世界末日,如果我想要不同的输出,我可以扔掉第一个输出。但是这会让人怀疑这是否是设计的(如果是这样的话为什么?)或者我是否以某种方式滥用发生器(如果是这样的话)?。

3 个答案:

答案 0 :(得分:8)

我不确定到底出现了什么问题(但是!),但您仍然可以按照以下方式初始化,而不会遇到问题(从here借来)。

#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;
}

您还可以使用非确定性的随机设备(它会从您的击键,鼠标移动和其他来源窃取计时信息以生成不可预测的数字)。这是你可以选择的最强大的种子,但如果你不需要强有力的保证,计算机的时钟是更好的方法,因为计算机可能会耗尽随机性&#34;如果你经常使用它(它需要许多击键和鼠标移动来生成一个真正随机的数字)。

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

因此chrono库提供了比ctime库更大的数字和更快速变化的数字(以纳秒为单位)。 random_device正在生成遍布地图的非确定性数字。因此看起来种子ctime产生的某种方式可能过于接近,因此部分映射到相同的内部状态?

我制作了另一个看起来像这样的程序:

#include <iostream>
#include <random>
using namespace std;

int main(){
  int oldval           = -1;
  unsigned int oldseed = -1;

  cout<<"Seed\tValue\tSeed 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。请记住,即使第一个数字与许多程序中的重要数字相同,也是由不同的数字生成的数字序列。

答案 1 :(得分:1)

使用std :: default_random_engine就像说'#34;给我一个惊喜!&#34;在一个糟糕的餐厅。你唯一确定的是结果会很差 - 因为<random>提供的生成器都不足 - 但你甚至不知道你必须处理哪些特殊缺陷。

Mersenne Twister可以是一个不错的选择,如果 - 并且只有 - 它被正确播种,并且其中存在摩擦。理想情况下,种子的每一位都应以相同的概率影响所得发生器状态的每一位;正如您所发现的那样,std :: mersenne_twister_engine的常见实现情况并非如此。

Mersenne Twisters通常使用更简单的PRNG的输出进行初始化,而PRNG又可以通过任何可用的熵进行播种。这有效地拉伸了更简单的PRNG的种子熵,而不是扭转的巨大状态。该标准的制定者为此目的提供了seed_seq接口;但是,似乎该库不包含任何使用生成器作为种子序列的适配器。

两种不同的播种概念之间也存在差异。在发电机侧,播种功能应采用传入的熵并将其忠实地映射到发电机状态,确保在过程中不会丢失熵。在用户方面,它是&#34;拿这些号码给我一个完全不同的序列&#34;,其中&#39;这些数字&#39;是{1,2,3,...}或clock()输出。

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

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

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

   return x;
}

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

另一个快速修复 - 没有制作seed_seq的努力 - 是使用依赖于(杂音混合)种子的参数调用生成器上的discard()。然而,对Mersenne Twister等大型发电机的影响有限,因为它们的状态变化非常缓慢,需要数十万次迭代才能从缺陷状态完全恢复。

答案 2 :(得分:0)

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

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