遗传算法耐心看完你就掌握了遗传算法文档格式.docx
《遗传算法耐心看完你就掌握了遗传算法文档格式.docx》由会员分享,可在线阅读,更多相关《遗传算法耐心看完你就掌握了遗传算法文档格式.docx(34页珍藏版)》请在冰豆网上搜索。
1.爬山法(最速上升爬山法):
从搜索空间中随机产生邻近的点,从中选择对应解最优的个体,替换原来的个体,不断重复上述过程。
因为只对“邻近”的点作比较,所以目光比较“短浅”,常常只能收敛到离开初始位置比较近的局部最优解上面。
对于存在很多局部最优点的问题,通过一个简单的迭代找出全局最优解的机会非常渺茫。
(在爬山法中,袋鼠最有希望到达最靠近它出发点的山顶,但不能保证该山顶是珠穆朗玛峰,或者是一个非常高的山峰。
因为一路上它只顾上坡,没有下坡。
)
2.模拟退火:
这个方法来自金属热加工过程的启发。
在金属热加工过程中,当金属的温度超过它的熔点(MeltingPoint)时,原子就会激烈地随机运动。
与所有的其它的物理系统相类似,原子的这种运动趋向于寻找其能量的极小状态。
在这个能量的变迁过程中,开始时。
温度非常高,使得原子具有很高的能量。
随着温度不断降低,金属逐渐冷却,金属中的原子的能量就越来越小,最后达到所有可能的最低点。
利用模拟退火的时候,让算法从较大的跳跃开始,使到它有足够的“能量”逃离可能“路过”的局部最优解而不至于限制在其中,当它停在全局最优解附近的时候,逐渐的减小跳跃量,以便使其“落脚”到全局最优解上。
(在模拟退火中,袋鼠喝醉了,而且随机地大跳跃了很长时间。
运气好的话,它从一个山峰跳过山谷,到了另外一个更高的山峰上。
但最后,它渐渐清醒了并朝着它所在的峰顶跳去。
3.遗传算法:
模拟物竞天择的生物进化过程,通过维护一个潜在解的群体执行了多方向的搜索,并支持这些方向上的信息构成和交换。
以面为单位的搜索,比以点为单位的搜索,更能发现全局最优解。
(在遗传算法中,有很多袋鼠,它们降落到喜玛拉雅山脉的任意地方。
这些袋鼠并不知道它们的任务是寻找珠穆朗玛峰。
但每过几年,就在一些海拔高度较低的地方射杀一些袋鼠,并希望存活下来的袋鼠是多产的,在它们所处的地方生儿育女。
)(后来,一个叫天行健的网游给我想了一个更恰切的故事:
从前,有一大群袋鼠,它们被莫名其妙的零散地遗弃于喜马拉雅山脉。
于是只好在那里艰苦的生活。
海拔低的地方弥漫着一种无色无味的毒气,海拔越高毒气越稀薄。
可是可怜的袋鼠们对此全然不觉,还是习惯于活蹦乱跳。
于是,不断有袋鼠死于海拔较低的地方,而越是在海拔高的袋鼠越是能活得更久,也越有机会生儿育女。
就这样经过许多年,这些袋鼠们竟然都不自觉地聚拢到了一个个的山峰上,可是在所有的袋鼠中,只有聚拢到珠穆朗玛峰的袋鼠被带回了美丽的澳洲。
)
下面主要介绍介绍遗传算法实现的过程。
遗传算法的实现过程
遗传算法的实现过程实际上就像自然界的进化过程那样。
首先寻找一种对问题潜在解进行“数字化”编码的方案。
(建立表现型和基因型的映射关系。
)然后用随机数初始化一个种群(那么第一批袋鼠就被随意地分散在山脉上。
),种群里面的个体就是这些数字化的编码。
接下来,通过适当的解码过程之后,(得到袋鼠的位置坐标。
)用适应性函数对每一个基因个体作一次适应度评估。
(袋鼠爬得越高,越是受我们的喜爱,所以适应度相应越高。
)用选择函数按照某种规定择优选择。
(我们要每隔一段时间,在山上射杀一些所在海拔较低的袋鼠,以保证袋鼠总体数目持平。
)让个体基因交叉变异。
(让袋鼠随机地跳一跳)然后产生子代。
(希望存活下来的袋鼠是多产的,并在那里生儿育女。
)遗传算法并不保证你能获得问题的最优解,但是使用遗传算法的最大优点在于你不必去了解和操心如何去“找”最优解。
(你不必去指导袋鼠向那边跳,跳多远。
)而只要简单的“否定”一些表现不好的个体就行了。
(把那些总是爱走下坡路的袋鼠射杀。
)以后你会慢慢理解这句话,这是遗传算法的精粹!
题外话:
这里想提一提一个非主流的进化论观点:
拉马克主义的进化论。
法国学者拉马克(Jean-BaptistedeLamarck,1744~1891)的进化论观点表述在他的《动物学哲学》(1809)一书中。
该书提出生物自身存在一种是结构更加复杂化的“内驱力”,这种内驱力是与生俱来的,在动物中表现为“动物体新器官的产生来自它不断感觉到的新需要。
”不过具体的生物能否变化,向什么方向变化,则要受环境的影响。
拉马克称其环境机制为“获得性遗传”,这一机制分为两个阶段:
一是动物器官的用与不用(即“用进废退”:
在环境的作用下,某一器官越用越发达,不使用就会退化,甚至消失。
);
二是在环境作用下,动物用与不用导致的后天变异通过繁殖传给后代(即“获得性遗传”)。
德国动物学家魏斯曼(AugustWeismann,1834~1914)对获得性遗传提出坚决的质疑。
他用老鼠做了一个著名的“去尾实验”,他切去老鼠的尾巴,并使之适应了短尾的生活。
用这样的老鼠进行繁殖,下一代老鼠再切去尾巴,一连切了22代老鼠的尾巴,第23代老鼠仍然长出正常的尾巴。
由此魏斯曼认为后天后天获得性不能遗传。
(择自《怀疑----科学探索的起点》)
我举出这个例子,一方面希望初学者能够更加了解正统的进化论思想,能够分辨进化论与伪进化论的区别。
另一方面想让读者知道的是,遗传算法虽然是一种仿生的算法,但我们不需要局限于仿生本身。
大自然是非常智慧的,但不代表某些细节上人不能比她更智慧。
另外,具体地说,大自然要解决的问题,毕竟不是我们要解决的问题,所以解决方法上的偏差是非常正常和在所难免的。
(下一章,读者就会看到一些非仿生而有效的算法改进。
)譬如上面这个“获得性遗传”我们先不管它在自然界存不存在,但是对于遗传算法的本身,有非常大的利用价值。
即变异不一定发生在产生子代的过程中,而且变异方向不一定是随机性的。
变异可以发生在适应性评估的过程当中,而且可以是有方向性的。
(当然,进一步的研究有待进行。
所以我们总结出遗传算法的一般步骤:
开始循环直至找到满意的解。
1.评估每条染色体所对应个体的适应度。
2.遵照适应度越高,选择概率越大的原则,从种群中选择两个个体作为父方和母方。
3.抽取父母双方的染色体,进行交叉,产生子代。
4.对子代的染色体进行变异。
5.重复2,3,4步骤,直到新种群的产生。
结束循环。
接下来,我们将详细地剖析遗传算法过程的每一个细节。
编制袋鼠的染色体----基因的编码方式
通过前一章的学习,读者已经了解到人类染色体的编码符号集,由4种碱基的两种配合组成。
共有4种情况,相当于2bit的信息量。
这是人类基因的编码方式,那么我们使用遗传算法的时候编码又该如何处理呢?
受到人类染色体结构的启发,我们可以设想一下,假设目前只有“0”,“1”两种碱基,我们也用一条链条把他们有序的串连在一起,因为每一个单位都能表现出1bit的信息量,所以一条足够长的染色体就能为我们勾勒出一个个体的所有特征。
这就是二进制编码法,染色体大致如下:
010*********
上面的编码方式虽然简单直观,但明显地,当个体特征比较复杂的时候,需要大量的编码才能精确地描述,相应的解码过程(类似于生物学中的DNA翻译过程,就是把基因型映射到表现型的过程。
)将过份繁复,为改善遗传算法的计算复杂性、提高运算效率,提出了浮点数编码。
染色体大致如下:
1.2–3.3–2.0–5.4–2.7–4.3
那么我们如何利用这两种编码方式来为袋鼠的染色体编码呢?
因为编码的目的是建立表现型到基因型的映射关系,而表现型一般就被理解为个体的特征。
比如人的基因型是46条染色体所描述的(总长度两米的纸条?
),却能解码成一个个眼,耳,口,鼻等特征各不相同的活生生的人。
所以我们要想为“袋鼠”的染色体编码,我们必须先来考虑“袋鼠”的“个体特征”是什么。
也许有的人会说,袋鼠的特征很多,比如性别,身长,体重,也许它喜欢吃什么也能算作其中一个特征。
但具体在解决这个问题的情况下,我们应该进一步思考:
无论这只袋鼠是长短,肥瘦,只要它在低海拔就会被射杀,同时也没有规定身长的袋鼠能跳得远一些,身短的袋鼠跳得近一些。
当然它爱吃什么就更不相关了。
我们由始至终都只关心一件事情:
袋鼠在哪里。
因为只要我们知道袋鼠在那里,我们就能做两件必须去做的事情:
(1)通过查阅喜玛拉雅山脉的地图来得知袋鼠所在的海拔高度(通过自变量求函数值。
)以判断我们有没必要把它射杀。
(2)知道袋鼠跳一跳后去到哪个新位置。
如果我们一时无法准确的判断哪些“个体特征”是必要的,哪些是非必要的,我们常常可以用到这样一种思维方式:
比如你认为袋鼠的爱吃什么东西非常必要,那么你就想一想,有两只袋鼠,它们其它的个体特征完全同等的情况下,一只爱吃草,另外一只爱吃果。
你会马上发现,这不会对它们的命运有丝毫的影响,它们应该有同等的概率被射杀!
只因它们处于同一个地方。
(值得一提的是,如果你的基因编码设计中包含了袋鼠爱吃什么的信息,这其实不会影响到袋鼠的进化的过程,而那只攀到珠穆朗玛峰的袋鼠吃什么也完全是随机的,但是它所在的位置却是非常确定的。
以上是对遗传算法编码过程中经常经历的思维过程,必须把具体问题抽象成数学模型,突出主要矛盾,舍弃次要矛盾。
只有这样才能简洁而有效的解决问题。
希望初学者仔细琢磨。
既然确定了袋鼠的位置作为个体特征,具体来说位置就是横坐标。
那么接下来,我们就要建立表现型到基因型的映射关系。
就是说如何用编码来表现出袋鼠所在的横坐标。
由于横坐标是一个实数,所以说透了我们就是要对这个实数编码。
回顾我们上面所介绍的两种编码方式,读者最先想到的应该就是,对于二进制编码方式来说,编码会比较复杂,而对于浮点数编码方式来说,则会比较简洁。
恩,正如你所想的,用浮点数编码,仅仅需要一个浮点数而已。
而下面则介绍如何建立二进制编码到一个实数的映射。
明显地,一定长度的二进制编码序列,只能表示一定精度的浮点数。
譬如我们要求解精确到六位小数,由于区间长度为2–(-1)=3,为了保证精度要求,至少把区间[-1,2]分为3×
106等份。
又因为
所以编码的二进制串至少需要22位。
把一个二进制串
转化位区间
里面对应的实数值通过下面两个步骤。
(1)将一个二进制串
代表的二进制数转化为10进制数
:
(2)
对应区间
内的实数:
例如一个二进制串<
1000101110110101000111>
表示实数值0.637197。
二进制串<
0000000000000000000000>
和<
1111111111111111111111>
则分别表示区间的两个端点值-1和2。
由于往下章节的示例程序几乎都只用到浮点数编码,所以这个“袋鼠跳”问题的解决方案也是采用浮点数编码的。
往下的程序示例(包括装载基因的类,突变函数)都是针对浮点数编码的。
(对于二进制编码这里只作简单的介绍,不过这个“袋鼠跳”完全可以用二进制编码来解决的,而且更有效一些。
所以读者可以自己尝试用二进制编码来解决。
小知识:
vector(容器)的使用。
在具体写代码的过程中,读者将会频繁用到vector这种数据结构,所以大家必须先对它有所了解。
std:
:
vector是STL(standardtemplatelibrary)库里面的现成的模板类。
它用起来就像动态数组。
利用vector(容器)我们可以方便而且高效的对容器里面的元素进行操作。
示例如下:
1.//添加头文件,并使用std名空间。
2.
3.#include<
vector>
4.
5.using
namespace
std;
6.
7.//定义一个vector,<
>
内的是这个vector所装载的类型。
8.
9.vector<
int>
MyVector;
10.
11.//为vector后面添加一个整型元素0。
12.
13.MyVector.push_back(0);
14.
15.//把vector的第一个元素的值赋给变量a。
值得注意的是如果vector的长度只有1,而你
16.
17.//去访问它的下一个元素的话,编译和运行都不会报错,它会返回一个随机值给你,所以使
18.
19.//用的时候一定要注意这个潜伏的BUG。
20.
21.int
a
=
MyVector[0];
22.
23.//把vector里面的元素全部清空。
24.
25.MyVector.clear();
26.
27.//返回vector里面的元素的个数。
28.
29.MyVector.size()
呵呵,如果你没用过这个模板类,请完全不必介意,因为现在为止,你已经学会了在本书里面将用到的所有功能。
另外,我也顺便提一提,为什么我用vector而不用其它数据结构比如数组,来承载一条基因,还有后面我们将会学到的神经网络中的权值向量。
诚然,用数组作为基因或者权值向量的载体,速度会快一些。
但是我用vector主要出于下面几个考虑。
首先,vector的使用比较方便,方便得到其大小,也方便添加和访问元素,还有排序。
其次,使用vector也便于代码的维护与及重用(在这本书的学习过程中,学习者将会逐步建立起遗传算法和人工神经网络的引擎,通过对代码少量的修改就能用于解决新的问题。
)。
另外,我还希望在研究更前缘的应用方向――通过遗传算法动态改变神经网络的拓扑结构的时候,大家仍然可以通过少量的修改后继续利用这些代码。
(因为动态地改变神经网络的拓扑结构非常需要不限定大小的容器。
我们定义一个类作为袋鼠基因的载体。
(细心的人会提出这样的疑问:
为什么我用浮点数的容器来储藏袋鼠的基因呢?
袋鼠的基因不是只用一个浮点数来表示就行吗?
恩,没错,事实上对于这个实例,我们只需要用上一个浮点数就行了。
我们这里用上容器是为了方便以后利用这些代码处理那些编码需要一串浮点数的问题。
1.class
CGenome
3.{
5.public:
7.
//定义装载基因的容器(事实上从英文解释来看,Weights是权值的意思,这用来表示
9.//基因的确有点名不符实,呵呵。
这主要是因为这些代码来自于GA-ANN引擎,所以在
11.//它里面基因实质就是神经网络的权值,所以习惯性的把它引入过来就只好这样了。
13.
vector
<
double>
vecWeights;
15.
//
dFitness用于存储对该基因的适应性评估。
17.
double
dFitness;
19.
//类的无参数初始化参数。
21.
CGenome():
dFitness(0){}
23.
//类的带参数初始化参数。
25.
CGenome(vector
w,
f):
vecWeights(w),
dFitness(f){}
27.};
好了,目前为止我们把袋鼠的染色体给研究透了,让我们继续跟进袋鼠的进化旅程。
物竞天择--适应性评分与及选择函数。
1.物竞――适应度函数(fitnessfunction)
自然界生物竞争过程往往包含两个方面:
生物相互间的搏斗与及生物与客观环境的搏斗过程。
但在我们这个实例里面,你可以想象到,袋鼠相互之间是非常友好的,它们并不需要互相搏斗以争取生存的权利。
它们的生死存亡更多是取决于你的判断。
因为你要衡量哪只袋鼠该杀,哪只袋鼠不该杀,所以你必须制定一个衡量的标准。
而对于这个问题,这个衡量的标准比较容易制定:
袋鼠所在的海拔高度。
(因为你单纯地希望袋鼠爬得越高越好。
)所以我们直接用袋鼠的海拔高度作为它们的适应性评分。
即适应度函数直接返回函数值就行了。
2.天择――选择函数(selection)
自然界中,越适应的个体就越有可能繁殖后代。
但是也不能说适应度越高的就肯定后代越多,只能是从概率上来说更多。
(毕竟有些所处海拔高度较低的袋鼠很幸运,逃过了你的眼睛。
)那么我们怎么来建立这种概率关系呢?
下面我们介绍一种常用的选择方法――轮盘赌(RouletteWheelSelection)选择法。
假设种群数目
某个个体
其适应度为
则其被选中的概率为:
比如我们有5条染色体,他们所对应的适应度评分分别为:
5,7,10,13,15。
所以累计总适应度为:
所以各个个体被选中的概率分别为:
呵呵,有人会问为什么我们把它叫成轮盘赌选择法啊?
其实你只要看看图2-2的轮盘就会明白了。
这个轮盘是按照各个个体的适应度比例进行分块的。
你可以想象一下,我们转动轮盘,轮盘停下来的时候,指针会随机地指向某一个个体所代表的区域,那么非常幸运地,这个个体被选中了。
(很明显,适应度评分越高的个体被选中的概率越大。
图2-2
那么接下来我们看看如何用代码去实现轮盘赌。
1.//轮盘赌函数
3.CGenome
GetChromoRoulette()
5.{
//产生一个0到人口总适应性评分总和之间的随机数.
9.
//中m_dTotalFitness记录了整个种群的适应性分数总和)
11.
Slice
(RandFloat())
*
m_dTotalFitness;
//这个基因将承载转盘所选出来的那个个体.
CGenome
TheChosenOne;
//累计适应性分数的和.
FitnessSoFar
0;
//遍历总人口里面的每一条染色体。
for
(int
i=0;
i<
m_iPopSize;
++i)
{
27.
//累计适应性分数.
29.
+=
m_vecPop[i].dFitness;
30.
31.
//如果累计分数大于随机数,就选择此时的基因.
32.
33.
if
(FitnessSoFar
Slice)
34.
35.
36.
37.
TheChosenOne
m_vecPop[i];
38.
39.
break;
40.
41.
}
42.
43.
44.
45.
//返回转盘选出来的个体基因
46.
47.
return
48.
49.}
遗传变异――基因重组(交叉)与基因突变。
应该说这两个步骤就是使到子代不同于父代的根本原因(注意,我没有说是子代优于父代的原因,只有经过自然的选择后,才会出现子代优于父代的倾向。
对于这两种遗传操作,二进制编码和浮点型编码在处理上有很大的差异,其中二进制编码的遗传操作过程,比较类似于自然界里面的过程,下面将分开讲述。
1.基因重组/交叉(recombination/crossover)
(1)二进制编码
回顾上一章介绍的基因交叉过程:
同源染色体联会的过程中,非姐妹染色单体(分别来自父母双方)之间常常发生交叉,并且相互交换一部分染色体,如图2-3。
事实上,二进制编码的基因交换过程也非常类似这个过程――随机把其中几个位于同一位置的编码进行交换,产生新的个体,如图2-4所示。
图2-3
图2-4
(2)浮点数编码
如果一条基因中含有多个浮点数编码,那么也可以用跟上面类似的方法进行基因交叉,不同的是进行交叉的基本单位不是二进制码,而是浮点数。
而如果对于单个浮点数的基因交叉,就有其它不同的重组方式了,比如中间重组:
这样只要随机产生
就能得到介于父代基因编码值和母代基因编码值之间的值作为子代基因编码的值。
考虑到“袋鼠跳”问题的具体情况――袋鼠的个体特征仅仅表现为它所处的位置。
可以想象,同一个位置的袋鼠的基因是完全相同的,而两条相同的基因进行交叉后,相当于什么都没有做,所以我们不打算在这个例子里面使用交叉这一个遗传操作步骤。
(当然硬要这个操作步骤也不是不行的,你可以把两只异地的袋鼠捉到一起,让它们交配,然后产生子代,再把它们送到它们应该到的地方。
性的起源
生命进化中另一个主要的重大进展是伴随着两性的发育――