内容


人工智能 Java 坦克机器人系列

遗传算法

系列内容:

此内容是该系列 # 部分中的第 # 部分: 人工智能 Java 坦克机器人系列

敬请期待该系列的后续内容。

此内容是该系列的一部分:人工智能 Java 坦克机器人系列

敬请期待该系列的后续内容。

遗传算法

遗传算法(Genetic Algorithm, GA)是近几年发展起来的一种崭新的全局优化算法。1962 年霍兰德 (Holland) 教授首次提出了 GA 算法的思想,它借用了仿真生物遗传学和自然选择机理,通过自然选择、遗传、变异等作用机制,实现各个个体的适应性的提高。从某种程度上说遗传算法是对生物进化过程进行的数学方式仿真。

这一点体现了自然界中"物竞天择、适者生存"进化过程。与自然界相似,遗传算法对求解问题的本身一无所知,它所需要的仅是对算法所产生的每个染色体进行评价 , 把问题的解表示成染色体,并基于适应值来选择染色体,使适应性好的染色体有更多的繁殖机会。在算法中也即是以二进制编码的串。并且,在执行遗传算法之前,给出一群染色体,也即是假设解。然后,把这些假设解置于问题的“环境”中,也即一个适应度函数中来评价。并按适者生存的原则,从中选择出较适应环境的染色体进行复制 , 淘汰低适应度的个体,再通过交叉,变异过程产生更适应环境的新一代染色体群。对这个新种群进行下一轮进化,至到最适合环境的值。

遗传算法已用于求解带有应用前景的一些问题,例如遗传程序设计、函数优化、排序问题、人工神经网络、分类系统、计算机图像处理和机器人运动规划等。

术语说明

由于遗传算法是由进化论和遗传学机理而产生的搜索算法,所以在这个算法中会用到很多生物遗传学知识,下面是我们将会用来的一些术语说明:

一、染色体 (Chronmosome)

染色体又可以叫做基因型个体 (individuals), 一定数量的个体组成了群体 (population), 群体中个体的数量叫做群体大小。

二、基因 (Gene)

基因是串中的元素,基因用于表示个体的特征。例如有一个串 S = 1011,则其中的 1,0,1,1 这 4 个元素分别称为基因。它们的值称为等位基因 (Alletes)。

三、基因地点 (Locus)

基因地点在算法中表示一个基因在串中的位置称为基因位置 (Gene Position),有时也简称基因位。基因位置由串的左向右计算,例如在串 S = 1101 中,0 的基因位置是 3。

四、基因特征值 (Gene Feature)

在用串表示整数时,基因的特征值与二进制数的权一致;例如在串 S=1011 中,基因位置 3 中的 1,它的基因特征值为 2;基因位置 1 中的 1,它的基因特征值为 8。

五、适应度 (Fitness)

各个个体对环境的适应程度叫做适应度 (fitness)。为了体现染色体的适应能力,引入了对问题中的每一个染色体都能进行度量的函数,叫适应度函数 . 这个函数是计算个体在群体中被使用的概率。

操作算法

霍兰德 (Holland) 教授最初提出的算法也叫简单遗传算法,简单遗传算法的遗传操作主要有三种:选择 (selection)、交叉 (crossover)、变异 (mutation) 这也是遗传算法中最常用的三种算法:

1 .选择 (selection)

选择操作也叫复制操作,从群体中按个体的适应度函数值选择出较适应环境的个体。一般地说,选择将使适应度高的个体繁殖下一代的数目较多,而适应度较小的个体,繁殖下一代的数目较少 , 甚至被淘汰。最通常的实现方法是轮盘赌 (roulette wheel) 模型。令Σ fi 表示群体的适应度值之总和,fi 表示种群中第 i 个染色体的适应度值,它被选择的概率正好为其适应度值所占份额 fi /Σ fi。如下图表中的数据适应值总和Σ fi=6650, 适应度为 2200 变选择的可能为 fi /Σ fi=2200/6650=0.394.

图 1. 轮盘赌模型
图 1. 轮盘赌模型
Fitness 值:220018001200950400100
选择概率:33310.2710.180.1430.060.015

2 .交叉 (Crossover)

交叉算子将被选中的两个个体的基因链按一定概率 pc 进行交叉,从而生成两个新的个体,交叉位置 pc 是随机的。其中 Pc 是一个系统参数。根据问题的不同,交叉又为了单点交叉算子(Single Point Crossover)、双点交叉算子(Two Point Crossover)、均匀交叉算子 (Uniform Crossover),在此我们只讨论单点交叉的情况。

