为什么我用不同的种子得到相同的数据?

Why do I get the same data with a different seed?

出于某种原因,seed=0 和 seed=1 给出了相同的结果,而我预计它会有所不同。

对于不同的结果,一切都按预期工作,只有 0 和 1 个种子才会出现问题。

是bug还是我没看懂?

重现代码。我在 gcc 和 g++ 编译器上试过了。

#include <vector>
#include <random>
#include <stdint.h>

int main()
{
  int32_t length = 100000;
  
  //first generated data 
  uint32_t seed1 = 0;
  std::default_random_engine generator1;
  generator1.seed(seed1);
  std::uniform_int_distribution<int8_t> distribution1(0, 1);

  std::vector<int8_t> sequence1(length);

  for (int32_t i = 0; i < length; i++) {
    sequence1[i] = distribution1(generator1);
  }

  // second generated data
  uint32_t seed2 = 1;
  std::default_random_engine generator2;
  generator2.seed(seed2);
  std::uniform_int_distribution<int8_t> distribution2(0, 1);

  std::vector<int8_t> sequence2(length);

  for (int32_t i = 0; i < length; i++) {
    sequence2[i] = distribution2(generator2);
  }


  //check if data the same
  bool sameData = true;
  for (int32_t i = 0; i < length; i++) {
    if (sequence1[i] != sequence2[i]) {
      sameData = false;
    }
  }
  
  std::cout << sameData; // true, but should be false
}

Why do i get same data with diffirent seed

所有种子值不一定产生唯一序列。

在您使用的标准库实现中,您选择的种子恰好与给定的随机引擎产生相同的序列。这可能是因为 0 恰好对那个特定的引擎来说是特殊的。

如果您使用的是 libstdc++,您观察到的行为由 this 实现解释:

  template<typename _UIntType, _UIntType __a, _UIntType __c, _UIntType __m>
    void
    linear_congruential_engine<_UIntType, __a, __c, __m>::
    seed(result_type __s)
    {
      if ((__detail::__mod<_UIntType, __m>(__c) == 0)
          && (__detail::__mod<_UIntType, __m>(__s) == 0))  // <-- true if seed == 0
        _M_x = 1;     // <-- seed is set to 1!
      else
        _M_x = __detail::__mod<_UIntType, __m>(__s);
    }

最简单的解决方法是选择另一对种子,但这里有一些更普遍有用的建议:

  • 如果您只想要两个不同的序列,则使用一个生成器并设置一次种子。这样你就不会意外地得到一对不幸的种子,并且你避免了第二次初始化引擎的开销。
  • 除非您想要可重复的序列,否则最好使用可变种子源,而不是对其进行硬编码。一个常见的策略是使用当前时间(在这种情况下,我会通过递增第一个种子来选择第二个种子;由于时钟的粒度,再次获取当前时间可能会导致相同的种子)。虽然这并不能解决所有问题,但不太可能遇到这种异常情况 0.
  • 除非你想要可重复的序列,否则你应该使用 std::random_device 来获得一个具有(希望)良好熵的种子。
  • 如果您需要良好的随机性属性,或者如果您需要跨系统的一致性,则不要依赖 std::default_random_engine,它可能是一个 LCG(不良随机性属性)并且可能因系统而异。 Mersenne twister 引擎 - 由标准库提供 - 被认为具有相当好的随机性,并且是一个不错的默认选择,除非你有理由选择另一个。

这种“奇怪”很可能可以简单地通过注意到您使用的 PRNG std::default_random_engine 不是很好来解释。

