Mersenne twister热身与再现性

时间:2013-04-18 09:09:14

标签: c++ random c++11 prng mersenne-twister

在我目前的C ++ 11项目中,我需要执行M模拟。对于每个模拟m = 1, ..., M,我使用std::mt19937对象随机生成数据集,构造如下:

std::mt19937 generator(m);
DatasetFactory dsf(generator);

根据https://stackoverflow.com/a/15509942/1849221https://stackoverflow.com/a/14924350/1849221,Mersenne Twister PRNG受益于热身阶段,目前我的代码中没有。为方便起见,我提出了建议的代码片段:

#include <random>

std::mt19937 get_prng() {
    std::uint_least32_t seed_data[std::mt19937::state_size];
    std::random_device r;
    std::generate_n(seed_data, std::mt19937::state_size, std::ref(r));
    std::seed_seq q(std::begin(seed_data), std::end(seed_data));
    return std::mt19937{q};
}

我的问题是我需要结果的可重复性,即在不同的执行中,对于每次模拟,数据集必须是相同的。这就是为什么在我目前的解决方案中,我使用当前的模拟种子Mersenne Twister PRNG。在我看来,使用std::random_device会阻止数据相同(AFAIK,这是std::random_device的确切目的)。

编辑 不同的执行我的意思是重新启动可执行文件。

如何在不影响重复性的情况下在代码中引入上述预热阶段?感谢。

可能的解决方案#1

这是基于@SteveJessop的第二个提案的暂定实施

#include <random>

std::mt19937 get_generator(unsigned int seed) {
        std::minstd_rand0 lc_generator(seed);
        std::uint_least32_t seed_data[std::mt19937::state_size];

        std::generate_n(seed_data, std::mt19937::state_size, std::ref(lc_generator));
        std::seed_seq q(std::begin(seed_data), std::end(seed_data));
        return std::mt19937{q};
    }

可能的解决方案#2

这是基于@SteveJassop和@AndréNeve共同贡献的暂定实施。 sha256功能改编自https://stackoverflow.com/a/10632725/1849221

#include <openssl/sha.h>
#include <sstream>
#include <iomanip>
#include <random>

 std::string sha256(const std::string str) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256_CTX sha256;
    SHA256_Init(&sha256);
    SHA256_Update(&sha256, str.c_str(), str.size());
    SHA256_Final(hash, &sha256);

    std::stringstream ss;
    for(int i = 0; i < SHA256_DIGEST_LENGTH; i++) 
        ss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];

    return ss.str();
}

std::mt19937 get_generator(unsigned int seed) {
    std::string seed_str = sha256(std::to_string(seed));
    std::seed_seq q(seed_str.begin(), seed_str.end());
    return std::mt19937{q};
}

编译:{{1​​}}

3 个答案:

答案 0 :(得分:5)

两个选项:

  1. 按照您的提议进行操作,但不要使用std::random_device r;为MT生成种子序列,而是使用另一种种子m的PRNG。当与小种子数据一起使用时,选择一个不像MT一样需要预热的一个:我怀疑LCG可能会这样做。对于大规模的过度杀伤,您甚至可以使用基于安全哈希的PRNG。如果您已经听说过,这很像密码学中的“关键拉伸”。实际上,您可以使用标准键拉伸算法,但是您使用它来生成长种子序列而不是大键材料。

  2. 继续使用m为您的MT播种,但discard在开始模拟之前会有大量恒定的数据。也就是说,忽略建议使用强大的种子,而是运行MT足够长的时间,以达到一个体面的内部状态。我不知道您需要丢弃多少数据,但我希望互联网能够丢弃。

答案 1 :(得分:4)

我认为您只需要存储初始种子(在您的情况下为std::uint_least32_t seed_data[std::mt19937::state_size]数组)和您所做的预热步骤的数量n(例如,如上所述使用discard(n) )对于您希望重现的每次运行/模拟。

使用此信息,您始终可以创建新的MT实例,使用之前的seed_data对其进行播种,并针对相同的n预热步骤运行它。这将生成相同的值序列,因为当实例结束时,MT实例将具有相同的内部状态。

当您提到影响可重复性的std::random_device时,我相信在您的代码中它只是用于生成种子数据。如果您将其用作随机数本身的,那么您将无法获得可重现的结果。由于您仅使用它来生成种子,因此应该没有任何问题。如果要重现值,每次都无法生成新的种子

std::random_device

的定义
  

“std :: random_device是一个均匀分布的整数随机数生成器,它产生非确定性随机数。”

因此,如果它不是确定性的,则无法重现由它产生的值序列。话虽如此,使用它简单地生成好的随机种子,只是为了重新运行它们。

希望这有帮助

编辑:

在与@SteveJessop讨论之后,我们得出的结论是,数据集(或其中的一部分)的简单散列足以用作您需要的合适种子。这允许每次运行模拟时生成相同种子的确​​定性方法。正如@Steve所提到的,与std::mt19937::state_size相比,您必须保证散列的大小不会太小。如果它太小,那么你可以连接m,m + M,m + 2M的哈希值......直到你有足够的数据,正如他所建议的那样。

我在这里发布更新的答案,因为使用哈希的想法是我的,但我会赞成@ SteveJessop的回答,因为他为此做出了贡献。

答案 2 :(得分:0)

对您链接的答案之一的评论表明:

  

巧合的是,默认的C ++ 11 seed_seq是Mersenne Twister预热序列(尽管现有的实现,例如libc ++的mt19937,在提供单值种子时使用更简单的预热)

因此,您可以使用std::seed_seq的当前固定种子为您进行热身。

std::mt19937 get_prng(int seed) {
    std::seed_seq q{seed, maybe, some, extra, fixed, values};
    return std::mt19937{q};
}