让STL以指定分布方式产生随机数

上一节中,我们了解了STL中的随机数引擎。使用引擎或其他方式生成随机数,只是完成了一半的工作。

另一个问题就是,我们需要怎么样的随机数?如何以编码的方式进行掷硬币?通常都会使用rand()%2的方式进行,因为其结果为0或1,也就分别代表着硬币的正反面。很公平,不是么;这样就不需要任何多余的库(估计只有数学专业知道,这样的方式获取不到高质量随机数)。

如果我们想要做成一个模型要怎么弄?比如:写成(rand() % 6) + 1,这个就表示所要生成的结果。对于这样简单的任务,有没有库进行支持?

当我们想要表示某些东西,并明确其概率为66%呢?Okay,我们可以写一个公式出来bool yesno = (rand() % 100 > 66)。(这里需要注意,是使用>=合适?还是使用>合适?)

如何设置一个不平等的模型,也就是生成数的概率完全不同呢?或是,让我们生成的随机数符合某种复杂的分布?有些问题会很快发展为一个科学问题。为了回到我们的关注点上,让我们了解一下STL所提供的具体工具。

STL有超过10种分布算法,能用来指定随机数的生成方式。本节中,我们将简单的了解一下这些分布,并且近距离了解一下其使用方法。

How to do it…

我们生成随机数,对生成数进行统计,并且将其分布部分在终端上进行打印。我们将了解到,当想要以特定的分布获取随机值时,应该使用哪种方式:

  1. 包含必要的头文件,并声明所使用的命名空间:

    1. #include <iostream>
    2. #include <iomanip>
    3. #include <random>
    4. #include <map>
    5. #include <string>
    6. #include <algorithm>
    7. using namespace std;
  2. 对于STL所提供的分布来说,我们将从打印出的直方图中看出每种分布的不同。随机采样时,可以将某种分布作为参数传入随机数生成器。然后,我们将实例化默认随机引擎和一个map。这个map将获取的值与其计数器进行映射,计数器表示这个数产生的频率。我们使用一个限定函数作为随机引擎的指定分布,然后通过随机引擎生成对应分布的随机值:

    1. template <typename T>
    2. void print_distro(T distro, size_t samples)
    3. {
    4. default_random_engine e;
    5. map<int, size_t> m;
  3. 使用samples变量来表示我们要进行多少次采样,并且在采样过程中对map中的计数器进行操作。这样,就能获得非常漂亮的直方图。单独调用e()时,随机数引擎将生成一个随机数,distro(e)会通过分布对象对随机数的生成进行限定。

    1. for (size_t i {0}; i < samples; ++i) {
    2. m[distro(e)] += 1;
    3. }
  4. 为了输出到终端窗口中的数据的美观性,需要了解计数器的最大值。max_element函数能帮助我们找到map中所有计数器中的最大的那一个,然后返回指向具有最大计数器那个节点的迭代器。知道了最大值,就可以让所有计数器对其进行除法,这样就能在终端窗口生成固定长度的图像了:

    1. size_t max_elm (max_element(begin(m), end(m),
    2. [](const auto &a, const auto &b) {
    3. return a.second < b.second;
    4. })->second);
    5. size_t max_div (max(max_elm / 100, size_t(1)));
  5. 现在来遍历map,然后对*进行打印,对于每一个计数器来说都有一个固定的长度:

    1. for (const auto [randval, count] : m) {
    2. if (count < max_elm / 200) { continue; }
    3. cout << setw(3) << randval << " : "
    4. << string(count / max_div, '*') << '\n';
    5. }
    6. }
  6. 主函数中,会对传入的参数进行检查,我们会指定每个分布所使用的采样率。如果用户给定的参数不合适,程序将报错:

    1. int main(int argc, char **argv)
    2. {
    3. if (argc != 2) {
    4. cout << "Usage: " << argv[0]
    5. << " <samples>\n";
    6. return 1;
    7. }
  7. std::stoull会将命令行中的参数转换成数字:

    1. size_t samples {stoull(argv[1])};
  8. 首先,来尝试uniform_int_distributionnormal_distribution,这两种都是用来生成随机数的经典分布。学过概率论的同学应该很熟悉。均匀分布能接受两个值,确定生成随机数的上限和下限。例如,0和9,那么生成器将会生成[0, 9]之间的随机数。正态分布能接受平均值和标准差作为传入参数:

    1. cout << "uniform_int_distribution\n";
    2. print_distro(uniform_int_distribution<int>{0, 9}, samples);
    3. cout << "normal_distribution\n";
    4. print_distro(normal_distribution<double>{0.0, 2.0}, samples);
  9. 另一个非常有趣的分布是piecewise_constant_distribution。其能接受两个输入范围作为参数。比如定义为0, 5, 10, 30,那么其中的间隔就是0到4,然后是5到9,最后一个间隔是10到29。另一个输入范围定义了权重。比如权重0.2, 0.3, 0.5,那么最后生成随机数落在以上三个间隔中的概率为20%,30%和50%。在每个间隔内,生成数的概率都是相同的:

    1. initializer_list<double> intervals {0, 5, 10, 30};
    2. initializer_list<double> weights {0.2, 0.3, 0.5};
    3. cout << "piecewise_constant_distribution\n";
    4. print_distro(
    5. piecewise_constant_distribution<double>{
    6. begin(intervals), end(intervals),
    7. begin(weights)},
    8. samples);
  10. piecewise_linear_distribution的构造方式与上一个类似,不过其权重值的特性却完全不同。对于每一个间隔的边缘值,只有一种权重值。从一个边界转换到另一个边界中,概率是线性的。这里我们使用同样的间隔列表,但权重值不同的方式对分布进行实例化:

    1. cout << "piecewise_linear_distribution\n";
    2. initializer_list<double> weights2 {0, 1, 1, 0};
    3. print_distro(
    4. piecewise_linear_distribution<double>{
    5. begin(intervals), end(intervals), begin(weights2)},
    6. samples);
  11. 伯努利分布是另一个非常重要的分布,因为其分布只有“是/否”,“命中/未命中”或“头/尾”值,并且这些值的可能性是指定的。其输出只有0或1。另一个有趣的分布,就是discrete_distribution。例子中,我们离散化了一组值1, 2, 4, 8。这些值可被解释为输出为0至3的权重:

    1. cout << "bernoulli_distribution\n";
    2. print_distro(std::bernoulli_distribution{0.75}, samples);
    3. cout << "discrete_distribution\n";
    4. print_distro(discrete_distribution<int>{ {1, 2, 4, 8} }, samples);
  12. 不同分布引擎之间有很大的不同。都非常特殊,也都在特定环境下非常有用。如果你没有听说过这些分布,应该对其特性不是特别的了解。不过,我们的程序中会生成非常漂亮的直方图,通过打印图,你会对这些分布有所了解:

    1. cout << "binomial_distribution\n";
    2. print_distro(binomial_distribution<int>{10, 0.3}, samples);
    3. cout << "negative_binomial_distribution\n";
    4. print_distro(
    5. negative_binomial_distribution<int>{10, 0.8},
    6. samples);
    7. cout << "geometric_distribution\n";
    8. print_distro(geometric_distribution<int>{0.4}, samples);
    9. cout << "exponential_distribution\n";
    10. print_distro(exponential_distribution<double>{0.4}, samples);
    11. cout << "gamma_distribution\n";
    12. print_distro(gamma_distribution<double>{1.5, 1.0}, samples);
    13. cout << "weibull_distribution\n";
    14. print_distro(weibull_distribution<double>{1.5, 1.0}, samples);
    15. cout << "extreme_value_distribution\n";
    16. print_distro(
    17. extreme_value_distribution<double>{0.0, 1.0},
    18. samples);
    19. cout << "lognormal_distribution\n";
    20. print_distro(lognormal_distribution<double>{0.5, 0.5}, samples);
    21. cout << "chi_squared_distribution\n";
    22. print_distro(chi_squared_distribution<double>{1.0}, samples);
    23. cout << "cauchy_distribution\n";
    24. print_distro(cauchy_distribution<double>{0.0, 0.1}, samples);
    25. cout << "fisher_f_distribution\n";
    26. print_distro(fisher_f_distribution<double>{1.0, 1.0}, samples);
    27. cout << "student_t_distribution\n";
    28. print_distro(student_t_distribution<double>{1.0}, samples);
    29. }
  13. 编译并运行程序,就可以得到如下输入。首先让我们对每个分布进行1000个的采样看看:

    让STL以指定分布方式产生随机数 - 图1

  14. 然后在用1,000,000个采样,这样获得的每个分布的直方图会更加的清楚。不过,在生成随机数的过程中,我们也会了解到那种引擎比较快,哪种比较慢:

    让STL以指定分布方式产生随机数 - 图2