这又是你的代码,除了现在它编译(缺少 #include <iostream>)并使用 std::mt19937 代替:

#include <vector>
#include <random>
#include <cstdint>
#include <iostream>

int main()
{
  int32_t length = 100000;
  
  //first generated data 
  uint32_t seed1 = 0;
  std::mt19937 generator1;
  generator1.seed(seed1);
  std::uniform_int_distribution<int8_t> distribution1(0, 1);

  std::vector<int8_t> sequence1(length);

  for (int32_t i = 0; i < length; i++) {
    sequence1[i] = distribution1(generator1);
  }

  // second generated data
  uint32_t seed2 = 1;
  std::mt19937 generator2;
  generator2.seed(seed2);
  std::uniform_int_distribution<int8_t> distribution2(0, 1);

  std::vector<int8_t> sequence2(length);

  for (int32_t i = 0; i < length; i++) {
    sequence2[i] = distribution2(generator2);
  }


  //check if data the same
  bool sameData = true;
  for (int32_t i = 0; i < length; i++) {
    if (sequence1[i] != sequence2[i]) {
      sameData = false;
    }
  }
  
  std::cout << sameData; // true, but should be false
}

Link to code
输出:

0

另一个小改动是 #include <stdint.h> => #include <cstdint>

每个 this 页面,如果您 Ctrl+f 对应 default_random_engine,您会看到它只是说它是实现定义的。这意味着编译器编写者可以为所欲为。

这也意味着对于不同的编译器,它可以有不同的行为,这是允许的。我没有证据证明这一点,因为我从未看过任何源代码,但我相信至少 gcc 和 clang 所做的是实现一个 非常 简单的,而不是所有的随机引擎在玩具示例中几乎不足以掷骰子。一旦我们转向使用具有严格要求的更好引擎,您更有可能看到预期结果。仍然不能保证。

这是因为 std::seed_seq 是确定性的,它是 <random> 中大多数 PRNG 在构建初始状态时使用的步骤之一。

std::mt19937也要用好

参考文献:
https://www.pcg-random.org/posts/cpp-seeding-surprises.html
https://kristerw.blogspot.com/2017/05/seeding-stdmt19937-random-number-engine.html

libstdc++,通常与 gcc 一起提供,将 std::default_random_engine 定义为 std::linear_congruential_engine<uint_fast32_t, 16807UL, 0UL, 2147483647UL> 的类型定义。源代码链接:

请注意,此引擎是所谓的“Lewis、Goodman 和 Miller 的经典最低标准 rand0”。该引擎在本文中有描述:https://www.researchgate.net/publication/220420979_Random_Number_Generators_Good_Ones_Are_Hard_to_Find.

在里面,您可以在第 1195 页找到它的算法。请注意该论文中的以下引用:

This generator must be initialized by assigning seed a value between 1 and 2147483646. ... Unfortunately, for most systems this version of Random is fatally flawed.

种子值0因此即使不满足这个要求。

一点调试...

首先让我们注意到

template<typename _UIntType, _UIntType __a, _UIntType __c, _UIntType __m>
      class linear_congruential_engine { ...

然后

typedef linear_congruential_engine<uint_fast32_t, 16807UL, 0UL, 2147483647UL>
minstd_rand0;
typedef minstd_rand0 default_random_engine;

所以我们有一个 linear congruential generator,参数 C 设置为零。现在...

   |114       template<typename _UIntType, _UIntType __a, _UIntType __c, _UIntType __m>                                                           │
   │115         void                                                                                                                              │
   │116         linear_congruential_engine<_UIntType, __a, __c, __m>::                                                                            │
   │117         seed(result_type __s)                                                                                                             │
   │118         {                                                                                                                                 │
   │119           if ((__detail::__mod<_UIntType, __m>(__c) == 0)                                                                                 │
   │120               && (__detail::__mod<_UIntType, __m>(__s) == 0))                                                                             │
  >│121             _M_x = 1;                                                                                                                     │
   │122           else                                                                                                                            │
   │123             _M_x = __detail::__mod<_UIntType, __m>(__s);                                                                                  │
   │124         }                                                                                                                                ​

因此该实现拒绝使用 0 作为种子,而是使用 1 作为种子。这也是一件好事,因为当 C==0 时,零种子根本不起作用。它将产生一个恒定的零流(在调试器中更改 _M_x 并查看...或只查看公式)。

不能保证不同的种子值会产生不同的伪随机序列。