遗传算法简介:
遗传算法(Genetic algorithm)属于演化计算( evolutionary computing),是随着人工智能领域发展而来的一种智能算法。正如它的名字所示,遗传算法是受达尔文进化论启发。简单来说,它是一种通过模拟自然进化过程搜索最优解的方法。
如果你想了解遗传算法相关的知识,可以学习实验楼上的教程:【Python实现遗传算法求解n-queens问题】,该实验分两节:
- 第一节介绍遗传算法的理论知识并用Python实现简单遗传算法求解函数极值。
- 第二节利用pyevolve库解决一些复杂的优化问题。
这篇文章主要介绍遗传算法的理论知识,并用Python实现简单遗传算法求解函数极值。希望对于想了解学习遗传算法的小伙伴有所帮助~
涉及知识点:
通过该文章,可以学习到以下知识点:
- 关于遗传算法的理论知识
- 实现遗传算法的流程
- 数据的处理与可视化
适合人群:
比较适合以下用户:
- 熟悉Python基础的用户
- 对智能算法比较感兴趣的用户
- 对多维问题优化有兴趣的用户
以下是【Python实现遗传算法求解n-queens问题】教程的第一节内容,基于实验楼在线开发环境制作。
开发准备
virtualenv环境配置
#安装virtualenv
sudo pip install virtualenv
#创建虚拟环境
virtualenv genetic_python
#cd genetic_python/bin/
source ./activate
配置成功之后的效果图:
在新配置的Python环境中安装相关模块
$ sudo pip install numpy
$ sudo apt-get install python-matplotlib
运行示例代码
cd Code/Code_genetic
Python binary_genetic.py
实验步骤
了解理论知识
遗传学背景知识
- 种群(Population):生物的进化以群体的形式进行,这样的一个群体称为种群。
- 个体:组成种群的单个生物。
- 基因 ( Gene ) :一个遗传因子。
- 染色体 ( Chromosome ) :包含一组的基因。
- 生存竞争,适者生存:对环境适应度高的个体参与繁殖的机会比较多,后代就会越来越多。适应度低的个体参与繁殖的机会比较少,后代就会越来越少。
- 遗传与变异:新个体会遗传父母双方各一部分的基因,同时有一定的概率发生基因变异。
- 简单说来就是:繁殖过程,会发生基因交叉( Crossover ) ,基因突变 ( Mutation ) ,适应度( Fitness )低的个体会被逐步淘汰,而适应度高的个体会越来越多。那么经过N代的自然选择后,保存下来的个体都是适应度很高的,其中很可能包含产生的适应度最高的个体 历史:进化策略(ES)是在1965年由Rechenberg和Schwefel独立提出的。之后,进化策略的思想被其他研究者推广,John Holland和他的学生和同事在此基础上发明了遗传算法,与此同时Holland 的著作”Adaption in Natural and Artificial Systems“与1975年发布。
1993年,John Koza使用遗传算法写成演化程序来解决特定的问题。他称这种程序为遗传编程(GP)。使用的编程语言是LISP,因为这种编程语言可以用解析树(parse tree)的形式呈现,而解析树问题也是遗传算法的工作对象。
染色体(Chromosome):
所有的生物体都是由细胞组成,每一个细胞中都有一定数量的染色体。染色体是DNA的序列和遗传信息(基因)的主要载体。每一个基因对特定的蛋白质进行编码,通常来说就是每一个基因会控制一种特征,例如眼睛的颜色。用来控制同一个特征(蓝色或棕色)的基因被称为等位基因(alleles)。每一个基因在染色体上的位置是固定的,这个位置被称为基因座(locus). 完整的遗传物质(所有染色体)称为基因组。基因组中的特定基因称为基因型。基因型是个体表现型的有关基因组成,包括生理和心理特征,如眼睛颜色、智力等。
繁殖(Reproduction):
在繁殖过程中,首先发生的是重组(或交叉(crossover))。父母的基因在以某种方式形成了整个新染色体。新创建的后代可以突变。突变意味着DNA的组成基因有些变化。这种变化主要是由于父母复制基因的错误造成的。 个体的适应性是个体对于生存环境的适应程度决定。
搜索空间(search space):
如果我们解决一些问题,我们通常做的就是寻找它的解,并且力求找到最优解。一个问题的所有可能解称为搜索空间,搜索空间中的每一个点都代表一个可能解。一个可能解的可能程度是通过他所对应的计算值或适应度评估。
大多时候,我们所求的最优解是找寻搜索空间中函数极大值或极小值所对应的解。这个过程还是很复杂的,因为我们不知道从何搜起。现有一些找寻合适解的方法:爬山法(hill climbing),禁忌搜索(tabu search),模拟退火法(simulated annealing),遗传算法等。这些方法都是被认为比较好的方法,因为我们不需要证明所得到的解是最优的。
适应度函数(Fitness):
适应度用于评价个体的优劣程度,适应度越大个体越好,反之适应度越小则个体越差;根据适应度的大小对个体进行选择,以保证适应性能好的个体有更多的机会繁殖后代,使优良特性得以遗传.因此,遗传算法要求适应度函数值必须是非负数,而在许多实际问题中,求解的目标通常是费用最小,而不是效益最大,因此需要将求最小的目标根据适应度函数非负原则转换为求最大目标的形式。
二进制编码(Binary Encoding)
这里我要对代码中的一些变量进行说明:
- popsize:每代的个体数
- chrosize:变量编码长度
- [xrangemin,xrangxmax] :变量取值范围
- crossrate:交叉率
- mutationrate :变异率
- generation:遗传代数
先导入需要用到的库:
from numpy import random
import numpy as np
import matplotlib.pyplot as plt
from math import pi,sin
import copy
二进制编码是最常用的的编码方式,每个个体都是由0,1组成。 表现型:一个具体的实数x 基因型:二进制编码(串长取决于求解精度) 我们可以随机生存0,1数组,然后在对随机数组对应到实数空间中进行解密。 编码长度越长,精度越高。如将6位二进制编码映射到$[-1,1]$内,则精度为:
,
则先将二进制转化为十进制,如
,
转化为$[-1,1]内为
。
初始化种群:
def initialpop(self):
pop = random.randint(0,2,size =(self.popsize,self.chrosize))
return pop
初始化产生10个个体如下,其中黄色标记为最优个体,即对应实数的函数适应度最大:
对群组在实数空间内解码:
#对十进制进行转换到求解空间中的数值
def get_declist(self,chroms):
step =(self.xrangemax - self.xrangemin)/float(2**self.chrosize-1)
self.chroms_declist =[]
for i in xrange(self.popsize):
chrom_dec =self.xrangemin+step*self.chromtodec(chroms[i])
self.chroms_declist.append(chrom_dec)
return self.chroms_declist
#将二进制数组转化为十进制
def chromtodec(self,chrom):
m = 1
r = 0
for i in xrange(self.chrosize):
r = r + m * chrom[i]
m = m * 2
return r
对应的实数空间为: [-0.9501466275659824, -0.7419354838709677, -0.7741935483870968, 1.63049853372434, -0.3724340175953079, -0.6744868035190617, 0.24926686217008798, 1.3870967741935485, 0.9736070381231672, 0.16715542521994142] 以下内容涉及到遗传算法的核心部分,有必要先了解一些具体流程和优化目标:
算法流程图:
我们的目标是求解函数:
上最大值,从函数曲线图可以看出,函数值跳跃性很强,用一般的穷举法求解较为困难。通过求导法,可以求解出函数的最优解为:f(1.85)=3.85。
原函数与适应度函数:
# 对传入的每个变量,计算函数值
def fun(self,x1):
return x1*sin(10*pi*x1)+2.0
#由于我们所求的是最大值,所有可以用函数值代替适应度
def get_fitness(self,x):
fitness = []
for x1 in x:
fitness.append(self.fun(x1))
return fitness
选择(selection)
下一代染色体是从进行交叉的父代个体中选择而来。问题在于,如何选择。根据达尔文的生物进化理论,适应性较好的个体会生存下来并且创造后代。有很多种方法来选择适应性较好的个体。例如:轮盘赌选择(roulette wheel selection),玻尔兹曼选择(Boltzman selection),联赛选择(tournament selection),排序选择(rank selection),稳定状态选择(steady state selection)等。
轮盘赌选择(Roulette Wheel Selection ):
又称比例选择方法.其基本思想是:各个个体被选中的概率与其适应度大小成正比。 从父代个体的适应度选择,适应度越好,被选择的概率越大。想象眼前有一个轮盘,把所有染色体放在轮盘上,每一个个体都有和适应度相关比例的空间大小。
很显然个体1的适应度最大,所有对应的空间大小也最大。但是其他较低适应度的个体也有被选择到的概率。 这样有什么好处?避免了所有的后代都选择概率最大的个体1(所谓的最优值问题),换句话,各个概率(各种情况)都相应的被使用到了,避免了陷入局部最优的问题。
下面具体介绍一下轮盘赌选择的实现过程:
(1) 计算群体中每个个体的适应度f(i =1,2,…….N);
(2) 计算出每一个个体被遗传到下一代群体中的概率:
(3) 计算每个个体的;累计概率:
其中q(xi)称为染色体x[i] (i=1, 2, …, n)的积累概率;
(4) 在[0,1]区间内产生一个均匀分布的伪随机数r;
(5) 若r
(6) 重复(4)、(5)共M次。
下面给出轮盘赌算法的代码:
#输入参数为上一代的种群,和上一代种群的适应度列表
def selection(self,popsel,fitvalue):
new_fitvalue = []
totalfit = sum(fitvalue)
accumulator = 0.0
for val in fitvalue:
#对每一个适应度除以总适应度,然后累加,这样可以使适应度大
#的个体获得更大的比例空间。
new_val =(val*1.0/totalfit)
accumulator += new_val
new_fitvalue.append(accumulator)
ms = []
for i in xrange(self.popsize):
#随机生成0,1之间的随机数
ms.append(random.random())
ms.sort() #对随机数进行排序
fitin = 0
newin = 0
newpop = popsel
while newin < self.popsize:
#随机投掷,选择落入个体所占轮盘空间的个体
if(ms[newin] < new_fitvalue[fitin]):
newpop[newin] = popsel[fitin]
newin = newin + 1
else:
fitin = fitin + 1
#适应度大的个体会被选择的概率较大
#使得新种群中,会有重复的较优个体
pop = newpop
return pop
根据轮盘赌选择的原理,适应度高的个体将会被选择下来的可能性更大,所以我们可以打印出初代选择之后的种群发现,最优个体被五次选择:
排序选择算法(Rank Selection ):
轮盘赌选择算法可能会在个体适应度相差很大的时候带来一些问题。比如,最好个体的适应度是占据了轮盘面积的90%,那么剩余的个体的生存几率将会很小,显然这会影响物种多样性。 排序选择首先会将按照个体适应度进行排序,按照排序的结果,每个个体将会重新获得一个数值。最差的个体的适应度为1,第二差的为2,以此类推最好的个体的适应度为N(种群个体的大小)。 可以对比一下按照个体适应度和排序数值的不同结果:
图:按照适应度分配空间
轮盘赌选择:
1 | 2 |
---|---|
Chromosomes | rate |
Chromosome1 | 0.90 |
Chromosome2 | 0.06 |
Chromosome3 | 0.03 |
Chromosome4 | 0.01 |
图:按照排序值分配空间
排序选择
Chromosomes | ranking | rate |
---|---|---|
Chromosome1 | 1 | 4/10=0.4 |
Chromosome2 | 2 | 0.3 |
Chromosome3 | 3 | 0.2 |
Chromosome4 | 4 | 0.1 |
排序选择将使所有个体都有将会被选择。但是这样也会导致种群不容易收敛,因为最好的个体与其他个体的差别减小。
交叉(Crossover):
交叉与变异是遗传算法的两个基本算子。遗传算法的结果主要由这两个算子决定。算子的编写和执行主要取决于编码方式和实际问题。实现交叉和变异的方式有很多种。
单点交叉(Single point crossover): 指在个体编码串中只随机设置一个交叉点,然后再该点相互交换两个配对个体的部分染色体。
11001011 + 11011111 = 11011111
def crossover(self,pop):
for i in xrange(self.popsize-1):
#近邻个体交叉,若随机数小于交叉率
if(random.random()
两点交叉(Two point crossover): 在个体编码串中随机设置了两个交叉点,然后再进行部分基因交换。
11001011 + 11011111 = 11011111
均匀交叉(Uniform crossover): 两个配对个体的每个基因座上的基因都以相同的交叉概率进行交换,从而形成两个新个体。
11001011 + 11011101 = 11011111
算术交叉(Arithmetic crossover): 由两个个体的线性组合而产生出两个新的个体。该操作对象一般是由浮点数编码表示的个体。
11001011 + 11011111 = 11001001 (AND)
变异(Mutation )
基因反转(Bit inversion)
11001001 => 10001001
def mutation(self,pop):
for i in xrange(self.popsize):
#反转变异,随机数小于变异率,进行变异
if (random.random()< self.mutationrate):
mpoint = random.randint(0,self.chrosize-1)
#将随机点上的基因进行反转。
if(pop[i][mpoint]==1):
pop[i][mpoint] = 0
else:
pop[mpoint] =1
return pop
精英保留策略(Elitism):
通过选择,交叉和变异创造新个体的时候,有可能会失去一部分优秀的个体。 总体思想是,判断经过选择,交叉,变异之后的种群中,是否产生了更优秀的个体,如果没有,则将上一代的精英个体替换较差个体。
精英保留策略会快速提高遗传算法性能,因为它可以防止最优个体的丢失。
def elitism(self,pop,popbest,nextbestfit,fitbest):
#输入参数为上一代最优个体,变异之后的种群,
#上一代的最优适应度,本代最优适应度。这些变量是在主函数中生成的。
if nextbestfit-fitbest <0:
#满足精英策略后,找到最差个体的索引,进行替换。
pop_worst =nextfitvalue.index(min(nextfitvalue))
pop[pop_worst] = popbest
return pop
串联整个流程
首先定义一个Gas类,并初始化变量:
class Gas():
#初始化一些变量
def __init__(self,popsize,chrosize,xrangemin,xrangemax):
self.popsize =popsize
self.chrosize =chrosize
self.xrangemin =xrangemin
self.xrangemax =xrangemax
self.crossrate =1
self.mutationrate =0.01
然后依次在class中添加上面介绍的算子,此处为了更好理解整个流程,只写出函数关键字。函数具体实现上面有介绍:
class Gas():
def __init__(self)#具体参见上面
...
def def initialpop(self):#初始化种群
...
def fun(self,x1): #定义函数
...
def get_fitness(self,x): #适应度函数
...
def selection(self,popsel,fitvalue): #选择
...
def crossover(self,pop): #交叉
...
def mutation(self,pop): #变异
...
def elitism(self,pop,popbest,nextbestfit,fitbest):#精英保留
...
def get_declist(self,chroms): #解码
...
def chromtodec(self,chrom): #二进制转十进制
...
运行主函数:
if __name__ == '__main__':
generation = 100 # 遗传代数
#引入Gas类,传入参数:种群大小,编码长度,变量范围
mainGas =Gas(100,10,-1,2)
pop =mainGas.initialpop() #种群初始化
pop_best = [] #每代最优个体
for i in xrange(generation):
#在遗传代数内进行迭代
declist =mainGas.get_declist(pop)#解码
fitvalue =mainGas.get_fitness(declist)#适应度函数
#选择适应度函数最高个体
popbest = pop[fitvalue.index(max(fitvalue))]
#对popbest进行深复制,以为后面精英选择做准备
popbest =copy.deepcopy(popbest)
#最高适应度
fitbest = max(fitvalue)
#保存每代最高适应度值
pop_best.append(fitbest)
################################进行算子操作,并不断更新pop
mainGas.selection(pop,fitvalue) #选择
mainGas.crossover(pop) # 交叉
mainGas.mutation(pop) #变异
################################精英策略前的准备
对变异之后的pop,求解最大适应度
nextdeclist = mainGas.get_declist(pop)
nextfitvalue =mainGas.get_fitness(nextdeclist)
nextbestfit = max(nextfitvalue)
################################精英策略
#比较深复制的个体适应度和变异之后的适应度
mainGas.elitism(pop,popbest,nextbestfit,fitbest)
对结果进行可视化分析:
if __name__ == '__main__':
...
t = [x for x in xrange(generation)]
s = pop_best
plt.plot(t,s)
plt.show()
plt.close()
最后可以通过matplot打印出计划图,个体从较小的适应度值,通过遗传进化爬升到较大值,并达到收敛。
关于遗传算法的框架有很多,matlab中专业的工具箱。但是由于代码都被封装,不易理解。本节通过理论与编程结合,在理论基础上实现了遗传算法的整体框架,并在实验中得到了较好的结果。
如果你想查看第二节内容:利用pyevolve库解决一些复杂的优化问题。 点击【Python实现遗传算法求解n-queens问题】即可查看~