遗传算法-智能鸟群
提纲:
一、遗传算法概述
1.1 群体
1.2 基因型和表现型
1.3 突变
1.4 选择
1.5 遗传
二、智能鸟群的实现
2.1 小鸟的基因型和表现型
2.2 小鸟的基因如何影响小鸟的飞行路线
2.3 小鸟的适应性
2.4 选择:选择适应度更高的小鸟
2.5 突变:增加基因的丰富性
2.6 繁殖:产生新的DNA
2.7 群体的繁殖
三
、总结和优化
智能鸟群
本文通过使用遗传算法实现了一个智能鸟群。场景如下图,小鸟从屏幕右侧开始,越过障碍物最终到达屏幕左侧的目标点。鸟群在初始状态时会在屏幕中乱飞(下图1),随着不断的进化,最终鸟群会直接飞向目标点(下图2),可点击这里看效果。下面先介绍下遗传算法。
一、遗传算法概述
遗传算法借用了达尔文自然选择的思想,即在一个群体中,适应性更高的个体,会更有可能将自己的基因(特性)遗传下去。下面是自然选择中的一些关键概念:
群体为自然选择提供了丰富的基因库,保证了个体的多样性,群体越大越容易产生更具适应性的个体。
我们看到的个体的外貌和行为等外在表现是由内部的基因决定的。基因型即为决定个体表现的内部数据,表现型为个体的外观和行为。比如数字125,即可以表示个体的颜色,也可以表示个体的高度。数字125即为个体的基因型,颜色、高度等为个体的表现型。 在设计遗传算法时,要着重设计个体的基因型和表现型。
遗传、突变和选择是达尔文进化论中的3个基本法则。遗传保证了子代能够继承父代的特性。突变保证了个体的多样性,如果没有突变子代和父代会永远保持一致,新的特性就永远不会出现,种群也不会进化。选择保证了群体向更具适应性的方向进化,使群体中某些个体能够繁殖而另一些个体却没有机会或有很少的机会繁殖。这就是通常说的“适者生存”。在遗传算法中,在每一代都会计算个体的适应性,适应性高的个体的基因会在下一代遗传下去,适应性低的个体的基因会被淘汰。
二、智能鸟群的实现
下面借助白鹭引擎实现智能鸟群
1. 小鸟的基因型和表现型
为了使小鸟不断飞行,在小鸟的生命周期的每一时刻都会为小鸟赋予一个推力,通过推力改变小鸟的加速度,进而影响到小鸟的速度和位置。在小鸟的整个生命周期中(假设为200帧),可以把每一帧上的推力所组成的数组作为小鸟的基因型。小鸟在一系列的推力作用下所形成的飞行路线,为小鸟的表现型。可以用DNA类来定义小鸟的基因,其中推力用一个二维向量来定义。
1 class DNA { 2 private _genes=[]; 3 private _fitness:number; 4 private _maxforce=0.5;//最大推力为0.5 5 private _lifetime=200;//小鸟的生命周期为200帧 6 7 public constructor() { 8 for(let i=0;i<this._lifetime;i++){ 9 let force=Vector2D.random2D(); 10 force.mult(Math.random()*this._maxforce);//初始状态,每一时刻的推力是随机的。 11 this._genes.push(force); 12 } 13 } 14 15 public get genes(){ 16 return this._genes; 17 } 18 }
2. 小鸟的基因如何影响小鸟的飞行路线
在小鸟的类中,我们定义了一个applyForce方法,会根据生命周期的不同时刻将DNA上对应的推力应用在小鸟身上,从而使小鸟的位置发生改变。下面例子中我们使用egret画了一个三角形代表小鸟。代码如下:
1 class Bird extends egret.Sprite { 2 3 public location:Vector2D; 4 public velocity:Vector2D; 5 public acceleration:Vector2D; 6 public mass:number; 7 private shape:egret.Shape; 8 9 public target:Vector2D; 10 public dna:DNA; 11 private geneCounter=0; 12 13 public constructor(mass:number,x:number,y:number,target:Vector2D=new Vector2D(0,0)) { 14 super(); 15 this.dna=new DNA(); 16 this.mass=mass; 17 this.location=new Vector2D(x,y);//小鸟位置 18 this.velocity=new Vector2D(0,0);//小鸟的速度 19 this.acceleration=new Vector2D(0,0);//小鸟加速度 20 this.target=target; 21 22 this.shape=new egret.Shape(); 23 let g=this.shape.graphics; 24 g.clear(); 25 g.beginFill(0xff0000); 26 g.moveTo(0,0); 27 g.lineTo(-10,-5); 28 g.lineTo(-10,5); 29 g.lineTo(0,0); 30 this.addChild(this.shape); 31 this.shape.x=this.location.x; 32 this.shape.y=this.location.y; 33 } 34 35 public run(){ 36 this.geneCounter++; 37 this.applyForce(this.dna.genes[this.geneCounter]);//将基因对应时刻的力作用在小鸟上 38 this.update(); 39 this.display(); 40 } 41 42 public applyForce(force:Vector2D){ 43 let f:Vector2D=Vector2D.div(force,this.mass);//力除以质量为加速度 44 this.acceleration.add(f);//改变加速度 45 } 46 47 public update(){ 48 this.velocity.add(this.acceleration); 49 this.location.add(this.velocity); 50 this.acceleration.mult(0); 51 } 52 53 public display(){ 54 this.shape.x=this.location.x; 55 this.shape.y=this.location.y; 56 let angle=this.velocity.heading2D()*180/Math.PI; 57 this.shape.rotation=angle; 58 } 59 }
3. 小鸟的适应性
在小鸟的生命周期结束时,我们通过判断小鸟离目标点的距离来判断小鸟的适应性,离目标点越近的小鸟适应性越高,否则适应性越低。可以通过小鸟离目标点的距离的倒数的平方作为小鸟的适应度。计算小鸟的适应度函数如下(在Bird类中定义):
1 /*适应度计算*/ 2 public fitness(){ 3 let d=Vector2D.dist(this.location,this.target);//计算小鸟离目标点的距离,this.target为小鸟的目标点 4 this._fitness=Math.pow(1/d,2); 5 } 6 7 public getFitness(){ 8 return this._fitness; 9 }
4. 选择:选择适应度更高的小鸟
我们定义一个Population类来管理所有的小鸟以及负责小鸟的选择和遗传。在Population类中定义了一个population数组来存储所有的小鸟,另外定义了一个matingPool数组作为交配池,我们根据小鸟适应性的强弱来将其放入交配池中,适应性越强的小鸟放入交配池中的数量越多,否则就越少。最后我们从交配池中随机的选择小鸟进行交配遗传,这样就保证了适应性强的小鸟选到的概率就越大。Population类中的选择函数如下:
1 public selection(){ 2 this.matingPool=[]; 3 let totalFitness=0; 4 for(let i=0;i<this.totalPopulation;i++){ 5 totalFitness+=this.population[i].getFitness(); 6 } 7 8 for(let i=0;i<this.totalPopulation;i++){ 9 let n=this.population[i].getFitness()/totalFitness*200;//适应性越大的小鸟,存入交配池中的数量就越多 10 if(n<1){ 11 continue; 12 } 13 for(let j=0;j<n;j++){ 14 this.matingPool.push(this.population[i]); 15 } 16 } 17 }
上面的方法,先计算所有小鸟的适应度之和,最后计算每个小鸟的适应度在所有小鸟的适应度中所占比例,然后将这个比例换算为相应的个数存入交配池。可以把这种选择方法想象成一个轮盘,某个个体的适应度所占的比例约大,它被选中的概率就越高。
5. 突变:增加基因的丰富性
为了增加基因的丰富性,从而产生适应性更强的个体,我们需要在每一代使小鸟的基因有一定的几率产生突变,我们在小鸟的DNA类中加入突变函数:
1 public mutate(mutationRate:number){ 2 for(let i=0;i<this._genes.length;i++){ 3 if(Math.random()<mutationRate){ 4 let force=Vector2D.random2D(); 5 force.mult(Math.random()*this._maxforce); 6 this._genes[i]=force; 7 } 8 } 9 }
并且为Bird类添加mutate接口
1 public mutate(mutationRate:number){ 2 this.dna.mutate(mutationRate); 3 }
6. 繁殖:产生新的DNA
在DNA类中,添加crossover方法,它接受另一个DNA实例,通过交叉组合生成新的DNA:
1 public crossover(partner:DNA):DNA{ 2 let child=new DNA(); 3 for(let i=0;i<this._genes.length;i++){ 4 let random=Math.random(); 5 if(random>0.5){ 6 child._genes[i]=this._genes[i]; 7 }else{ 8 child._genes[i]=partner._genes[i]; 9 } 10 } 11 return child; 12 }
在上面方法中,在生命周期的每一帧中随机选择双亲对应节点的数据作为子代的基因。下面为Bird类添加繁殖的方法:
/*小鸟的繁殖*/ public crossover(b:Bird):Bird{ let bird=new Bird(1,initX,initY);//小鸟质量为1,初始位置为(initX,initY) let dna=this.dna.crossover(b.dna); bird.dna=dna; return bird; }
7. 群体的繁殖
接下来在Population中添加reproduction方法,用来产生下一代。在reproduction方法中,我们从交配池中随机的选择两个小鸟作为双亲,产生新的小鸟,并使小鸟发生突变,最后把新产生的小鸟加入数组population中。
1 public reproduction(){ 2 3 for(let i=0;i<this.population.length;i++){ 4 let a=Math.floor(Math.random()*this.matingPool.length); 5 let b=Math.floor(Math.random()*this.matingPool.length); 6 let partnerA=this.matingPool[a]; 7 let partnerB=this.matingPool[b]; 8 let child=partnerA.crossover(partnerB); 9 child.target=this.target;//为小鸟设定目标 10 11 child.mutate(this.mutationRate); 12 this.removeChild(this.population[i]); 13 this.population[i]=child; 14 this.addChild(child); 15 } 16 }
三
、总结和优化
通过上面的示例,我们可以总结出使用遗传算法时的几个关键步骤:
1. 定义个体的基因型和表现型,基因型发生改变表现型也会随之变化。
2. 计算群体中每个个体的适应性;
3. 选择适应性更高的个体作为下一代的双亲(可以通过交配池实现);
4. 通过双亲繁殖下一代,产生的下一代会发生基因突变;
5. 返回2进行下下一代的繁殖;
上例中,小鸟在进行多代繁殖后,最终会沿直线朝目标飞去。为了体现遗产算法的强大,可以在小鸟和目标之间加入障碍物,当小鸟碰到障碍物时会停止,并且碰到障碍物的小鸟的适应性会急速下降。可以看看经过几代的进化后聪明的小鸟会如何绕过障碍物到达目标点。
上面只贴出了整个项目中关键几步的代码,整个项目可以访问我的GitHub,欢迎提交issues交流。