学习理解遗传算法(2个实例+百度云源码)--人工智能解决组合问题

先来干货,百度云链接:https://pan.baidu.com/s/1hS-mfFSDwwQOGNogWcR_SQ 密码:gis6

云盘中的源码是纯JS写的算法,直接在网页中打开即可。本文直接结合问题、代码对遗传算法进行讲解。

***************路由器排布问题:

路由器个数即圆的个数,路由器半径即圆的半径,种群大小是遗传算法中重要概念。此题是要求在这个500×500区域里,给定路由器个数和覆盖半径,要求尽可能多的覆盖这片区域,且中心区域需要二次覆盖。

此例是初学遗传算法时所写,写得不好,有兴趣的朋友可以查看源码研究。这里不讲述此问题

*********************机器人拣货路径问题

问题如下:在500×500区域内有若干个货物,机器人要在尽可能短的路径下拾取所有货物,即TSP问题。简单理解:遍历所有点,且总路径最小。下图为(标准)遗传算法解决机器人拣货路径.html,输入货物个数与种群个体数,点击“确定”出现如下界面(之后可点击"迭代100次"进行迭代,第一次点会很卡,因为计算量很大,一直点“迭代100次”可观察)

研究一个组合问题,首先需要说明问题的三要素:

(1)约束条件:区域面积等

(2)决策变量:货物个数

(3)目标函数:总距离

问题确立,接下来简单说明遗传算法,再详细讲解如何实施:

遗传算法即利用达尔文进化论观点生成的群体算法。

此段较抽象,是理论方法,之后会介绍详细方法:第一步,生成初代种群,一个种群中有若干个个体,每一个个体都是一种解法,对于每一个个体进行适应度函数计算,取适应度最大的个体作为当前最优解。第二步,进行繁衍,迭代法不断生成子代种群,如何生成子代种群,首先对父级种群进行编码,一般是生成01001000100111...类似的二进制编码作为染色体,每个个体生成一条染色体,随机抓取两条染色体交叉生成子代染色体(随机抓取的策略有轮盘赌、胜者为王、平均主义等,轮盘赌可自行百度,是一种符合遗传学的选择策略),若子代染色体不符合标准则舍弃(舍弃规则需要根据设定的编码规则来),直到生成足够数量且符合标准的子代。对子代染色体进行变异,最后将染色体解码生成子代种群,此时,同样适应度最大为最优解。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>(标准)遗传算法解决机器人拣货路径问题(即TSP问题具体应用)</title>
</head>
<body>
货物数量:<input id="goodsNum" style="width: 300"></input>
种群个体数:<input id="GeneNum" style="width: 300"></input>
<input type="button" value="确 定" name="formSubmit" onclick="checkFinish();">
<input type="button" value="迭代100次" name="NextSubmit" onclick="Next();">
<canvas id="drawing" width="500" height="500" style="border:#0F0 solid 1px">A Drawing of something</canvas>
<script>
var drawing=document.getElementById("drawing");
if(drawing.getContext){
	var context=drawing.getContext("2d");
}
var goodsNumInput=document.getElementById("goodsNum");//货物数量input
var goodsNum=0;//货物数量
var GeneNumInput=document.getElementById("GeneNum");//种群个体数input
var GeneNum=0;//种群个体数
var goodsLocation;//位置矩阵
var C;//距离矩阵
var Pop;//种群
function checkFinish(){
	drawing.height=drawing.height;//清空
	//获取当前各参数
	goodsNum=goodsNumInput.value;//货物数量
	GeneNum=GeneNumInput.value;//种群个体数
	if(goodsNum==""||GeneNum==""){
		alert("不允许存在空的参数");
		return ;
	}
	//随机生成货物位置,货物位置为2倍货物数量的数组,存放x,y坐标
	goodsLocation=initGoodsLocation(goodsNum);
	//绘制货物
	drawGoods(goodsLocation,goodsNum);
	//生成距离矩阵C
	C = distanceArr(goodsLocation,goodsNum);
	//生成初代种群,表现为二维数组[种群大小][货物数]
	Pop = initPopulation(goodsNum,GeneNum);
	//计算适应度函数,注输入应为数字表现的种群
	var PopScore = adaptCount(Pop,C);
	//计算初代种群适应度最大并绘制图样
	var maxIndexOf=PopScore.indexOf(Math.max.apply(Math, PopScore));
	drawPic(goodsLocation,Pop[maxIndexOf]);
}

