Logo
Section 1 / 4

理解随机性

了解随机性对遗传算法的影响。

导入

理解随机性

了解随机性对遗传算法的影响。

测试与优化你的代码

在上一章中,我们学习了三种不同的方式来优化框架,以充分发挥 Elixir 和 BEAM 的性能。到目前为止,我们已经创建了一个用于编写遗传算法的框架,能够解决各种各样的问题。然而,你尚未对这个框架进行测试和分析,以确保代码的正确性和清晰性。

测试是任何开发流程中的关键环节。通常,我们会先根据规范或行为编写测试,然后再编写代码。这种称为测试驱动开发(Test-Driven Development)的方法要求先编写小型单元测试,然后逐步改进代码以符合规范。Elixir 高度重视测试,并让编写单元测试变得非常简单。

...
测试驱动开发

除了测试之外,开发过程中的另一个关键方面是确保代码简洁易懂,不仅对你自己,也对可能参与项目开发的其他开发者来说都是如此。幸运的是,Elixir 提供了一些工具包,可以轻松进行代码分析,例如 credo 和 dialyxir。

在本章中,我们将学习如何使用 ExUnit 和 StreamData 为代码编写基于属性的测试。同时,我们还将学习如何利用 credo 和 dialyxir 对框架中的代码进行分析和优化。通常情况下,我们应在开发早期就使用这些工具;但在本课程中,为了更专注于遗传算法的核心概念,这些工具被推迟到了现在。本章将帮助我们建立起使用这些工具来改进现有代码库的良好习惯。

使用随机性进行测试

为遗传算法编写测试的一个挑战在于其随机性。当某件事具有随机性时,它的结果由随机过程决定,通常伴随着一定的概率。例如,回顾我们在 「实现常见变异策略」 一课中实现的翻转变异函数,其代码如下:

1def flip(chromosome, p) do 2 genes = 3 chromosome.genes 4 |> Enum.map( 5 fn g -> 6 if :rand.uniform < p do 7 g ^^^ 1 8 else 9 g 10 end 11 end 12 ) 13 %Chromosome{genes: genes, size: chromosome.size} 14end

请记住,该函数会以概率 p 对染色体中的基因执行位翻转。

注意上述代码中第 6 行的高亮部分:if :rand.uniform < p 这个条件相当于一次抛硬币,以概率 p 为真,以概率 1 - p 为假。如果我们用概率 0.5 测试这个函数,平均而言,染色体中 50% 的基因会被翻转。如果尝试直接测试,很快就会遇到问题——因为函数的行为是随机的。即使对同一个染色体运行该函数 100 次,每次也可能得到不同的结果。

由于测试通常依赖于我们能够明确预测和理解函数的输出,而大多数遗传算法中的函数都依赖随机性,因此我们无法准确预知函数的输出,这使得编写测试变得困难。不过,我们可以通过一些方法有效应对随机性带来的挑战。

一种针对包含随机性的框架函数的测试方法是使用基于属性的测试,我们将在下一课中深入学习。目前,我们将了解两种替代方案:种子初始化和模拟。

种子初始化

种子初始化依赖于随机数生成器需要一个初始种子来生成后续的随机数。我们一直在使用的 :rand 模块实现了伪随机数生成(PRNG)算法。这意味着这些算法并非真正随机,而是模拟出随机的效果。真正随机的算法能够在无限次生成中不重复。:rand 模块中的 PRNG 周期约为 2^64,即在调用约 2^64 次后才会重复。实际上,我们无需担心算法的随机性不足。

我们可以向 :rand 提供一个随机种子,从而控制生成数字的行为。为了演示这一点,请在已打开的 iex 终端中依次输入以下命令:

1// 要运行的命令 2 3:rand.seed(:exsss, 1) 4 5:rand.uniform(100000000000000) 6 7:rand.seed(:exsss, 2) 8 9:rand.uniform(100000000000000) 10 11:rand.seed(:exsss, 1) 12 13:rand.uniform(100000000000000)
Terminal

如果我们无限重复这一过程,每次都会得到相同的数值。我们可以采用同样的策略,在运行测试前对 PRNG 进行种子初始化,从而保证每次随机函数的行为一致。这种方法的一个缺点是,我们必须手动追踪函数的行为,才能确定在特定种子下的预期结果。

模拟

另一种处理随机性测试的方法是使用模拟(mocking)。模拟指的是创建一个替代模块,用来替换 :rand 的行为,使其变为可预测的形式。为此,我们需要在测试时创建一个模块,用来替代 :rand,并提供可预测的行为。这种方法常用于测试那些与离线 API 交互的函数行为;然而,它在处理随机性方面效果并不理想。

优化你的算法

使用 ExUnit 编写属性测试