遗传算法的基本概念
遗传算法(Genetic Algorithm, GA)是由Holland提出来的,是受遗传学中的自然选择和遗传机制启发发展起来的一种优化算法,它的基本思想是模拟生物和人类进化的方法求解复杂的优化问题。
基本定义
- 个体(individual):在遗传学中表示的是基因编码,在优化问题中指的是每一个解。
- 适应值(fitness):评价个体好坏的标准,在优化问题中指的是优化函数。
- 群体(population): 由个体组成的集合
- 遗传操作:遗传操作的主要目的是用于在当前的群体中产生新的群体,主要的操作包括:选择(selection)、交叉(crossover)、变异(mutation)。
遗传算法的基本流程
遗传算法的过程中主要包括这样几个要素:1、参数的编码。2、初始群体的设定。3、适应度函数的设计。4、遗传操作设计。5、控制参数的设定。
基本遗传算法的具体过程如下:
遗传算法过程中的具体操作
参数的编码
遗传算法中的参数编码的方式主要有:1、二进制编码。2、Gray编码。3、实数编码。4、有序编码。
二进制编码
二进制编码是最原始的编码方式,遗传算法最初是在二进制编码的方式下进行运算的。二进制编码也是遗传算法中使用最为直接的运算编码方式。二进制编码是指利用
0
和
1
对问题的解向量进行编码。例如,对于如下的优化问题:
其三维图像如下图所示:
在对这样的优化问题进行二进制编码的过程中,是将问题的可能解编码为二进制位串,例如问题的可能解为实数对 (x1,x2) [ ,首先必须将 x1 和 x2 分别使用二进制位串表示,然后将他们的二进制位串组合在一起。对于每一个变量的二进制位串的长度取决于变量的定义域所要求的精度。
二进制位串的长度的计算方法如下:
假设
aj≤xj≤bj
,所要求的精度是小数点后
t
位。这要求将区间划分为至少
(bj−aj)10t
份。假设表示变量
xj
的位串的长度用
lj
表示,则
lj
可取为满足下列不等式的最小数
m
:
即有:
对于上述的优化问题,假设精度为小数点后
4
位,则:
那么表示变量 x1 的二进制位串的长度为 l1=18 。
同理,对于变量 x2 :
表示变量 x2 的二进制位串的长度为 l2−15 。
此时,个体可以表示为:
其中,前 18 位表示的是 x1 ,后 15 位表示的是 x2 。
Gray编码
这种编码方式在求解优化问题时,本人基本上没做过任何研究。
实数编码
在二进制编码的过程中存在这样的一个问题,即在计算适应值的时候需要将二进制编码转换成十进制的编码进行运算,这样,很显然会想到能否直接使用十进制编码直接进行运算,如上例中的 (x1,x2) 这样的编码方式。
有序编码
有序编码主要使用在TSP问题中,在本文中主要涉及二进制编码和实数编码
。
初始群体的设定
在解决了个体的编码问题后,需要解决的问题是如何利用个体表示群体。在上述中,我们知道,群体是个体的集合。假设初始群体的大小为
N=20
。对于二进制编码方式与实数编码方式产生
20
个初始解。如:
对应的实数编码的方式则为:
对于二进制编码则是随机初始化 20 组这样的初始解,每组初始解随机初始化 33 位的
码。而对于实数编码方式,则是在区间上随机初始化 20 组初始解。
适应度函数的计算
适应度函数的目的是评价个体的好坏,如上面的优化问题中,即为最终的优化目标函数。对于二进制编码,则需要先将二进制编码转换成实数编码,再进行适应值函数的计算,对于实数编码方式,则直接进行适应值函数的计算。
遗传操作设计
遗传操作主要包括:选择(selection)、交叉(crossover)、变异(mutation),遗传操作的主要目的是从当前的群体中产生新的群体,这样便能使得产生新的更优的个体。
选择(selection)
选择操作的目的是选择出父体,用于参加交叉(crossover)和变异(mutation)操作。一般使用较多的方式是轮盘赌的选择策略(Roulette Wheel Selection)。根据每个个体的适应值,计算出相对适应值大小,即:
现在在 [0,1] 上产生一个随机数 r ,若:
则选择第 i 个个体(适应值大的被选择的概率大)。
重复此操作 N 次,选择出 N 个父体。轮盘赌的算法过程如下所示:
交叉(crossover)
交叉操作也称为杂交,其目的是产生新的个体。
对于二进制编码方式,主要有单点杂交和多点杂交。单点杂交是指在二进制串中随机选择一位,交换两个父体中该位以后的二进制串,用以产生新的个体,操作如下图所示:
多点杂交是指在二进制串中选择某几位进行杂交,其中以两点杂交最为常见,其过程如下图所示:
具体的操作过程为:设定一个杂交的概率
pc
,对选择操作中产生的
N
个父体,每个父体产生一个
[0,1]
区间上的随机数
rk
,若
rk<pc
,则将第
k
个个体用于杂交,若选择出来的个体数目是奇数,则在父体集合中再随机挑选一个,以保证挑选出的是偶数个,之后进行两两杂交操作。
对于实数编码形式,可以将实数转换成二进制编码的形式进行杂交运算,但是这样同样存在效率的问题,在实数编码中,主要采用的是算术杂交方式,算术杂交分为:部分算术杂交和整体算术杂交。部分算术杂交是指在父体向量中选择一部分分量进行算术运算,而整体算术杂交是指全部的分量都进行算术运算。我们以整体算术杂交为例:先在
[0,1]
生成
n
个随机数
a1,a2,⋯,an
,经杂交算子后,所得到的两个后代为:
变异(mutation)
变异操作的目的是使得基因突变,在优化算法中,可以防止算法陷入局部最优,从而跳出局部最优,帮助算法找到全局最优解。
二进制编码时的变异算子非常简单,只是依一定的概率(称为变异概率)将所选个体的位取反。即若是
1
,则取
0
;若是
0
,则取
1 。
具体的操作为:设定一个变异概率
pm
,对杂交操作后的
N
个父体,对父体中的每一个位产生一个
[0,1]
区间上的随机数
rk,j
,若
rk,j<pm
,则该位变异。
对于实数编码方式,可以采用均匀变异和非均匀变异方式,在均匀变异中,假设
x=(x1,x2,⋯,xn)
是要变异的个体,随机产生一个随机整数
k∈[1,n]
,产生新的后代
x=(x1,x2,⋯,x′k,⋯,xn)
,其中
x′k
是
[lk,uk]
中服从均匀分布的一个随机数。
另一种是非均匀变异,,假设
x=(x1,x2,⋯,xn)
是要变异的个体,随机产生一个随机整数
k∈[1,n]
,产生新的后代
x=(x1,x2,⋯,x′k,⋯,xn)
,其中:
这里 t 是当前演化代数,函数 Δ(t,y) 返回 [0,y] 中的一个值,并且 Δ(t,y) 随 t 的增加而趋于 0 的概率增大。函数 Δ(t,y) 具体形式为:
或者
其中, r 是 [0,1] 上的一个随机数, T 表示最大演化代数。 b 是确定非均匀度的一个参数,通常取 2∼5 。
控制参数的设定
控制参数主要包括种群的规模 N ,演化代数 T ,杂交概率 pc ,变异概率 pm 等等。
在实现遗传算法时,一个常用的方法是将到当前代为止演化的最好个体单独存放起来,在遗传算法结束后,将演化过程中发现的最好个体作为问题的最优解或近似最优解。
求解优化问题的实例
问题描述
其中,
问题分析
这是一道不带约束条件的函数优化的问题,既可以采用二进制编码方式,也可以采用十进制的编码方式,在本题的解决过程中,采用十进制的编码方式。首先通过Matlab得到的函数图像大致如下,从图像中可以观察到当
n=2
时,我们可以在
(0,0)
附近取得函数的最小值。
算法设计
基于以上的分析,当 n=2 时,以下分别从个体的编码、适应值函数、选择策略、杂交算子、变异算子、参数设置、初始化以及终止条件这八个方面对程序的设计作简要的描述:
个体编码
采用实数向量编码,每一个个体是一实数对 (x1,x2) 。
适应值函数
该优化问题是一个极小化问题,可对目标函数作简单变换,同时考虑到在选择策略时选择的是轮盘赌的选择策略,轮盘赌的选择策略有一个要求就是个体的适应值要为正数,因此,可以作如下的变换: F=30−f(x1,x2) ,这里的 30 是取的一个上界。这样,既保证了变换后的适应值函数式中为正,而且我们可以将极小化问题转换成一个极大值问题考虑。
选择策略
采用轮盘赌的选择策略,因为在计算适应值时已经作了处理,即适应值始终为正,这样就可以使用轮盘赌的选择策略。轮盘赌的选择策略是一种基于适应值比例的选择策略,适应值越大被选择到下一代的概率也会越大。
杂交算子
采用整体算术杂交,即给定两个父体,产生一个随机数,经杂交后得到两个后代个体, v1=(x1,x2) , v 2=(y1,y2) ,产生一个随机数 α∈[0,1] ,经杂交后得到两个后代个体: v′1=(αx1+(1−α)y1,αx2+(1−α)y2) , v′2=(αy1+(1−α)x1,αy2+(1−α)x2) 。
变异算子
采用非均匀变异,即对于要变异的个体
x=(x1,x2)
,随机产生整数
k∈{1,2}
,例如
k=1
,然后产生后代
x′=(x′1,x2)
,其中
参数设置
- 种群规模
N=100
- 个体长度
Size=2
- 演化代数
T=3000
- 杂交概率
pc=0.7
- 变异概率
pm=0.1
- 函数上界
Upp=30.0
初始化
在区间内随机初始化种群的个体,并置个体的适应值,适应值之和以及相对适应值比例为 0 。
终止条件
采用代数作为终止条件,当算法运行到指定的最大代数时,程序停止。
实验代码
#include<iostream>
#include<time.h>
#include<stdlib.h>
#include<cmath>
#include<fstream>
using namespace std;
const int COLONY_SIZE=100; //个体数目
const int Size=2;//个体的长度
const int Generation=3000;//代数
const double OVER=0.7;//杂交的概率
const double MUTATE=0.1;//变异的概率
const double UPPER=30.0;//函数的上界
struct Indival
{
double code[Size];
double fitness;
double cfitness;
double rfitness;
}Group[COLONY_SIZE];
Indival newGroup[COLONY_SIZE];
Indival bestChrom;//记录最好的个体
int GenNum=0;
double random(double, double);
void initiate();
void calvalue();
void select();
void crossOver();
void xOver(int,int);
void mutate();
double delta(int,double,double,double);
void sort();
/*****************主函数***************/
int main()
{
ofstream output;
srand((unsigned)time(NULL));
initiate();
calvalue();
output.open("data.txt");
while(GenNum<=Generation)
{
GenNum++;
select();
crossOver();
mutate();
calvalue();
sort();
if (bestChrom.fitness<Group[0].fitness)
{
bestChrom.code[0]=Group[0].code[0];
bestChrom.code[1]=Group[0].code[1];
bestChrom.fitness=Group[0].fitness;
}
// output<<"gen: "<<GenNum<<"最优解为:"<<endl;
// output<<"x1: "<<bestChrom.code[0]<<" x2: "<<bestChrom.code[1]<<" 函数值为: "<<(30-bestChrom.fitness)<<endl;
output<<GenNum<<" "<<(30-bestChrom.fitness)<<endl;
}
output.close();
cout<<"运行结束!"<<endl;//提示运行结束
return 0;
}
/******************************函数的实现*****************************************/
double random(double start, double end){//随机产生区间内的随机数
return start+(end-start)*rand()/(RAND_MAX + 1.0);
}
void initiate()//初始化
{
for(int i=0;i<COLONY_SIZE;i++)
{
Group[i].code[0]=random(-30,30);
Group[i].code[1]=random(-30,30);
Group[i].fitness=0;//适应值
Group[i].cfitness=0;//相对适应值比例之和
Group[i].rfitness=0;//相对适应值比例
}
}
void calvalue()//计算适应值
{
double x1,x2;
double sum=0;
double part1,part2;//将函数分成几个部分
for(int i=0;i<COLONY_SIZE;i++)
{
x1=Group[i].code[0];
x2=Group[i].code[1];
part1=-0.2*sqrt((x1*x1+x2*x2)/Size);
part2=(cos(2*3.1415926*x1)+cos(2*3.1415926*x2))/Size;
Group[i].fitness=UPPER-(-20*exp(part1)-exp(part2)+20+2.71828);//适应值
sum+=Group[i].fitness;//计算适应值之和
}
for(int mem=0;mem<COLONY_SIZE;mem++)//轮盘赌选择机制里所要求的几个参数
{
Group[mem].rfitness=Group[mem].fitness/sum;//适应值的比例
}
Group[0].cfitness=Group[0].rfitness;
for(mem=1;mem<COLONY_SIZE;mem++)
{
Group[mem].cfitness=Group[mem-1].cfitness+Group[mem].rfitness;//模拟轮盘
}
}
void select()
{
double p;
for(int i=0;i<COLONY_SIZE;i++)//挑选出N个个体
{
p=random(0,1);//随机产生0到1之间的随机数
if(p<Group[0].cfitness)
newGroup[i]=Group[0];
else
{
for(int j=1;j<COLONY_SIZE;j++)//往轮盘后走
{
if(p>=Group[j-1].cfitness&&p<Group[j].cfitness)
{
newGroup[i]=Group[j];
break;
}
}
}
}
for(i=0;i<COLONY_SIZE;i++)//从newGroup复制到Group中
Group[i]=newGroup[i];
}
void crossOver()
{
int mem,one;
int first=0;//记录杂交的数目
double x;
for(mem=0;mem<COLONY_SIZE;mem++)
{
x=random(0,1);
if(x<OVER)
{
++first;
if(first%2==0)//若为偶数
xOver(one,mem);
else
one=mem;
}
}
}
void xOver(int one,int two)
{
double point;
point=random(0,1);
Group[one].code[0]=Group[one].code[0]*point+Group[two].code[0]*(1-point);
Group[one].code[1]=Group[one].code[1]*point+Group[two].code[1]*(1-point);
Group[two].code[0]=Group[one].code[0]*(1-point)+Group[two].code[0]*point;
Group[two].code[1]=Group[one].code[1]*(1-point)+Group[two].code[1]*point;
}
void mutate()
{
double x;
for(int i=0;i<COLONY_SIZE;i++)
{
for(int j=0;j<Size;j++)
{
x=random(0,1);
if (x<MUTATE)
{
Group[i].code[j]=delta(GenNum,Group[i].code[j],30,-30);
}
}
}
}
double delta(int t,double x,double u,double l)
{
double temp1;
double temp2;
double y;
double r=random(0,1);
temp1=pow((1-t/Generation),4);
temp2=pow(r,temp1); //变异函数可另选
int a=(int)random(0,2);
if(a==0)
{
y=u-x;
return (x+y*(1-temp2));
}else
{
y=x-l;
return (x-y*(1-temp2));
}
}
void sort()//排序
{
Indival temp;
for(int i=0;i<COLONY_SIZE-1;i++)
{
for(int j=i+1;j<COLONY_SIZE;j++)
{
if(Group[i].fitness<Group[j].fitness)
{
temp=Group[i];
Group[i]=Group[j];
Group[j]=temp;
}
}
}
}