单点交叉操作的简单方式是将被选择出的两个个体 S1 和 S2 作为父母个体,将两者的部分基因码值进行交换。假设如下两个 8 位的个体:

S1 	 1000  1111 	 S2 	 1110  1100

产生一个在 1 到 7 之间的随机数 c,假如现在产生的是 2,将 S1 和 S2 的低二位交换:S1 的高六位与 S2 的低六位组成数串 10001100,这就是 S1 和 S2 的一个后代 P1 个体;S2 的高六位与 S1 的低二位组成数串 11101111,这就是 S1 和 S2 的一个后代 P2 个体。其交换过程如下图所示:

Crossover11110000Crossover11110000
S11000 1111S21110 1100
P11000 1100P21110 1111

3 .变异 (Mutation)

这是在选中的个体中,将新个体的基因链的各位按概率 pm 进行异向转化,最简单方式是改变串上某个位置数值。对二进制编码来说将 0 与 1 互换:0 变异为 1,1 变异为 0。

如下 8 位二进制编码:

1 	 1 	 1 	 0 	 1 	 1 	 0 	 0

随机产生一个 1 至 8 之间的数 i,假如现在 k=6,对从右往左的第 6 位进行变异操作,将原来的 1 变为 0,得到如下串:

1 	 1 	 0 	 0 	 1 	 1 	 0 	 0

整个交叉变异过程如下图:

图 2. 交叉变异过程
图 2. 交叉变异过程
图 2. 交叉变异过程
图 2. 交叉变异过程

4 .精英主义 (Elitism)

仅仅从产生的子代中选择基因去构造新的种群可能会丢失掉上一代种群中的很多信息。也就是说当利用交叉和变异产生新的一代时,我们有很大的可能把在某个中间步骤中得到的最优解丢失。在此我们使用精英主义(Elitism)方法,在每一次产生新的一代时,我们首先把当前最优解原封不动的复制到新的一代中,其他步骤不变。这样任何时刻产生的一个最优解都可以存活到遗传算法结束。

上述各种算子的实现是多种多样的,而且许多新的算子正在不断地提出,以改进 GA 某些性能。比如选择算法还有分级均衡选择等等。

遗传算法的所需参数

说简单点遗传算法就是遍历搜索空间或连接池,从中找出最优的解。搜索空间中全部都是个体,而群体为搜索空间的一个子集。并不是所有被选择了的染色体都要进行交叉操作和变异操作,而是以一定的概率进行,一般在程序设计中交叉发生的概率要比变异发生的概率选取得大若干个数量级。大部分遗传算法的步骤都很类似,常使用如下参数:

Fitness 函数:见上文介绍。

Fitnessthreshold(适应度阀值):适合度中的设定的阀值,当最优个体的适应度达到给定的阀值,或者最优个体的适应度和群体适应度不再上升时 ( 变化率为零 ),则算法的迭代过程收敛、算法结束。否则,用经过选择、交叉、变异所得到的新一代群体取代上一代群体,并返回到选择操作处继续循环执行。

P:种群的染色体总数叫种群规模,它对算法的效率有明显的影响,其长度等于它包含的个体数量。太小时难以求出最优解,太大则增长收敛时间导致程序运行时间长。对不同的问题可能有各自适合的种群规模,通常种群规模为 30 至 160。

pc:在循环中进行交叉操作所用到的概率。交叉概率(Pc)一般取 0.6 至 0.95 之间的值,Pc 太小时难以向前搜索,太大则容易破坏高适应值的结构。

Pm:变异概率,从个体群中产生变异的概率,变异概率一般取 0.01 至 0.03 之间的值变异概率 Pm 太小时难以产生新的基因结构,太大使遗传算法成了单纯的随机搜索。

另一个系统参数是个体的长度,有定长和变长两种。它对算法的性能也有影响。由于 GA 是一个概率过程,所以每次迭代的情况是不一样的 , 系统参数不同,迭代情况也不同。

遗传步骤

了解了上面的基本参数,下面我们来看看遗传算法的基本步骤。