function Next(){
	drawing.height=drawing.height;//清空
	goodsNum=goodsNumInput.value;//货物数量
	GeneNum=GeneNumInput.value;//种群个体数
	drawGoods(goodsLocation,goodsNum);//绘制货物
	for(var D=0;D<100;D++){
		//将种群个体编码,便于之后进行选择交叉
		var PopCode = coder(Pop,goodsNum);//一维,长度为种群大小
		//进行选择交叉,选择策略为轮盘赌
		var PopScore = adaptCount(Pop,C);//获取适应度分数,一维,长度为种群大小
		var scoreArr = new Array();//百分比适应度记录数组
		var totalScore=0;
		for(var i=0;i<PopScore.length;i++){
			totalScore+=PopScore[i];
		}
		for(var i=0;i<PopScore.length;i++){
			scoreArr.push(PopScore[i]/totalScore);
		}
		for(var i=1;i<PopScore.length;i++){
			scoreArr[i]=scoreArr[i]+scoreArr[i-1];
		}
		var childPop=new Array();
		while(childPop.length<GeneNum){
			//轮盘赌
			var father,mother;
			var randomNum=Math.random();
			for(var i=0;i<GeneNum;i++){
				if(randomNum<=scoreArr[i]){
					father=i;
					break;
				}
			}
			randomNum=Math.random();
			for(var i=0;i<GeneNum;i++){
				if(randomNum<=scoreArr[i]){
					mother=i;
					break;
				}
			}
			//交叉遗传
			var child = getChild(PopCode[father],PopCode[mother]);//单个子代编码
			//解码
			var childArr = DeCoder(child,goodsNum);//单个子代数组模式
			//判断是否满足要求,即遍历所有位置,且仅有一次
			if(childKiller(childArr,goodsNum)){
				childPop.push(childArr);
			}else{continue;}
		}
		Pop=childPop;
		//变异
		Pop=getChange(Pop);
	}
	
	
	//计算适应度最大并绘制图样
	PopScore = adaptCount(Pop,C);
	var maxIndexOf=PopScore.indexOf(Math.max.apply(Math, PopScore));
	drawPic(goodsLocation,Pop[maxIndexOf]);
}


//初始化货物位置
function initGoodsLocation(goodsNum){
	var goodsLocation=new Array();
	for(var i=0;i<goodsNum;i++){
		goodsLocation.push(Math.random()*500);//x坐标
		goodsLocation.push(Math.random()*500);//y坐标
	}
	return goodsLocation;
}

//绘制货物方法
function drawGoods(goodsLocation,goodsNum){
	context.fillStyle="black";
	context.font='25px Arial';
	for(var i=0;i<goodsNum;i++){
		context.fillText(i+1,goodsLocation[2*i],goodsLocation[2*i+1]);
	}
	
}

//生成距离矩阵C
function distanceArr(goodsLocation,goodsNum){
	var Arr = new Array(goodsNum);
	for(var i=0;i<goodsNum;i++){
		var Arr2 = new Array(goodsNum);
		Arr[i]=Arr2;
	}
	for(var i=0;i<goodsNum;i++){
		for(var j=i;j<goodsNum;j++){
			if(j==i){
				Arr[i][j]=0;
			}else{
				var x_x=parseFloat(goodsLocation[2*i]-goodsLocation[2*j]);
				var y_y=parseFloat(goodsLocation[2*i+1]-goodsLocation[2*j+1]);
				var distance = Math.pow(x_x,2)+Math.pow(y_y,2);
				Arr[i][j]=Math.sqrt(distance);
				Arr[j][i]=Arr[i][j];
			}
		}
	}
	return Arr;
}

//初代种群生成
function initPopulation(goodsNum,GeneNum){
	var initArrReturn = new Array();
	for(var j=0;j<GeneNum;j++){
		var initArr = new Array();
		for(var i=0;i<goodsNum;i++){
			initArr.push(i);
		}
		for(var i=0;i<initArr.length;i++){
			var rand=parseInt(Math.random () *initArr.length);
			var k=initArr[i];
			initArr[i]=initArr[rand];
			initArr[rand]=k;
		}
		initArrReturn.push(initArr);
	}
	
	return initArrReturn;
}

//适应度得分
function adaptCount(initPop,C){
	var adaptArr = new Array();
	for(var i=0;i<initPop.length;i++){
		var sum=0;
		for(var j=0;j<initPop[i].length-1;j++){
			sum+=parseFloat(C[initPop[i][j]][initPop[i][j+1]]);
		}
		if(i==initPop.length[i]-1){
			sum+=parseFloat(C[initPop[i][initPop.length[i]-1]][initPop[i][0]]);
		}
		adaptArr.push(1/sum);
	}
	return adaptArr;
}

