这个专栏以工具实践为主,因此我们先讲进化算法工具包geatpy的使用。
关于遗传算法的推导原理我们举一个最简单的袋鼠爬山的例子,然后看遗传算法从头到尾怎么“编码-计算适应度-选择淘汰-基因交叉-基因突变-不断进化” 的。
一、geatpy框架
Geatpy2整体上看由工具箱内核函数(内核层)和面向对象进化算法框架(框架层)两部分组成。其中面向对象进化算法框架主要有四个大类:Problem问题类、Algorithm算法模板类、Population种群类和PsyPopulation多染色体种群类。UML图如下所示:
1、Population 类是一个表示种群的类
一个种群包含很多个个体,而每个个体都有一条染色体(若要用多染色体,则使用多个种群、并把每个种群对应个体关联起来即可)。除了染色体外,每个个体都有一个译码矩阵Field(或俗称区域描述器) 来标识染色体应该如何解码得到表现型,同时也有其对应的目标函数值以及适应度。种群类就是一个把所有个体的这些数据统一存储起来的一个类。比如里面的Chrom 是一个存储种群所有个体染色体的矩阵,它的每一行对应一个个体的染色体;ObjV 是一个目标函数值矩阵,每一行对应一个个体的所有目标函数值,每一列对应一个目标。对于Population 类中各属性的详细含义可查看Population.py 源码以及下一章“Geatpy 数据结构”。
PsyPopulation类是继承了Population的支持多染色体混合编码的种群类。一个种群包含很多个个体,而每个个体都有多条染色体。用Chroms列表存储所有的染色体矩阵(Chrom);Encodings列表存储各染色体对应的编码方式(Encoding);Fields列表存储各染色体对应的译码矩阵(Field)。
2、Algorithm 类是进化算法的核心类
它既存储着跟进化算法相关的一些参数,同时也在其继承类中实现具体的进化算法。比如Geatpy 中的moea_NSGA3_templet.py 是实现了多目标优化NSGA-III 算法的进化算法模板类,它是继承了Algorithm 类的具体算法的模板类。关于Algorithm 类中各属性的含义可以查看Algorithm.py 源码。这些算法模板通过调用Geatpy 工具箱提供的进化算法库函数实现对种群的进化操作,同时记录进化过程中的相关信息,其基本层次结构如下图:
二、例子1:单目标优化
对于求解一个优化问题,无论是单目标优化、还是多目标优化、抑或是组合优化、约束优化等,需要做两件工作:(1)自定义问题类;(2)编写执行代码。一般来说自定义问题类和执行代码可以专门分别写在独立的文件中,下面的代码中为了方便直接把两者写在同一个地方。
自定义问题类是把与问题模型有关的内容在此处进行定义和设置,比如变量范围、待优化的目标、约束条件的处理等;执行代码是调用算法模板进行进化优化。
下面代码有详尽的注释。对于实际问题,只需要更改问题类中变量的设置以及目标函数aimFunc()的定义,然后编写执行代码调用算法模板进行求解即可。
对于约束条件,Geatpy提供两种方式来进行处理:罚函数法和可行性法则。其中罚函数法比较自由,只需在问题类的“aimFunc()”函数定义里面把非可行解找到,然后对相应的个体的目标函数值作出惩罚即可。如果使用可行性法则,则需要生成一个CV矩阵(种群个体违反约束程度矩阵),该矩阵的每一行对应一个个体,每一列对应一个约束条件。
1、数学模型
该案例展示了一个带等式约束的连续型决策变量最大化目标的单目标优化问题。
该函数存在多个欺骗性很强的局部最优点。
max f = 4*x1 + 2*x2 + x3
s.t.
2*x1 + x2 - 1 <= 0
x1 + 2*x3 - 2 <= 0
x1 + x2 + x3 - 1 == 0
0 <= x1,x2 <= 1
0 < x3 < 2
2、定义决策变量/目标函数/约束条件
# -*- coding: utf-8 -*-
"""MyProblem.py"""
import numpy as np
import geatpy as ea
class MyProblem(ea.Problem): # 继承Problem父类
def __init__(self):
name = 'MyProblem' # 初始化name(函数名称,可以随意设置)
M = 1 # 初始化M(目标维数)
maxormins = [-1] # 初始化maxormins(目标最小最大化标记列表,1:最小化该目标;-1:最大化该目标)
Dim = 3 # 初始化Dim(决策变量维数)
varTypes = [0] * Dim # 这是一个list,初始化varTypes(决策变量的类型,元素为0表示对应的变量是连续的;1表示是离散的)
lb = [0,0,0] # 决策变量下界
ub = [1,1,2] # 决策变量上界
lbin = [1,1,0] # 决策变量下边界(0表示不包含该变量的下边界,1表示包含)
ubin = [1,1,0] # 决策变量上边界(0表示不包含该变量的上边界,1表示包含)
# 调用父类构造方法完成实例化
ea.Problem.__init__(self, name, M, maxormins, Dim, varTypes, lb, ub, lbin, ubin)
def aimFunc(self, pop): # 目标函数
Vars = pop.Phen # 得到决策变量矩阵
x1 = Vars[:, [0]] # 取出第一列得到所有个体的x1组成的列向量
x2 = Vars[:, [1]] # 第二列
x3 = Vars[:, [2]] # 第三列
pop.ObjV = 4*x1 + 2*x2 + x3 # 计算目标函数值,赋值给pop种群对象的ObjV属性
# 采用可行性法则处理约束,numpy的hstack()把x1、x2、x3三个列向量拼成CV矩阵
pop.CV = np.hstack([2*x1 + x2 - 1,
'''约束条件1,即2*x1 + x2 - 1<= 0或者2*x1 + x2 <= 1,如果是2*x1 + x2 >= 1,则取负写作(-2*x1-x2+1)
'''
x1 + 2*x3 - 2,
np.abs(x1 + x2 + x3 - 1)])
def calReferObjV(self): # 设定目标数参考值(本问题目标函数参考值设定为理论最优值),这个函数其实可以不要
referenceObjV = np.array([[2.5]])
return referenceObjV
3、解决问题
# -*- coding: utf-8 -*-
import numpy as np
import geatpy as ea # import geatpy
from MyProblem import MyProblem # 导入自定义问题接口
if __name__ == '__main__':
"""================================实例化问题对象==========================="""
problem = MyProblem() # 生成问题对象
"""==================================种群设置==============================="""
Encoding = 'RI' # 编码方式
NIND = 100 # 种群规模
Field = ea.crtfld(Encoding, problem.varTypes, problem.ranges, problem.borders) # 创建区域描述器
population = ea.Population(Encoding, Field, NIND) # 实例化种群对象(此时种群还没被初始化,仅仅是完成种群对象的实例化)
"""================================算法参数设置============================="""
myAlgorithm = ea.soea_DE_rand_1_L_templet(problem, population) # 实例化一个算法模板对象
myAlgorithm.MAXGEN = 500 # 最大进化代数
myAlgorithm.mutOper.F = 0.5 # 差分进化中的参数F
myAlgorithm.recOper.XOVR = 0.7 # 重组概率
"""===========================调用算法模板进行种群进化======================="""
[population, obj_trace, var_trace] = myAlgorithm.run() # 执行算法模板
population.save() # 把最后一代种群的信息保存到文件中
# 输出结果
best_gen = np.argmin(problem.maxormins * obj_trace[:, 1]) # 记录最优种群个体是在哪一代
best_ObjV = obj_trace[best_gen, 1]
print('最优的目标函数值为:%s'%(best_ObjV))
print('最优的决策变量值为:')
for i in range(var_trace.shape[1]):
print(var_trace[best_gen, i])
print('有效进化代数:%s'%(obj_trace.shape[0]))
print('最优的一代是第 %s 代'%(best_gen + 1))
print('评价次数:%s'%(myAlgorithm.evalsNum))
print('时间已过 %s 秒'%(myAlgorithm.passTime))
结果:
种群信息导出完毕。
最优的目标函数值为:2.497034988230468
最优的决策变量值为:
0.4981039440998086
0.0027231559310421823
0.4991728999691491
有效进化代数:500
最优的一代是第 464 代
评价次数:147200
时间已过 1.5450232028961182 秒
三、例子2:多目标优化
下面以一个多目标优化问题为例,这个例子比较难,我们后面再详细解释,先给代码和结果:
import numpy as np
import geatpy as ea
classMyProblem(ea.Problem):#继承Problem父类
def __init__(self):name ='BNH'#初始化name(函数名称,可以随意设置)
M = 2#初始化M(目标维数)
maxormins = [1] * M#初始化
maxorminsDim = 2#初始化Dim(决策变量维数)
varTypes = [0] * Dim#初始化varTypes(决策变量的类型,0:实数;1:整数)
lb = [0] * Dim#决策变量下界
ub = [5, 3]#决策变量上界
lbin = [1] * Dim#决策变量下边界
ubin = [1] * Dim#决策变量上边界
#调用父类构造方法完成实例化
ea.Problem.__init__(self, name, M, maxormins, Dim, varTypes, lb,ub, lbin, ubin)
def aimFunc(self, pop):
#目标函数
Vars = pop.Phen
#得到决策变量矩阵
x1 = Vars[:, [0]]
x2 = Vars[:, [1]]
f1 = 4*x1**2 + 4*x2**2
f2 = (x1 - 5)**2 + (x2 - 5)**2
#采用可行性法则处理约束
pop.CV = np.hstack(
[(x1 - 5)**2 + x2**2 - 25,
-(x1 - 8)**2 - (x2 - 3)**2 + 7.7])
#把求得的目标函数值赋值给种群pop的ObjV
pop.ObjV = np.hstack([f1, f2])
def calReferObjV(self):#计算全局最优解,如果实际问题并不知道最优解,可省略
N = 10000
#欲得到10000个真实前沿点
x1 = np.linspace(0, 5, N)
x2 = x1.copy()
x2[x1 >= 3] = 3
returnnp.vstack((4 * x1**2 + 4 * x2**2,(x1 - 5)**2 + (x2 - 5)**2)).T
"""=========================实例化问题对象==========================="""
problem = MyProblem()
# 实例化问题对象
"""===========================种群设置=============================="""
Encoding ='RI'#编码方式
NIND = 100#种群规模
Field = ea.crtfld(Encoding, problem.varTypes, problem.ranges,problem.borders)#创建区域描述器
population = ea.Population(Encoding, Field, NIND)#实例化种群对象(此时种群还没被真正初始化,仅仅是生成一个种群对象)"
""=========================算法参数设置============================"""
myAlgorithm = ea.moea_NSGA2_templet(problem, population)#实例化一个算法模板对象,NSGA2是一个多目标优化算法
myAlgorithm.MAXGEN = 200#最大遗传代数m
yAlgorithm.drawing = 1#设置绘图方式
"""===================调用算法模板进行种群进化=======================调用run执行算法模板,得到帕累托最优解集NDSet。NDSet是一个种群类Population的对象。NDSet.ObjV为最优解个体的目标函数值;NDSet.Phen为对应的决策变量值。详见Population.py中关于种群类的定义。"""
NDSet = myAlgorithm.run()#执行算法模板,得到非支配种群
NDSet.save()#把结果保存到文件中
#输出
print('用时:%f秒'%(myAlgorithm.passTime))
print('评价次数:%d次'%(myAlgorithm.evalsNum))
print('非支配个体数:%d个'%(NDSet.sizes))
print('单位时间找到帕累托前沿点个数:%d个'%(int(NDSet.sizes //myAlgorithm.passTime))
#计算指标
PF = problem.getReferObjV()
#获取真实前沿
if PF is not None and NDSet.sizes != 0:
GD = ea.indicator.GD(NDSet.ObjV, PF)#计算GD指标
IGD = ea.indicator.IGD(NDSet.ObjV, PF)#计算IGD指标
HV = ea.indicator.HV(NDSet.ObjV, PF)#计算HV指标
Spacing = ea.indicator.Spacing(NDSet.ObjV)#计算Spacing指标
print('GD:%f'%GD)
print('IGD:%f'%IGD)
print('HV:%f'%HV)
print('Spacing:%f'%Spacing)
"""=====================进化过程指标追踪分析========================"""
if PF is not None:
metricName = [['IGD'], ['HV']]
[NDSet_trace, Metrics] =ea.indicator.moea_tracking(myAlgorithm.pop_trace, PF,metricName, problem.maxormins)
#绘制指标追踪分析图
ea.trcplot(Metrics, labels = metricName, titles = metricName)
种群信息导出完毕。
用时:0.781933 秒
评价次数:20000 次
非支配个体数:100 个
单位时间找到帕累托前沿点个数:632 个
GD 0.000198173830767891
IGD 0.02059977212182223
HV 0.841122031211853
Spacing 0.0012533325944347649
正在进行进化追踪指标分析,请稍后......
指标追踪分析结束,进化记录器中有效进化代数为: 500
四、遗传算法简介
1、简介
遗传算法是从代表问题可能潜在的解集的一个种群(population)开始的,而一个种群则由经过基因(gene)编码(encoding)的一定数目的个体(individual)组成。每个个体实际上是染色体(chromosome)带有特征的实体。
染色体作为遗传物质的主要载体,即多个基因的集合,其内部表现(即基因型)是某种基因组合,它决定了个体的形状的外部表现,如黑头发的特征是由染色体中控制这一特征的某种基因组合决定的。因此,在一开始需要实现从表现型到基因型的映射即编码工作。由于仿照基因编码的工作很复杂,我们往往进行简化,如二进制编码。
初代种群产生之后,按照适者生存和优胜劣汰的原理,逐代(generation)演化产生出越来越好的近似解,在每一代,根据问题域中个体的适应度(fitness)大小选择(selection)个体,并借助于自然遗传学的遗传算子(genetic operators)进行组合交叉(crossover,形象点就是种群个体之间进行交配)和变异(mutation,基因变异),产生出代表新的解集的种群。
这个过程将导致种群像自然进化一样的后生代种群比前代更加适应于环境,末代种群中的最优个体经过解码(decoding),可以作为问题近似最优解。
2、例子:袋鼠爬山
(1)袋鼠怎么爬山
下面我们用袋鼠跳中的步骤一一对应解释,以方便大家理解:首先寻找一种对问题潜在解进行“数字化”编码的方案。(建立表现型和基因型的映射关系)
随机初始化一个种群(那么第一批袋鼠就被随意地分散在山脉上),种群里面的个体就是这些数字化的编码。
接下来,通过适当的解码过程之后(得到袋鼠的位置坐标)。
用适应性函数对每一个基因个体作一次适应度评估(袋鼠爬得越高当然就越好,所以适应度相应越高)。
用选择函数按照某种规定择优选择(每隔一段时间,射杀一些所在海拔较低的袋鼠,以保证袋鼠总体数目持平。)。
让个体基因变异(让袋鼠随机地跳一跳)。
然后产生子代(希望存活下来的袋鼠是多产的,并在那里生儿育女)。
比如山长这样,我们要找到最高点:
(2)为袋鼠编码
编码无非就是建立从基因型到表现型的映射关系。这里的表现型可以理解为个体特征(比如身高、体重、毛色等等)。那么,在此问题下,我们关心的个体特征就是:袋鼠的位置坐标(因为我们要把海拔低的袋鼠给杀掉)。无论袋鼠长什么样,爱吃什么。我们关心的始终是袋鼠在哪里,并且只要知道了袋鼠的位置坐标(位置坐标就是相应的染色体编码,可以通过解码得出)。
在上面我们把极大值比喻为山峰,那么,袋鼠的位置坐标可以比喻为区间[-1, 2]的某一个x坐标(有了x坐标,再通过函数表达式可以算出函数值 <==> 得到了袋鼠染色体编码,解码得到位置坐标,在喜马拉雅山脉地图查询位置坐标算出海拔高度)。这个x坐标是一个实数,现在,说白了就是怎么对这个x坐标进行编码。下面我们以二进制编码为例讲解,不过这种情况下以二进制编码比较复杂就是了。(如果以浮点数编码,其实就很简洁了,就一浮点数而已。)
我们说过,一定长度的二进制编码序列,只能表示一定精度的浮点数。在这里假如我们要求解精确到六位小数,由于区间长度为2 - (-1) = 3 ,为了保证精度要求,至少把区间[-1,2]分为3 × 10^6等份。又因为2^21 = 2097152 < 3*10^6 < 2^22 = 4194304
所以编码的二进制串至少需要22位。
把一个二进制串(b0,b1,....bn)转化为区间里面对应的实数值可以通过下面两个步骤:将一个二进制串代表的二进制数转化为10进制数:
然后通过一下公式转化为对应的实数:
例如一个二进制串(1000101110110101000111)2通过上面换算以后,表示实数值0.637197。
(3)评价适应度
在这里适应度就是袋鼠位置越高越好。评价个体适应度的一般过程为:对个体编码串进行解码处理后,可得到个体的表现型。
由个体的表现型可计算出对应个体的目标函数值。
根据最优化问题的类型,由目标函数值按一定的转换规则求出个体的适应度
(4)选择函数-射杀位置低的袋鼠
算子有:
下面介绍几种常用的选择算子:轮盘赌选择(Roulette Wheel Selection):是一种回放式随机采样方法。每个个体进入下一代的概率等于它的适应度值与整个种群中个体适应度值和的比例。选择误差较大。
随机竞争选择(Stochastic Tournament):每次按轮盘赌选择一对个体,然后让这两个个体进行竞争,适应度高的被选中,如此反复,直到选满为止。
最佳保留选择:首先按轮盘赌选择方法执行遗传算法的选择操作,然后将当前群体中适应度最高的个体结构完整地复制到下一代群体中。
等
下面以轮盘赌选择为例给大家讲解一下:
假如有5条染色体,他们的适应度分别为5、8、3、7、2。
那么总的适应度为:F = 5 + 8 + 3 + 7 + 2 = 25。
那么各个个体的被选中的概率为:
α1 = ( 5 / 25 ) * 100% = 20%
α2 = ( 8 / 25 ) * 100% = 32%
α3 = ( 3 / 25 ) * 100% = 12%
α4 = ( 7 / 25 ) * 100% = 28%
α5 = ( 2 / 25 ) * 100% = 8%
所以转盘如下:
当指针在这个转盘上转动,停止下来时指向的个体就是天选之人啦。可以看出,适应性越高的个体被选中的概率就越大。
(5)染色体交叉
遗传算法的交叉操作,是指对两个相互配对的染色体按某种方式相互交换其部分基因,从而形成两个新的个体。
适用于二进制编码个体或浮点数编码个体的交叉算子:单点交叉(One-point Crossover):指在个体编码串中只随机设置一个交叉点,然后再该点相互交换两个配对个体的部分染色体。
两点交叉与多点交叉
等
抓一个最简单的二进制单点交叉为例来给大家讲解讲解。
二进制编码的染色体交叉过程非常类似高中生物中所讲的同源染色体的联会过程――随机把其中几个位于同一位置的编码进行交换,产生新的个体。
对应的二进制交叉:
(6)基因变异
遗传算法中的变异运算,是指将个体染色体编码串中的某些基因座上的基因值用该基因座上的其它等位基因来替换,从而形成新的个体。
例如下面这串二进制编码:
101101001011001
经过基因突变后,可能变成以下这串新的编码:
001101011011001
以下变异算子适用于二进制编码和浮点数编码的个体:基本位变异(Simple Mutation):对个体编码串中以变异概率、随机指定的某一位或某几位仅因座上的值做变异运算。
均匀变异(Uniform Mutation):分别用符合某一范围内均匀分布的随机数,以某一较小的概率来替换个体编码串中各个基因座上的原有基因值。
等
(7)不断进化
经过上述(1)-(6)的过程产生新的种群,这个过程将导致种群像自然进化一样的后生代种群比前代更加适应于环境,末代种群中(比如:我们限定最多迭代500次 或者 设定目标函数达到某个值就停止迭代)的最优个体经过解码(decoding),可以作为问题近似最优解。
参考