基本过程为:

  1. 对待解决问题进行编码 , 我们将问题结构变换为位串形式编码表示的过程叫编码;而相反将位串形式编码表示变换为原问题结构的过程叫译码。
  2. 随机初始化群体 P(0):=(p1, p2, … pn);
  3. 计算群体上每个个体的适应度值 (Fitness)
  4. 评估适应度 , 对当前群体 P(t) 中每个个体 Pi 计算其适应度 F(Pi),适应度表示了该个体的性能好坏
  5. 按由个体适应度值所决定的某个规则应用选择算子产生中间代 Pr(t)
  6. 依照 Pc 选择个体进行交叉操作
  7. 仿照 Pm 对繁殖个体进行变异操作
  8. 没有满足某种停止条件,则转第 3 步,否则进入 9
  9. 输出种群中适应度值最优的个体

程序的停止条件最简单的有如下二种:完成了预先给定的进化代数则停止;种群中的最优个体在连续若干代没有改进或平均适应度在连续若干代基本没有改进时停止。

根据遗传算法思想可以画出如右图所示的简单遗传算法框图:

图 3. 简单遗传算法框图
图 3. 简单遗传算法框图

下面伪代码简单说明了遗传算法操作过程:

 choose an intial population 
 For each h in population,compute Fitness(h) 
 While(max Fitness(h) < Fitnessthreshold) 
 do selection 
    do crossover 
 do mutation  
 update population 
 For each h in population,compute Fitness(h) 
 Return best Fitness

Robocode 说明

能有效实现遗传算法的应用例子有很多,像西洋双陆棋、国际名模等等都是遗传程序设计学习的工具,但是 Robocode 有着其他几个无可比拟的优势:

  1. 它是基于面向对象语言 Java 开发,而遗传算法本身的思想也是存在继承等面向对象概念;
  2. Robocode 是一种基于游戏与编程语言之间的平台,有一个充满竞技与乐趣的坦克战斗平台,你能很快的通过与其他坦克机器比赛而测试自己的遗传算法;
  3. Robocode 社群有 4000 个左右各种策略的例子机器人可供你选择,这些机器人足以让我们模拟真实的遗传环境。而且很多代码可直接开放源代码供我们借鉴 ;
  4. Robocode 是一个开源软件,你可直接上 Robocode 控制器上加入自己的遗传特点,而加快遗传过程的收敛时间;
  5. Robocoe 是一个很容易使用的机器人战斗仿真器,您在此平台上创建自己的坦克机器人,并与其它开发者开发的机器人竞技。以得分排名的方式判定优胜者。每个 Robocode 参加者都要利用 Java 语言元素创建他或她的机器人,这样就使从初学者到高级黑客的广大开发者都可以参与这一娱乐活动。如果您对 Robocode 不是很了解,请参考 developerWorks 网站 Java 技术专区文章:“重锤痛击 Robocode”;

在 Robocode 中其实有很多种遗传算法方法来实现进化机器人,从全世界的 Robocode 流派中也发展几种比较成熟的方法,比如预设策略遗传、自开发解释语言遗传、遗传移动我们就这几种方法分别加以介绍。由于遗传算法操作过程都类似,所以前面二部分都是一些方法的介绍和部分例子讲解,后面部分会给出使用了遗传算法的移动机器人人例子。 在附录中,也提供了机器人仓库中有关遗传算法机器人的下载,大家可参考。

预设策略进化机器人

Robocode 坦克机器人所有行为都离不开如移动、射击、扫描等基本操作。所以在此把这些基本操作所用到的策略分别进化如下编码:移动策略 move-strategy (MS), 子弹能量 bullet-power-strategy (BPS), 雷达扫描 radar-strategy (RS), 和瞄准选择策略 target- strategy (TS)。由于 Robocode 爱好者社群的发展,每一种基本操作都发展了很多比较成熟的策略,所有在此我们直接在下面预先定义的这些策略如下表:

MSBPSRSTS
randomdistance-basedalways-turnHeadOn
Linearlight-fasttarget-focusLinear
circularPowerful-slowtarget-scope-focusCircular
PerpendicularMediumnearest robot
arbitaryhit-rate basedLog
anti gravityStatistic
StopAngular
Bullet avoidwave
wall avoid
track
Oscillators

