在random_device和seed_seq之间做出决定,为多个随机数序列生成种子

Deciding between random_device and seed_seq to generate seeds for multiple random number sequences

本文关键字:种子 决定 随机数序列 device random seed seq 之间      更新时间:2023-10-16

当编写需要多个独立随机数分布/序列的代码时(下面有两个示例),似乎有两种典型的方法可以实现(伪)随机数生成。一种是简单地使用random_device对象为两个独立的引擎生成两个随机种子:

std::random_device rd;
std::mt19937 en(rd());
std::mt19937 en2(rd());
std::uniform_real_distribution<> ureald{min,max};
std::uniform_int_distribution<> uintd{min,max};

另一个涉及使用random_device对象使用多个随机性"源"创建seed_seq对象:

// NOTE: keeping this here for history, but a (hopefully) corrected version of
// this implementation is posted below the edit
std::random_device rd;
std::seed_seq seedseq{rd(), rd(), rd()}; // is there an optimal number of rd() to use?
std::vector<uint32_t> seeds(5);
seedseq.generate(seeds.begin(), seeds.end());
std::mt19937 en3(seeds[0]);
std::mt19937 en4(seeds[1]);
std::uniform_real_distribution<> ureald{min,max};
std::uniform_int_distribution<> uintd{min,max};

在这两种方法中,有没有首选方法?为什么?如果是后者,是否有用于生成seed_seq对象的最佳数量的random_device"源"?

有没有比我上面概述的这两种实现更好的随机数生成方法?

谢谢!


编辑

(希望)更正了多个发行版的seed_seq实现版本:

std::random_device rd;
std::seed_seq seedseq1{rd(), rd(), rd()}; // is there an optimal number of rd() to use?
std::seed_seq seedseq2{rd(), rd(), rd()};
std::mt19937 en3(seedseq1);
std::mt19937 en4(seedseq2);
std::uniform_real_distribution<> ureald{min,max};
std::uniform_int_distribution<> uintd{min,max};

如果您不信任默认实现来正确初始化正在使用的引擎的状态,则通常打算使用std::seed_seq

在许多≥C++11实现中,std::default_random_enginestd::mt19937的别名,这是Mersenne Twister伪随机数生成算法的特定变体。查看std::mt19937的规范,我们看到它的状态大小为 624 个无符号整数,这足以容纳它打算包含的 19937 位状态(这就是它的名字的由来)。传统上,如果您只使用单个uint32_t值为其设定种子(如果rdstd::random_device对象,则从调用rd()一次会得到该值),那么您将保留其绝大多数状态未初始化。

现在,对于任何即将对他们种子不佳的 Mersenne Twister 引擎感到恐慌的人来说,好消息是,如果您使用单个uint32_t值(如std::default_random_engine engine{rd()};)构造一个std::mt19937,则需要实现通过排列原始种子值来初始化状态的其余部分,因此,虽然对rd()的单次调用会产生有限范围的实际不同引擎状态, 至少正确初始化引擎仍然足够。这将产生一个"高质量"随机数生成器。

但是,如果您担心引擎没有正确设定种子,无论是出于加密原因(尽管请注意std::mt19937本身在加密上并不安全!)还是仅仅是出于统计原因,您可以使用std::seed_seq手动指定整个状态,使用rd()填充每个值,以便您可以相对自信地保证引擎已正确设定种子。

对于随意使用或并非绝对需要实现高质量随机数的场景,只需通过一次调用std::random_device::operator()进行初始化即可。

如果你想使用一个std::seed_seq,请确保你正确地设置了它(你的原始代码中的例子绝对不正确,至少对于std::mt19937来说,实际上会产生比简单地使用rd()更糟糕的结果!这篇关于CodeReview的文章包含经过适当审查的代码。

编辑:

对于 Mersenne Twister 的预定义模板,状态大小始终为 19968 位,略大于实际需要,但也是可以使用uint32_t值完全表示范围的最小值。这相当于 624 个单词,每个单词32 位。因此,如果您计划使用种子序列,您将通过 624 次调用来正确初始化它以rd()

//Code copied from https://codereview.stackexchange.com/questions/109260/seed-stdmt19937-from-stdrandom-device
std::vector<uint32_t> random_data(624);
std::random_device source;
std::generate(random_data.begin(), random_data.end(), std::ref(source));
std::seed_seq seeds(random_data.begin(), random_data.end());
std::mt19937 engine(seeds);
//Or:
//std::mt19937_64 engine(seeds);

如果您使用的是std::mersenne_twister_engine的非标准实例化,则可以通过将state_size乘以word_size然后除以 32 来查询该特定情况所需的状态大小。

using mt_engine = std::mersenne_twister_engine</*...*/>;
constexpr size_t state_size = mt_engine::state_size * mt_engine::word_size / 32;
std::vector<uint32_t> random_data(state_size);
std::random_device source;
std::generate(random_data.begin(), random_data.end(), std::ref(source));
std::seed_seq seeds(random_data.begin(), random_data.end());
mt_engine engine (seeds);

对于其他引擎类型,您需要根据具体情况对其进行评估。std::linear_congruential_engine及其预定义的变体使用其字大小的单个整数,因此它们只需要一次调用rd()即可初始化,因此种子序列是不必要的。我不确定std::subtract_with_carry_engine或其相关的使用std::discard_block_engine如何工作,但似乎它们也只包含一个状态