演化算法(一) 基本概念

演化算法

1、简介

演化算法,又称为进化算法(Evolutionary Algorithm)、进化计算(EvolutionaryComputing)或遗传算法(Genetic Algorithm),是一种元启发式(metaheuristic)方法(定义见:http://en.wikipedia.org/wiki/Metaheuristic)。前面谈到的模拟退火算法也是一种元启发式算法。二者的主要区别也是演化算法的主要特点:虽然它们都在解空间内进行搜索,但模拟退火算法是一种轨迹式(trajectory)方法,而演化算法是一种基于种群(population-based)的方法。

具体地说,前者在搜索解空间时是沿着某一条轨迹前进的,最终收敛到某个解的局部。每次迭代时只对一个解操作。而后者的搜索过程是从一个初始解的集合(称为初始种群)开始的,种群中的每一个解都沿着一定的轨迹搜索,每前进一步称为种群的进化,得到的解集称为种群的一代(generation)。这样便增加了在庞大解空间中找到最优解的概率。在种群进化时,种群中的解会发生变异、杂交和选择等操作从而生成新的解,构成新一代种群。如下面两图所示。

这个过程与自然界生物的进化很类似,生物总群的每一代都会有或多或少的变异发生,而不用个体之间的杂交也是很普遍的现象。变异和杂交所产生的新个体不一定会适应生存环境,这就是自然选择的过程:适应的个体存活下来,不适应的个体遭到淘汰,最后存活下来的个体组成新一代的种群。

                                                                                                                                                          


2. 伪代码

一个演化算法的基本结果如下:

P <- GenerateInitialPopulation()

Evaluate(P)

while

termination conditions not metdo

  P’ <- Recombine(P)

  P’’ <- Mutate(P’)

 Evaluate(P’’)

P <- Select(P’’ U P)

endwhile

注意,在循环中的Recombine和Mutate操作即对应杂交算子和变异算子,有时重组(Recombine)也称作交叉(crossover)。这两种操作是必需的,但是顺序不是固定的,即也可以先进行变异然后再交叉。选择(select)操作是演化的关键一步,这里采用的方式是综合交叉和变异产生的后代和当代个体,选择优势个体(评估函数较好)组成下一代种群。选择的依据就是每一个解的评估函数值(通过Evaluate计算)。另一种方案是仅从新个体中选择适当数量的个体作为下一代种群。

3.设计要素

设计一个演化算法有以下几点需要考虑:

a.  表示方式

一个演化算法或是算法中的算子是否有效很大程度上依赖于解的表示方式。有些算子对一种表示方式的解有很好地效果,而对另一种表示方式的解却是无效的甚至是无法实现的。例如,求解SAT问题时经常使用的一种算子是反位操作,即对每一个变量的布尔值进行取反。这时表达所有变量真值情况(一个解)的很自然的方式就是使用一个长度为n的二进制位向量(n是变量的个数),以1表示变量取true值,以0表示变量取false值。

然而,同样的算子却不能用于TSP问题。TSP问题要求找到旅行商的一个城市环游路线,使得耗费最小。如果使用整数标记了每座城市,那么城市标号的一个排列就是一个解,这也是一种很自然的表示方式。对这样的解进行变换的常用算子就是2-交换算子:随机交换两个城市的位置形成新的排列。显然,这种算子是二进制位向量表示无法实现的。

总之,解的表示方式会对算子的设计甚至整个算法的效果产生决定性影响,在设计时要具体问题具体分析。

b.  评估函数

评估函数最常见的选择就是优化目标。显然,如果每次选择新一代个体都使用导致较好优化目标的个体,那么最终的结果是令人满意的。但有时候也会使用其他标准作为评估函数(考虑种群多样性等其他因素,详见第4节)。设计评估函数的常识性指导原则是:最优解应该被赋予最优的评估值

c.  变化算子

变化算子是演化算法的基础。种群能够不断地演化最后接近最优个体得益于变化算子的作用。把算子分成交叉和变异两类也是来自自然界的灵感。自然界中的生物的染色体(解)上的基因(部分解)发生变异时,生物的性状通常会发生显著变化。从长远来看,这种变异是有利于生物体生存的。不同生物体之间的杂交会使双亲个体的优良性状遗传到后代个体中,也同样有利于生物的生存。这就是交叉和变异算子会有助于产生近似最优解得原因。

变化算子的设计通常是问题相关的。某一种算子对特定问题有好的结果,却不一定是通用的。例如,一种常用的变异算子是反序(inverse),它使一个解的随机一段子序列中的元素反序。如下图,解s1中两个间隔点之间的部分反序后得到的新解为s2。

S1: 2 5 6 |3 4 8 9| 1 7

S2: 2 5 6 |9 8 4 3| 1 7

这个算子适用TSP问题的原因是,TSP的一个解中可能含有违反几何原理的子路径存在,如下图,1,2,3,4四个城市之间的距离(耗费)可能有如图所示的关系。路径1->3->4->2->1显然要比路径1->4->3->2->1要短,而反序算子就可以打破类似后者的子路径。



然而,这个算子在其他问题如生产调度问题中不一定有效。

d. 选择

选择算子的基本方法就是选择评估函数较优的个体进入下一代种群。选择的过程可以是确定的,也可以是随机的。但是要保证一点:评估值越好,被选中的概率应该越大。

选择的具体方式有很多。例如,可以定义如下选择概率:


Pi定义了第i个个体被选择的概率,Fi是其评估函数值,分母是所有个体的评估函数值得平均值。如果某个个体的评估函数值大于或等于平均值,则确定被选中;如果某个个体的评估函数值小于均值,则以这个概率被选中。显然,评估函数值越大接近均值,越容易被选中(这里定义评估函数值较大为优)。

另一种方式是,对当前种群进行多次随机取样,每次从样本中选取一个或多个最优解,直到选择了足够多的解为止。

e. 初始解

初始种群对算法的结果有一定的影响。在解空间中,如果初始解距离最优解很近,那么进过有限几次迭代,种群就可以进化到较优的状态;如果初始解选择的不好,那么算法效果就会受影响。

4. 解的强化(intensification)和多样化(diversification)

在种群进化的过程中,解的变化主要有两个方向:强化和多样化。强化是指解的质量有越变越好的趋势。然而,如果一直沿着某个解或某些解的领域范围搜索,容易陷入局部最优(local optimal)的情况,导致过早收敛(prematureconvergence)。如下图所示,



解S1就是一个局部最优解,而显然解S2(可能也是局部最优)比它要好,而接下来的搜索如果不跳出S1的邻域就无法获得较为精确的近似解。多样化方向策略就是为了解决这个问题。简单地说,多样化就是尽量使得搜索范围均匀分布在解空间中,而不只是局限于某个小的范围内。看起来,解的强化和多样化是相互矛盾的两个进化方向,但是二者对于得到较优的解都是不可或缺的。因此,考虑两个方向的平衡显得尤为重要,这也是设计演化算法的关键所在。

在演化算法的变化算子中,交叉算子就是为了实现解的多样化。通过使不同解的一部分按照一定的规则进行重组,得到一些与原来的解有一定距离的新解,消除了解的局部最优性。而变异算子就是为了实现解的强化。通过对解本身进行一定程度的扰动,进行局部邻域的搜索,找到局部领域中质量较好的解。这两种算子的设计与特定问题紧密相关,没有百试不爽的算子可用。

5.实现举例


这里借助TSP问题来举一例,看一看演化算法的具体实现。

首先定义一些算法必备的变量,具体功能详见注释。其中反转率是指在实现变异算子反转操作时,被反转的城市范围所占整个解维度(城市数量)的比例。问题解的表示方式:一个解就是城市编号的一个排列。


    //种群的大小
    private int pSize;
    //初始种群
    private ArrayList> population;
    //交叉后的种群
    private ArrayList> crossPop;
    //变异后的种群
    private ArrayList> mutatePop;
    //最大迭代次数
    private int maxIter;
    //反转率
    private double mrate;

接下来是算法的主体部分。整个演化过程执行一定的迭代次数,可以多次执行取平均值。在演化之前要生成一个初始种群。这里采用随机生成一个排列的方法生成一个解。

public void run() {
		//首先生成初始种群
		long start,end;
		double sumTime = 0;
		double sumDist = 0;
		int times = 50;
		while(times-- > 0)
		{
			start = System.currentTimeMillis();
			initialPopulation();
			int count = 0;
			while (++count <= maxIter) {
				// 进行重组操作
				recombinePop();
				// 进行变异操作
				mutatePop();
				// 进行选择操作
				selectNextGen();
			}
			shortestDist = getOptimal();
			end = System.currentTimeMillis();
			sumDist += shortestDist;
			sumTime += end - start;
		}
		System.out.println("50 次平均结果:" + sumDist / 50 + "\t" + sumTime / 50);
	}


private void initialPopulation() {
		int c = 0;
		ArrayList solution;
		while(++c <= pSize)
		{
			solution = randomSolution();
			population.add(solution);
		}
		
	}
        private ArrayList randomSolution() {
		// 随机产生一个解
		ArrayList res = new ArrayList();
		Random rand = new Random(System.nanoTime());
		ArrayList seed = new ArrayList();
		int j,index;
		for(j = 0; j < number; j++)seed.add(j);
		while(!seed.isEmpty())
		{
			index = rand.nextInt(seed.size());
			res.add(seed.get(index));
			seed.remove(index);
		}
		return res;
	}
第一步是重组操作,算子的思想是:选取一个切入点,保留第一个个体的从第一个城市到切入点城市的部分,剩下部分由第二个个体的城市无重复填满。两个个体随机从种群中选取。

private void recombinePop() {
		// 交叉思想:选取一个切入点 保留第一个个体的从第一个城市到切入点城市的部分 剩下部分由第二个个体的城市无重复填满
		crossPop = new ArrayList>();
		int i,j,cut,m,n,c = 0;
		Random rand = new Random(System.nanoTime());
		
		int[] isUsed;
		while(++c <= pSize)
		{
			isUsed = new int[number];
			for(int index : isUsed)index = 0;
			i = rand.nextInt(pSize);
			do j = rand.nextInt(pSize);
			while(i == j);
			
			ArrayList newSol = new ArrayList();
			cut = rand.nextInt(number);
			for(n= 0; n <= cut; n++)
			{
				newSol.add(population.get(i).get(n));
				isUsed[population.get(i).get(n)] = 1;
			}
			for(m = 0; m < number; m++)
			{
				if(isUsed[population.get(j).get(m)] == 0)
				{
					newSol.add(population.get(j).get(m));
				}
			}
			crossPop.add(newSol);
		}
		//population = (ArrayList>)crossPop.clone();
	}

第二步是变异操作,算子的思想是:对每一个解简单执行inverse算子。

private void mutatePop() {
		// 变异思想:对每一个解简单地进行inverse算子 反转的部分解大小为mrate*number
		mutatePop = new ArrayList>();
		int size = (int)mrate * number;
		Random rand = new Random(System.nanoTime());
		int start,end;
		
		for(ArrayList sol : crossPop)
		{
			start = rand.nextInt(number - size + 1);
			end = start + size - 1;
			sol = inverseMove(sol, start, end);
			mutatePop.add(sol);
		}
		
	}

其中inverse算子的实现如下:

private ArrayList inverseMove(ArrayList sol, int cut1, int cut2) {
		// TODO Auto-generated method stub
		int i = cut1;
		int j = cut2;
		while(i < j)
		{
			int job1 = sol.get(i);
			int job2 = sol.get(j);
			
			sol.remove(j);
			sol.add(j, job1);
			sol.remove(i);
			sol.add(i, job2);
			
			i++;
			j--;
		}
		return sol;
	}


最后一步是选择操作,算子的思想是:在原来种群和经过变异和交叉操作后的种群中选择较好的个体进入下一轮。这里的评估函数就是旅行商的旅行距离,也就是优化目标本身。

private void selectNextGen() {
		// 在原来种群和经过变异和交叉操作后的种群中选择较好的个体进入下一轮
		Candidate can = null;
		mutatePop.addAll(population);
		ArrayList canList = new ArrayList();
		
		for(ArrayList sol : mutatePop)
		{
			can =  new Candidate();
			can.setList(sol);
			can.setSortIndex(getTotalDist(sol));
			canList.add(can);
		}
		
		Collections.sort(canList, new MyComparator1());
		
		population = new ArrayList>();
		int i,size = pSize;
		for(i = 0;  i < size; i++)
		{
			//System.out.println(canList.get(i).getSortIndex());
			population.add(canList.get(i).getList());
		}
		
	}

另外,计算旅行距离的函数实现如下。

public static double getTotalDist(ArrayList list, double[][] distances) {
		
	    int number = list.size();
		double res = 0;
		int i = 0;
		for(; i < number - 1; i++)
			res += distances[list.get(i)][list.get(i + 1)];
		if(number > 1)
			res += distances[list.get(number - 1)][0];
		return res;
	}

}

6.参考资料

a. 维基百科

b. 如何求解问题——现代启发式方法

c.Metaheuristic in Combinatorial Optimization:Overview and Conceptual Comparison

(2012年3月25日)

注:今后我仍将继续学习演化算法,尤其算子的设计,初始解的构造等方面,并更新有关内容。

你可能感兴趣的:(碎片)