下面是基本移动策略的说明:

  • Random:随机移动主要用来混乱敌人的预测,其最大的一个缺点是有可能撞击到其他机器人
  • Linear:直线移动 , 机器人做单一的直线行走
  • circular:圆周移动,这种移动是以某一点为圆心,不停的绕圈
  • Perpendicular:正对敌人移动,这是很多人采用的一种移动方式,这在敌人右边, 以随时调整与敌人的相对角
  • Arbitrary:任意移动
  • AntiGravity:假设场地有很多力点的反重力移动,本方法是模拟在重力场作用下,物体总是远离重力势高的点,滑向重力势低的点,开始战场是一个平面然后生成一些势点重力势大的势点的作用就像是一个山 ( 起排斥作用),其衰减系数与山的坡度对应。重力势小的势点的作用就像是一个低谷(起吸引作用),其衰减系数与谷的坡度对应。这样使本来的平面变得不平了,从来物体沿着最陡的方向向下滑动
  • Track:跟踪敌人,敌人移动到哪,机器人也移动到哪,但是总与敌人保持一定最佳躲避子弹距离和角度
  • Oscillators:重复做一振荡移动
  • Bullet avoid:每当雷达觉察到敌人时有所动作。机器人保持与敌人成 30 度倾向角。自身成 90 度角静止并逐渐接近目标。如果机器人觉察到能量下降介于 0.1 和 3.0 之间(火力范围),那么机器人就立即切换方向,向左或向右移动。
  • wall avoid:这是一种使自己的机器人不会被困在角落里或者不会撞墙的移动方式

瞄准策略说明如下:

  • Headon:正对瞄准
  • Linear:直线瞄准
  • circular:圆周瞄准
  • nearest robot:接近机器人瞄准
  • Log:保存每次开火记录
  • Statistic:统计学瞄准,分析所有打中及未打中的次数,以其中找出最高打中敌人的概率为准则
  • Angular:找到最佳角度瞄准
  • Wave:波形瞄准,子弹以波的方式进行探测

Robocode 行为事件

坦克的主要都定义在一个主循环中,我们在程序中定义为上面四个策略定义四种战略如 Move,Radar,Power,Target,当某一事件发生,基于这个事件而定的行为就会触发。而每个战略中都有不同的行为处理方式。这些行为通过遗传算法触发,遗传算法将调用这些基本动作并搜索这些策略的最佳组合。基于这些基本动作将有 4224 (=4*11*4*3*8) 种可能发生。在 Robocode AdvancedRobot 类下有如下的移动函数:

  • setAhead 和 ahead:让机器人向前移动一定距离 .
  • setBack 和 back:让机器人向后移动一定距离
  • setMaxTurnRate:设置机器人最大的旋转速度
  • setMaxVelocity:设置机器人最大的运动速度
  • setStop 和 stop:停止移动或暂停机器人,并记住停止的位置
  • setResume 和 resume:重新开始移动停止的机器人
  • setTurnLeft 和 turnLeft:向左旋转机器人
  • setTurnRight 和 turnRight:向右旋转机器人

下面是 doMove 移动方法中使用部分程序代码:

Random:

 switch(Math.random()*2) { 
	 case 0:	 setTurnRight(Math.random()*90); 
 break; 
 case 1:	 setTurnLeft(Math.random()*90); 
	 break; 	 } 
	 execute();

Linear:

 ahead(200); 
 setBack(200);

Circular:

 setTurnRight(1000); 
 setMaxVelocity(4); 
 ahead(1000);

anti gravity:

double forceX = 0; 
double forceY = 0; 
for (int i=0; i<targetInfo.size(); i++) { 
    TargetInformation ti = (TargetInformation)targetInfo.get(i); 
    double targetToMeX = getX()-ti.x; 
    double targetToMeY = getY()-ti.y; 
    double targetDistance = Math.sqrt(ti.x * ti.x + ti.y * ti.y); 
    forceX += (targetToMeX/(ti.distance * ti.distance)); 
    forceY += (targetToMeY/(ti.distance * ti.distance)); 
} 
forceX += 1/(getX()); 
forceY += 1/(getY()); 
forceX += 1/(getX()-getBattleFieldWidth()); 
forceY += 1/(getY()-getBattleFieldHeight()); 
double forceMagnitude = Math.sqrt(forceX*forceX+forceY*forceY); 
forceX*=8/forceMagnitude; 
forceY*=8/forceMagnitude; 
desiredX = getX() + forceX; 
desiredY = getY() + forceY; 
…

这里我们用遗传算法来控制机器人移动位置。这些策略是基于下面几点:机器人人自己的位置、速度和方位 ; 对手的位置(x,y 坐标)、速度、方位以及相对角 ; 所有机器人和子弹位置,方位及速度 ; 场地大小等参数。

