Logo
Section 1 / 6

深入探究遗传算法

了解设计遗传算法结构所涉及的组件和过程。

导入

深入探究遗传算法

了解设计遗传算法结构所涉及的组件和过程。

遗传算法结构

基于我们目前已掌握的知识,可以理解到每个遗传算法都遵循相同的基本步骤。尽管针对不同问题的算法可能采用不同的技术、概率或策略,但它们都共享相同的结构。作为程序员,我们应该充分利用这一点。

编程中的黄金法则之一是“不要重复自己”(DRY),其核心思想是避免重复编写不必要的代码。我们可以利用遗传算法之间的共性结构,避免在不同算法之间重复编写相同的部分。遗憾的是,我们必须从零开始。

那么,如何从头设计一个通用的框架呢?从基础开始。所有遗传算法都遵循相同的结构:它们都使用染色体和种群,并且都需要类似的输入。我们可以利用这一点,自底向上地进行设计。

染色体与种群

染色体代表问题的解。在 One-Max 问题中,我们的解由一系列 0 和 1 组成。然而,并非所有问题都是如此。有些问题使用实数值编码,有些使用排列编码,还有些使用字符编码。此外,某些问题需要使用列表以外的数据结构来表示解。

这意味着具体的编码方式因问题而异,各不相同。我们可以利用 Enumerable 协议,使框架尽可能通用,从而适用于各种编码方式。

在 Elixir 中,协议允许我们在库中实现多态性。任何实现了 Enumerable 协议的数据结构都可以传递给 Enum 库中的任意函数。这意味着我们可以使用任何实现了 Enumerable 的数据结构来编码染色体——包括我们自己构建的数据结构。

另一方面,种群只是染色体的集合。下图展示了染色体与种群之间的区别:

...
染色体与种群的区别

在大多数情况下,种群的编码方式不会改变。只要种群是由一系列染色体组成,具体使用哪种数据结构并不重要。为简化起见,本课程使用列表来存储染色体。

此外,种群的大小并不关键。较大的种群在变换时耗时更长,但可能更快收敛到最优解;较小的种群更容易处理,但可能需要更长时间才能收敛。

基于目前的理解,我们可以开始制定框架必须遵守的一些规则,例如:

  • 为了实现多态性,应使用实现了 Enumerable 协议的数据结构来编码染色体。

  • 因为种群以列表形式存储,所以可以使用 Enum 或 List 库中的任意函数来实现变换操作。

  • 算法应能适用于任意大小的种群。

通过强制执行这些规则,我们将能够将框架扩展到多种不同类型的问题。

初始化种群

每个遗传算法的第一步是初始化种群。通常,初始种群是随机生成的——就像在搜索空间中随机撒点一样。其目的是从大量不同的解开始探索,逐步逼近最优解。

我们已经确定种群是一个列表,因此这一步骤所实现的函数必须返回一个染色体列表。但我们必须确保该函数适用于所有类型的问题,而不包含关于染色体编码的具体细节。为此,我们可以将一个生成染色体的函数作为输入参数。

Elixir 允许我们将函数作为参数传递给其他函数。我们可以确保其中一个参数是一个返回染色体的函数。我们不关心这个函数的具体实现方式,只关心它能返回一个染色体。

通过这一步,我们有两个主要目标:

  • 初始化步骤必须生成一个初始种群,即一组可能的解。

  • 用于初始化种群的函数应与染色体的具体编码方式无关。为此,我们可以接受一个返回染色体的函数作为参数。

评估种群

评估步骤负责根据某个适应度函数对每个染色体进行评估,并按适应度对种群进行排序。这使得提取最佳染色体变得容易,也便于在选择步骤中处理种群。

正如编码方式因问题而异,适应度函数也是如此。不同问题对“解的好坏”有不同的衡量标准。如果我们试图寻找两点之间的最短路径,就会根据路径长度来评估解;如果我们要优化一个股票投资组合,则会根据预期收益来评估组合。本质上,我们不关心适应度函数是如何实现的,也不关心它返回什么值——我们只需要一个能够比较各个染色体之间适应度的度量标准。

这为我们带来了评估步骤的以下目标或要求:

  • 评估步骤必须以种群作为输入。

  • 评估步骤必须返回按适应度排序后的种群。

  • 用于评估种群的函数应接受一个参数,该参数是一个返回每个染色体适应度的函数。

  • 适应度函数可以返回任意类型的值,只要这些值可以被排序即可。

继续前进

选择、交叉、变异和终止