1. 遗传编程简介
0x1:什么是遗传编程算法,和传统机器学习算法有什么区别
传统上,我们接触的机器学习算法,都是被设计为解决某一个某一类问题的确定性算法。对于这些机器学习算法来说,唯一的灵活性体现在参数搜索空间上,向算法输入样本,算法借助不同的优化手段,对参数进行调整,以此来得到一个对训练样本和测试样本的最佳适配参数组。
遗传编程算法完全走了另一外一条路,遗传编程算法的目标是编写一个程度,这个程序会尝试自动构造出解决某一问题的最佳程度。从本质上看,遗传编程算法构造的是一个能够构造算法的算法。
另一方面,我们曾经讨论过遗传算法,遗传算法是一种优化技术,就优化技术而言,无论是何种形式的优化,算法或度量都是预先设定好的,而优化算法所做的工作就是尝试为其找到最佳参数。和优化算法一样,遗传编程也需要一种方法来度量题解的优劣程度。但与优化算法不同的是,遗传编程中的题解并不仅仅是一组用于给定算法的参数,相反,在遗传编程中,连同算法本身及其所有参数,都是需要搜索确定的。
从某种程度上来说,遗传编程和遗传算法的区别在于,进化的基本单位不同,
- 遗传优化:进化的基本单位是模型可变参数
- 遗传编程:进化的基本单位是新算法以及新算法的参数
0x2:遗传编程和进化论的关系
遗传算法是受达尔文的进化论的启发,借鉴生物进化过程而提出的一种启发式搜索算法,因此遗传算法 ( GA , Genetic Algorithm ) 也称进化算法 。 因此,在讨论遗传编程的时候,会大量借用进化论中的术语和概念,为了更好地讨论遗传算法,我们先介绍一些基本生物进化概念,
- 基因 ( Gene ):一个遗传因子,种群中的最基本单元。
- 染色体 ( Chromosome ):一组的基因。
- 个体 ( individual ):单个生物。在遗传算法中,个体一般只包含一条染色体。
- 种群 ( Population ):由个体组成的群体。生物的进化以种群的形式进化。
- 适者生存 ( The survival of the fittest ):对环境适应度高的个体参与繁殖的机会比较多,后代就会越来越多。适应度低的个体参与繁殖的机会比较少,后代就会越来越少。
生物所处的环境起到一个提供生存压的作用(反馈),虽然纵观整个地球历史,环境的因素是在不断变化的(有时甚至变化的还很快),但是在某个时间段内(例如5000年内)是基本保持不变的,而物种进化的目的就是通过一代代的繁衍,逐渐适应(拟合)当前的环境,并和其他物种达到最优平衡(纳什均衡)。
遗传编程算法就是模拟了生物进化的过程,简单说来说,
- 生物进化的环境由一个用户定义的任务(user-defined task)所决定,算法由一组初始的题解(程序)开始展开竞争。这里所谓的任务可以是多种形式,
- 一种竞赛(game):各个题解(程序)在竞赛中直接展开竞争
- 个体测试:测出哪个题解(程序)的执行效果更好
- 遗传算法将基因抽象为题解中最小的随机变量因子(例如模型中的可变参数)
- 一个问题的解由很多这样的随机变化因子组成,算法将问题的解编码成个体的染色体(染色体是基因的集合)
- 单个个体包含若干个染色体,个体包含的染色体(题解)越多和越好,则个体的适应度就越好。在实际工程中,为了简化算法,常常假设一个个体只有一条染色体
- 多个个体组成种群,种群中适应度(Fitness)高的个体获得较高概率的繁殖机会,从而导致适应度高的个体会越来越多,经过N代的自然选择后,保存下来的个体都是适应度很高的
- 繁殖过程中,算法会评估并挑选出本轮表现最好的一部分题解题解(程序),并对程序的某些部分以随机(一定概率)的方式进行修改,包括:
- 基因交叉(Acrossover):在最优题解之间,挑选部分随机变量因子进行彼此互换。遗传算法交叉比人体内染色体交叉要简单许多。遗传算法的染色体是单倍体,而人体内的真正的染色体是双倍体。下图是遗传算法中两条染色体在中间进行交叉的示意图,
- 基因突变(Mutation):在最优题解上,直接对某些随机变量因子(基因位)进行随机修改。下图是遗传算法中一条染色体在第二位发生基因变异的示意图,
- 基因交叉(Acrossover):在最优题解之间,挑选部分随机变量因子进行彼此互换。遗传算法交叉比人体内染色体交叉要简单许多。遗传算法的染色体是单倍体,而人体内的真正的染色体是双倍体。下图是遗传算法中两条染色体在中间进行交叉的示意图,
- 经过繁殖过程,新的种群(即新的一组解)产生,称为“下一代”,理论上,这些新的题解基于原来的最优程序,但又不同于它们。这些新产生的题解和旧的最优题解会一起进入下一轮自然选择阶段
- 上述繁殖过程重复多次,直到达到收敛条件,包括,
- 找到了全局最优解
- 找到了表现足够好的解
- 题解在历经数代之后都没有得到任何改善
- 繁衍的代数达到了规定的限制
- 最终,历史上适应度最高个体所包含的解,作为遗传算法的输出
下图是遗传算法的流程图,
0x3:遗传编程的不同类型
从大的方面看,遗传编程的两个重要概念是基因型和表现型,
- 基因型就是种群个体的编码;
- 表现型是种群个体所表示的程序片段;
其实遗传算法领域的研究中,这两个方面的研究都有,但是,因为遗传编程很难直接处理程序片段(表现型)(例如一段C++可执行代码、或者是一段python代码),因为基于随机变异得到的新代码很可能无法通过编译器语法检查。
但是相比之下,遗传算法反而容易处理程序片段的内在结构(基因型)(例如C++代码的AST抽象语法树)。
所以,笔者认为基因型的遗传算法研究才是更有研究价值的一个方向,本文的讨论也会围绕基因型的遗传算法展开。
根据基因型形态的不同,遗传编程方法可以分为三种:
- 线性遗传编程
- 基于树的遗传编程
- 基于图的遗传编程
1. 线性遗传编程
线性遗传编程有广义和狭义之分,
- 广义线性遗传编程将候选程序编码进定长或者变长的字符串,即基因型是线性字符串,包括
- Multi-Expression Programming (MEP)
- Grammatical Evolution (GE)
- Gene Expression Programming (GEP)
- Cartesian Genetic Programming (CGP):该算法是一种很适合电路设计的遗传编程算法,比如我们要用两个加操作两个减操作和两个乘操作得到如下运算,
- 笛卡尔遗传编程将下面的一个候选程序编写进字符串"001 100 131 201 044 254 2573"。字符串中的三位数字“xyz"表示x操作的输入是y和z两个连线,字符串中最后的四位数字"opqr"表示输出opqr四个连线。笛卡尔遗传编程只用变异操作,而不用交叉操作。
- Genetic Algorithm for Deriving Software (GADS)
- 狭义线性遗传编程中的候选程序是汇编语言或者高级编程语言程序(例如C程序)。一个狭义线性遗传编程的个体可以是一段简单 C 语言指令,这些指令作用在一定数量预先定义的变量或者常量上(变量数量一般为指令个数的4倍)。下图是一个狭义线性遗传编程候选程序的示例,
-
,可以看到,变量数量和指令数量都是固定的,通过不同的排列组合方式得到不同的代码表现形式
http://www.doc88.com/p-630428999834.html https://pdfs.semanticscholar.org/958b/f0936eda72c3fc03a09a0e6af16c072449a1.pdf
2. 基于树的遗传编程
基于树的遗传编程的基因型是树结构。基于树的遗传编程是遗传编程最早的形态,也是遗传编程的主流方法。
大多数编程语言,在编译或解释时,首先会被转换成一棵解析树(Lisp编程语言及其变体,本质上就是一种直接访问解析树的方法),例如下图,
树上的节点有可能是枝节点也可能是叶节点,
- 枝节点代表了应用于其子节点之上的某一种操作
- 叶节点代表了某个参数或常量值
例如上图中,圆形节点代表了应用于两个分支(Y变量和常量值5)之上的求和操作。一旦我们求出了此处的计算值,就会将计算结果赋予上方的节点处。相应的,这一计算过程会一直向下传播,直到遍历所有的叶子节点(深度优先递归遍历)。
如果对整棵树进行遍历,我们会发现它相当于下面这个python函数:
在遗传变异方面,基于树的遗传编程的演化操作有两种,变异和交叉,
- 变异:基于树的遗传编程的变异操作有两种(区别在于变异的范围不同),
- 一种是随机变换树中的符号或者操作符
- 另一种是随机变换子树
,该图左下角是变换符号或者操作符的结果,右下角是变换子树的结果。
- 交叉:两个颗树之间随机交换子树
,两棵树之间的部分节点发生了随机互换
3. 基于图的遗传编程
树是一种特殊的图,因此人们很自然地想到将基于树的遗传编程扩展到基于图的遗传编程。下图就是基于图的遗传编程的基因型的一个示例。
Relevant Link:
《Adaptation in Natural and Artificial Systems》 John Henry Holland 1992 http://www.algorithmdog.com/%e9%81%97%e4%bc%a0%e7%ae%97%e6%b3%95%e7%b3%bb%e5%88%97%e4%b9%8b%e4%b8%80%e9%81%97%e4%bc%a0%e7%ae%97%e6%b3%95%e7%ae%80%e4%bb%8b 《Evolving Evolutionary Algorithms using Linear Genetic Programming (2005)》 《A comparison of several linear genetic programming techniques》Oltean, Mihai, and Crina Grosan. Complex Systems 14.4 (2003): 285-314. https://www.jianshu.com/p/a953066cb2eb
2. 遗传编程的数学基础
这个章节,我们用数学的形式化视角,来重新审视一下遗传算法。
0x1:基本数学符号定义
I | 种群中的个体 |
m | 所有可能个体的数量 |
n | 种群大小 |
pm | 变异概率 |
pc | 交叉概率 |
f(I) | 个体I的适应度。 |
p(I)t | 第t代种群中,个体I出现的概率 |
![]() |
第t代种群平均适应度。第t代种群中个体适应度的平均值。 |
因为遗传算法中有各种各样的编码方式、变异操作、交叉操作和选择操作,遗传算法的形态呈现多样性。
为了简化分析,我们这里假设一个典型遗传算法,即,
- 编码方式是二进制编码:基因的取值只能是0或者1
- 变异操作将所有染色体所有基因位以恒定 pm 的概率翻转
- 交叉操作选择选择相邻的个体,以 pc 的概率决定是否需要交叉。如果需要交叉,随机选择一个基因位,并交换这个基因位以及之后的所有基因
- 每一代的新种群选择操作采用轮盘赌算法(依据概率大小):有放回地采样出原种群大小的新一代种群,个体 Ii 的采样概率如下所示,
0x2:模式定理 - 概率视角看基因模式的遗传
模式定理是遗传算法创始人 J.Holland 在其突破性著作《Adaptation in Natural and Artificial Systems》引入的,用于分析遗传算法的工作原理。
模式是指基因编码空间中,由一类相似的基因组抽象得到的pattern,比如 [0,*,*,1] 就是一个模式。染色体[0,1,0,1]和[0,0,0,1]都包含该模式。
在具体讨论模式定理之前,我们先介绍一些符号,
L(H) | 模式的长度。第一固定基因位和最后一个固定基因位的距离,其中L([0,*,*,1])=3。 |
O(H) | 模式的阶。固定基因位的个数,其中O([0,*,*,1])=2。 |
![]() |
模式平均适应度。种群中包含该模式的个体适应度的平均值。 |
p(H)t | 在第t代种群中,模式H出现的概率。 |
【模式定理】
在本章定义的典型遗传算法中,下面公式成立:
这个公式看起来有点复杂,其实非常简单,我们逐个部分来分解,
- 选择操作对模式H在下一代出现的影响是固定的,即:
- 某个模式在繁衍中,既有可能发生变异,也有可能发生交叉,所以有后面两个括号相乘
- 某个模式在变异中,变异操作将所有基因位以 pm 的概率翻转,因此模式H不被破坏的概率为(1−pm)O(H)。当0<=x<=1和n=1,...时,不等式(1−pm)O(H) >= 1−O(H)∗pm成立,从而经过变异操作,模式H的出现概率为,
- 某个模式在交叉中,交叉操作选择选择相邻的个体,以 pc 的概率决定是否需要交叉。如果需要交叉,随机选择一个基因位,并交换这个基因位以及之后的所有基因。因此模式H不被破坏的概率为(1−pc)(1−L(H)/L−1) >= 1 − pc∗L(H)/L−1。经过交叉操作,模式H的出现概率为,
总体来说,遗传算法需要在,选择操作引起的收敛性和变异交叉操作引起的多样性之间取得平衡。
模式定理的通俗说法是这样的,低阶、短序以及平均适应度高于种群平均适应度的模式在子代中呈指数增长。
低阶、短长以及平均适应度高于种群平均适应度的模式H,
此时,
即模式H呈现指数增长。
0x3:马尔柯夫链分析 - 遗传编程收敛性分析
这个小节我们来讨论一个有些理论化的问题,即:遗传编程算法经过一定次数的迭代后,是否会收敛到某个稳态?如果会达到稳态,遗传编程的收敛速度是多少?
要解决这个问题,我们需要引入数学工具,马尔柯夫链,有如下定义。
- 用 pt 表示第 t 时刻的不同状态的概率
- P 表示转移概率矩阵,其中 Pi,j表示从第 i 个状态转移到第 j 个状态的概率
- 齐次马尔科夫链的第 t+1 时刻的状态只和第 t 时刻有关,可以用公式 pt+1=ptP 表示
- 若存在一个自然数 k,使得 Pk 中的所有元素大于0,则称 P 为素矩阵。随着 k 趋近于无穷,Pk 收敛于 P∞=1Tp∞, 其中p∞=p0 limk→∞Pk=p0 是和初始状态无关的唯一值,并且所有元素大于0。这其实是由马尔柯夫链稳态定理决定的。
我们把整个种群的状态看成马尔科夫链的一个状态 s,交叉、变异和选择操作则构建了一个概率转移矩阵。一般情况下,0<pm<1,0<=pc<=1,即物种变异一定会发生,但不是必然100%发生。我们来分析一下这时的概率转移矩阵的性质。
- 让 C,M,S 分别表示交叉、变异和选择操作带来的概率转移,整体概率转移矩阵 P=CMS
- 经过变异操作,种群状态 si 转化成种群状态 sj 的概率 Mi,j=(pm)h(1−pm)n∗l-h>0,其中h是两个种群之间不同值的基因位数量。也就是说,M 是素矩阵
- 经过选择操作,种群状态 si 保持不变的概率
,也就是说, S 的所有列必定有一元素大于0。我们也可以知道概率转移矩阵 P 是素矩阵
标准的优化算法分析第一个要关心的问题是,优化算法能不能收敛到全局最优点。假设全局最优点的适应度值为maxf,收敛到全局最优点的定义如下,
一言以蔽之,典型遗传算法并不收敛。
根据概率转移矩阵收敛定理,我们可以知道典型遗传算法会收敛到一个所有种群状态概率都大于0的概率分布上(稳态)。因此之后,不包含全局最优解的种群一定会不停出现,从而导致上面的公式不成立。
但是笔者这里要强调的是,这章讨论的典型遗传算法在实际工程中是几乎不存在的,实际上,几乎所有遗传算法代码都会将保持已发现最优解。加了这个变化之后的遗传算法是收敛的。
还是根据上述概率转移矩阵收敛定理,我们可以知道遗传算法会收敛到一个所有种群状态概率都大于0的概率分布上,那么包含全局最优解的种群一定会不停出现,保持已发现最优解的做法会使得上面的公式成立。
Relevant Link:
Adaptation in Natural and Artificial Systems: An Introductory Analysis with Applications to Biology, Control, and Artificial Intelligence Rudolph, Günter. “Convergence analysis of canonical genetic algorithms.” Neural Networks, IEEE Transactions on 5.1 (1994): 96-101. http://www.algorithmdog.com/%e9%81%97%e4%bc%a0%e7%ae%97%e6%b3%95%e7%b3%bb%e5%88%97%e4%b9%8b%e4%b8%89%e6%95%b0%e5%ad%a6%e6%91%86%e6%91%86%e6%89%8b%ef%bc%8c%e5%be%88%e6%83%ad%e6%84%a7%ef%bc%8c%e5%8f%aa%e5%81%9a%e4%ba%86
3. 典型遗传算法的一些变种衍生算法
自 John Henry Holland 在1992年提出《Adaptation in Natural and Artificial Systems》论文后,遗传编程又得到了大量研究者的关注和发展,提出了很多改进型的衍生算法。虽然这些算法在工业场景中不一定都适用,但是笔者觉得我们有必要了解和学习一下它们各自的算法思想,有利于我们在遇到各自的实际问题的时候,举一反三。
0x1:交叉变种
典型交叉变异随机选择两条染色体,按照pc的概率决定是否交叉,如果选择交叉则随机选择一点并将这点之后的基因交换。这种交叉方式被称为单点杂交。
1. 多点杂交
多点杂交指定了多个交换点用于父本的基因交换重组,具体的执行过程如下图所示,
多点杂交改进的是突变率。
2. 均匀杂交
单点和多点杂交算法存在一个问题,杂交的染色体中某些部分的基因会被过早地舍弃,这是由于在交换前它们必须确定交换父本染色体交换位前面还是后面的基因,从而对于那些无关的基因段在交换前就已经收敛了。
均匀杂交算法(Uniform Crossover)就可以解决上述算法的这种局限性,该算法的主要过程如下:
- 首先随机选择染色体上的交换位
- 然后随机确定交换的基因是父本染色体上交换位的前部分基因,还是后部分基因(随机过程)
- 最后对父本染色体的基因进行重组从而产生新的下一代个体
3. 洗牌杂交
洗牌杂交的最大特点是通常将染色体的中点作为基因的交换点,即从每个父本中取它们一半的基因重组成新的个体。
另外针对于实值编码方式,还有离散杂交、中间杂交、线性杂交和扩展线性杂交等算法。
0x2:选择策略变种
精英保留策略是一种典型的选择策略。精英保留策略是指每次迭代都保留已发现的最优解。这个策略是显而易见的,我们不可能舍弃已发现的最优解,而只使用最后一代种群的最优解。同时,采用精英保留策略的典型遗传算法是保证收敛到全局最优解的。
1. 轮盘赌选择策略
轮盘赌选择策略是基于概率进行选择策略。轮盘赌算法有放回地采样出原种群大小的新一代种群,个体 Ii 的采样概率如下所示,
从概率上看,在某一轮中,即使是适应度最差的个体,也存在一定的几率能进入到下一轮,这种策略提高了多样性,但减缓了收敛性。
2. 锦标赛选择策略
锦标赛法从大小为 n 的种群随机选择 k(k小于n) 个个体,然后在 k 个个体中选择适应度最大的个体作为下一代种群的一个个体。反复多次,直到下一代种群有 n 个个体。
0x3:种群繁衍策略变种 - 多种群并行
在大自然,物种的进化是以多种群的形式并发进行的。一般来说,一个物种只有一个种群了,意味着这个物种有灭亡的危险(例如恐龙)。
受此启发,人们提出了多种群遗传算法。多种群遗传算法保持多个种群同时进化,具体流程如下图所示,
多种群遗传算法和遗传算法执行多次的区别在于移民,种群之间会通过移民的方式交换基因。这种移民操作会带来更多的多样性。
0x4:自适应遗传算法
遗传算法中,决定个体变异长度的主要因素有两个:交叉概率pc,和变异概率pm。
在实际工程问题中,需要针对不同的优化问题和目标,反复实验来确定pc和pm,调参成本很高。
而且在遗传算法训练的不同阶段,我们需要不同的pc和pm,
- 当种群中各个个体适应度趋于一致或者趋于局部最优时,使pc和pm增加,增加扰动。使得种群具有更大的多样性,跳出潜在的局部最优陷阱
- 当群体适应度比较分散时,使pc和pm减少。使得适应度高的个体和适应度低的个体保持分开,加快种群收敛速度
- 不同个体也应该有不同的pc和pm:
- 对于适应度高的个体,我们应该减少pc和pm以保护他进入下一代
- 反之对适应度低的个体,我们应该增加pc和pm以增加扰动,提高个体多样性
Srinivas.M and Patnaik.L.M (1994) 为了让遗传算法具备更好的自适应性,提出来自适应遗传算法。在论文中,pc和pm的计算公式如下:
0x5:混合遗传算法
遗传算法的全局搜索能力强,但局部搜索能力较弱。这句话怎么理解呢?
比如对于一条染色体,遗传算法并不会去看看这条染色体周围局部的染色体适应度怎么样,是否比这条染色体好。遗传算法会通过变异和交叉产生新的染色体,但新产生的染色体可能和旧染色差的很远。因此遗传算法的局部搜索能力差。
相对的,梯度法、爬山法和贪心法等算法的局部搜索能力强,运算效率也高。
受此启发,人们提出了混合遗传算法,将遗传算法和这些算法结合起来。混合遗传算法的框架是遗传算法的,只是生成新一代种群之后,对每个个体使用局部搜索算法寻找个体周围的局部最优点。
总体来说,遗传算法和梯度法分别代表了随机多样性优化和渐进定向收敛性优化的两种思潮,取各自的优点是一种非常好的思路。
Relevant Link:
Srinivas M, Patnaik L M. Adaptive probabilities of crossover and mutation in genetic algorithms[J]. Systems, Man and Cybernetics, IEEE Transactions on, 1994, 24(4): 656-667. http://www.algorithmdog.com/%e9%81%97%e4%bc%a0%e7%ae%97%e6%b3%95%e7%b3%bb%e5%88%97%e4%b9%8b%e5%9b%9b%e9%81%97%e4%bc%a0%e7%ae%97%e6%b3%95%e7%9a%84%e5%8f%98%e7%a7%8d
4. 用遗传编程自动生成一个能够拟合特定数据集的函数
0x1:用多项式回归拟合一个数据集
这个章节,我们来完成一个小实验,我们现在有一个数据集,数据集的生成算法如下:
# -*- coding: utf-8 -*- from random import random,randint,choice def hiddenfunction(x,y): return x**2 + 2*y + 3*x + 5 def buildhiddenset(): rows = [] <