当上面的信息在下一回移动中使用时,出输出一对坐标值,根据这对坐标在 Robocode 就能得到距离和角度。要想让移动实现遗传必须要让它实现在线学习:所以我们的代码必须做下面几件事:要有一个函数收集适应度值,在 Robocode 运行过程中要运用到遗传操作,遗传后代要在 Robocode 运行中产生,而不是事后由手写入代码。

遗传操作

本例中遗传算法为实现移动用到两个类 GA 和 MovePattern。此处的 GA 比较简单主要完成数据和群体的定义,以及这些定义的读写文件操作。基中包括如下参数:群体大小、交叉概率、变异概率、精英概率(既告诉从当前群体到下一代中有多少移动不需要改变)、方程式中使用的加权系数大小,它通过一个主循环完成 MovePattern 的封装。MovePattern 类中实现交叉、变异方法等方法,完成移动模式操作。而所有的输出保存在一个 vector 函数当中。Vector 函数拥有一对实数数组,一个用于计算 x 坐标,另一个用于计算 y 坐标。通过对 x,y 坐标的计算,从而得到距离、角度等值,并产生相就在移动策略。如下,MovePattern 包含三个参数,grad 表示 vector 函数排列顺序,input 即表示算法给出的输入编号,rang 是加权的范围。

 public class MovePatteren implements Comparable { 
	 private int grad, input; 
	 private double range; 
	 protected double fitness=0; 
	 protected double[] weightsX, weightsY;    
… }

交叉操作:每一个交叉操作执行如下步骤,先在交叉操作中产生一个特征码。这个特征码是个 0 到 1 之间的变量数组。有关交叉的基本原理可参考上面部分。最后通过遍历 vector 函数,把相应的加权值进行交叉操作。

 protected MovePatteren crossOver(MovePatteren mate, boolean[] maskx, boolean[] masky) { 
		 double[] wx= new double[weightsX.length]; 
		 double[] wy= new double[weightsX.length]; 
		 for(int mask=0; mask<maskx.length; mask++) { 
			 for(int g=0; g<=grad; g++) { 
				 int i=mask*(grad+1)+g; 
				 wx[i]=(maskx[mask]?this:mate).weightsX[i]; 
				 wy[i]=(masky[g]?this:mate).weightsY[i]; 
			 } 
		 } 
		 return new MovePatteren(grad, input, range, wx, wy); 
	 }

这里的变异操作比较简单。把加权范围内的随机数值去代替 0 到数组长之间的随机数并保存到移动模式中。则完成整个数组的变异过程:

 protected void mutate() { 
 weightsX[(int)(Math.random()*weightsX.length)]=Math.random()*range*2-range; 
 weightsY[(int)(Math.random()*weightsX.length)]=Math.random()*range*2-range; 
 }

从上面的例子我们知道了遗传算法的大概实现,但并没有告诉我们这些组件是如何一起工作的。当 Robocode 开始时,如果文件中没有数据,所以系统会依照输入的策略随机生成一个移动模式,如果文件中有数据,则加载这些数据。每一个移动模式在开始都会给出了一个适应度值。当所有的移动模式都接收到适应度值,并完成各自的编号后,下面的操作将开始执行:

  1. 对所有的移动模式依据它们的适应度值进行分级处理
  2. 执行精英操作
  3. 执行交叉操作
  4. 应用变异操作
  5. 保存加权
  6. 算法重新开始

适应度值在进行运算过程中由机器人程序不断调整,以找到最优适应度。

限于篇副其他的一些策略本文不与详细说明,上面所有提到的策略和行为程序都可在网上或 IBM 的开发杂志上找到成熟的讲解和例子机器人。有兴趣的朋友可以把这些策略都加入到自己的遗传算法中来。我们取群体大小为 50,选择概率为 0.7,交叉概率为 0.6,变异概率为 0.3,与 Robocode 部分例子机器人测试,经过 150 代后你会发现系统产生了很多有趣的策略。比如撞击策略,这些策略都不在我们定义的策略之中。

中间解释程序进化机器人

