谁养鱼(四):遗传算法的实现
1 简介
根据达尔文的进化论,生物种群从低级、简单的类型逐渐发展成为高级、复杂的类型。各种生物要生存下去就必须进行生存斗争,具有较强生存能力的生物个体容易存活下来,并有较多的机会产生后代;具有较低生存能力的生物则被淘汰,或者产生后代的机会越来越少,直至消亡。
遗传算法借鉴了生物界自然选择、遗传变异机制,将种群代表一组问题的解,通过对当前种群施加选择、交叉和变异等一系列遗传操作,从而产生新一代的种群,并逐渐使种群进化到包含近似最优解的状态。遗传算法能够解决传统方法感到困难的许多复杂优化问题。
遗传算法很有趣,上过生物课的人很容易理解它,但是要设计一个好的遗传算法却并不容易。当看到谁养鱼这个问题时,觉得可以用遗传算法来试一试,现在我已经用Java实现了遗传算法,但是实验结果并不是很理想,权当做了一次实验证明遗传算法并不适合这类问题吧(当然也有可能是我的遗传算法没有设计好)。尽管这次实验结果不甚理想,下面还是介绍一下如何实现遗传算法。
设计一个遗传算法主要有五大要素:编码方式、种群规模的设定、适应度函数的设计、遗传算子的设计和终止条件的设定。
2 编码
当用遗传算法解决问题时,必须在问题空间和遗传算法的个体基因结构之间建立联系,即编码和解码的方法。编码的策略对于遗传算子,尤其是交叉、变异算子的功能和设计有很大的影响。编码应满足三个原则:
- 完备性:原问题空间中的点都能成为编码后的点。
- 健全性:编码后的空间中的点能对应原问题空间所有的点。
- 非冗余性:编码前后空间的点一一对应。
我采用了谁养鱼(二)中介绍的方法进行编码。对于每一类属性(颜色、国籍、饮料、宠物和香烟),都先将其五种可能分别映射到1、2、3、4、5,如此一类属性的排列就映射成了1、2、3、4、5的排列,再使用谁养鱼(二)中的方法映射到[0,119]的整数域。每一个[0,119]的整数可以用1个字节表示,因此我们用5个字节就可以表示一个可能的解。在这里将解称为个体,将五个字节分别称为颜色染色体、国籍染色体、饮料染色体、宠物染色体和香烟染色体,而每个字节代表了该染色体上的基因排列,注意[120,127]之间的基因排列是无效的。
五种属性我是按照红黄蓝绿白,英挪瑞德丹,猫狗鱼马鸟,水咖啤奶茶,Dunhill、Blends、PallMall、Prince、BlueMaster的顺序映射到1、2、3、4、5的,因此谁养鱼(一)中给出的最终解的编码是<30, 43, 22, 17, 0>。
这种编码方式显然满足了三个原则,但是也存在问题,比如最优解(匹配15条线索)和近似最优解(匹配绝大多数线索)的编码之间似乎没有什么联系,我找不到将近似最优解进行很小变动就得到最优解的方法,整个算法的过程有点类似随机的搜索。
3 种群规模
种群中个体的数目称为种群规模,在执行算法之前,必须已经有一个若干初始解组成的初始种群。在工程问题中,往往并不存在问题空间的先验知识,所以很难确定最优解的数量及其在可行解空间中的情况。所以一般是随机产生初始种群,种群规模通常在几十到几百之间。种群规模过小会限制群体的多样性,导致搜索过早收敛;规模过大,导致计算量增加,会削弱算法的效率。
我采用了随机生成初始种群的方法,而种群规模设为50左右。
4 适应度函数
为了执行适者生存的原则必须对个体的适应性进行评价。适应度函数体现了个体的生存环境,根据适应度函数计算出个体的适应度,就可以判断它在此环境下的生存能力。一般来说较好的个体基因结构具有较高的适应度函数值,即可以获得较高的评价。由于适应度函数是种群中个体生存机会的唯一确定性指标,所以适应度函数的设计直接决定了种群的遗传行为。适应度函数的设计一般需要满足以下条件:
- 连续、非负。
- 尽可能简单。
- 对某一类具体问题,应尽可能通用。
对于特殊设计的遗传算法,也不必完全遵守上述规则。我以谁养鱼(三)为基础,将个体能匹配的线索数作为适应度函数的主体,匹配的线索越多适应度就越高,这种方法非常自然也很简单。
5 遗传算子
标准的遗传算子包括选择、交叉和变异三种,它们构成了遗传算法的核心,使得算法具有强大的搜索能力。
5.1 选择算子
选择算子就是确定父代种群中哪些个体可以遗传到下一代种群中,它根据个体的适应度决定被选择的概率。目前常用的方法有适应度比例选择、轮盘式选择和竞争式选择等形式。我采用的是轮盘式选择,即根据个体适应度大小分配轮盘面积,面积代表了个体被挑选到交配池中的概率。具体的实现方法可以先计算种群中个体适应度之和fitnessAmount,产生一个0到fitnessAmount的随机数,遍历种群,累计个体的适应度,遇到的第一个累计适应度大于随机数的个体就被选中了,一直重复直到交配池达到种群规模。
/* * 轮盘赌选择个体进行交叉 * 单独测试能反映优胜劣汰 * */ public void select(){ Iterator<Individual> iter = this.iterator(); //计算种群适应度总和 fitnessAmount=0; while(iter.hasNext()){ Individual indi = iter.next(); fitnessAmount+=indi.fitness; } //不断选择个体进入交配池 int number = this.size(); while(this.size()<number+God.SIZE){ double points; Individual winner=null; points = God.RAN.nextDouble()*fitnessAmount; double amount=0; for(int i=0;i<number;++i){ Individual indi=get(i); amount+=indi.fitness; if(amount>=points){ winner=(Individual)indi.clone(); break; } } this.add(winner); } this.removeRange(0,number); }
5.2 交叉算子
交叉算子是遗传算法最主要的操作,它模仿自然界有性繁殖的基因重组过程,其作用就是将选择操子选出来的优良基因遗传到下一代种群中。通常的做法是:随机确定一个或多个位置为交叉点,由此将一对父体的基因序列分为有限个片段,再以一定概率交换相应片段得到新的个体。根据交叉点的数量可以分为单点交叉、多点交叉和均匀交叉等。交叉的概率太高,则优良物种被取走的速度越快,产生新物种的速度也越快;交叉的概率太低,则搜索会停滞不前。因为交叉前已经做过选择,所以较一般随机算法更好。
我在实现时以染色体为单位进行单点交叉,交叉的概率Pc设为0.9左右。注意到交叉可能产生不符合[0,119]范围的染色体,遇到这种情况就重新交叉。
/* * 每个染色体都是单点交叉 * */ public void recombine(Individual indi){ //遍历个体的染色体 for(int i=0;i<size();++i){ int x,y; do{ x=this.get(i); y=indi.get(i); double point = God.RAN.nextDouble(); if(point>God.Pc) continue; int mask=(int)Math.pow(2, (int)((point/God.Pc)*God.CHROMOSOME_LENGTH)+1)-1; //注意java中“-”的优先级高于“&” x = (x-(x&mask))+(x&mask)^(y&mask); y = (y-(y&mask))+(x&mask)^(y&mask); x = (x-(x&mask))+(x&mask)^(y&mask); }while(x>God.CHROMOSOME_UPPERLIMIT||y>God.CHROMOSOME_UPPERLIMIT); this.set(i, x); indi.set(i, y); } }
5.3 变异算子
完全依靠选择和交叉操作可能导致无法创造出具有新特性的个体,这有点类似Packing问题里的局部最小值陷阱?所以引入突变使个体跳出局部解范围,产生全局最优解。变异操作通常是将个体基因序列中的某些基因位上的基因值用该基因位的其他等位基因来替换,从而产生新个体。变异的概率一般取值很小,否则就等于随机搜索了。
我将每条染色体发生变异的概率Pm定为0.04,这样一个个体发生变异的概率大概是18%,当个体的适应度较小时,将变异率修正为1。注意到变异可能产生不符合[0,119]范围的染色体,遇到这种情况就重新变异。
public void mutate(){ double pm = God.Pm; if(fitness<0.6) pm=1; for(int i=0;i<size();++i){ int x; do{ x=this.get(i); double point = God.RAN.nextDouble(); if(point>God.Pm) continue; x=x^(1<<(int)(God.CHROMOSOME_LENGTH*point/pm)); }while(x>=God.CHROMOSOME_UPPERLIMIT); set(i, x); } }
6 终止条件
一般为遗传算法设定一个最大代数作为终止条件,这种办法很简单,但是需要多次调试才能找到合适的代数。
7 实验结果
我将种群规模设为50,最大代数设为50000,然后输入不同数量的线索进行测试:
- 测试10条以内的线索可以很快找到解,一般运行10000代以内;
- 测试11条线索大多数情况都找到解,通常需要运行2、3万代;
- 测试12、13条线索可能找到解,概率在50%左右;
- 测试14条线索偶尔能得到解,大多数是有一条线索未匹配;
- 测试全部15条线索还未得到解,大多数有一线索未匹配。
当我不限制代数测试15条线索时,通常会收敛到匹配14条线索就停滞不前了。
8 讨论
遗传算法适合那种解空间特别大,又找不到合适的算法可以短时间内解决的问题,例如NP难度问题。谁养鱼这个问题的解空间是 120^5=24,883,200,000,暴力解法当然可以尝试,但是就不好玩了。这个问题我用遗传算法并没有得到好的结果,是否有其它更好的算法我会继续关注,而且这次遗传算法的设计还有很多不尽如人意的地方,还希望能有高人指点一二。
参考文献
[1] 韩慧等。数据仓库与数据挖掘。清华大学出版社。