//绘制直线
function drawPic(goodsLocation,Pop){
	for(var i=0;i<Pop.length-1;i++){
		context.beginPath();
		context.moveTo(goodsLocation[2*Pop[i]],goodsLocation[2*Pop[i]+1]);
		context.lineTo(goodsLocation[2*Pop[i+1]],goodsLocation[2*Pop[i+1]+1]);
		context.lineWidth=1;
		context.strokeStyle="red";
		context.stroke();
		context.closePath();
	}
	context.beginPath();
	context.moveTo(goodsLocation[2*Pop[Pop.length-1]],goodsLocation[2*Pop[Pop.length-1]+1]);
	context.lineTo(goodsLocation[2*Pop[0]],goodsLocation[(2*Pop[0])+1]);
	context.lineWidth=1;
	context.strokeStyle="red";
	context.stroke();
	context.closePath();
}

//编码
function coder(Pop,goodsNum){
	var coderReturn = new Array();
	//定义需要几位二进制数
	var codeNum = 1;
	var goodsNum_code=parseInt(goodsNum);
	while(goodsNum_code>=2){
		goodsNum_code=parseInt(goodsNum_code/2);
		codeNum++;
	}
	for(var i=0;i<Pop.length;i++){
		var codeString="";
		for(var j=0;j<Pop[i].length;j++){
			var codeStringPart = Pop[i][j].toString(2);
			if(codeStringPart.length<codeNum){
				for(var k=0;k<(parseInt(codeNum)-codeStringPart.length+1);k++){
					codeStringPart="0"+codeStringPart;
				}
			}
			codeString+=codeStringPart;
		}
		coderReturn.push(codeString);
	}
	return coderReturn;
}
//通过两个编码即父母,获得子代编码
function getChild(father,mother){
	var fatherArr = father.split("");
	var motherArr = mother.split("");
	var child="";
	for(var i=0;i<fatherArr.length;i++){
		if(Math.random()>0.5){
			child+=fatherArr[i];
		}else{
			child+=motherArr[i];
		}
	}
	return child;
}

//解码
function DeCoder(child,goodsNum){
	var coderReturn = new Array();
	//定义需要几位二进制数
	var codeNum = 1;
	var goodsNum_code=parseInt(goodsNum);
	while(goodsNum_code>=2){
		goodsNum_code=parseInt(goodsNum_code/2);
		codeNum++;
	}
	
	var childArr = child.split("");
	for(var i=0;i<childArr.length/codeNum;i++){
		var numBy2="";//二进制数拆分
		for(var j=0;j<codeNum;j++){
			numBy2+=childArr[i*codeNum+j];
		}
		coderReturn.push(parseInt(numBy2,2));
	}
	return coderReturn;
}
//判断子代是否符合要求
//sort()慎用!!!直接改变了原数组的顺序,所以该方法中赋值不能直接childArr_help=childArr,储存地址指向同一位置
function childKiller(childArr,goodsNum){
	var childArr_help=new Array();
	for(var i=0;i<childArr.length;i++){
		childArr_help[i]=childArr[i];
	}
	childArr_help=childArr_help.sort();
	for(var i=0;i<childArr_help.length;i++){
		if(childArr_help[i]>=goodsNum){
			return false;
		}
	}
	for(var i=1;i<childArr_help.length;i++){
		if(childArr_help[i]==childArr_help[i-1]){
			return false;
		}
	}
	return true;
}
//变异,种群变异率这里定义为0.1,基因变异为一对
function getChange(Pop){
	var changeRate=0.1;
	for(var i=0;i<Pop.length*changeRate;i++){
		var changePop = parseInt(Math.random()*Pop.length)
		var gene = Pop[changePop];
		var x = parseInt(Math.random()*Pop[i].length);
		var y = parseInt(Math.random()*Pop[i].length);
		var genePart = gene[x];
		gene[x]=gene[y];
		gene[y]=genePart;
		Pop[changePop]=gene;
	}
	return Pop;
}
</script>
</body>
</html>

对于此题,首先需要架构环境,利用html自带的canvas进行绘制,并写好网页的各框等。

初始化问题:需要生成货物,假设有N个,货物是由X,Y坐标定义的,定义一个一位数组goodsLocation储存所有点的距离[X1,Y1,X2,Y2...],再定义一个N阶方阵(即N×N的二维数组)记录点与点之间的距离,距离矩阵为C,比如点1和点2之间的距离调用C[0][0]即可(注意下标)。

生成初代种群,用二维数组Pop记录,Pop第一维大小为种群大小即个体数,第二维大小为货物数,比如该种群我们定义2个个体,有5个货物,Pop的形式如[[1,2,3,4,5],[2,3,5,1,4]],对应两种解法--第一种先捡1号货物,再捡2号、3号、4号、5号,第二种解法是先捡2号,再3、5、1、4号。之后个体的储存形式都是如此。

定义适应度函数,计算每个个体的适应度大小。最后将适应度最大的个体在图中绘制,作为初代最优解。