遗传算法可被看做任意基因组字符串。但是你必须决定这些字符所代表的意义,也就是说如何解释每一个基因组。最简单的方法是把每一个基因组视为 java 代码,编译并运行它们。但是这些程序编译都很困难,所以也就有可能不能工作。Jacob Eisenstein 设计了一种机器翻译语言 TableRex 来解决这个问题。在 java 中,TableRex 被用于进化和解释动行时的 Robocode 机器人。通过测试,只要我把 TableRex 解释程序作为文件放入 Robocode 控制器目录中,这些控制器就会读取文件并开始战斗。TableRex 是一些最适合遗传算法的二进制编程。只要符合 TableRex 程序文法,每个程序都能被解释。

编码

下表中显示了 TableRex 编码结构,它由一个行动作函数,二个输入和一个输出组成。如行 6 的值 ,这是个布尔型的表达式“值 line4 小于 90”,这个结果会在最后一行输出布尔为 1 的值。

FunctionInput 1Input 2Output
1. Randomignoreignore0,87
2. DivideConst_1Const_20,5
3. Greater ThanLine 1Line 21
4. Normalize AngleEnemy bearingignore-50
5. Absolute ValueLine 4ignore50
6. Less ThanLine 4Const_901
7. AndLine 6Line 31
8. MultiplyConst_10Const_10100
9. Less ThanEnemy distanceLine 80
10. AndLine 9Line 70
11. MultiplyLine 10Line 40
12 OutputTurn gun leftLine 110

输入的函数我们依照 Robocode 定义而定。如下表:

 velocity 
 energy 
 heading 
 gunHeading 
 gunHeat 
 distance to west wall 
 distance to north wall 
 distance to east wall 
 distance to south wall 
 constant: 1 
 constant: 2 
 constant: 10 
 constant: 90 
 enemyVelocity 
 enemyEnergy 
 enemyHeading 
 enemyBearing 
 enemyDistance

TableRex 有三个设计标准:

  1. 它是一种解释程序,能更快的进化程序 , 基于 TableRex 设计的机器人能有效的读写遗传数据;
  2. 拥有一个容易编码的固定基因组 , 使遗传中更容易交叉操作;
  3. 只要给 TableRex 一个简单的输入,它就很容易通过操作命令输出要的命令序列。如上表的最后输出左转炮管;

而整个 TableRex 解释程序由三部分组成:

  • SmallBrain:TableRex 的实现部分 , 此部分直接写在例子机器人处,也即自己写的测试机器人;
  • BrainWorld:这是实现遗传算法的主方法,直接写入 Robocode 控制器当中,在 Robocode 运行当中运行;
  • GeneticAlgorithm:这是遗传算法的定义部分,里面直接定义了所要用到的遗传操作函数;

下面我们来分析一个机器人如何通过 TableRex 达到遗传。

GeneticAlgorithm:

主要是声明选择、交叉、变异的方法。