How it works…

我们通常都不会太在意随机数引擎,不过随着我们对随机数分布的要求和对生成速度的要求,我们就需要通过随机数引擎来解决这个问题。

为了使用任意的分布,首先实例化一个分布对象。会看到不同分布的构造函数所需要的构造参数并不相同。本节的描述中,只使用了一部分分布引擎,因为其中大部分的用途非常特殊,或是使用起来非常复杂。不用担心,所有分布的描述都可以在C++ STL文档中查到。

不过,当具有一个已经实例化的分布时,我们可以像函数一样对其进行调用(只需要一个随机数引擎对象作为参数)。然后,随机数生成引擎会生成一个随机数,通过特定的分布进行对随机值进行限定,然后得到了所限定的随机数。这就导致不同的直方图具有不同的分布,也就是我们程序输出的结果。

可以使用我们刚刚编写的程序,来确定不同分布的功能。另外,我们也对几个比较重要的分布进行了总结。程序中使用到分布并不都会在下表出现,如果你对某个没出现的分布感兴趣,可以查阅C++ STL文档的相关内容。

分布 描述
uniform_int_distribution 该分布接受一组上下限值作为构造函数的参数。之后得到的随机值就都是在这个范围中。我们可能得到的每个值的可能性是相同的,直方图将会是一个平坦的图像。这个分布就像是掷骰子,因为掷到骰子的每一个面的概率都是相等的。
normal_distribution 正态分布或高斯分布,自然界中几乎无处不在。其STL版本能接受一个平均值和一个标准差作为构造函数的参数,其直方图的形状像是屋顶一样。其分布于人类个体高度或动物的IQ值,或学生的成绩,都符合这个分布。
bernoulli_distribution 当我们想要一个掷硬币的结果时,使用伯努利分布就非常完美。其只会产生0和1,并且其构造函数的参数是产生1的概率。
discrete_distribution 当我们有一些限制的时候,我们就可以使用离散分布,需要我们为每个间隔的概率进行定义,从而得到离散集合的值。其构造函数会接受一个权重值类别,并且通过权重值的可能性产生相应的随机数。当我们想要对血型进行随机建模时,每种血型的概率是不一样,这样这种引擎就能很完美地契合这种需求。