适应度函数:一般来讲,遗传算法的适应度函数定义为越大适应度越好,此题中目标是距离最小,而距离和与目标相反,所以这里的适应度取距离和的倒数。

接下来就是遗传算法的核心,进行编码,轮盘赌策略进行选择,基因交叉,解码生成子代,淘汰不合要求子代,最后变异。

编码:生成二进制的过程。比如现有2个个体[1,2,3,4,5]和[2,3,5,1,4],1生成001,2生成010...所以编码生成001010011100101,010011101001100,此处注意二进制化过程中,1默认生成1,2生成10,4生成100,它们的位数不一样,所以为了统一,我们补零,生成位数一样的两个个体。

轮盘赌进行选择:轮盘赌不想多解释,不懂自行百度

选择2个个体进行交叉:比如001010011100101,010011101001100这两个个体,第一位均为0,无论怎么交叉仍为0,第二位的数字则不一样了,随机选择一位生成子代

解码:假设生成了001001001001001001的子代,解码就是编码的逆过程,生成[1,1,1,1,1]

淘汰过程:题目要求遍历所有点且只能一次,所以[1,1,1,1,1]的子代是不合要求的,进行淘汰,只有当所有数字有且仅出现一次才留存。同时,还有可能生成形同[1,1,1,1,7]的子代,为什么?因为三位编码最大值为111(二进制),对应7(十进制),对于超过N范畴的子代同样淘汰。

一直执行编码、交叉,解码,淘汰的过程直到生成N个子代个体,生成了满足种群数量的子代种群

变异:首先需要确定变异率和变异策略,这段代码中变异率为0.1,即10个个体中会有一个变异,变异策略为其中一对基因变异,比如变异个体开始是[1,2,3,4,5],随机取两个位置进行调换生成[2,1,3,4,5],作为变异个体。

至此,子代种群生成了,执行适应度函数,评价适应度,适应度最大值最为当前最优解。

遗传算法就实现了!!!

接下来是遗传算法更深层次的理解:

不断迭代100次,你会发现一个问题:结果不稳定

思考:不稳定应该是由变异造成的,所以写了一份(变异率)遗传算法解决机器人拣货路径.html,可改变变异率,当变异率设定为0时,你会发现在有限次迭代后,结果稳定下来,不再改变,因为种群统一了,即所有的个体都是一样的,如果采用胜者为王策略这种收敛速度会更快,但是这个解是局部最优解

局部最优解是遗传算法的一大弊端,所以这是变异存在的原因:变异提供了一种跳出局部最优解的策略。但在我这个程序中,由于变异有可能将当前最优解反而变异掉了,得不偿失!

思考如何化解,首先想到是改变适应度函数,参见(适应度函数)遗传算法解决机器人拣货路径.html,适应度函数计算为总距离和的倒数,第一想法会不会因为由于倒数过小反而影响了决策,所以更换为“常数-总距离”作为新适应度函数。写完之后一想,完全不如倒数的方法,懂点数学应该都能想到。

再次思考,改良遗传算法,参见:结合禁忌算法中“禁忌表”思维,那就记录下每代中最佳的个体,保证最佳个体完全出现在后一代中,即保证其绝对不交叉与变异。当出现更加优秀的个体,再记录这个更优秀的个体。这样就能保证子代的最优解永远不低于父代的最优解。同时变异策略改变,之前的变异策略为更改一对,比如[1,2,3,4,5]变成[2,1,3,4,5](改变其中2个数的位置),现在随机改变对数,比如随机改变3次,[1,2,3,4,5]->[2,1,3,4,5]->[2,3,1,4,5]->[2,3,1,5,4],这样就能加快找到全局最优解的速度。可打开(改良)遗传算法解决机器人拣货路径.html,测试一下是不是收敛状况与速度都较之前有了很大的提升(改良遗传算法的方法有很多,比如神经网络+遗传算法等,智者见智吧)

结论:

1.适应度函数将影响遗传算法的收敛速度和性能(教授给我说是:适应度函数由问题和建立的模型决定

2.遗传算法中选择策略、交叉策略等直接决定了算法的优劣。所以同样的问题,同样的遗传算法,不同的人设计出来性能完全不一样

3.变异是为了跳出局部最优解,变异策略和变异率影响跳出速度

4.若没有变异,遗传算法迭代到最后会生成完全一样的个体组成的种群。该解可能只是局部最优解,而当种群大小最够大或者说无穷大时,迭代无数代后总会生成全局最优解。(所以遗传算法需要考虑种群大小和迭代次数)

如有疑惑或者想要交流,可以给我留言,我会尽快回复您,如有错误,请指出,谢谢!

猜你喜欢

转载自blog.csdn.net/qq_36187544/article/details/84027832
今日推荐