遗传算法是根据自然界的“物竞天择、适者生存”现象而提出的一种随机搜索算法,70年代由美国的密执根大学的Holland教授首先提出。该算法将优化问题看作是自然界中生物的进化过程,通过模拟大自然中生物进化过程中的遗传规律,来达到寻优的目的。近年来,遗传算法作为一种有效的工具,已广泛地应用于最优化问题求解之中。
生物进化与遗传算法
根据达尔文的进化论,在生物进化的过程中,只有那些最能适应环境的种群才得以生存下来。生物的进化过程可以用如图7.7所示的进化圈表示。
该进化圈以群体为一个循环的起点。按照优胜劣汰的原则,经过自然选择后,一部分群体由于无法适应环境而遭淘汰退出进化圈。在自然界中,自然选择包括恶劣的天气和气候,食物的短缺,天敌的侵害等。另一部分群体,由于适应环境的能力强而生存下来。它们或者是因为身体强壮而逃脱天敌的侵害,或者因为耐寒冷、耐饥饿能力强而存活下来。总之,这些群体之所以能够生存,是因为它们具有某种适应周围环境的能力。从适应环境的能力方面来说,这些群体从总体上考察是优良的,被淘汰的是那些体弱病残、不能适应环境生存的个体。即便因偶然因素一些弱者侥幸生存了下来,但在婚配的竞争中它们也往往处于劣势。因此可以说只有那些适应环境能力强的优良品种才能够成为种群。经过种群的交配,繁衍出下一代子群。一般来说子群中的个体遗传了父代双亲的优势,加快了后代的进化,使之更能够适应环境。比如,一个身体强壮的个体和一个耐寒冷能力强的个体交配后,繁殖的后代可能会同时具有身体强壮和耐寒冷的能力。在进化的过程中,个别的个体可能会发生一些变异,从而产生新的个体,使得群体的组成更加多样化。综合以上过程,经过一个循环的进化,新的群体生长起来,取代旧群体,又进入新的一轮进化之中。经过多次的竞争、淘汰、进化过程,最终的群体中生存下来的是那些最能适用环境生存的优秀个体。
虽然生物进化的一些细节我们还不是很清楚,但一些进化理论的特性已经普遍被研究者所接受。刘勇等在其著作中对生物进化特性总结如下:
(1)进化过程发生在染色体上,而不是发生在它们所编码的生物体上;
(2)自然选择把染色体以及由它们所译成的结构的表现联系在一起,那些适应性好的个体的染色体经常比差的染色体有更多的繁殖的机会。
(3)繁殖过程是进化发生的那一刻。变异可以使生物体子代的染色体不同于它们父代的染色体。通过结合两个父代染色体中的物质,重组过程可以在子代中产生有很大差异的染色体;
(4)生物进化没有记忆。有关产生个体的信息包含在个体所携带的染色体的集合以及染色体编码的结构之中,这些个体会很好地适应它们的环境。
总之,生物在进化过程中,经过优胜劣汰的自然选择,会使得种群逐步优化,经过长期的演化,优良的物种得以保留。不同的环境,不同物种的基因结构,导致最终的物种群不同,但它们都有一个共同的特征:最能适应自己所处的生存环境。
受达尔文进化论“物竞天择、适者生存”思想的启发,美国密执根大学的Holland教授把优化问题求解与生物进化过程对应起来,将生物进化的思想引入到复杂问题求解中,提出了求解优化问题的遗传算法。
在遗传算法中,首先对优化问题的解进行编码,编码后的一个解称为一个染色体,组成染色体的元素称为基因。一个群体由若干个染色体组成,染色体的个数称为群体的规模。与自然界中的生存环境相对应,在遗传算法中用适应函数表示环境,它是已编码的解的函数,是一个解适应环境程度的评价。适应函数的构造一般与优化问题的指标函数相关,简单的情况下,直接用指标函数或者指标函数经过简单的变换后作为适应函数使用。一般情况下,适应函数值大表示所对应的染色体适应环境的能力强。适应函数起着自然界中环境的作用,当适应函数确定后,自然选择规律将以适应函数值的大小来决定一个染色体是否继续生存下去的概率。生存下来的染色体成为种群,它们中的部分或者全部以一定的概率进行交配,繁衍得到下一代的群体。交配是一个生殖过程,发生在两个染色体之间,作为双亲的两个染色体,交换部分基因之后,生殖出两个新的染色体,即问题的新解。交配或者称作杂交,是遗传算法区别于其他优化算法的最主要的特征。在进化的过程中,染色体的某些基因可能会发生变异,即表示染色体的编码发生了某些变化。一个群体的进化需要染色体的多样性,而变异对保持群体的多样性具有一定的作用。
表7.9给出了遗传算法与生物进化之间的对应关系。
表7.9:生物进化与遗传算法之间的对应关系
生物进化中的概念 | 遗传算法中的作用 |
环境 | 适应函数 |
适应性 | 适应值函数 |
适者生存 | 适应函数值最大的解被保留的概率最大 |
个体 | 问题的一个解 |
染色体 | 解的编码 |
基因 | 编码的元素 |
群体 | 被选定的一组解(以编码形式表示) |
种群 | 根据适应函数选择的一组解(以编码形式表示) |
交配 | 以一定的方式由双亲产生后代的过程 |
变异 | 编码的某些分量发生变化的过程 |
选择、交配和变异是遗传算法的三个主要操作。
依据适应值的大小,选择操作从规模为N的群体中随机的选择若干染色体构成种群,种群的规模可以与原来的群体的规模一致,也可以不一致,我们这里假设二者的规模是一致的,即从群体中选择N个染色体构成种群。由于是依据适应值的大小进行随机选择的,因此虽然种群的规模与群体的规模是一致的,但二者并不完全一样,因为适应值大的染色体可能会被多次从群体选出,而适应值小的染色体有可能会失去被选中的机会。因此一些适应值大的染色体可能会重复出现在种群中,而一些适应值小的染色体则可能被淘汰。这一点体现的正是自然界中“优胜劣汰、适者生存”的选择规律。
可以有多种方式从群体中选择存活的染色体。其中被经常使用的是一种被称之为“轮盘赌”的方法。其方法简述如下:
设群体的规模为N,F(xi)(i=1, …, N)是其中N个染色体的适应值。则第i个染色体被选中的概率由下式给出:
有一个共有N个格子的转盘,每一个xi在转盘上占有一格,而格子的大小与p(xi)成正比关系。在选择一个染色体时,先转动轮盘,待转盘停下后,指针指向的格子所对应的xi就是被选中的染色体。
实际程序实现时,可以这样来模拟“轮盘赌”:
(1)r=random(0, 1),s=0,i=0;
(2)如果s≥r,则转(4);
(3)s=s+p(xi),转(2)
(4)xi即为被选中的染色体,输出i;
(5)结束。
其中random(0, 1)是一个产生在[0, 1]之间均匀分布的随机数函数。这样经过N次“轮盘赌”之后,就得到了规模为N的种群。
另一种选择方法被称之为“确定性”选择方法。其方法如下:
对于规模为N的群体,一个选择概率为p(xi)的染色体xi被选择次数的期望值e(xi):
对于群体中的每一个xi,首先选择次。其中符号“”表示向下取整。这样共得到个染色体。然后按照从大到小对染色体排序,依次取出个染色体,这样就得到了N个染色体,以这N个染色体组成种群。
交配发生在两个染色体之间,由两个被称之为双亲的父代染色体,经杂交以后,产生两个具有双亲的部分基因的新的染色体。当染色体采用二进制形式编码时,交配过程是以这样一种形式进行的:
设a、b是两个交配的染色体:
a:a1a2…aiai+1…an
b:b1b2…bibi+1…bn
其中ai、bi∈{0, 1}。随机的产生一个交配位设为i,则a、b两个染色体从i+1以后的基因进行交换,得到两个新的染色体。图7.9给出了两个染色体的交配示意图。
例如,对于x1=11001和x2=01111两个二进制编码的染色体,当交配位等于2时,则产生y1和y2两个子代染色体:
在进化过程中,通常交配是以一定的概率发生,而不是100%的发生。
变异发生在染色体的某一个基因上,当以二进制编码时,变异的基因由0变成1,或者由1变成0。如对于染色体x=11001,如果变异位发生在第三位,则变异后的染色体变成了y=11101。
变异对于一个群体保持多样性具有好处,但也有很强的破坏作用,因此总是以一个很小的概率来控制变异的发生。
遗传算法的控制参数包括群体的规模N、算法的停止准则,以及交配概率pc和变异概率pm。
下面我们给出遗传算法的具体描述。其中,每一代中群体的规模是固定的,变量t表示当前的代数,以适应值最大者为最优解。
遗传算法
(1)给定群体规模N,交配概率pc和变异概率pm,t=0;
(2)随机生成N个染色体作为初始群体;
(3)对于群体中的每一个染色体xi(i=1, 2,…, N)分别计算其适应值F(xi);
(4)如果算法满足停止准则,则转(10);
(5)对群体中的每一个染色体xi依式(60)计算概率;
(6)依据计算得到的概率值,从群体中随机的选取N个染色体,得到种群;
(7)依据交配概率pc从种群中选择染色体进行交配,其子代进入新的群体,种群中未进行交配的染色体,直接复制到新群体中;
(8)依据变异概率pm从新群体中选择染色体进行变异,用变异后的染色体代替新群体中的原染色体;
(9)用新群体代替旧群体,t=t+1,转(3);
(10)进化过程中适应值最大的染色体,经解码后作为最优解输出;
(11)结束。
下面讲讲适应性函数,和加速进化(有时候被选择的概率相差不大,很难进化出最优解)的方法:
由于任何一个最小化优化问题都可以转化为最大化优化问题,因此在下面的讨论中,我们均假定以最大化为问题的优化目标。一般情况下,我们可以直接选取问题的指标函数作为适应函数。如求函数f(x)的最大值,就可以直接采用f(x)为适应函数。
但在有些情况下,函数f(x)在最大值附近的变化可能会非常小,以至于他们的适应值非常接近,很难区分出那个染色体占优。在这种情况下,希望定义新的适应函数,要求该适应函数与问题的指标函数具有相同的变化趋势,但变化的速度更快。
一种方法是非线性加速适应函数。该方法利用已有的信息构造适应函数:
其中f(x)是问题的指标函数,fmax是当前得到的最优指标函数值,M是一个充分大的数。M值的大小将影响到算法以怎样的概率选取种群。M不一定是一个常量,可以随着算法的进行而变化,开始时可以相对小一些,以保证种群的多样性,然后可以逐步增大。
与非线性加速适应函数相对应的是线性加速适应函数。其定义如下:
其中xi(i=1,2,…,m)为当前代中的染色体。
上式中的第一个方程表示变换前后的平均值不变,第二个方程表示将当前的最优值放大为平均值的M倍。这样,通过选择适当的M值,可以拉开不同染色体间适应值的差距。
求解方程组(7.70)可得解如下:
另一种定义适应函数的方法是利用染色体指标函数值从小到大的排列序号作为适应函数值,利用该值采用轮盘赌的方法得到每个染色体被选中的概率。
设染色体按照其指标函数值从小到大排序,其序号分别为1到m。按照轮盘赌的方法,染色体i被选中的概率为:
该方法的特点是一个染色体被选中的概率与指标函数的区分度无关,只与该染色体在当前群体中,按指标函数值从小到大排序的位置有关。具有最大指标函数值的染色体总是固定的以 2/(m+1)的概率被选中,而具有最小指标函数值的染色体被选中的概率则总是2/m/(m+1) 。最大概率是最小概率的m倍。
以上内容来源
马少平,朱小燕,人工智能,清华大学出版社
因为main函数传递的“int argc,char *argv[]”,所以仍建议用命令行调试(参见http://blog.csdn.net/bizer_csdn/article/details/48859931)
代码使用了非线性加速(即前文的“染色体指标函数值从小到大的排列序号作为适应函数值,利用该值采用轮盘赌的方法得到每个染色体被选中的概率”的方法)。
下面给出代码:
GA.h
#ifndef GA_H
#define GA_H
#include <iostream>
#include <fstream>
#include <vector>
#include<stdlib.h>
#include <time.h>
using namespace std;
const double PC=0.8; //交配概率
const double PM=0.01;//变异概率
const int TMAX=1500;//最大迭代次数
typedef struct
{
char name;
double x;
double y;
}_city;
class GA{
private:
int CityNum; //读取城市个数
int groupSize; //种群大小
_city city;
vector<_city> path; //每个温度下平衡状态
double pointDist[26][26]; //每两个城市间的距离
public:
void input(const string &);
double calPointDist(char,char);
double calLength(vector<_city> );
vector<_city> newChrom(vector<_city>);
vector<vector<_city> > newGroup(vector<vector<_city> >,double []); //用"轮盘赌"生成新种群
vector<vector<_city> > mating(vector<vector<_city> >); //交配生成新种群
vector<vector<_city> > variation(vector<vector<_city> >); //变异生成新种群
void solve(); //GA算法
void output(const string &); //输出路径
GA(){
srand((int)time(0));//初始化随机数
}
};
#endif
GA.cpp
#include "GA.h"
#include<string>
void GA::input(const string &fileName){
ifstream fin;
fin.open(fileName.c_str());
if(!fin){ //判断文件是否正常打开
cout<<"Unable to open the file!"<<endl;
exit(1);
}
string line;
int i=0,j=0;
getline(fin,line);
CityNum=atoi(line.c_str()); //读取城市个数
while(getline(fin,line)){ //逐行读取输入流中的文件,直至该输入流结束
city.name=line[0];
string::size_type pos1, pos2;
pos1 = line.find_first_of('\t');//找到line第一个'\t'
pos2 = line.find_last_of('\t'); //找到line最后一个'\t'
city.x = atof(line.substr(pos1+1,6).c_str());
city.y = atof(line.substr(pos2+1,6).c_str());
path.push_back(city);
}
double l;
for(int i=0;i<CityNum;i++) //用二维数组存储两城市间距离
for(int j=i+1;j<CityNum;j++){
l=(path[j].x-path[i].x)*(path[j].x-path[i].x);
l+=(path[j].y-path[i].y)*(path[j].y-path[i].y);
pointDist[i][j]=sqrt(l);
pointDist[j][i]=sqrt(l);
}
}
double GA::calPointDist(char c1,char c2){ //返回两城市间距离
return pointDist[c1-65][c2-65];
}
//随机生成一个染色体
vector<_city> GA::newChrom(vector<_city> curPath){
int i,j;
do
{
i=(int)(CityNum*rand()/(RAND_MAX+1.0)); //生成[0,CityNum-1];
j=(int)(CityNum*rand()/(RAND_MAX+1.0));
} while (!(i<j)); //生成两个不同随机数
while (i<j)
{
swap(curPath[i++], curPath[j--]); //逆序法生成新解
}
return curPath;
}
double GA::calLength(vector<_city> curPath){ //计算当前路径距离,并作为适应值函数
double totalLength=0;
for(int i=0;i<CityNum-1;i++)
totalLength+=calPointDist(curPath[i].name,curPath[i+1].name);
totalLength+=calPointDist(curPath[CityNum-1].name,curPath[0].name);
return totalLength;
}
//"轮盘赌"算法生成新种群
vector<vector<_city> > GA::newGroup(vector<vector<_city> > gru,double p[]){
vector<vector<_city> > newgru;
for(int ii=0;ii<groupSize;ii++){
int i=0;
double r=0,s=0;
r=rand()/(RAND_MAX+1.0); //(0,1)随机数
while (true){
if(s>r||s==r){
if(i>groupSize||i==0){ //防止越界
i=0;s=0;r=rand()/(RAND_MAX+1.0);
continue;
}
newgru.push_back(gru[i-1]);
break;
}
s+=p[i++];
}
}
return newgru;
}
//交配
vector<vector<_city> > GA::mating(vector<vector<_city> > gru){
int mating_num=(int)groupSize*PC; //PC交配概率
if(mating_num%2!=0) //保证两两配对
mating_num--;
for(int i=0;i<mating_num;i++){
int j=(int)((gru.size()-1)*rand()/(RAND_MAX+1.0));
int k=(int)((gru.size()-1)*rand()/(RAND_MAX+1.0));
if(j==k)
continue;
swap(gru[j],gru[k]);
}
//交配过程
for(int i=0;i<(mating_num/2);i++){
int ii=2*i;
int n1=ii,n2=++ii; //n1和n2为交配染色体号
vector<_city> temp=gru[n1];
//染色体的就按着ABCDEFGHIJ编码,按着基于常规方法交配
int pos=(int)((CityNum-1)*rand()/(RAND_MAX+1.0)); //交配位置
//pos之前不变,pos之后从另一父代中依次选取不冲突的
//子代1
for(int j=pos+1;j<CityNum;j++)
for(int k=0;k<CityNum;k++){
for(int s=0;s<j;s++)
if(gru[n1][s].name==gru[n2][k].name){
break;
}else if(s==(j-1))
{
gru[n1][j]=gru[n2][k];
}
}
//子代2
for(int j=pos+1;j<CityNum;j++)
for(int k=0;k<CityNum;k++){
for(int s=0;s<j;s++)
if(gru[n2][s].name==temp[k].name){
break;
}else if(s==(j-1))
{
gru[n2][j]=temp[k];
}
}
}
return gru;
}
//变异生成新种群
vector<vector<_city> > GA::variation(vector<vector<_city> > gru){
int var_num=(int)gru.size()*PM; //变异染色体数目
for(int i=0;i<var_num;i++){
int j=(int)(gru.size()*rand()/(RAND_MAX+1.0));
gru[j]=newChrom(gru[j]);
}
return gru;
}
void GA::solve(){
vector<_city> curPath;
curPath=path;
//随机生成CityNum个染色体;
vector<vector<_city> > group; //种群
groupSize=2*CityNum;
//随机生成群体
for(int i=0;i<groupSize;i++){
group.push_back(curPath);
curPath=newChrom(curPath);
}
for(int t=0;t<TMAX;t++){
//计算种群中每个染色体的适应值,直接用距离用作适应值函数,由于各离散值间距小,需要进化很长时间
/*double adaption[100]={0.0};
double sum_ada=0.0;
for(int i=0;i<groupSize;i++){
adaption[i]=calLength(group[i]);
sum_ada+=adaption[i];
}
//计算每个染色体被选中的概率
double choose[100]={0.0};
for(int i=0;i<groupSize;i++)
choose[i]=1-adaption[i]/sum_ada; //由于求最小值,此处1-该值,使其概率最大*/
//计算种群中每个染色体的适应值,按着指标函数从大到小排序
double adaption[100]={0.0};double temp;
for(int i=0;i<groupSize;i++){
adaption[i]=calLength(group[i]);
}
//从大到小排序
for(int i=0;i<groupSize-1;i++)
for(int j=0;j<groupSize-1-i;j++){
if(adaption[j]<adaption[j+1]){
temp=adaption[j];
adaption[j]=adaption[j+1];
adaption[j+1]=temp;
swap(group[j],group[j+1]);
}
}
//计算被选中概率,以次序/次序总和作为概率,加速进化过程
double choose[100]={0.0};
for(int i=0;i<groupSize;i++)
choose[i]=((double)(2*(i+1)))/(groupSize*(groupSize+1));
group=newGroup(group,choose);//选择
group=mating(group); //交配
group=variation(group); //变异
//找出每个种群中最短路径
double min=calLength(group[0]); int min_pos=0;
int sz=group.size();
for(int s=1;s<sz;s++){
double temp=calLength(group[s]);
if(temp<min){
min=temp;
min_pos=s;
}
}
curPath=group[min_pos];
if(calLength(curPath)<calLength(path))
path=curPath;
/*for(int i=0;i<CityNum;i++)
cout<<curPath[i].name<<" ";
cout<<calLength(curPath)<<endl;*/
for(int i=0;i<CityNum;i++)
cout<<path[i].name<<" ";
cout<<calLength(path)<<endl;
}
}
void GA::output(const string &fileName){
ofstream fout(fileName.c_str());
if(!fout){
cout<<"error!"<<endl;
exit(1);
}
for(int i=0;i<CityNum;i++)
fout<<path[i].name<<" "; //逐个写入输出文件
fout<<calLength(path)<<endl;
}
main.cpp
#include "GA.h"
void main(int argc,char *argv[]){
clock_t start,end;
start=clock();
GA tsp;
//tsp.input("G:\\homework4\\TSP20.txt");
tsp.input(argv[1]);
tsp.solve( );
//tsp.output("G:\\homework4\\output_TSP20.txt");
tsp.output(argv[2]);
end=clock();
cout<<endl;
cout<<"run time:"<<(double)(end-start)/CLOCKS_PER_SEC<<"s"<<endl;
//getchar();
}