遗传算法基本原理
本文从非专业角度解析遗传算法。
可以先复制下面的整体代码,直接尝试运行调试一下,会更有助于理解。
主要涉及知识点
下面三点直接代码中体现
遗传算法的编码
编码方式比较多,最常见的就是二进制编码。其实也可以用四进制,用四进制刚好对应了组成DNA的四种碱基:腺嘌呤(A)、胞嘧啶(C)、鸟嘌呤(G)和胸腺嘧啶(T)。不过都是模拟,不碍事。
一般遗传学中直接将一个个体简化到了一行矩阵,即1 * n的矩阵,每一列或几列组成一种基因形状(一般也就是理解一列代表一个基因)。使用二进制就是简化到了0 1两种取值。本文也就是二进制的编码方式。
编码长度的选取
那么二进制编码的作用清楚了。现在就涉及到了有多少个基因,即这个n的取值问题了。
一般需要从两个方面考虑:自变量的范围(更准确点应该是决策变量的范围)
和搜索精度
自变量的范围
一般自变量的取值范围是估计出来的(
能直接算出来还用啥遗传算法)比如说。x的取值很明显是 2 嘛,那我们就可以合理假设值是在范围之间,不用太精确,但是肯定是越精确越好。范围越大 n 越大。搜索精度
其实就相当于是,即结果的误差精度,一般就是假设,精确度高就是1e-5(),要求不高就是0.01的样子(这里就用0.01距离了)。精度越精细 n 越大。
好了距离的范围()和精度(0.01)都确定了。那么也就是我们的答案是是在之间,但是二进制数没办法表示小数。这里的转化思路就是乘100,就相当于自变量范围为,精度为1,这下就可以表示了。所以我们需要用二进制表示至少1000个数字,也就是。这个时候可以很轻易得到。
其实这个n可以用公式计算:
所以这里就是:
这个用计算器算一下就可以了。注意要完全能够容纳这么多数字,所以需要将结果向上取整。
表示形式就是:
解码
在上面的例子中,n取10,即可以表示1024个数字,即0 ~ 1023。
这里要先说明一个概念,刚才的“放大”是直接乘100,即用0 ~ 1000 表示区间,那么9.99就是表示为999,10.00就是10.00。但实际上我们是用1023表示的10.00。这样就不会是简单的对应关系了。
也就是说解码过程要将二进制数先表示为十进制数,然后再缩小1023倍。
公式表示:
即:
所以之后把每一个解码后的十进制数乘以0.009775就行了。
于是:
代码讲解
问题
这里从如下问题展开来讨论代码。也是解决该问题的代码。这可以类似归结为单目标规划问题的求解,因为约束条件只有:,所以是最简单的规划问题。
求解在的最大值和取到最大值时的自变量。
这里先简单画个图看一下:
全局参数
popsize=20; 群体大小。这里相当于一个个体就是代表一个解
chromlength=20; 串的长度,每一列相当于是一个基因,个体基因序列个数
自变量取值范围:,搜索精度:
pc=0.6; 发生交叉的概率
pm=0.1; 发生变异的概率
xlim = [0,50]; 解的范围(题目中规定了在这个区间的最大值)
G = 100 ; 迭代次数
x = zeros(1, G); 每一代的适应度的 x 坐标
y = zeros(1, G); 每一代的适应度的 y 坐标
pop 每个个体对应的基因序列,初始值的形式可提前看下主函数中的pop初始数据图。
相关函数
Matlab自带函数
如下的函数中如果有不清楚的,可以去查一下文档。
- rand()
- round()
- zeros()
- min()
- max()
- plot()
- subplot()
- title()
- disp()
- sum()
- cumsum()
- sort()
- randperm()
- abs()
- cos()
- sin()
- pause()
自定义二进制转十进制函数(解码)
1 | function dec = bintodec(pop, popsize, chromlength, xlim) |
参数解释:
- pop:是初始的二进制群体,也就是主要待转换的东西
- popsize:是群体的个数,这里是20
- chromellength:就是基因的个数 即
- xlim:自变量范围
xlim(1)
就是;xlim(2)-xlim(1) / (2^chromlength-1)
就是
主要公式:
返回结果:一个1 * popsize的矩阵。每一列表示一个个体对象的解码后的值。
计算目标函数
1 | function fx = calobjvalue(decpop) % 参数为十进制解 |
- 参数解释:这里的参数是十进制解,也就是上一步得到的十进制序列。是一个 1 * popsize的矩阵,每一列表示一个可能的解
- 返回结果:将解代入研究函数中,求得一组解,返回一个1 * popsize的矩阵,每一列表示该位置的x经过fx求解之后的十进制结果。
绘制图像
1 | function plotfig(decpop , fx ,xlim, k) |
就是一个画图,然后停顿一下给个过程,不过多解释。
计算适应度
1 | function fitvalue = calfitvalue(fx) |
- 返回结果:1 * popsize的矩阵:每一列表示对应的x解得得fx对于本问题的适应度(合适或者不合适的程度)
这里求最大值,并且函数值又都大于0,所以直接使用函数值本身作为适应度值。
一般来说,不同的问题适应度函数构造方法是不一样的。打个比方,如果是求最小值:那么可能是适应度就是10-fx
之类的。
选择操作
这里其实可以类比自然界的选择,有很多策略,例如根据某个个体的适应度选择复制某个个体的基因可以保留下去(自然界中其实就是这样)。这里选择的轮盘赌的策略,其实就是根据个体的适应度,来给它分配被遗传的概率(也就是下面说的复制概率),适应度高的,被复制(也就是基因被保留)的概率更大。
1 | function newx = copyx(pop, fitvalue, popsize) |
参数描述:
- pop:这一代个体的二进制序列 popsize * chormlength的矩阵
- fitvalue:每一个个体的适应度 1 * popsize 的矩阵
- popsize:个体数量
轮盘赌策略:
假设有一张饼图,将一根飞针扎上去,每部分都有一个概率。
最简单的情况:假设一张饼图被分成三份:第一份A占0.2,第二份B占0.3,第三份C占0.5。(不必按大小排序)。实际的区间个数就是popsize个。每个区间所占的比例与每一个个体的适应度有关。这里的转化公式就是:
那么对应的概率就是占的份数,放在一个区间上表示:0~0.2表示A,0.2~0.5表示B,0.5~1表示C。这里的转化公式就是: 。在Matlab中有对应的函数可以执行类似的累加操作,就是
cumsum()
。现在随机扎针:
rand(popsize, 1)
即popsize针同时一起扎完。假设是四针,顺序为:0.78,0.12,0.55,0.33。这里就是要判断:分别多少针再ABC区域。传统方法就是遍历这四个值,即先找0.78所在区间,先比较是否大于0又小于0.2,再判断是否大于0.2又小于0.5,再判断是否大于0.5又小于1。之后再找0.12,0.55,0.33对应的区间。这样且不说写代码的难度(每一代的区间分配不一样),光是区间数量就很难人,如果有一万个区间呢。
所以直接记录每个区间的右边界。即a = [0.2,0.5,1]。然后将扎的针排序,即b = [0.12,0.33,0.55,0.78]。接下来遍历排序后的针。用
a(i)
表示第i
个区间的右边界,用j
表示当前随机数,即b(j)
表示当前随机数。- 如果
b(j) < a(i)
就说明b(j)
就是第i
个区间的。 - 如果
b(j) > a(i)
就说明该到下一个区间了,并且可以知道b
数组中的剩下数都是属于后面的区间的。所以i = i + 1
- 如果
函数解释:
解释了轮盘赌策略之后,主要就是解释为什么用这个,这里其实就是模拟自然选择,适者生存。正常情况,如果合理就是随机扎针,每个区间一针,就不会有遗传其他个体的可能,但是如果有一个个体的适应度很高,占据轮盘的面积很大,那么就很有可能下一代会有好几个遗传到它的基因,同时也就抛弃了原始基因。
返回结果:新的一代的popsize个个体的新基因序列 popsize * chromlength的矩阵
说明:这里的策略可以选择很多种,并不一定是轮盘赌策略。对于不同的问题需要思考用何种策略更好。
这里提供一些选择方案:
轮盘赌选择(Roulette Wheel Selection):是一种回放式随机采样方法。每个个体进入下一代的概率等于它的适应度值与整个种群中个体适应度值和的比例。选择误差较大。
随机竞争选择(Stochastic Tournament):每次按轮盘赌选择一对个体,然后让这两个个体进行竞争,适应度高的被选中,如此反复,直到选满为止。
最佳保留选择:首先按轮盘赌选择方法执行遗传算法的选择操作,然后将当前群体中适应度最高的个体结构完整地复制到下一代群体中。
无回放随机选择(也叫期望值选择Excepted Value Selection):根据每个个体在下一代群体中的生存期望来进行随机选择运算。方法如下:
(1) 计算群体中每个个体在下一代群体中的生存期望数目N。
(2) 若某一个体被选中参与交叉运算,则它在下一代中的生存期望数目减去0.5,若某一个体未 被选中参与交叉运算,则它在下一代中的生存期望数目减去1.0。
(3) 随着选择过程的进行,若某一个体的生存期望数目小于0时,则该个体就不再有机会被选中。确定式选择:按照一种确定的方式来进行选择操作。具体操作过程如下:
(1) 计算群体中各个个体在下一代群体中的期望生存数目N。
(2) 用N的整数部分确定各个对应个体在下一代群体中的生存数目。
(3) 用N的小数部分对个体进行降序排列,顺序取前M个个体加入到下一代群体中。至此可完全确定出下一代群体中M个个体。无回放余数随机选择:可确保适应度比平均适应度大的一些个体能够被遗传到下一代群体中,因而选择误差比较小。
均匀排序:对群体中的所有个体按期适应度大小进行排序,基于这个排序来分配各个个体被选中的概率。
最佳保存策略:当前群体中适应度最高的个体不参与交叉运算和变异运算,而是用它来代替掉本代群体中经过交叉、变异等操作后所产生的适应度最低的个体。
随机联赛选择:每次选取几个个体中适应度最高的一个个体遗传到下一代群体中。
排挤选择:新生成的子代将代替或排挤相似的旧父代个体,提高群体的多样性。
交叉操作
这里选择的两点交叉,同样根据不同的问题,选择不同的合适的方案。
1 | function newx = crossover(pop, pc, popsize, chromlength) |
- 参数解释:
- pop:每个个体对应的基因序列
- pc:发生交叉的概率
- popsize:个体数量
- chromlength:串的长度,每一列相当于是一个基因,个体基因序列个数
- 返回结果:
- newx:即新一代的结果,会覆盖上一代,和pop的size一样 popsize * chromlength的矩阵
函数说明:
使用上述方式时需要着重注意个体数是偶数和奇数的区别。
一般常见的交叉方案:
单点交叉(One-point Crossover):指在个体编码串中只随机设置一个交叉点,然后再该点相互交换两个配对个体的部分染色体。
两点交叉与多点交叉:
(1) 两点交叉(Two-point Crossover):在个体编码串中随机设置了两个交叉点,然后再进行部分基因交换。
(2) 多点交叉(Multi-point Crossover)均匀交叉(也称一致交叉,Uniform Crossover):两个配对个体的每个基因座上的基因都以相同的交叉概率进行交换,从而形成两个新个体。
算术交叉(Arithmetic Crossover):由两个个体的线性组合而产生出两个新的个体。该操作对象一般是由浮点数编码表示的个体。
变异操作
这里选择的基本位变异的方案,同样有很多方案可选,根据题目选择合适的。
1 | function newx = mutation(pop, pm, popsize, chromlength) |
参数解释:
pop:每个个体对应的基因序列
pm:发生变异的概率
popsize:个体数量
chromlength:串的长度,每一列相当于是一个基因,个体基因序列个数
返回结果:
- newx:即新一代的结果,会覆盖上一代,和pop的size一样 popsize * chromlength的矩阵
一般常见的变异操作方案:
基本位变异(Simple Mutation):对个体编码串中以变异概率、随机指定的某一位或某几位仅因座上的值做变异运算。
均匀变异(Uniform Mutation):分别用符合某一范围内均匀分布的随机数,以某一较小的概率来替换个体编码串中各个基因座上的原有基因值。(特别适用于在算法的初级运行阶段)
边界变异(Boundary Mutation):随机的取基因座上的两个对应边界基因值之一去替代原有基因值。特别适用于最优点位于或接近于可行解的边界时的一类问题。
非均匀变异:对原有的基因值做一随机扰动,以扰动后的结果作为变异后的新基因值。对每个基因座都以相同的概率进行变异运算之后,相当于整个解向量在解空间中作了一次轻微的变动。
高斯近似变异:进行变异操作时用符号均值为P的平均值,方差为P**2的正态分布的一个随机数来替换原有的基因值。
主函数
初始的pop变量数据如图:
主函数代码:
1 | clc, clear; |
整体代码
可直接复制下面的代码,自己运行调试,更加有助于理解。
1 | %%主程序 |