前言
在智能控制方向相关的一众控制方法中,最为典型的有遗传算法、模糊控制和神经网络。本文主要介绍遗传算法的基本概念以及如何使用遗传算法解决经典的八皇后问题。遗传算法(Genetic Algorithm,GA)最早由John Holland教授于上世纪70年代提出,它的灵感来源于自然界生物体的演化规律,是一种模拟达尔文生物演化论的自然选择与遗传机理的方法,其可通过模拟自然演化的过程搜索问题的最优解。遗传算法常被用于组合优化、超参数搜索、路径规划等方面。本文将简要介绍遗传算法的相关术语、算法思想、算法流程以及算法的简单示例。
相关术语
遗传算法的相关术语主要包括:基因、染色体、表现型、个体、种群。此外还包括:选择、复制、交叉、变异、编码、解码和适应度。下面对这些术语做简单介绍:
- 基因:携带个体的基本特性信息,个体的生长、死亡均与基因有关;
- 染色体:一定数量的基因的载体;
- 表现型:个体的基因与环境交互作用的产物,即特定的基因在一定环境条件下的表现形式;
- 个体:具备生命体所有特性的实体;
- 种群:若干个体的合集;
- 选择:淘汰种群中适应性差的个体,保留适应性强的个体的过程;
- 复制:种群中个体自我复制的过程;
- 交叉:两条染色体之间交换部分基因的过程;
- 变异:染色体上的若干基因发生突变的过程;
- 编码:数据编码到染色体的过程;
- 解码:染色体编码的逆过程;
- 适应度:用来评估个体生存能力的指标。
算法设计
经典遗传算法主要的设计部分包括:染色体编码、适应度函数、选择复制、交叉变异和基因变异。下面结合一个经典的用遗传算法求解函数最大值的例子依次对几个部分做简单介绍。在此之前,我们先简要介绍一下函数求极大值的例子:
假设我们定义有一个标量函数f(x),且已知x的取值范围为 [a, b],这里不妨设定a=0,b=31。我们的目标是使用遗传算法求解出函数f(x)=x^2在x取值范围为 [a,b] 上的最大值,且求得的最值解x的精度为1,即仅包含整数解的情况。当然这个最值求解问题是非常简单的,定义域只有离散的32个值,一个一个试不就得了,根本没有需要使用遗传算法嘛,不过这个问题的确是理解遗传算法非常好的例子。那么针对此问题的遗传算法设计如下所述:
染色体编码
遗传算法常见的染色体编码方法包括二进制编码、浮点数编码和位置编码等。其中二进制编码是相关教材必讲的方法,同时对于上面的给定精度求函数最大值的例子而言,二进制编码也是比较理想的编码方式。其具体的编码过程如下:
对于给定精度1的x的取值范围 [0, 31],采用二进制数编码的方式将这个给定精度和取值范围的十进制数映射到二进制,映射后得到的二进制位数可由下面式子求得:
2
l
−
1
=
U
m
a
x
−
U
m
i
n
δ
2^l-1=\frac{U_{max}-U_{min}}{\delta}
2l−1=δUmax−Umin
其中
l
l
l为编码后二进制数的位数,
δ
\delta
δ为指定的精度,在本例中
δ
=
1
\delta=1
δ=1,
U
m
a
x
U_{max}
Umax和
U
m
i
n
U_{min}
Umin分别表示待编码数值的取值上限和下限,在本例中
U
m
a
x
=
31
U_{max}=31
Umax=31,
U
m
i
n
=
0
U_{min}=0
Umin=0。代入数据计算可得本例中
l
l
l的值为5,即需要5位二进制数来对x的取值进行编码。
例如我们对实数17和23进行二进制编码时,得到的染色体分别为 [10001] 和 [10111],即每条染色体固定有5个基因,且染色体基因的取值只能是二进制数0或1。当我们需要对染色体进行解码时,直接转换二进制数到十进制数即可。
适应度函数
适应度函数的作用是评估种群中个体的环境适应能力。通常情况下,适应度好的个体存活的几率越大(达尔文演化论提出的观点)。在函数求最大值的例子中,一个个体即对应函数的一个解x,那么我们自然地希望个体的染色体解码后得到的x所对应的函数f(x)值越大越好,因此在这个例子中,适应度函数可直接指定为待求解的目标函数f(x)。
选择复制
在遗传算法中,典型的选择复制方法包括轮盘法和竞技法等。其中竞赛法的思想非常简单,它随机从种群中选出两个或者多个个体进行比赛,适应度最高的个体获胜,随后可复制自己到下一代的种群当中。循环往复此过程,直到下一代种群的个体全部确定。
相比之下,轮盘法要稍微复杂一些,它首先根据种群中所有个体的适应度统计得到归一化概率,然后依次计算种群中个体对应的累加概率,得到每个个体所属的概率范围,最后通过随机的概率数对个体进行选择及复制到下一代种群。可以看出,整个选择复制的过程类似于在轮盘上先划定每块区域的大小,然后再进行随机选择,这是称为轮盘法的原因。对于函数求最大值的例子,使用轮盘法进行个体选择复制的过程举例如下:
假设种群中有4个个体,其个体的适应度依次为 [49,25,9,81]。对个体的适应度进行归一化(这里为了简单表示,四舍五入后保留两位小数)得到有 [0.30,0.15,0.06,0.49],接着对归一化后的概率依次进行累加得到有 [0.30,0.45,0.51,1.00],最后用 [0,1] 之间的随机数对个体进行选择,即当随机数落在 [0,0.30) 范围内时,第一个个体被选中,当落在 [0.30,0.45) 范围内时,第二个个体被选中,当落在 [0.45,0.51) 范围内时,第三个个体被选中,而当落在 [0.51,1.00] 范围内时,第四个个体被选中。假设一个群体由N个个体组成,那么通常一次完整的轮盘选择过程需要进行N次随机选择以构成下一代新的种群。
交叉变异
遗传算法中交叉变异的方法有单点交叉和多点交叉等。其中单点交叉的思路为:随机选出一个种群中的两个个体以及随机确定两个个体的染色体上某个共同位置的基因,将两个个体的染色体上对应位置的基因进行交换从而得到各自新的染色体。多点交叉的思路与单点交叉类似,它们的区别在于,前者选择染色体上多个共同位置的基因而不是单个进行基因的交换。对于上面函数求最大值的例子,使用单点交叉变异的举例如下:
假设已经从种群中随机选出了两个个体,且它们的染色体均为二进制编码方式,对应的染色体分别为 [10001] 和 [10111],当随机指定染色体中间位置的基因进行交叉时,交叉后得到的染色体分别为 [10101] 和 [10011]。
基因变异
遗传算法中基因变异的方法包括单点变异和多点变异等。既随机选择染色体上一个或多个位置的基因进行变异操作。以二进制编码为例,当染色体上某个位置的基因被选中变异时,将该位置的二进制值取反即完成基因的变异。以上面交叉变异例子中得到的染色体 [10011] 为例,假设选中染色体的最后一个基因位进行变异,则得到的染色体为 [10010]。
在确定上述五个部分的方法后,遗传算法的设计就已基本完成,剩下的仅是设置算法的几个主要的超参数。要设置的超参数一般包括:种群中个体数目、交叉变异概率、基因变异概率和种群迭代的次数。
算法流程
经典遗传算法的执行流程图如下,遗传算法的计算流程可概括为:首先初始化由若干个体组成的一个群体(也可同时初始化多个群体并行计算),接着用设计的适应度函数对种群中所有个体进行适应度评估,如果有个体的评估结果达到要求,则搜索成功,遗传算法终止。如果评估的结果均不合格,则对群体中的个体依次进行选择复制、交叉变异和基因变异以产生下一代个体替换原有种群中的个体,接着再对种群中新一代的个体进行适应度评估。如此循环往复,直到种群中存在个体的评估结果达到要求,或者运行到指定的种群迭代次数程序停止。
简单示例
为更好地理解和使用遗传算法,这里提供一个简单的例子,即使用遗传算法解决八皇后问题。例子的算法设计思路及Python代码实现我已放到了GitHub上,地址是遗传算法解决八皇后问题,因此就不在这里重复赘述了,感兴趣的读者可以看看。
有意思的是,在我自己编写八皇后问题中判定一对棋子是否位置有冲突的代码部分时,起初我的思路是以一个棋子为准对其他棋子进行遍历以判断位置(行和列及斜对角)是否有冲突,但这样做不仅代码比较冗长,而且计算复杂度也相对比较大,完全是新手级别简单暴力的实现。在写好之后我又看了下网上推荐的做法,发现前人在经过统计分析后得出有更为简单的范式,其实现的代码量以及计算复杂度均吊打我的暴力版本,因此不得不感慨,这便是高阶和低阶编程的区别。
结束语
遗传算法是解决如TSP问题的经典方法,不仅如此,它也可用于对如机器学习中超参数或者算法架构的优化搜索当中。对于从事相关领域方向的学生来说,遗传算法很有学习的必要。有意思的是,不像深度神经网络那样没有实际的理论支持,遗传算法有一套算法理论,其解释了为什么在一个群体中对个体进行选择复制、交叉变异和基因变异后,整个群体的适应度会整体上升,且在经过若干次迭代后群体中个体的适应度会逐步趋近于预先设定的目标值。