GeneticAlgorithm 是一个静态类,其中定义了遗传所要的基本参数,如下:

 public abstract class GeneticAlgorithm { 
    public int population_size;  // 群体长度
    public int genome_size;    // 基因个体长度
    public GFPair population[];  // 产生的群体
    public int best_index;  
    public double best_fitness = Double.MIN_VALUE;  // 最优适应度
    public double mutation = 0.03;  // 变异概率
 public double crossover = 0.9;   // 交叉概率
 public double copy = 0.1; 
 public int tourney_size = 3; 
…

其中变异概率取 0.03, 交叉概率取 0.9,最优适应度为实型的最小。此部分是从保存的文件中读取各个基本参数遗传初始化群体。

依照适应度值选择群体中个体:

 public String tourneySelect (){ 
	 double best_fit = -1; 
	 int best_guy = -1; 
	 for (int i = 0; i < tourney_size; i++){ 
	    int cur_guy = (int) (Math.random() * population.length); 	
	    if (population[cur_guy].fitness > best_fit){ 
		 best_fit = population[cur_guy].fitness; 
		 best_guy = cur_guy; 
	    } 
	 } 
	…
	 return population[best_guy].genome; 
 }

交叉操作:通过从字符串中取子串的方法达到交叉操作:

 public static String crossover (String g1, String g2){ 
	 int num_points = (int) Math.round (Math.random() * 4f); 
	 int point = (int) (g1.length() * Math.random()); 
	 return g1.substring (0, point) + g2.substring (point); 
    }

变异操作:此部分先把基因转换为字符串流,通过 setCharAt 函数从指定的位置取反字符而达到变异:

 public static String mutate (String genome, double p_mutation){ 
 StringBuffer genome_b = new StringBuffer (genome); 
			 if (genome_b.charAt (point) == '1'){ 
		    genome_b.setCharAt(point, '0'); 
		 } 
		 else { 
		    genome_b.setCharAt (point, '1'); 
		…
	 return new String (genome_b); 
    }

BrainWorld:

BrainWorld 直接嵌入到 Robocode 控制器中,通过实现 RobocodeListener 接口来完成遗传的实例化。其最重要的有两个方法,计算最优适应度和产生遗传后代。

1. 实例化 GeneticAlgorithm:

 genome_size = num_events * num_boxes * (input_bits * 2 + func_bits); 
 int population_size = Integer.parseInt (in.readLine()); 
 ga = new GeneticAlgorithm (population_size, genome_size);

2. 通过文件读取操作从遗传保存文件中读取参数到遗传类中,文件格式如下所示:

       Robocode Location 
       Storage_directory 
       population_size 
       elitism 
       crossover 
       copy 
       base_mutation 
       ...

3. 计算最优适应度:

 protected void evaluateAll (){ 
	 ga.best_fitness = Double.MIN_VALUE; 
	 ga.worst_fitness = Double.MAX_VALUE; 
   …
	 for (int i = 0; i < ga.population_size; i++){ 
	    ga.population[i].fitness = 0; 
	    for (int j = 0; j < rspecs.length; j++){ 
		 ga.population[i].fitness += 
		    Math.pow (2.0, (all_results[i][j] - all_means[j]) / all_stdevs[j]);  
	    } 
	    ga.mean_fitness += ga.population[i].fitness; 
	    if (ga.population[i].fitness > ga.best_fitness) { 
		 ga.best_fitness = ga.population[i].fitness; 
		 ga.best_index = i; 
	    } 
	   …
	 } 
	…
    }

通过三个循环遍历整个群体,对各个适应度进行比较后找出最优适应度。

4. 产生遗传后代

 public void newGeneration (){ 
…
	 String copy_pop[] = ga.copy (ga.population, ga.copy); 
	 String cross_pop[] = ga.crossover (ga.population, ga.crossover); 
	 copy_pop = ga.mutate (copy_pop, ga.mutation); 
	 cross_pop = ga.mutate (cross_pop, ga.mutation); 
	 for (int i = 0; i < copy_pop.length; i++){ 
	    ga.population[i+elite_pop.length].genome = copy_pop[i]; 
	 } 
	 for (int i = 0; i < cross_pop.length; i++){ 
	    ga.population[i+elite_pop.length+copy_pop.length].genome = cross_pop[i]; 
	 } 
…	
	 current_generation++; 
	 evaluateAll(); 
    }

通过复制 (ga.copy)、交叉 (ga.crossover)、变异 (ga.mutate) 操作,产生出遗传后代。

SmallBrain:

SmallBrain 也即我们写的利用遗传算法的例子机器人,它开始读取遗传文件"genome.dat",产生新的编码,当扫描到敌人时把所有相关的信息写入数组 robot_data,再通过循环操作进化写入输入运算,最后遍历输入运算决定输出机器人的动作。

1. 编码:

 public void parseGenome (String genome){ 
	 functions = new int [num_boxes][num_events]; 
	 inputs1 = new int [num_boxes][num_events]; 
	 inputs2 = new int [num_boxes][num_events]; 
	 robot_data = new double [num_boxes + num_system_inputs][num_events];

通过 parseGenome 方法,设置 function,input1,input2 等数组的参数,对要操作的机器人进行编码。这部分和最上面提来的 TableRex 编码表是一致的。

2. 写入状态信息

 public void onScannedRobot (ScannedRobotEvent e){ 
   	 robot_data[0][kSCAN_EVENT] = getVelocity(); 
	 robot_data[1][kSCAN_EVENT] = getEnergy(); 
	 robot_data[2][kSCAN_EVENT] = getHeading(); 		
… .

3. 根据函数数组写入输入运算

for (int i = 0; i < num_boxes; i++){ 
    switch (functions[i][event_num]){ 
        case 0: //greater than 
        robot_data [i + num_system_inputs][event_num] = 
            robot_data[inputs1[i][event_num]][event_num] >
        robot_data[inputs2[i][event_num]][event_num]?1f:0f; 
        break; 
        …
    case 2: //equal to 
        break; 
    case 15:  //output 
        handleOutput (inputs1[i][event_num], 
	    robot_data [inputs2[i][event_num]][event_num]);
    break;

此处注意最后是根据写入的操作运算进行输出

4. 输出机器人动作命令

  public void handleScanOutput (int outputType, double value){ 
	 switch (outputType % 16){ 
	 case 0:
	    ahead (value); break; 
	 case 1:
	    back (value); break; 
	 case 2:
	    //maybe shouldn't use mod here 
	    fire (value % 3); break; 
	…

最后我们可以看出 TableRex 程序中 ,smallBrain 和 BrainWorld 之间以文件方式并行交互,smallBrain 扫描信息,写入文件。BrainWorld 根据文件数据进化机器人,并把进化结果写入文件,smallBrain 根据进化后的数据产生机器人的动作。

GPBot 小型遗传机器人

Geep 的机器人 GPBot 系统正是采用了 TableRex 解释程序的简单例子。

GPBot 仅由四行代码组成(每行都以分号结束),它做了如下一些定制达到代码最优化:忽略雷达旋转,让它直接随着炮管而转动 TurnGunRight(INFINITY); 把行为做为常量来实现,让它们能显示在进化代码序列的任意点。

 OnScannedRobot() { 
 MoveTank(<GP#1>); 
 TurnTankRight(<GP#2>); 
 TurnGunRight(<GP#3>); 
 }

GPBot 所有事件都写在 ScannedRobotEvent 事件。每个方法都利用到了遗传算法进化机器人。第一行代码移动系统进化,适应度值依照个体躲避子弹,避墙和敌人的能力而设置 ; 第二行代码指示坦克旋转指定的方向角。第三行代码瞄准系统进化指示炮管旋转指定的方向角 , 适应度值依照个体打中敌人概率来设置。GPBot 群体大小为 256 个个体,变异、交叉概率分别为 0.9,选择长度为 5。在最附录中提供了例子机器人人下载。

测试结果

最后我们给出一些测试数据,看看我们的程序不同的测试结果。

变异概率变化测试 :

群体大小 : 25 
变异概率 (Mutation): tested 
精英概率 (Elitism): 0.1 
比赛回合 : 20 rounds 
后代 : 25

我们选择变异概率分级从 0.01 到 0.5 这间。依照上面的遗传算法介绍,0.01 的概率是比较合理的,所以我们以此为初始化值,下图中显示了所有的测试概率数据,从下图我们可以看出,开始一个很小的适应度值,从 6 代开始图形就增长很慢了,在 13 代的时候,有一个小的变化,到最后每个后代都相当逼近。

图 4. 变异测试
图 4. 变异测试
图 4. 变异测试
图 5. 6 到 9 代放大特写
图 5. 6 到 9 代放大特写
图 5. 6 到 9 代放大特写

测试说明:

我们从上可以看出当我们给出初始的适应度在很小时,在第一代增长很多,经过一定数量的后代开始汇聚到一些最大的适应度值。由此我们得到证明,机器人在我们的学习环境中聪明的开始躲避子弹、墙、和其他机器人。而且它自己找到一个很好的瞄准位置给与敌人打击。

JGAP 的使用

最后不能不说一下 Jgap 的使用 , JGAP 是一款用 Java 编写的遗传算法包,由 sourceforce 上开发而来。它提供了基本的遗传算法,你可以使用它来解决一些适合用遗传算法解决的问题。而且它给出了很多例子程序,可用于我们一些遗传算法的测试工作。由于它来自于开源组织 sourceforce,所以开源的遗传功能将是研究简单遗传算法很好工具。

AI-CODE

近来在研究人工智能过程和坦克机器人时,发现国内也开发出了一个类似于 Robocode 仿真器的平台 AI-CODE,其思想延用 Robocode,但在 Robocode 基础上做了很多的改进,封装了一些函数模块,让开发者更侧重于算法和程序设计的学习。最有意思的这个平台能同时支持 Java,C,C++,C# 语言,从理论上看它支持任何语言。美中不足的是国内应用的例子还不是很多,远没有 Robocode 那么多可参考的例子。如果大家有兴趣可尝试在 AI-CODE 平台上用不同语言做一些遗传算法的测试。我想能帮助更多人工智能爱好者。其相关网站大家可到 http:///www.ai-code.org去了解。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=144229
ArticleTitle=人工智能 Java 坦克机器人系列: 遗传算法
publish-date=06302006